mirror of
https://github.com/ionic-team/ionic-framework.git
synced 2025-08-19 11:41:20 +08:00
fix(overlays): trap focus inside overlay components except toast (#21716)
fixes #21647
This commit is contained in:
@ -26,6 +26,7 @@ import { mdLeaveAnimation } from './animations/md.leave';
|
||||
export class ActionSheet implements ComponentInterface, OverlayInterface {
|
||||
|
||||
presented = false;
|
||||
lastFocus?: HTMLElement;
|
||||
animation?: any;
|
||||
private wrapperEl?: HTMLElement;
|
||||
private groupEl?: HTMLElement;
|
||||
@ -250,7 +251,10 @@ export class ActionSheet implements ComponentInterface, OverlayInterface {
|
||||
onIonBackdropTap={this.onBackdropTap}
|
||||
>
|
||||
<ion-backdrop tappable={this.backdropDismiss}/>
|
||||
<div class="action-sheet-wrapper" role="dialog" ref={el => this.wrapperEl = el}>
|
||||
|
||||
<div tabindex="0"></div>
|
||||
|
||||
<div class="action-sheet-wrapper ion-overlay-wrapper" role="dialog" ref={el => this.wrapperEl = el}>
|
||||
<div class="action-sheet-container">
|
||||
<div class="action-sheet-group" ref={el => this.groupEl = el}>
|
||||
{this.header !== undefined &&
|
||||
@ -292,6 +296,8 @@ export class ActionSheet implements ComponentInterface, OverlayInterface {
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div tabindex="0"></div>
|
||||
</Host>
|
||||
);
|
||||
}
|
||||
|
@ -1,6 +1,40 @@
|
||||
import { testActionSheet, testActionSheetAlert, testActionSheetBackdrop } from '../test.utils';
|
||||
import { newE2EPage } from '@stencil/core/testing';
|
||||
|
||||
const DIRECTORY = 'basic';
|
||||
const getActiveElementText = async (page) => {
|
||||
const activeElement = await page.evaluateHandle(() => document.activeElement);
|
||||
return await page.evaluate(el => el && el.textContent, activeElement);
|
||||
}
|
||||
|
||||
test('action-sheet: focus trap', async () => {
|
||||
const page = await newE2EPage({ url: '/src/components/action-sheet/test/basic?ionic:_testing=true' });
|
||||
|
||||
await page.click('#basic');
|
||||
await page.waitForSelector('#basic');
|
||||
|
||||
let actionSheet = await page.find('ion-action-sheet');
|
||||
|
||||
expect(actionSheet).not.toBe(null);
|
||||
await actionSheet.waitForVisible();
|
||||
|
||||
await page.keyboard.press('Tab');
|
||||
|
||||
const activeElementText = await getActiveElementText(page);
|
||||
expect(activeElementText).toEqual('Delete');
|
||||
|
||||
await page.keyboard.down('Shift');
|
||||
await page.keyboard.press('Tab');
|
||||
await page.keyboard.up('Shift');
|
||||
|
||||
const activeElementTextTwo = await getActiveElementText(page);
|
||||
expect(activeElementTextTwo).toEqual('Cancel');
|
||||
|
||||
await page.keyboard.press('Tab');
|
||||
|
||||
const activeElementTextThree = await getActiveElementText(page);
|
||||
expect(activeElementTextThree).toEqual('Delete');
|
||||
});
|
||||
|
||||
test('action-sheet: basic', async () => {
|
||||
await testActionSheet(DIRECTORY, '#basic');
|
||||
|
@ -34,6 +34,7 @@ export class Alert implements ComponentInterface, OverlayInterface {
|
||||
private gesture?: Gesture;
|
||||
|
||||
presented = false;
|
||||
lastFocus?: HTMLElement;
|
||||
|
||||
@Element() el!: HTMLIonAlertElement;
|
||||
|
||||
@ -514,7 +515,9 @@ export class Alert implements ComponentInterface, OverlayInterface {
|
||||
|
||||
<ion-backdrop tappable={this.backdropDismiss}/>
|
||||
|
||||
<div class="alert-wrapper" ref={el => this.wrapperEl = el}>
|
||||
<div tabindex="0"></div>
|
||||
|
||||
<div class="alert-wrapper ion-overlay-wrapper" ref={el => this.wrapperEl = el}>
|
||||
|
||||
<div class="alert-head">
|
||||
{header && <h2 id={hdrId} class="alert-title">{header}</h2>}
|
||||
@ -527,6 +530,8 @@ export class Alert implements ComponentInterface, OverlayInterface {
|
||||
{this.renderAlertButtons()}
|
||||
|
||||
</div>
|
||||
|
||||
<div tabindex="0"></div>
|
||||
</Host>
|
||||
);
|
||||
}
|
||||
|
@ -1,6 +1,40 @@
|
||||
import { testAlert } from '../test.utils';
|
||||
import { newE2EPage } from '@stencil/core/testing';
|
||||
|
||||
const DIRECTORY = 'basic';
|
||||
const getActiveElementText = async (page) => {
|
||||
const activeElement = await page.evaluateHandle(() => document.activeElement);
|
||||
return await page.evaluate(el => el && el.textContent, activeElement);
|
||||
}
|
||||
|
||||
test('alert: focus trap', async () => {
|
||||
const page = await newE2EPage({ url: '/src/components/alert/test/basic?ionic:_testing=true' });
|
||||
|
||||
await page.click('#multipleButtons');
|
||||
await page.waitForSelector('#multipleButtons');
|
||||
|
||||
let alert = await page.find('ion-alert');
|
||||
|
||||
expect(alert).not.toBe(null);
|
||||
await alert.waitForVisible();
|
||||
|
||||
await page.keyboard.press('Tab');
|
||||
|
||||
const activeElementText = await getActiveElementText(page);
|
||||
expect(activeElementText).toEqual('Open Modal');
|
||||
|
||||
await page.keyboard.down('Shift');
|
||||
await page.keyboard.press('Tab');
|
||||
await page.keyboard.up('Shift');
|
||||
|
||||
const activeElementTextTwo = await getActiveElementText(page);
|
||||
expect(activeElementTextTwo).toEqual('Cancel');
|
||||
|
||||
await page.keyboard.press('Tab');
|
||||
|
||||
const activeElementTextThree = await getActiveElementText(page);
|
||||
expect(activeElementTextThree).toEqual('Open Modal');
|
||||
});
|
||||
|
||||
test(`alert: basic`, async () => {
|
||||
await testAlert(DIRECTORY, '#basic');
|
||||
|
@ -1,5 +1,42 @@
|
||||
import { newE2EPage } from '@stencil/core/testing';
|
||||
|
||||
const getActiveElementText = async (page) => {
|
||||
const activeElement = await page.evaluateHandle(() => document.activeElement);
|
||||
return await page.evaluate(el => el && el.textContent, activeElement);
|
||||
}
|
||||
|
||||
test('datetime/picker: focus trap', async () => {
|
||||
const page = await newE2EPage({ url: '/src/components/datetime/test/basic?ionic:_testing=true' });
|
||||
|
||||
await page.click('#datetime-part');
|
||||
await page.waitForSelector('#datetime-part');
|
||||
|
||||
let datetime = await page.find('ion-datetime');
|
||||
|
||||
expect(datetime).not.toBe(null);
|
||||
await datetime.waitForVisible();
|
||||
|
||||
// TODO fix
|
||||
await page.waitFor(100);
|
||||
|
||||
await page.keyboard.press('Tab');
|
||||
|
||||
const activeElementText = await getActiveElementText(page);
|
||||
expect(activeElementText).toEqual('Cancel');
|
||||
|
||||
await page.keyboard.down('Shift');
|
||||
await page.keyboard.press('Tab');
|
||||
await page.keyboard.up('Shift');
|
||||
|
||||
const activeElementTextTwo = await getActiveElementText(page);
|
||||
expect(activeElementTextTwo).toEqual('1920');
|
||||
|
||||
await page.keyboard.press('Tab');
|
||||
|
||||
const activeElementTextThree = await getActiveElementText(page);
|
||||
expect(activeElementTextThree).toEqual('Cancel');
|
||||
});
|
||||
|
||||
test('datetime: basic', async () => {
|
||||
const page = await newE2EPage({
|
||||
url: '/src/components/datetime/test/basic?ionic:_testing=true'
|
||||
|
@ -27,6 +27,7 @@ export class Loading implements ComponentInterface, OverlayInterface {
|
||||
private durationTimeout: any;
|
||||
|
||||
presented = false;
|
||||
lastFocus?: HTMLElement;
|
||||
|
||||
@Element() el!: HTMLIonLoadingElement;
|
||||
|
||||
@ -194,7 +195,10 @@ export class Loading implements ComponentInterface, OverlayInterface {
|
||||
}}
|
||||
>
|
||||
<ion-backdrop visible={this.showBackdrop} tappable={this.backdropDismiss} />
|
||||
<div class="loading-wrapper" role="dialog">
|
||||
|
||||
<div tabindex="0"></div>
|
||||
|
||||
<div class="loading-wrapper ion-overlay-wrapper" role="dialog">
|
||||
{spinner && (
|
||||
<div class="loading-spinner">
|
||||
<ion-spinner name={spinner} aria-hidden="true" />
|
||||
@ -203,6 +207,8 @@ export class Loading implements ComponentInterface, OverlayInterface {
|
||||
|
||||
{message && <div class="loading-content" innerHTML={sanitizeDOMString(message)}></div>}
|
||||
</div>
|
||||
|
||||
<div tabindex="0"></div>
|
||||
</Host>
|
||||
);
|
||||
}
|
||||
|
@ -1,6 +1,40 @@
|
||||
import { testLoading } from '../test.utils';
|
||||
import { newE2EPage } from '@stencil/core/testing';
|
||||
|
||||
const DIRECTORY = 'basic';
|
||||
const getActiveElementText = async (page) => {
|
||||
const activeElement = await page.evaluateHandle(() => document.activeElement);
|
||||
return await page.evaluate(el => el && el.textContent, activeElement);
|
||||
}
|
||||
|
||||
test('loading: focus trap', async () => {
|
||||
const page = await newE2EPage({ url: '/src/components/loading/test/basic?ionic:_testing=true' });
|
||||
|
||||
await page.click('#html-content-loading');
|
||||
await page.waitForSelector('#html-content-loading');
|
||||
|
||||
let loading = await page.find('ion-loading');
|
||||
|
||||
expect(loading).not.toBe(null);
|
||||
await loading.waitForVisible();
|
||||
|
||||
await page.keyboard.press('Tab');
|
||||
|
||||
const activeElementText = await getActiveElementText(page);
|
||||
expect(activeElementText).toEqual('Click impatiently to load faster');
|
||||
|
||||
await page.keyboard.down('Shift');
|
||||
await page.keyboard.press('Tab');
|
||||
await page.keyboard.up('Shift');
|
||||
|
||||
const activeElementTextTwo = await getActiveElementText(page);
|
||||
expect(activeElementTextTwo).toEqual('Click impatiently to load faster');
|
||||
|
||||
await page.keyboard.press('Tab');
|
||||
|
||||
const activeElementTextThree = await getActiveElementText(page);
|
||||
expect(activeElementTextThree).toEqual('Click impatiently to load faster');
|
||||
});
|
||||
|
||||
test('loading: basic', async () => {
|
||||
await testLoading(DIRECTORY, '#basic-loading');
|
||||
|
@ -34,6 +34,7 @@ export class Modal implements ComponentInterface, OverlayInterface {
|
||||
// Whether or not modal is being dismissed via gesture
|
||||
private gestureAnimationDismissing = false;
|
||||
presented = false;
|
||||
lastFocus?: HTMLElement;
|
||||
animation?: Animation;
|
||||
|
||||
@Element() el!: HTMLIonModalElement;
|
||||
@ -289,11 +290,16 @@ export class Modal implements ComponentInterface, OverlayInterface {
|
||||
<ion-backdrop visible={this.showBackdrop} tappable={this.backdropDismiss}/>
|
||||
|
||||
{mode === 'ios' && <div class="modal-shadow"></div>}
|
||||
|
||||
<div tabindex="0"></div>
|
||||
|
||||
<div
|
||||
role="dialog"
|
||||
class="modal-wrapper"
|
||||
class="modal-wrapper ion-overlay-wrapper"
|
||||
>
|
||||
</div>
|
||||
|
||||
<div tabindex="0"></div>
|
||||
</Host>
|
||||
);
|
||||
}
|
||||
|
@ -1,6 +1,43 @@
|
||||
import { testModal } from '../test.utils';
|
||||
import { newE2EPage } from '@stencil/core/testing';
|
||||
|
||||
const DIRECTORY = 'basic';
|
||||
const getActiveElementText = async (page) => {
|
||||
const activeElement = await page.evaluateHandle(() => document.activeElement);
|
||||
return await page.evaluate(el => el && el.textContent, activeElement);
|
||||
}
|
||||
|
||||
test('modal: focus trap', async () => {
|
||||
const page = await newE2EPage({ url: '/src/components/modal/test/basic?ionic:_testing=true' });
|
||||
|
||||
await page.click('#basic-modal');
|
||||
await page.waitForSelector('#basic-modal');
|
||||
|
||||
let modal = await page.find('ion-modal');
|
||||
|
||||
expect(modal).not.toBe(null);
|
||||
await modal.waitForVisible();
|
||||
|
||||
// TODO fix
|
||||
await page.waitFor(50);
|
||||
|
||||
await page.keyboard.press('Tab');
|
||||
|
||||
const activeElementText = await getActiveElementText(page);
|
||||
expect(activeElementText).toEqual('Dismiss Modal');
|
||||
|
||||
await page.keyboard.down('Shift');
|
||||
await page.keyboard.press('Tab');
|
||||
await page.keyboard.up('Shift');
|
||||
|
||||
const activeElementTextTwo = await getActiveElementText(page);
|
||||
expect(activeElementTextTwo).toEqual('Dismiss Modal');
|
||||
|
||||
await page.keyboard.press('Tab');
|
||||
|
||||
const activeElementTextThree = await getActiveElementText(page);
|
||||
expect(activeElementTextThree).toEqual('Dismiss Modal');
|
||||
});
|
||||
|
||||
test('modal: basic', async () => {
|
||||
await testModal(DIRECTORY, '#basic-modal');
|
||||
|
@ -21,6 +21,7 @@ import { iosLeaveAnimation } from './animations/ios.leave';
|
||||
})
|
||||
export class Picker implements ComponentInterface, OverlayInterface {
|
||||
private durationTimeout: any;
|
||||
lastFocus?: HTMLElement;
|
||||
|
||||
@Element() el!: HTMLIonPickerElement;
|
||||
|
||||
@ -236,7 +237,10 @@ export class Picker implements ComponentInterface, OverlayInterface {
|
||||
tappable={this.backdropDismiss}
|
||||
>
|
||||
</ion-backdrop>
|
||||
<div class="picker-wrapper" role="dialog">
|
||||
|
||||
<div tabindex="0"></div>
|
||||
|
||||
<div class="picker-wrapper ion-overlay-wrapper" role="dialog">
|
||||
<div class="picker-toolbar">
|
||||
{this.buttons.map(b => (
|
||||
<div class={buttonWrapperClass(b)}>
|
||||
@ -259,6 +263,8 @@ export class Picker implements ComponentInterface, OverlayInterface {
|
||||
<div class="picker-below-highlight"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div tabindex="0"></div>
|
||||
</Host>
|
||||
);
|
||||
}
|
||||
|
@ -28,6 +28,7 @@ export class Popover implements ComponentInterface, OverlayInterface {
|
||||
private usersElement?: HTMLElement;
|
||||
|
||||
presented = false;
|
||||
lastFocus?: HTMLElement;
|
||||
|
||||
@Element() el!: HTMLIonPopoverElement;
|
||||
|
||||
@ -219,10 +220,15 @@ export class Popover implements ComponentInterface, OverlayInterface {
|
||||
onIonBackdropTap={this.onBackdropTap}
|
||||
>
|
||||
<ion-backdrop tappable={this.backdropDismiss} visible={this.showBackdrop}/>
|
||||
<div class="popover-wrapper">
|
||||
|
||||
<div tabindex="0"></div>
|
||||
|
||||
<div class="popover-wrapper ion-overlay-wrapper">
|
||||
<div class="popover-arrow"></div>
|
||||
<div class="popover-content"></div>
|
||||
</div>
|
||||
|
||||
<div tabindex="0"></div>
|
||||
</Host>
|
||||
);
|
||||
}
|
||||
|
@ -1,7 +1,42 @@
|
||||
import { testPopover } from '../test.utils';
|
||||
import { newE2EPage } from '@stencil/core/testing';
|
||||
|
||||
const DIRECTORY = 'basic';
|
||||
|
||||
const getActiveElementText = async (page) => {
|
||||
const activeElement = await page.evaluateHandle(() => document.activeElement);
|
||||
return await page.evaluate(el => el && el.textContent, activeElement);
|
||||
}
|
||||
|
||||
test('popover: focus trap', async () => {
|
||||
const page = await newE2EPage({ url: '/src/components/popover/test/basic?ionic:_testing=true' });
|
||||
|
||||
await page.click('#basic-popover');
|
||||
await page.waitForSelector('#basic-popover');
|
||||
|
||||
let popover = await page.find('ion-popover');
|
||||
|
||||
expect(popover).not.toBe(null);
|
||||
await popover.waitForVisible();
|
||||
|
||||
await page.keyboard.press('Tab');
|
||||
|
||||
const activeElementText = await getActiveElementText(page);
|
||||
expect(activeElementText).toEqual('Item 0');
|
||||
|
||||
await page.keyboard.down('Shift');
|
||||
await page.keyboard.press('Tab');
|
||||
await page.keyboard.up('Shift');
|
||||
|
||||
const activeElementTextTwo = await getActiveElementText(page);
|
||||
expect(activeElementTextTwo).toEqual('Item 3');
|
||||
|
||||
await page.keyboard.press('Tab');
|
||||
|
||||
const activeElementTextThree = await getActiveElementText(page);
|
||||
expect(activeElementTextThree).toEqual('Item 0');
|
||||
});
|
||||
|
||||
test('popover: basic', async () => {
|
||||
await testPopover(DIRECTORY, '#basic-popover');
|
||||
});
|
||||
|
@ -75,10 +75,10 @@
|
||||
<ion-content>
|
||||
<ion-list>
|
||||
<ion-list-header><ion-label>Ionic</ion-label></ion-list-header>
|
||||
<ion-item><ion-label>Item 0</ion-label></ion-item>
|
||||
<ion-item><ion-label>Item 1</ion-label></ion-item>
|
||||
<ion-item><ion-label>Item 2</ion-label></ion-item>
|
||||
<ion-item><ion-label>Item 3</ion-label></ion-item>
|
||||
<ion-item button><ion-label>Item 0</ion-label></ion-item>
|
||||
<ion-item button><ion-label>Item 1</ion-label></ion-item>
|
||||
<ion-item button><ion-label>Item 2</ion-label></ion-item>
|
||||
<ion-item button><ion-label>Item 3</ion-label></ion-item>
|
||||
</ion-list>
|
||||
</ion-content>
|
||||
`;
|
||||
|
@ -35,6 +35,7 @@ export interface OverlayController {
|
||||
export interface HTMLIonOverlayElement extends HTMLStencilElement {
|
||||
overlayIndex: number;
|
||||
backdropDismiss?: boolean;
|
||||
lastFocus?: HTMLElement;
|
||||
|
||||
dismiss(data?: any, role?: string): Promise<boolean>;
|
||||
}
|
||||
|
@ -3,6 +3,7 @@ import { getIonMode } from '../global/ionic-global';
|
||||
import { ActionSheetOptions, AlertOptions, Animation, AnimationBuilder, BackButtonEvent, HTMLIonOverlayElement, IonicConfig, LoadingOptions, ModalOptions, OverlayInterface, PickerOptions, PopoverOptions, ToastOptions } from '../interface';
|
||||
|
||||
import { OVERLAY_BACK_BUTTON_PRIORITY } from './hardware-back-button';
|
||||
import { getElementRoot } from './helpers';
|
||||
|
||||
let lastId = 0;
|
||||
|
||||
@ -62,19 +63,128 @@ export const createOverlay = <T extends HTMLIonOverlayElement>(tagName: string,
|
||||
return Promise.resolve() as any;
|
||||
};
|
||||
|
||||
const focusableQueryString = '[tabindex]:not([tabindex^="-"]), input, textarea, button, select, .ion-focusable';
|
||||
|
||||
const focusFirstDescendant = (ref: Element, overlay: HTMLIonOverlayElement) => {
|
||||
let firstInput = ref.querySelector(focusableQueryString) as HTMLElement | null;
|
||||
|
||||
const shadowRoot = firstInput && firstInput.shadowRoot;
|
||||
if (shadowRoot) {
|
||||
firstInput = shadowRoot.querySelector('input, textarea, button, select');
|
||||
}
|
||||
|
||||
if (firstInput) {
|
||||
firstInput.focus();
|
||||
} else {
|
||||
// Focus overlay instead of letting focus escape
|
||||
overlay.focus();
|
||||
}
|
||||
};
|
||||
|
||||
const focusLastDescendant = (ref: Element, overlay: HTMLIonOverlayElement) => {
|
||||
const inputs = Array.from(ref.querySelectorAll(focusableQueryString)) as HTMLElement[];
|
||||
let lastInput = inputs.length > 0 ? inputs[inputs.length - 1] : null;
|
||||
|
||||
const shadowRoot = lastInput && lastInput.shadowRoot;
|
||||
if (shadowRoot) {
|
||||
lastInput = shadowRoot.querySelector('input, textarea, button, select');
|
||||
}
|
||||
|
||||
if (lastInput) {
|
||||
lastInput.focus();
|
||||
} else {
|
||||
// Focus overlay instead of letting focus escape
|
||||
overlay.focus();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Traps keyboard focus inside of overlay components.
|
||||
* Based on https://w3c.github.io/aria-practices/examples/dialog-modal/alertdialog.html
|
||||
* This includes the following components: Action Sheet, Alert, Loading, Modal,
|
||||
* Picker, and Popover.
|
||||
* Should NOT include: Toast
|
||||
*/
|
||||
const trapKeyboardFocus = (ev: Event, doc: Document) => {
|
||||
const lastOverlay = getOverlay(doc);
|
||||
const target = ev.target as HTMLElement | null;
|
||||
|
||||
// If no active overlay, ignore this event
|
||||
if (!lastOverlay || !target) { return; }
|
||||
|
||||
/**
|
||||
* If we are focusing the overlay, clear
|
||||
* the last focused element so that hitting
|
||||
* tab activates the first focusable element
|
||||
* in the overlay wrapper.
|
||||
*/
|
||||
if (lastOverlay === target) {
|
||||
lastOverlay.lastFocus = undefined;
|
||||
|
||||
/**
|
||||
* Otherwise, we must be focusing an element
|
||||
* inside of the overlay. The two possible options
|
||||
* here are an input/button/etc or the ion-focus-trap
|
||||
* element. The focus trap element is used to prevent
|
||||
* the keyboard focus from leaving the overlay when
|
||||
* using Tab or screen assistants.
|
||||
*/
|
||||
} else {
|
||||
/**
|
||||
* We do not want to focus the traps, so get the overlay
|
||||
* wrapper element as the traps live outside of the wrapper.
|
||||
*/
|
||||
const overlayRoot = getElementRoot(lastOverlay);
|
||||
const overlayWrapper = overlayRoot.querySelector('.ion-overlay-wrapper');
|
||||
|
||||
if (!overlayWrapper) { return; }
|
||||
|
||||
/**
|
||||
* If the target is inside the wrapper, let the browser
|
||||
* focus as normal and keep a log of the last focused element.
|
||||
*/
|
||||
if (overlayWrapper.contains(target)) {
|
||||
lastOverlay.lastFocus = target;
|
||||
} else {
|
||||
/**
|
||||
* Otherwise, we must have focused one of the focus traps.
|
||||
* We need to wrap the focus to either the first element
|
||||
* or the last element.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Once we call `focusFirstDescendant` and focus the first
|
||||
* descendant, another focus event will fire which will
|
||||
* cause `lastOverlay.lastFocus` to be updated before
|
||||
* we can run the code after that. We will cache the value
|
||||
* here to avoid that.
|
||||
*/
|
||||
const lastFocus = lastOverlay.lastFocus;
|
||||
|
||||
// Focus the first element in the overlay wrapper
|
||||
focusFirstDescendant(overlayWrapper, lastOverlay);
|
||||
|
||||
/**
|
||||
* If the cached last focused element is the
|
||||
* same as the active element, then we need
|
||||
* to wrap focus to the last descendant. This happens
|
||||
* when the first descendant is focused, and the user
|
||||
* presses Shift + Tab. The previous line will focus
|
||||
* the same descendant again (the first one), causing
|
||||
* last focus to equal the active element.
|
||||
*/
|
||||
if (lastFocus === doc.activeElement) {
|
||||
focusLastDescendant(overlayWrapper, lastOverlay);
|
||||
}
|
||||
lastOverlay.lastFocus = doc.activeElement as HTMLElement;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const connectListeners = (doc: Document) => {
|
||||
if (lastId === 0) {
|
||||
lastId = 1;
|
||||
// trap focus inside overlays
|
||||
doc.addEventListener('focusin', ev => {
|
||||
const lastOverlay = getOverlay(doc);
|
||||
if (lastOverlay && lastOverlay.backdropDismiss && !isDescendant(lastOverlay, ev.target as HTMLElement)) {
|
||||
const firstInput = lastOverlay.querySelector('input,button') as HTMLElement | null;
|
||||
if (firstInput) {
|
||||
firstInput.focus();
|
||||
}
|
||||
}
|
||||
});
|
||||
doc.addEventListener('focus', ev => trapKeyboardFocus(ev, doc), true);
|
||||
|
||||
// handle back-button click
|
||||
doc.addEventListener('ionBackButton', ev => {
|
||||
@ -247,16 +357,6 @@ export const isCancel = (role: string | undefined): boolean => {
|
||||
return role === 'cancel' || role === BACKDROP;
|
||||
};
|
||||
|
||||
const isDescendant = (parent: HTMLElement, child: HTMLElement | null) => {
|
||||
while (child) {
|
||||
if (child === parent) {
|
||||
return true;
|
||||
}
|
||||
child = child.parentElement;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
const defaultGate = (h: any) => h();
|
||||
|
||||
export const safeCall = (handler: any, arg?: any) => {
|
||||
|
Reference in New Issue
Block a user