Compare commits

...

17 Commits

Author SHA1 Message Date
ionitron
6693737719 chore(): add updated snapshots 2024-04-16 16:04:28 +00:00
amandaesmith3
fec3b0b573 Merge branch 'FW-5463' of https://github.com/ionic-team/ionic-framework into FW-5463 2024-04-16 10:52:14 -05:00
amandaesmith3
6fd0e78c1b revert flaky diff 2024-04-16 10:48:44 -05:00
ionitron
e97811a1aa chore(): add updated snapshots 2024-04-16 10:48:43 -05:00
amandaesmith3
cfac073c2a add tests 2024-04-16 10:46:54 -05:00
amandaesmith3
525a25fe15 lint 2024-04-16 10:46:53 -05:00
amandaesmith3
69969e04b4 fix(popover): adjust position to account for approximate safe area 2024-04-16 10:46:52 -05:00
Sean Perkins
2220d83d32 Merge remote-tracking branch 'origin/feature-8.0' into feature-8.1 2024-04-09 14:54:14 -04:00
Rahul Rajesh
6945adc3cc feat(datetime): pass roles to overlay when dismissing (#29221)
Issue number: resolves #28298

---------

<!-- Please do not submit updates to dependencies unless it fixes an
issue. -->

<!-- Please try to limit your pull request to one type (bugfix, feature,
etc). Submit multiple pull requests if needed. -->

## What is the current behavior?
<!-- Please describe the current behavior that you are modifying. -->
- the ion-datetime didn't provide a role(source trigger for closing the
overlay) on default buttons while closing parent overlay

## What is the new behavior?
<!-- Please describe the behavior or changes that are being added by
this PR. -->

- ion-datetime provides a role to default buttons while closing the
parent overlay

## Does this introduce a breaking change?

- [ ] Yes
- [x] No

<!--
  If this introduces a breaking change:
1. Describe the impact and migration path for existing applications
below.
  2. Update the BREAKING.md file with the breaking change.
3. Add "BREAKING CHANGE: [...]" to the commit description when merging.
See
https://github.com/ionic-team/ionic-framework/blob/main/.github/CONTRIBUTING.md#footer
for more information.
-->


## Other information
N/A
<!-- Any other information that is important to this PR such as
screenshots of how the component looks before and after the change. -->

---------

