mirror of
https://github.com/ionic-team/ionic-framework.git
synced 2025-11-10 00:27:41 +08:00
Issue number: resolves 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. --> Using `Tab` or `Shift + Tab` to focus through elements in a modal won't behave as expected when using `ion-checkbox` or `ion-radio` within an `ion-item`. Previously, the behavior would result in the last item in a list getting focus styling, but `document.activeElement` would still be the first actionable item in the overlay ## What is the new behavior? <!-- Please describe the behavior or changes that are being added by this PR. --> For checkboxes, the `ion-checkbox` element itself will be focused rather than the encapsulating `ion-item` For radios, the `ion-radio-group` will be used to focus the appropriate element. This will be the first `ion-radio` if there is no "checked" item, or the "checked" item if one exists. ## 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/docs/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. -->
93 lines
3.8 KiB
TypeScript
93 lines
3.8 KiB
TypeScript
import { focusVisibleElement } from '@utils/helpers';
|
|
|
|
/**
|
|
* This query string selects elements that
|
|
* are eligible to receive focus. We select
|
|
* interactive elements that meet the following
|
|
* criteria:
|
|
* 1. Element does not have a negative tabindex
|
|
* 2. Element does not have `hidden`
|
|
* 3. Element does not have `disabled` for non-Ionic components.
|
|
* 4. Element does not have `disabled` or `disabled="true"` for Ionic components.
|
|
* Note: We need this distinction because `disabled="false"` is
|
|
* valid usage for the disabled property on ion-button.
|
|
*/
|
|
export const focusableQueryString =
|
|
'[tabindex]:not([tabindex^="-"]):not([hidden]):not([disabled]), input:not([type=hidden]):not([tabindex^="-"]):not([hidden]):not([disabled]), textarea:not([tabindex^="-"]):not([hidden]):not([disabled]), button:not([tabindex^="-"]):not([hidden]):not([disabled]), select:not([tabindex^="-"]):not([hidden]):not([disabled]), ion-checkbox:not([tabindex^="-"]):not([hidden]):not([disabled]), ion-radio:not([tabindex^="-"]):not([hidden]):not([disabled]), .ion-focusable:not([tabindex^="-"]):not([hidden]):not([disabled]), .ion-focusable[disabled="false"]:not([tabindex^="-"]):not([hidden])';
|
|
|
|
/**
|
|
* Focuses the first descendant in a context
|
|
* that can receive focus. If none exists,
|
|
* a fallback element will be focused.
|
|
* This fallback is typically an ancestor
|
|
* container such as a menu or overlay so focus does not
|
|
* leave the container we are trying to trap focus in.
|
|
*
|
|
* If no fallback is specified then we focus the container itself.
|
|
*/
|
|
export const focusFirstDescendant = <R extends HTMLElement, T extends HTMLElement>(ref: R, fallbackElement?: T) => {
|
|
const firstInput = ref.querySelector<HTMLElement>(focusableQueryString);
|
|
|
|
focusElementInContext(firstInput, fallbackElement ?? ref);
|
|
};
|
|
|
|
/**
|
|
* Focuses the last descendant in a context
|
|
* that can receive focus. If none exists,
|
|
* a fallback element will be focused.
|
|
* This fallback is typically an ancestor
|
|
* container such as a menu or overlay so focus does not
|
|
* leave the container we are trying to trap focus in.
|
|
*
|
|
* If no fallback is specified then we focus the container itself.
|
|
*/
|
|
export const focusLastDescendant = <R extends HTMLElement, T extends HTMLElement>(ref: R, fallbackElement?: T) => {
|
|
const inputs = Array.from(ref.querySelectorAll<HTMLElement>(focusableQueryString));
|
|
const lastInput = inputs.length > 0 ? inputs[inputs.length - 1] : null;
|
|
|
|
focusElementInContext(lastInput, fallbackElement ?? ref);
|
|
};
|
|
|
|
/**
|
|
* Focuses a particular element in a context. If the element
|
|
* doesn't have anything focusable associated with it then
|
|
* a fallback element will be focused.
|
|
*
|
|
* This fallback is typically an ancestor
|
|
* container such as a menu or overlay so focus does not
|
|
* leave the container we are trying to trap focus in.
|
|
* This should be used instead of the focus() method
|
|
* on most elements because the focusable element
|
|
* may not be the host element.
|
|
*
|
|
* For example, if an ion-button should be focused
|
|
* then we should actually focus the native <button>
|
|
* element inside of ion-button's shadow root, not
|
|
* the host element itself.
|
|
*/
|
|
const focusElementInContext = <T extends HTMLElement>(
|
|
hostToFocus: HTMLElement | null | undefined,
|
|
fallbackElement: T
|
|
) => {
|
|
let elementToFocus = hostToFocus;
|
|
|
|
const shadowRoot = hostToFocus?.shadowRoot;
|
|
if (shadowRoot) {
|
|
// If there are no inner focusable elements, just focus the host element.
|
|
elementToFocus = shadowRoot.querySelector<HTMLElement>(focusableQueryString) || hostToFocus;
|
|
}
|
|
|
|
if (elementToFocus) {
|
|
const radioGroup = elementToFocus.closest('ion-radio-group');
|
|
|
|
if (radioGroup) {
|
|
radioGroup.setFocus();
|
|
} else {
|
|
focusVisibleElement(elementToFocus);
|
|
}
|
|
} else {
|
|
// Focus fallback element instead of letting focus escape
|
|
fallbackElement.focus();
|
|
}
|
|
};
|