Skip to content

Commit

Permalink
fix(overlays): focus management with checkbox/radio (#30026)
Browse files Browse the repository at this point in the history
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. -->
  • Loading branch information
tanner-reits authored Nov 21, 2024
1 parent 23763ab commit 8ee42bb
Show file tree
Hide file tree
Showing 3 changed files with 17 additions and 3 deletions.
1 change: 1 addition & 0 deletions core/src/components.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2303,6 +2303,7 @@ export namespace Components {
* The name of the control, which is submitted with the form data.
*/
"name": string;
"setFocus": () => Promise<void>;
/**
* the value of the radio group.
*/
Expand Down
9 changes: 8 additions & 1 deletion core/src/components/radio-group/radio-group.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { ComponentInterface, EventEmitter } from '@stencil/core';
import { Component, Element, Event, Host, Listen, Prop, Watch, h } from '@stencil/core';
import { Component, Element, Event, Host, Listen, Method, Prop, Watch, h } from '@stencil/core';
import { renderHiddenInput } from '@utils/helpers';

import { getIonMode } from '../../global/ionic-global';
Expand Down Expand Up @@ -217,6 +217,13 @@ export class RadioGroup implements ComponentInterface {
}
}

/** @internal */
@Method()
async setFocus() {
const radioToFocus = this.getRadios().find((r) => r.tabIndex !== -1);
radioToFocus?.setFocus();
}

render() {
const { label, labelId, el, name, value } = this;
const mode = getIonMode(this);
Expand Down
10 changes: 8 additions & 2 deletions core/src/utils/focus-trap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import { focusVisibleElement } from '@utils/helpers';
* 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-focusable:not([tabindex^="-"]):not([hidden]):not([disabled]), .ion-focusable[disabled="false"]:not([tabindex^="-"]):not([hidden])';
'[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
Expand Down Expand Up @@ -78,7 +78,13 @@ const focusElementInContext = <T extends HTMLElement>(
}

if (elementToFocus) {
focusVisibleElement(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();
Expand Down

0 comments on commit 8ee42bb

Please sign in to comment.