Co-authored-by: Liam DeBeasi <liamdebeasi@users.noreply.github.com>
2024-04-03 15:21:03 -04:00
Shawn Taylor
90a7e70a1c feat(content): add fixedSlotPlacement prop (#29233)
Issue number: Internal

---------

<!-- Please do not submit updates to dependencies unless it fixes an
issue. -->

<!-- Please try to limit your pull request to one type (bugfix, feature,
etc). Submit multiple pull requests if needed. -->

## What is the current behavior?
<!-- Please describe the current behavior that you are modifying. -->
Content in the `fixed` slot is always placed after the main content in
the DOM.

## What is the new behavior?
<!-- Please describe the behavior or changes that are being added by
this PR. -->

- A new `fixedSlotPlacement` prop on Content allows developers to place
fixed content either before or after the main content in the DOM

## Does this introduce a breaking change?

- [ ] Yes
- [x] No

<!--
  If this introduces a breaking change:
1. Describe the impact and migration path for existing applications
below.
  2. Update the BREAKING.md file with the breaking change.
3. Add "BREAKING CHANGE: [...]" to the commit description when merging.
See
https://github.com/ionic-team/ionic-framework/blob/main/.github/CONTRIBUTING.md#footer
for more information.
-->


## Other information

Dev build: `8.0.0-dev.11712072527.1dd97c66`

<!-- Any other information that is important to this PR such as
screenshots of how the component looks before and after the change. -->

⚠️This feature will not be part of the v8.0 release. As a result, do not
merge this into `feature-8.0`. However, I am putting this PR up based
off `feature-8.0` so it can get reviewed by the team.

---------

Co-authored-by: Liam DeBeasi <liamdebeasi@users.noreply.github.com>
2024-04-03 13:48:03 -04:00
Liam DeBeasi
0f5d1c02d2 feat(input): add clearInputIcon property (#29246)
Issue number: resolves #26974

---------

<!-- Please do not submit updates to dependencies unless it fixes an
issue. -->

<!-- Please try to limit your pull request to one type (bugfix, feature,
etc). Submit multiple pull requests if needed. -->

## What is the current behavior?
<!-- Please describe the current behavior that you are modifying. -->

In https://github.com/ionic-team/ionic-framework/pull/26354 we updated
the clear icon to use an ionicon instead of an hardcoded SVG. This has
made it challenging to customize the icon.

## What is the new behavior?
<!-- Please describe the behavior or changes that are being added by
this PR. -->

- Added `clearInputIcon` property to allow developers to customize the
ionicon used.

## Does this introduce a breaking change?

- [ ] Yes
- [x] No

<!--
  If this introduces a breaking change:
1. Describe the impact and migration path for existing applications
below.
  2. Update the BREAKING.md file with the breaking change.
3. Add "BREAKING CHANGE: [...]" to the commit description when merging.
See
https://github.com/ionic-team/ionic-framework/blob/main/.github/CONTRIBUTING.md#footer
for more information.
-->


## Other information

<!-- Any other information that is important to this PR such as
screenshots of how the component looks before and after the change. -->
2024-04-03 13:48:03 -04:00
amandaesmith3
6503c60519 revert flaky diff 2024-03-04 09:26:11 -06:00
Amanda Johnston
530e7232e8 Merge branch 'feature-7.8' into FW-5463 2024-03-04 07:22:41 -08:00
ionitron
21b285a988 chore(): add updated snapshots 2024-02-16 19:16:28 +00:00
amandaesmith3
ee626e0b76 add tests 2024-02-16 13:03:43 -06:00
amandaesmith3
c4918b93a3 lint 2024-02-16 12:55:05 -06:00
amandaesmith3
0d7497abe0 fix(popover): adjust position to account for approximate safe area 2024-02-16 12:52:47 -06:00
59 changed files with 340 additions and 50 deletions

View File

@@ -361,6 +361,7 @@ ion-col,css-prop,--ion-grid-columns
ion-content,shadow
ion-content,prop,color,"danger" | "dark" | "light" | "medium" | "primary" | "secondary" | "success" | "tertiary" | "warning" | string & Record<never, never> | undefined,undefined,false,true
ion-content,prop,fixedSlotPlacement,"after" | "before",'after',false,false
ion-content,prop,forceOverscroll,boolean | undefined,undefined,false,false
ion-content,prop,fullscreen,boolean,false,false,false
ion-content,prop,scrollEvents,boolean,false,false,false
@@ -553,6 +554,7 @@ ion-input,prop,autocomplete,"name" | "email" | "tel" | "url" | "on" | "off" | "h
ion-input,prop,autocorrect,"off" | "on",'off',false,false
ion-input,prop,autofocus,boolean,false,false,false
ion-input,prop,clearInput,boolean,false,false,false
ion-input,prop,clearInputIcon,string | undefined,undefined,false,false
ion-input,prop,clearOnEdit,boolean | undefined,undefined,false,false
ion-input,prop,color,"danger" | "dark" | "light" | "medium" | "primary" | "secondary" | "success" | "tertiary" | "warning" | string & Record<never, never> | undefined,undefined,false,true
ion-input,prop,counter,boolean,false,false,false

View File

@@ -762,6 +762,10 @@ export namespace Components {
* The color to use from your application's color palette. Default options are: `"primary"`, `"secondary"`, `"tertiary"`, `"success"`, `"warning"`, `"danger"`, `"light"`, `"medium"`, and `"dark"`. For more information on colors, see [theming](/docs/theming/basics).
*/
"color"?: Color;
/**
* Controls where the fixed content is placed relative to the main content in the DOM. This can be used to control the order in which fixed elements receive keyboard focus. For example, if a FAB in the fixed slot should receive keyboard focus before the main page content, set this property to `'before'`.
*/
"fixedSlotPlacement": 'after' | 'before';
/**
* If `true` and the content does not cause an overflow scroll, the scroll interaction will cause a bounce. If the content exceeds the bounds of ionContent, nothing will change. Note, this does not disable the system bounce on iOS. That is an OS level setting.
*/
@@ -1162,6 +1166,10 @@ export namespace Components {
* If `true`, a clear icon will appear in the input when there is a value. Clicking it clears the input.
*/
"clearInput": boolean;
/**
* The icon to use for the clear button. Only applies when `clearInput` is set to `true`.
*/
"clearInputIcon"?: string;
/**
* If `true`, the value will be cleared after focus upon edit. Defaults to `true` when `type` is `"password"`, `false` for all other types.
*/
@@ -5478,6 +5486,10 @@ declare namespace LocalJSX {
* The color to use from your application's color palette. Default options are: `"primary"`, `"secondary"`, `"tertiary"`, `"success"`, `"warning"`, `"danger"`, `"light"`, `"medium"`, and `"dark"`. For more information on colors, see [theming](/docs/theming/basics).
*/
"color"?: Color;
/**
* Controls where the fixed content is placed relative to the main content in the DOM. This can be used to control the order in which fixed elements receive keyboard focus. For example, if a FAB in the fixed slot should receive keyboard focus before the main page content, set this property to `'before'`.
*/
"fixedSlotPlacement"?: 'after' | 'before';
/**
* If `true` and the content does not cause an overflow scroll, the scroll interaction will cause a bounce. If the content exceeds the bounds of ionContent, nothing will change. Note, this does not disable the system bounce on iOS. That is an OS level setting.
*/
@@ -5886,6 +5898,10 @@ declare namespace LocalJSX {
* If `true`, a clear icon will appear in the input when there is a value. Clicking it clears the input.
*/
"clearInput"?: boolean;
/**
* The icon to use for the clear button. Only applies when `clearInput` is set to `true`.
*/
"clearInputIcon"?: string;
/**
* If `true`, the value will be cleared after focus upon edit. Defaults to `true` when `type` is `"password"`, `false` for all other types.
*/

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 17 KiB

View File

@@ -75,6 +75,15 @@ export class Content implements ComponentInterface {
*/
@Prop() fullscreen = false;
/**
* Controls where the fixed content is placed relative to the main content
* in the DOM. This can be used to control the order in which fixed elements
* receive keyboard focus.
* For example, if a FAB in the fixed slot should receive keyboard focus before
* the main page content, set this property to `'before'`.
*/
@Prop() fixedSlotPlacement: 'after' | 'before' = 'after';
/**
* If `true` and the content does not cause an overflow scroll, the scroll interaction will cause a bounce.
* If the content exceeds the bounds of ionContent, nothing will change.
@@ -423,7 +432,7 @@ export class Content implements ComponentInterface {
}
render() {
const { isMainContent, scrollX, scrollY, el } = this;
const { fixedSlotPlacement, isMainContent, scrollX, scrollY, el } = this;
const rtl = isRTL(el) ? 'rtl' : 'ltr';
const mode = getIonMode(this);
const forceOverscroll = this.shouldForceOverscroll();
@@ -446,6 +455,9 @@ export class Content implements ComponentInterface {
}}
>
<div ref={(el) => (this.backgroundContentEl = el)} id="background-content" part="background"></div>
{fixedSlotPlacement === 'before' ? <slot name="fixed"></slot> : null}
<div
class={{
'inner-scroll': true,
@@ -467,7 +479,7 @@ export class Content implements ComponentInterface {
</div>
) : null}
<slot name="fixed"></slot>
{fixedSlotPlacement === 'after' ? <slot name="fixed"></slot> : null}
</Host>
);
}

View File

@@ -0,0 +1,31 @@
import { newSpecPage } from '@stencil/core/testing';
import { Content } from '../content';
describe('content: fixed slot placement', () => {
it('should should fixed slot after content', async () => {
const page = await newSpecPage({
components: [Content],
html: '<ion-content></ion-content>',
});
const content = page.body.querySelector('ion-content')!;
const fixedSlot = content.shadowRoot!.querySelector('slot[name="fixed"]')!;
const scrollEl = content.shadowRoot!.querySelector('[part="scroll"]')!;
expect(fixedSlot.nextElementSibling).not.toBe(scrollEl);
});
it('should should fixed slot before content', async () => {
const page = await newSpecPage({
components: [Content],
html: `<ion-content fixed-slot-placement="before"></ion-content>`,
});
const content = page.body.querySelector('ion-content')!;
const fixedSlot = content.shadowRoot!.querySelector('slot[name="fixed"]')!;
const scrollEl = content.shadowRoot!.querySelector('[part="scroll"]')!;
expect(fixedSlot.nextElementSibling).toBe(scrollEl);
});
});

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 25 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 22 KiB

View File

@@ -541,7 +541,7 @@ export class Datetime implements ComponentInterface {
}
if (closeOverlay) {
this.closeParentOverlay();
this.closeParentOverlay(CONFIRM_ROLE);
}
}
@@ -566,7 +566,7 @@ export class Datetime implements ComponentInterface {
this.ionCancel.emit();
if (closeOverlay) {
this.closeParentOverlay();
this.closeParentOverlay(CANCEL_ROLE);
}
}
@@ -616,13 +616,13 @@ export class Datetime implements ComponentInterface {
return Array.isArray(activeParts) ? activeParts[0] : activeParts;
};
private closeParentOverlay = () => {
private closeParentOverlay = (role: string) => {
const popoverOrModal = this.el.closest('ion-modal, ion-popover') as
| HTMLIonModalElement
| HTMLIonPopoverElement
| null;
if (popoverOrModal) {
popoverOrModal.dismiss();
popoverOrModal.dismiss(undefined, role);
}
};
@@ -2645,5 +2645,7 @@ export class Datetime implements ComponentInterface {
}
let datetimeIds = 0;
const CANCEL_ROLE = 'datetime-cancel';
const CONFIRM_ROLE = 'datetime-confirm';
const WHEEL_ITEM_PART = 'wheel-item';
const WHEEL_ITEM_ACTIVE_PART = `active`;

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.4 KiB

After

Width:  |  Height:  |  Size: 5.6 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.8 KiB

After

Width:  |  Height:  |  Size: 8.5 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.0 KiB

After

Width:  |  Height:  |  Size: 9.1 KiB

View File

@@ -0,0 +1,41 @@
import { expect } from '@playwright/test';
import { configs, test } from '@utils/test/playwright';
configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ config, title }) => {
test.describe(title('datetime: overlay roles'), () => {
test.beforeEach(async ({ page }) => {
await page.setContent(
`
<ion-modal>
<ion-datetime></ion-datetime>
</ion-modal>
`,
config
);
});
test('should pass role to overlay when calling confirm method', async ({ page }) => {
const ionModalDidDismiss = await page.spyOnEvent('ionModalDidDismiss');
const modal = page.locator('ion-modal');
const datetime = page.locator('ion-datetime');
await modal.evaluate((el: HTMLIonModalElement) => el.present());
await datetime.evaluate((el: HTMLIonDatetimeElement) => el.confirm(true));
await ionModalDidDismiss.next();
expect(ionModalDidDismiss).toHaveReceivedEventDetail({ data: undefined, role: 'datetime-confirm' });
});
test('should pass role to overlay when calling cancel method', async ({ page }) => {
const ionModalDidDismiss = await page.spyOnEvent('ionModalDidDismiss');
const modal = page.locator('ion-modal');
const datetime = page.locator('ion-datetime');
await modal.evaluate((el: HTMLIonModalElement) => el.present());
await datetime.evaluate((el: HTMLIonDatetimeElement) => el.cancel(true));
await ionModalDidDismiss.next();
expect(ionModalDidDismiss).toHaveReceivedEventDetail({ data: undefined, role: 'datetime-cancel' });
});
});
});

View File

@@ -0,0 +1,66 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Datetime - Overlay Roles</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0" />
<link href="../../../../../css/ionic.bundle.css" rel="stylesheet" />
<link href="../../../../../scripts/testing/styles.css" rel="stylesheet" />
<script src="../../../../../scripts/testing/scripts.js"></script>
<script type="module" src="../../../../../dist/ionic/ionic.esm.js"></script>
<style>
ion-modal.ios,
ion-popover.datetime-popover.ios {
--width: 350px;
--height: 420px;
}
ion-modal.md,
ion-popover.datetime-popover.md {
--width: 350px;
--height: 450px;
}
ion-datetime {
width: 350px;
}
</style>
</head>
<body>
<ion-app>
<ion-header translucent="true">
<ion-toolbar>
<ion-title>Datetime - Overlay Roles</ion-title>
</ion-toolbar>
</ion-header>
<ion-content class="ion-padding">
<ion-button onclick="presentModal()">Present Modal</ion-button>
</ion-content>
</ion-app>
<script>
const presentModal = async () => {
const modal = await createModal();
await modal.present();
console.log(await modal.onDidDismiss());
};
const createModal = () => {
// create component to open
const element = document.createElement('div');
element.innerHTML = `
<ion-datetime show-default-buttons="true"></ion-datetime>
`;
// present the modal
const modalElement = Object.assign(document.createElement('ion-modal'), {
component: element,
});
const app = document.querySelector('ion-app');
app.appendChild(modalElement);
return modalElement;
};
</script>
</body>
</html>

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

After

Width:  |  Height:  |  Size: 44 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

After

Width:  |  Height:  |  Size: 43 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

After

Width:  |  Height:  |  Size: 28 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 39 KiB

After

Width:  |  Height:  |  Size: 36 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

After

Width:  |  Height:  |  Size: 29 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 39 KiB

After

Width:  |  Height:  |  Size: 36 KiB

View File

@@ -92,6 +92,11 @@ export class Input implements ComponentInterface {
*/
@Prop() clearInput = false;
/**
* The icon to use for the clear button. Only applies when `clearInput` is set to `true`.
*/
@Prop() clearInputIcon?: string;
/**
* If `true`, the value will be cleared after focus upon edit. Defaults to `true` when `type` is `"password"`, `false` for all other types.
*/
@@ -681,11 +686,13 @@ export class Input implements ComponentInterface {
}
render() {
const { disabled, fill, readonly, shape, inputId, labelPlacement, el, hasFocus } = this;
const { disabled, fill, readonly, shape, inputId, labelPlacement, el, hasFocus, clearInputIcon } = this;
const mode = getIonMode(this);
const value = this.getValue();
const inItem = hostContext('ion-item', this.el);
const shouldRenderHighlight = mode === 'md' && fill !== 'outline' && !inItem;
const defaultClearIcon = mode === 'ios' ? closeCircle : closeSharp;
const clearIconData = clearInputIcon ?? defaultClearIcon;
const hasValue = this.hasValue();
const hasStartEndSlots = el.querySelector('[slot="start"], [slot="end"]') !== null;
@@ -784,7 +791,7 @@ export class Input implements ComponentInterface {
}}
onClick={this.clearTextInput}
>
<ion-icon aria-hidden="true" icon={mode === 'ios' ? closeCircle : closeSharp}></ion-icon>
<ion-icon aria-hidden="true" icon={clearIconData}></ion-icon>
</button>
)}
<slot name="end"></slot>

View File

@@ -99,3 +99,19 @@ describe('input: label rendering', () => {
expect(labelText.textContent).toBe('Label Prop Text');
});
});
// https://github.com/ionic-team/ionic-framework/issues/26974
describe('input: clear icon', () => {
it('should render custom icon', async () => {
const page = await newSpecPage({
components: [Input],
html: `
<ion-input clear-input-icon="foo" clear-input="true"></ion-input>
`,
});
const icon = page.body.querySelector<HTMLIonIconElement>('ion-input ion-icon')!;
expect(icon.getAttribute('icon')).toBe('foo');
});
});

View File

@@ -53,20 +53,8 @@ export const iosEnterAnimation = (baseEl: HTMLElement, opts?: any): Animation =>
);
const padding = size === 'cover' ? 0 : POPOVER_IOS_BODY_PADDING;
const margin = size === 'cover' ? 0 : 25;
const {
originX,
originY,
top,
left,
bottom,
checkSafeAreaLeft,
checkSafeAreaRight,
arrowTop,
arrowLeft,
addPopoverBottomClass,
} = calculateWindowAdjustment(
const { originX, originY, top, left, bottom, arrowTop, arrowLeft, addPopoverBottomClass } = calculateWindowAdjustment(
side,
results.top,
results.left,
@@ -75,7 +63,6 @@ export const iosEnterAnimation = (baseEl: HTMLElement, opts?: any): Animation =>
bodyHeight,
contentWidth,
contentHeight,
margin,
results.originX,
results.originY,
results.referenceCoordinates,
@@ -122,20 +109,8 @@ export const iosEnterAnimation = (baseEl: HTMLElement, opts?: any): Animation =>
contentEl.style.setProperty('bottom', `${bottom}px`);
}
const safeAreaLeft = ' + var(--ion-safe-area-left, 0)';
const safeAreaRight = ' - var(--ion-safe-area-right, 0)';
let leftValue = `${left}px`;
if (checkSafeAreaLeft) {
leftValue = `${left}px${safeAreaLeft}`;
}
if (checkSafeAreaRight) {
leftValue = `${left}px${safeAreaRight}`;
}
contentEl.style.setProperty('top', `calc(${top}px + var(--offset-y, 0))`);
contentEl.style.setProperty('left', `calc(${leftValue} + var(--offset-x, 0))`);
contentEl.style.setProperty('left', `calc(${left}px + var(--offset-x, 0))`);
contentEl.style.setProperty('transform-origin', `${originY} ${originX}`);
if (arrowEl !== null) {

View File

@@ -56,7 +56,6 @@ export const mdEnterAnimation = (baseEl: HTMLElement, opts?: any): Animation =>
bodyHeight,
contentWidth,
contentHeight,
0,
results.originX,
results.originY,
results.referenceCoordinates

View File

@@ -16,18 +16,43 @@
import { popoverController } from '../../../../dist/ionic/index.esm.js';
window.popoverController = popoverController;
</script>
<style>
.safe-area-cover {
background-color: white;
position: absolute;
top: 5%;
bottom: 5%;
left: 0;
right: 0;
@media (orientation: landscape) {
left: 5%;
right: 5%;
}
}
</style>
</head>
<body>
<ion-app>
<ion-content>
<p style="text-align: center">Click everywhere to open the popover.</p>
<div style="text-align: center">
<p>Click everywhere to open the popover.</p>
<ion-checkbox id="safe-area-cb">Show Safe Area Approximation</ion-checkbox>
</div>
<div class="safe-area-cover"></div>
</ion-content>
</ion-app>
<script>
document.querySelector('ion-content').addEventListener('click', handleButtonClick);
document.querySelector('#safe-area-cb').addEventListener('ionChange', toggleSafeArea);
async function handleButtonClick(ev) {
if (ev.target.tagName === 'ION-CHECKBOX') return;
const popover = await popoverController.create({
component: 'popover-example-page',
event: ev,
@@ -37,6 +62,16 @@
popover.present();
}
function toggleSafeArea(ev) {
const content = document.querySelector('ion-content');
if (ev.detail.checked) {
content.style.setProperty('--background', 'lightblue');
} else {
content.style.removeProperty('--background');
}
}
customElements.define(
'popover-example-page',
class PopoverContent extends HTMLElement {

View File

@@ -1,5 +1,5 @@
import { expect } from '@playwright/test';
import { configs, test } from '@utils/test/playwright';
import { configs, test, Viewports } from '@utils/test/playwright';
/**
* This behavior does not vary across modes/directions.
@@ -33,5 +33,45 @@ configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) =>
expect(box.y > 0).toBe(true);
});
test('should account for vertical safe area approximation in portrait mode', async ({ page }) => {
await page.goto('/src/components/popover/test/adjustment', config);
const ionPopoverDidPresent = await page.spyOnEvent('ionPopoverDidPresent');
await page.mouse.click(0, 0);
await ionPopoverDidPresent.next();
const popoverContent = page.locator('ion-popover .popover-content');
const box = (await popoverContent.boundingBox())!;
/**
* The safe area approximation should move the y position by 5%
* of the screen height. We use 10px as the threshold to give
* wiggle room and help prevent flakiness.
*/
expect(box.x < 10).toBe(true);
expect(box.y > 10).toBe(true);
});
test('should account for vertical and horizontal safe area approximation in landscape mode', async ({ page }) => {
await page.goto('/src/components/popover/test/adjustment', config);
const ionPopoverDidPresent = await page.spyOnEvent('ionPopoverDidPresent');
await page.setViewportSize(Viewports.tablet.landscape);
await page.mouse.click(0, 0);
await ionPopoverDidPresent.next();
const popoverContent = page.locator('ion-popover .popover-content');
const box = (await popoverContent.boundingBox())!;
/**
* The safe area approximation should move the y position by 5%
* of the screen height. We use 10px as the threshold to give
* wiggle room and help prevent flakiness.
*/
expect(box.x > 10).toBe(true);
expect(box.y > 10).toBe(true);
});
});
});

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 34 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 61 KiB

After

Width:  |  Height:  |  Size: 60 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

After

Width:  |  Height:  |  Size: 30 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 34 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 61 KiB

After

Width:  |  Height:  |  Size: 60 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

After

Width:  |  Height:  |  Size: 30 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.9 KiB

After

Width:  |  Height:  |  Size: 6.9 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 55 KiB

After

Width:  |  Height:  |  Size: 48 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 77 KiB

After

Width:  |  Height:  |  Size: 77 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

After

Width:  |  Height:  |  Size: 41 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 54 KiB

After

Width:  |  Height:  |  Size: 55 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 88 KiB

After

Width:  |  Height:  |  Size: 89 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 41 KiB

After

Width:  |  Height:  |  Size: 43 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 55 KiB

After

Width:  |  Height:  |  Size: 49 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 77 KiB

After

Width:  |  Height:  |  Size: 77 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

After

Width:  |  Height:  |  Size: 41 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

After

Width:  |  Height:  |  Size: 46 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 93 KiB

After

Width:  |  Height:  |  Size: 95 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 45 KiB

After

Width:  |  Height:  |  Size: 45 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

After

Width:  |  Height:  |  Size: 46 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 94 KiB

After

Width:  |  Height:  |  Size: 96 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 45 KiB

After

Width:  |  Height:  |  Size: 45 KiB

View File

@@ -1,3 +1,4 @@
import { win } from '@utils/browser';
import { getElementRoot, raf } from '@utils/helpers';
import type { PopoverSize, PositionAlign, PositionReference, PositionSide, TriggerAction } from './popover-interface';
@@ -814,7 +815,6 @@ export const calculateWindowAdjustment = (
bodyHeight: number,
contentWidth: number,
contentHeight: number,
safeAreaMargin: number,
contentOriginX: string,
contentOriginY: string,
triggerCoordinates?: ReferenceCoordinates,
@@ -837,24 +837,61 @@ export const calculateWindowAdjustment = (
const triggerHeight = triggerCoordinates ? triggerCoordinates.height : 0;
let addPopoverBottomClass = false;
/**
* Approximate the safe area margins. Getting exact values would necessitate
* using window.getComputedStyle(), which is very expensive, so we use "close
* enough" values for now.
*
* 5% is derived from the iPhone 14 top safe area margin (47pt / 844 pt ~= 0.05).
* Source: https://useyourloaf.com/blog/iphone-14-screen-sizes/
*
* TODO(FW-5982): Investigate a more robust solution that uses the actual
* safe area margins through alternate means.
*/
let horizontalSafeAreaApprox = 0;
let verticalSafeAreaApprox = 0;
if (win?.matchMedia !== undefined) {
verticalSafeAreaApprox = win.innerHeight * 0.05;
/**
* We only want to check horizontal safe area on landscape.
* Most devices do not have horizontal safe area margins in
* portrait mode, so enforcing it would lead to popovers
* being misaligned with the trigger when we don't want them
* to move.
*/
if (win.matchMedia('(orientation: landscape)').matches) {
horizontalSafeAreaApprox = win.innerWidth * 0.05;
}
}
/**
* Adjust popover so it does not
* go off the left of the screen.
*/
if (left < bodyPadding + safeAreaMargin) {
left = bodyPadding;
if (left < bodyPadding + horizontalSafeAreaApprox) {
left = bodyPadding + horizontalSafeAreaApprox;
checkSafeAreaLeft = true;
originX = 'left';
/**
* Adjust popover so it does not
* go off the right of the screen.
*/
} else if (contentWidth + bodyPadding + left + safeAreaMargin > bodyWidth) {
} else if (contentWidth + bodyPadding + left + horizontalSafeAreaApprox > bodyWidth) {
checkSafeAreaRight = true;
left = bodyWidth - contentWidth - bodyPadding;
left = bodyWidth - contentWidth - bodyPadding - horizontalSafeAreaApprox;
originX = 'right';
}
/**
* Ensure the popover doesn't sit above the safe area approxmation.
* If popover is on the left or right of the trigger, we should not
* adjust top margins.
*/
if (side === 'top' || side === 'bottom') {
top = Math.max(top, verticalSafeAreaApprox + bodyPadding);
}
/**
* Adjust popover so it does not
* go off the top of the screen.
@@ -862,7 +899,10 @@ export const calculateWindowAdjustment = (
* the trigger, then we should not adjust top
* margins.
*/
if (triggerTop + triggerHeight + contentHeight > bodyHeight && (side === 'top' || side === 'bottom')) {
if (
triggerTop + triggerHeight + contentHeight + verticalSafeAreaApprox > bodyHeight &&
(side === 'top' || side === 'bottom')
) {
if (triggerTop - contentHeight > 0) {
/**
* While we strive to align the popover with the trigger
@@ -875,6 +915,12 @@ export const calculateWindowAdjustment = (
* it is not right up against the edge of the screen.
*/
top = Math.max(12, triggerTop - contentHeight - triggerHeight - (arrowHeight - 1));
/**
* Ensure the popover doesn't sit below the safe area approxmation.
*/
top = Math.min(top, bodyHeight - verticalSafeAreaApprox - contentHeight - bodyPadding);
arrowTop = top + contentHeight;
originY = 'bottom';
addPopoverBottomClass = true;

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

@@ -592,7 +592,7 @@ export declare interface IonCol extends Components.IonCol {}
@ProxyCmp({
inputs: ['color', 'forceOverscroll', 'fullscreen', 'scrollEvents', 'scrollX', 'scrollY'],
inputs: ['color', 'fixedSlotPlacement', 'forceOverscroll', 'fullscreen', 'scrollEvents', 'scrollX', 'scrollY'],
methods: ['getScrollElement', 'scrollToTop', 'scrollToBottom', 'scrollByPoint', 'scrollToPoint']
})
@Component({
@@ -600,7 +600,7 @@ export declare interface IonCol extends Components.IonCol {}
changeDetection: ChangeDetectionStrategy.OnPush,
template: '<ng-content></ng-content>',
// eslint-disable-next-line @angular-eslint/no-inputs-metadata-property
inputs: ['color', 'forceOverscroll', 'fullscreen', 'scrollEvents', 'scrollX', 'scrollY'],
inputs: ['color', 'fixedSlotPlacement', 'forceOverscroll', 'fullscreen', 'scrollEvents', 'scrollX', 'scrollY'],
})
export class IonContent {
protected el: HTMLElement;
@@ -955,7 +955,7 @@ export declare interface IonInfiniteScrollContent extends Components.IonInfinite
@ProxyCmp({
inputs: ['autocapitalize', 'autocomplete', 'autocorrect', 'autofocus', 'clearInput', 'clearOnEdit', 'color', 'counter', 'counterFormatter', 'debounce', 'disabled', 'enterkeyhint', 'errorText', 'fill', 'helperText', 'inputmode', 'label', 'labelPlacement', 'max', 'maxlength', 'min', 'minlength', 'mode', 'multiple', 'name', 'pattern', 'placeholder', 'readonly', 'required', 'shape', 'spellcheck', 'step', 'type', 'value'],
inputs: ['autocapitalize', 'autocomplete', 'autocorrect', 'autofocus', 'clearInput', 'clearInputIcon', 'clearOnEdit', 'color', 'counter', 'counterFormatter', 'debounce', 'disabled', 'enterkeyhint', 'errorText', 'fill', 'helperText', 'inputmode', 'label', 'labelPlacement', 'max', 'maxlength', 'min', 'minlength', 'mode', 'multiple', 'name', 'pattern', 'placeholder', 'readonly', 'required', 'shape', 'spellcheck', 'step', 'type', 'value'],
methods: ['setFocus', 'getInputElement']
})
@Component({
@@ -963,7 +963,7 @@ export declare interface IonInfiniteScrollContent extends Components.IonInfinite
changeDetection: ChangeDetectionStrategy.OnPush,
template: '<ng-content></ng-content>',
// eslint-disable-next-line @angular-eslint/no-inputs-metadata-property
inputs: ['autocapitalize', 'autocomplete', 'autocorrect', 'autofocus', 'clearInput', 'clearOnEdit', 'color', 'counter', 'counterFormatter', 'debounce', 'disabled', 'enterkeyhint', 'errorText', 'fill', 'helperText', 'inputmode', 'label', 'labelPlacement', 'max', 'maxlength', 'min', 'minlength', 'mode', 'multiple', 'name', 'pattern', 'placeholder', 'readonly', 'required', 'shape', 'spellcheck', 'step', 'type', 'value'],
inputs: ['autocapitalize', 'autocomplete', 'autocorrect', 'autofocus', 'clearInput', 'clearInputIcon', 'clearOnEdit', 'color', 'counter', 'counterFormatter', 'debounce', 'disabled', 'enterkeyhint', 'errorText', 'fill', 'helperText', 'inputmode', 'label', 'labelPlacement', 'max', 'maxlength', 'min', 'minlength', 'mode', 'multiple', 'name', 'pattern', 'placeholder', 'readonly', 'required', 'shape', 'spellcheck', 'step', 'type', 'value'],
})
export class IonInput {
protected el: HTMLElement;

View File

@@ -659,7 +659,7 @@ export declare interface IonCol extends Components.IonCol {}
@ProxyCmp({
defineCustomElementFn: defineIonContent,
inputs: ['color', 'forceOverscroll', 'fullscreen', 'scrollEvents', 'scrollX', 'scrollY'],
inputs: ['color', 'fixedSlotPlacement', 'forceOverscroll', 'fullscreen', 'scrollEvents', 'scrollX', 'scrollY'],
methods: ['getScrollElement', 'scrollToTop', 'scrollToBottom', 'scrollByPoint', 'scrollToPoint']
})
@Component({
@@ -667,7 +667,7 @@ export declare interface IonCol extends Components.IonCol {}
changeDetection: ChangeDetectionStrategy.OnPush,
template: '<ng-content></ng-content>',
// eslint-disable-next-line @angular-eslint/no-inputs-metadata-property
inputs: ['color', 'forceOverscroll', 'fullscreen', 'scrollEvents', 'scrollX', 'scrollY'],
inputs: ['color', 'fixedSlotPlacement', 'forceOverscroll', 'fullscreen', 'scrollEvents', 'scrollX', 'scrollY'],
standalone: true
})
export class IonContent {

View File

@@ -262,6 +262,7 @@ export const IonCol = /*@__PURE__*/ defineContainer<JSX.IonCol>('ion-col', defin
export const IonContent = /*@__PURE__*/ defineContainer<JSX.IonContent>('ion-content', defineIonContent, [
'color',
'fullscreen',
'fixedSlotPlacement',
'forceOverscroll',
'scrollX',
'scrollY',
@@ -402,6 +403,7 @@ export const IonInput = /*@__PURE__*/ defineContainer<JSX.IonInput, JSX.IonInput
'autocorrect',
'autofocus',
'clearInput',
'clearInputIcon',
'clearOnEdit',
'counter',
'counterFormatter',