Skip to content

Commit 8ee42bb

Browse files
authored
fix(overlays): focus management with checkbox/radio (#30026)
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. -->
1 parent 23763ab commit 8ee42bb

File tree

3 files changed

+17
-3
lines changed

3 files changed

+17
-3
lines changed

core/src/components.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2303,6 +2303,7 @@ export namespace Components {
23032303
* The name of the control, which is submitted with the form data.
23042304
*/
23052305
"name": string;
2306+
"setFocus": () => Promise<void>;
23062307
/**
23072308
* the value of the radio group.
23082309
*/

core/src/components/radio-group/radio-group.tsx

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type { ComponentInterface, EventEmitter } from '@stencil/core';
2-
import { Component, Element, Event, Host, Listen, Prop, Watch, h } from '@stencil/core';
2+
import { Component, Element, Event, Host, Listen, Method, Prop, Watch, h } from '@stencil/core';
33
import { renderHiddenInput } from '@utils/helpers';
44

55
import { getIonMode } from '../../global/ionic-global';
@@ -217,6 +217,13 @@ export class RadioGroup implements ComponentInterface {
217217
}
218218
}
219219

220+
/** @internal */
221+
@Method()
222+
async setFocus() {
223+
const radioToFocus = this.getRadios().find((r) => r.tabIndex !== -1);
224+
radioToFocus?.setFocus();
225+
}
226+
220227
render() {
221228
const { label, labelId, el, name, value } = this;
222229
const mode = getIonMode(this);

core/src/utils/focus-trap.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import { focusVisibleElement } from '@utils/helpers';
1313
* valid usage for the disabled property on ion-button.
1414
*/
1515
export const focusableQueryString =
16-
'[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-focusable:not([tabindex^="-"]):not([hidden]):not([disabled]), .ion-focusable[disabled="false"]:not([tabindex^="-"]):not([hidden])';
16+
'[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])';
1717

1818
/**
1919
* Focuses the first descendant in a context
@@ -78,7 +78,13 @@ const focusElementInContext = <T extends HTMLElement>(
7878
}
7979

8080
if (elementToFocus) {
81-
focusVisibleElement(elementToFocus);
81+
const radioGroup = elementToFocus.closest('ion-radio-group');
82+
83+
if (radioGroup) {
84+
radioGroup.setFocus();
85+
} else {
86+
focusVisibleElement(elementToFocus);
87+
}
8288
} else {
8389
// Focus fallback element instead of letting focus escape
8490
fallbackElement.focus();

0 commit comments

Comments
 (0)