mirror of
https://github.com/ionic-team/ionic-framework.git
synced 2026-03-13 10:22:08 +08:00
Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e8d41a7a6f | ||
|
|
e09d5c82b3 | ||
|
|
6dfedec4c0 | ||
|
|
1a4494199e | ||
|
|
2220d83d32 | ||
|
|
6945adc3cc | ||
|
|
90a7e70a1c | ||
|
|
0f5d1c02d2 |
@@ -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
|
||||
@@ -829,6 +831,7 @@ ion-modal,prop,backdropDismiss,boolean,true,false,false
|
||||
ion-modal,prop,breakpoints,number[] | undefined,undefined,false,false
|
||||
ion-modal,prop,canDismiss,((data?: any, role?: string | undefined) => Promise<boolean>) | boolean,true,false,false
|
||||
ion-modal,prop,enterAnimation,((baseEl: any, opts?: any) => Animation) | undefined,undefined,false,false
|
||||
ion-modal,prop,focusTrap,boolean,true,false,false
|
||||
ion-modal,prop,handle,boolean | undefined,undefined,false,false
|
||||
ion-modal,prop,handleBehavior,"cycle" | "none" | undefined,'none',false,false
|
||||
ion-modal,prop,htmlAttributes,undefined | { [key: string]: any; },undefined,false,false
|
||||
@@ -977,6 +980,7 @@ ion-popover,prop,componentProps,undefined | { [key: string]: any; },undefined,fa
|
||||
ion-popover,prop,dismissOnSelect,boolean,false,false,false
|
||||
ion-popover,prop,enterAnimation,((baseEl: any, opts?: any) => Animation) | undefined,undefined,false,false
|
||||
ion-popover,prop,event,any,undefined,false,false
|
||||
ion-popover,prop,focusTrap,boolean,true,false,false
|
||||
ion-popover,prop,htmlAttributes,undefined | { [key: string]: any; },undefined,false,false
|
||||
ion-popover,prop,isOpen,boolean,false,false,false
|
||||
ion-popover,prop,keepContentsMounted,boolean,false,false,false
|
||||
|
||||
32
core/src/components.d.ts
vendored
32
core/src/components.d.ts
vendored
@@ -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.
|
||||
*/
|
||||
@@ -1715,6 +1723,10 @@ export namespace Components {
|
||||
* Animation to use when the modal is presented.
|
||||
*/
|
||||
"enterAnimation"?: AnimationBuilder;
|
||||
/**
|
||||
* If `true`, focus will not be allowed to move outside of this overlay. If 'false', focus will be allowed to move outside of the overlay. In most scenarios this property should remain set to `true`. Setting this property to `false` can cause severe accessibility issues as users relying on assistive technologies may be able to move focus into a confusing state. We recommend only setting this to `false` when absolutely necessary. Developers may want to consider disabling focus trapping if this overlay presents a non-Ionic overlay from a 3rd party library. Developers would disable focus trapping on the Ionic overlay when presenting the 3rd party overlay and then re-enable focus trapping when dismissing the 3rd party overlay and moving focus back to the Ionic overlay.
|
||||
*/
|
||||
"focusTrap": boolean;
|
||||
/**
|
||||
* Returns the current breakpoint of a sheet style modal
|
||||
*/
|
||||
@@ -2131,6 +2143,10 @@ export namespace Components {
|
||||
* The event to pass to the popover animation.
|
||||
*/
|
||||
"event": any;
|
||||
/**
|
||||
* If `true`, focus will not be allowed to move outside of this overlay. If 'false', focus will be allowed to move outside of the overlay. In most scenarios this property should remain set to `true`. Setting this property to `false` can cause severe accessibility issues as users relying on assistive technologies may be able to move focus into a confusing state. We recommend only setting this to `false` when absolutely necessary. Developers may want to consider disabling focus trapping if this overlay presents a non-Ionic overlay from a 3rd party library. Developers would disable focus trapping on the Ionic overlay when presenting the 3rd party overlay and then re-enable focus trapping when dismissing the 3rd party overlay and moving focus back to the Ionic overlay.
|
||||
*/
|
||||
"focusTrap": boolean;
|
||||
"getParentPopover": () => Promise<HTMLIonPopoverElement | null>;
|
||||
"hasController": boolean;
|
||||
/**
|
||||
@@ -5478,6 +5494,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 +5906,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.
|
||||
*/
|
||||
@@ -6441,6 +6465,10 @@ declare namespace LocalJSX {
|
||||
* Animation to use when the modal is presented.
|
||||
*/
|
||||
"enterAnimation"?: AnimationBuilder;
|
||||
/**
|
||||
* If `true`, focus will not be allowed to move outside of this overlay. If 'false', focus will be allowed to move outside of the overlay. In most scenarios this property should remain set to `true`. Setting this property to `false` can cause severe accessibility issues as users relying on assistive technologies may be able to move focus into a confusing state. We recommend only setting this to `false` when absolutely necessary. Developers may want to consider disabling focus trapping if this overlay presents a non-Ionic overlay from a 3rd party library. Developers would disable focus trapping on the Ionic overlay when presenting the 3rd party overlay and then re-enable focus trapping when dismissing the 3rd party overlay and moving focus back to the Ionic overlay.
|
||||
*/
|
||||
"focusTrap"?: boolean;
|
||||
/**
|
||||
* The horizontal line that displays at the top of a sheet modal. It is `true` by default when setting the `breakpoints` and `initialBreakpoint` properties.
|
||||
*/
|
||||
@@ -6787,6 +6815,10 @@ declare namespace LocalJSX {
|
||||
* The event to pass to the popover animation.
|
||||
*/
|
||||
"event"?: any;
|
||||
/**
|
||||
* If `true`, focus will not be allowed to move outside of this overlay. If 'false', focus will be allowed to move outside of the overlay. In most scenarios this property should remain set to `true`. Setting this property to `false` can cause severe accessibility issues as users relying on assistive technologies may be able to move focus into a confusing state. We recommend only setting this to `false` when absolutely necessary. Developers may want to consider disabling focus trapping if this overlay presents a non-Ionic overlay from a 3rd party library. Developers would disable focus trapping on the Ionic overlay when presenting the 3rd party overlay and then re-enable focus trapping when dismissing the 3rd party overlay and moving focus back to the Ionic overlay.
|
||||
*/
|
||||
"focusTrap"?: boolean;
|
||||
"hasController"?: boolean;
|
||||
/**
|
||||
* Additional attributes to pass to the popover.
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
31
core/src/components/content/test/content.spec.ts
Normal file
31
core/src/components/content/test/content.spec.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
@@ -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`;
|
||||
|
||||
@@ -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' });
|
||||
});
|
||||
});
|
||||
});
|
||||
66
core/src/components/datetime/test/overlay-roles/index.html
Normal file
66
core/src/components/datetime/test/overlay-roles/index.html
Normal 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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { createGesture } from '@utils/gesture';
|
||||
import { clamp, raf } from '@utils/helpers';
|
||||
import { FOCUS_TRAP_DISABLE_CLASS } from '@utils/overlays';
|
||||
|
||||
import type { Animation } from '../../../interface';
|
||||
import type { GestureDetail } from '../../../utils/gesture';
|
||||
@@ -91,7 +92,7 @@ export const createSheetGesture = (
|
||||
* as inputs should not be focusable outside
|
||||
* the sheet.
|
||||
*/
|
||||
baseEl.classList.remove('ion-disable-focus-trap');
|
||||
baseEl.classList.remove(FOCUS_TRAP_DISABLE_CLASS);
|
||||
};
|
||||
|
||||
const disableBackdrop = () => {
|
||||
@@ -105,7 +106,7 @@ export const createSheetGesture = (
|
||||
* Adding this class disables focus trapping
|
||||
* for the sheet temporarily.
|
||||
*/
|
||||
baseEl.classList.add('ion-disable-focus-trap');
|
||||
baseEl.classList.add(FOCUS_TRAP_DISABLE_CLASS);
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
present,
|
||||
createTriggerController,
|
||||
setOverlayId,
|
||||
FOCUS_TRAP_DISABLE_CLASS,
|
||||
} from '@utils/overlays';
|
||||
import { getClassMap } from '@utils/theme';
|
||||
import { deepReady, waitForMount } from '@utils/transition';
|
||||
@@ -257,6 +258,25 @@ export class Modal implements ComponentInterface, OverlayInterface {
|
||||
*/
|
||||
@Prop() keepContentsMounted = false;
|
||||
|
||||
/**
|
||||
* If `true`, focus will not be allowed to move outside of this overlay.
|
||||
* If 'false', focus will be allowed to move outside of the overlay.
|
||||
*
|
||||
* In most scenarios this property should remain set to `true`. Setting
|
||||
* this property to `false` can cause severe accessibility issues as users
|
||||
* relying on assistive technologies may be able to move focus into
|
||||
* a confusing state. We recommend only setting this to `false` when
|
||||
* absolutely necessary.
|
||||
*
|
||||
* Developers may want to consider disabling focus trapping if this
|
||||
* overlay presents a non-Ionic overlay from a 3rd party library.
|
||||
* Developers would disable focus trapping on the Ionic overlay
|
||||
* when presenting the 3rd party overlay and then re-enable
|
||||
* focus trapping when dismissing the 3rd party overlay and moving
|
||||
* focus back to the Ionic overlay.
|
||||
*/
|
||||
@Prop() focusTrap = true;
|
||||
|
||||
/**
|
||||
* Determines whether or not a modal can dismiss
|
||||
* when calling the `dismiss` method.
|
||||
@@ -905,7 +925,8 @@ export class Modal implements ComponentInterface, OverlayInterface {
|
||||
};
|
||||
|
||||
render() {
|
||||
const { handle, isSheetModal, presentingElement, htmlAttributes, handleBehavior, inheritedAttributes } = this;
|
||||
const { handle, isSheetModal, presentingElement, htmlAttributes, handleBehavior, inheritedAttributes, focusTrap } =
|
||||
this;
|
||||
|
||||
const showHandle = handle !== false && isSheetModal;
|
||||
const mode = getIonMode(this);
|
||||
@@ -926,6 +947,7 @@ export class Modal implements ComponentInterface, OverlayInterface {
|
||||
[`modal-card`]: isCardModal,
|
||||
[`modal-sheet`]: isSheetModal,
|
||||
'overlay-hidden': true,
|
||||
[FOCUS_TRAP_DISABLE_CLASS]: focusTrap === false,
|
||||
...getClassMap(this.cssClass),
|
||||
}}
|
||||
onIonBackdropTap={this.onBackdropTap}
|
||||
|
||||
@@ -2,6 +2,7 @@ import { h } from '@stencil/core';
|
||||
import { newSpecPage } from '@stencil/core/testing';
|
||||
|
||||
import { Modal } from '../../modal';
|
||||
import { FOCUS_TRAP_DISABLE_CLASS } from '@utils/overlays';
|
||||
|
||||
describe('modal: htmlAttributes inheritance', () => {
|
||||
it('should correctly inherit attributes on host', async () => {
|
||||
@@ -15,3 +16,26 @@ describe('modal: htmlAttributes inheritance', () => {
|
||||
await expect(modal.getAttribute('data-testid')).toBe('basic-modal');
|
||||
});
|
||||
});
|
||||
|
||||
describe('modal: focus trap', () => {
|
||||
it('should set the focus trap class when disabled', async () => {
|
||||
const page = await newSpecPage({
|
||||
components: [Modal],
|
||||
template: () => <ion-modal focusTrap={false} overlayIndex={1}></ion-modal>,
|
||||
});
|
||||
|
||||
const modal = page.body.querySelector('ion-modal')!;
|
||||
|
||||
expect(modal.classList.contains(FOCUS_TRAP_DISABLE_CLASS)).toBe(true);
|
||||
});
|
||||
it('should not set the focus trap class by default', async () => {
|
||||
const page = await newSpecPage({
|
||||
components: [Modal],
|
||||
template: () => <ion-modal overlayIndex={1}></ion-modal>,
|
||||
});
|
||||
|
||||
const modal = page.body.querySelector('ion-modal')!;
|
||||
|
||||
expect(modal.classList.contains(FOCUS_TRAP_DISABLE_CLASS)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -5,7 +5,15 @@ import { CoreDelegate, attachComponent, detachComponent } from '@utils/framework
|
||||
import { addEventListener, raf, hasLazyBuild } from '@utils/helpers';
|
||||
import { createLockController } from '@utils/lock-controller';
|
||||
import { printIonWarning } from '@utils/logging';
|
||||
import { BACKDROP, dismiss, eventMethod, prepareOverlay, present, setOverlayId } from '@utils/overlays';
|
||||
import {
|
||||
BACKDROP,
|
||||
dismiss,
|
||||
eventMethod,
|
||||
prepareOverlay,
|
||||
present,
|
||||
setOverlayId,
|
||||
FOCUS_TRAP_DISABLE_CLASS,
|
||||
} from '@utils/overlays';
|
||||
import { isPlatform } from '@utils/platform';
|
||||
import { getClassMap } from '@utils/theme';
|
||||
import { deepReady, waitForMount } from '@utils/transition';
|
||||
@@ -236,6 +244,25 @@ export class Popover implements ComponentInterface, PopoverInterface {
|
||||
*/
|
||||
@Prop() keyboardEvents = false;
|
||||
|
||||
/**
|
||||
* If `true`, focus will not be allowed to move outside of this overlay.
|
||||
* If 'false', focus will be allowed to move outside of the overlay.
|
||||
*
|
||||
* In most scenarios this property should remain set to `true`. Setting
|
||||
* this property to `false` can cause severe accessibility issues as users
|
||||
* relying on assistive technologies may be able to move focus into
|
||||
* a confusing state. We recommend only setting this to `false` when
|
||||
* absolutely necessary.
|
||||
*
|
||||
* Developers may want to consider disabling focus trapping if this
|
||||
* overlay presents a non-Ionic overlay from a 3rd party library.
|
||||
* Developers would disable focus trapping on the Ionic overlay
|
||||
* when presenting the 3rd party overlay and then re-enable
|
||||
* focus trapping when dismissing the 3rd party overlay and moving
|
||||
* focus back to the Ionic overlay.
|
||||
*/
|
||||
@Prop() focusTrap = true;
|
||||
|
||||
@Watch('trigger')
|
||||
@Watch('triggerAction')
|
||||
onTriggerChange() {
|
||||
@@ -656,7 +683,7 @@ export class Popover implements ComponentInterface, PopoverInterface {
|
||||
|
||||
render() {
|
||||
const mode = getIonMode(this);
|
||||
const { onLifecycle, parentPopover, dismissOnSelect, side, arrow, htmlAttributes } = this;
|
||||
const { onLifecycle, parentPopover, dismissOnSelect, side, arrow, htmlAttributes, focusTrap } = this;
|
||||
const desktop = isPlatform('desktop');
|
||||
const enableArrow = arrow && !parentPopover;
|
||||
|
||||
@@ -676,6 +703,7 @@ export class Popover implements ComponentInterface, PopoverInterface {
|
||||
'overlay-hidden': true,
|
||||
'popover-desktop': desktop,
|
||||
[`popover-side-${side}`]: true,
|
||||
[FOCUS_TRAP_DISABLE_CLASS]: focusTrap === false,
|
||||
'popover-nested': !!parentPopover,
|
||||
}}
|
||||
onIonPopoverDidPresent={onLifecycle}
|
||||
|
||||
@@ -3,6 +3,8 @@ import { newSpecPage } from '@stencil/core/testing';
|
||||
|
||||
import { Popover } from '../../popover';
|
||||
|
||||
import { FOCUS_TRAP_DISABLE_CLASS } from '@utils/overlays';
|
||||
|
||||
describe('popover: htmlAttributes inheritance', () => {
|
||||
it('should correctly inherit attributes on host', async () => {
|
||||
const page = await newSpecPage({
|
||||
@@ -15,3 +17,26 @@ describe('popover: htmlAttributes inheritance', () => {
|
||||
await expect(popover.getAttribute('data-testid')).toBe('basic-popover');
|
||||
});
|
||||
});
|
||||
|
||||
describe('popover: focus trap', () => {
|
||||
it('should set the focus trap class when disabled', async () => {
|
||||
const page = await newSpecPage({
|
||||
components: [Popover],
|
||||
template: () => <ion-popover focusTrap={false} overlayIndex={1}></ion-popover>,
|
||||
});
|
||||
|
||||
const popover = page.body.querySelector('ion-popover')!;
|
||||
|
||||
expect(popover.classList.contains(FOCUS_TRAP_DISABLE_CLASS)).toBe(true);
|
||||
});
|
||||
it('should not set the focus trap class by default', async () => {
|
||||
const page = await newSpecPage({
|
||||
components: [Popover],
|
||||
template: () => <ion-popover overlayIndex={1}></ion-popover>,
|
||||
});
|
||||
|
||||
const popover = page.body.querySelector('ion-popover')!;
|
||||
|
||||
expect(popover.classList.contains(FOCUS_TRAP_DISABLE_CLASS)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -199,7 +199,7 @@ const trapKeyboardFocus = (ev: Event, doc: Document) => {
|
||||
* behind the sheet should be focusable until
|
||||
* the backdrop is enabled.
|
||||
*/
|
||||
if (lastOverlay.classList.contains('ion-disable-focus-trap')) {
|
||||
if (lastOverlay.classList.contains(FOCUS_TRAP_DISABLE_CLASS)) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -990,3 +990,5 @@ const revealOverlaysToScreenReaders = () => {
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const FOCUS_TRAP_DISABLE_CLASS = 'ion-disable-focus-trap';
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user