Skip to content

Commit cbdf710

Browse files
authored
fix: Fix focusing radio, checkbox, and switch on press (#7677)
* Fix iOS delay for click event * Fix focusing radio, checkbox, and switch on label press
1 parent c4bd19d commit cbdf710

File tree

4 files changed

+14
-17
lines changed

4 files changed

+14
-17
lines changed

packages/@react-aria/interactions/src/usePress.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -498,7 +498,9 @@ export function usePress(props: PressHookProps): PressResult {
498498
// However, iOS and Android do not focus or fire onClick after a long press.
499499
// We work around this by triggering a click ourselves after a timeout.
500500
// This timeout is canceled during the click event in case the real one fires first.
501-
// In testing, a 0ms delay is too short. 5ms seems long enough for the browser to fire the real events.
501+
// The timeout must be at least 32ms, because Safari on iOS delays the click event on
502+
// non-form elements without certain ARIA roles (for hover emulation).
503+
// https://github.com/WebKit/WebKit/blob/dccfae42bb29bd4bdef052e469f604a9387241c0/Source/WebKit/WebProcess/WebPage/ios/WebPageIOS.mm#L875-L892
502504
let clicked = false;
503505
let timeout = setTimeout(() => {
504506
if (state.isPressed && state.target instanceof HTMLElement) {
@@ -509,7 +511,7 @@ export function usePress(props: PressHookProps): PressResult {
509511
state.target.click();
510512
}
511513
}
512-
}, 5);
514+
}, 40);
513515
// Use a capturing listener to track if a click occurred.
514516
// If stopPropagation is called it may never reach our handler.
515517
addGlobalListener(e.currentTarget as Document, 'click', () => clicked = true, true);

packages/@react-aria/interactions/test/usePress.test.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -261,7 +261,7 @@ describe('usePress', function () {
261261
expect(shouldFocus).toBe(true);
262262

263263
// Mouse events are not fired in this case, and the browser does not focus the element.
264-
act(() => jest.advanceTimersByTime(10));
264+
act(() => jest.advanceTimersByTime(50));
265265
expect(document.activeElement).toBe(el);
266266

267267
expect(events).toEqual([
@@ -352,7 +352,7 @@ describe('usePress', function () {
352352
expect(shouldClick).toBe(true);
353353
fireEvent(el, pointerEvent('pointerout', {pointerId: 1, pointerType: 'mouse', clientX: 0, clientY: 0}));
354354

355-
act(() => jest.advanceTimersByTime(10));
355+
act(() => jest.advanceTimersByTime(50));
356356

357357
expect(events).toEqual([
358358
{

packages/@react-aria/radio/src/useRadio.ts

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -63,16 +63,14 @@ export function useRadio(props: AriaRadioProps, state: RadioGroupState, ref: Ref
6363
state.setSelectedValue(value);
6464
};
6565

66+
// Handle press state for keyboard interactions and cases where labelProps is not used.
6667
let {pressProps, isPressed} = usePress({
6768
isDisabled
6869
});
6970

70-
// iOS does not toggle radios if you drag off and back onto the label, so handle it ourselves.
71+
// Handle press state on the label.
7172
let {pressProps: labelProps, isPressed: isLabelPressed} = usePress({
72-
isDisabled,
73-
onPress() {
74-
state.setSelectedValue(value);
75-
}
73+
isDisabled
7674
});
7775

7876
let {focusableProps} = useFocusable(mergeProps(props, {
@@ -97,7 +95,7 @@ export function useRadio(props: AriaRadioProps, state: RadioGroupState, ref: Ref
9795
useFormValidation({validationBehavior}, state, ref);
9896

9997
return {
100-
labelProps: mergeProps(labelProps, {onClick: e => e.preventDefault()}),
98+
labelProps,
10199
inputProps: mergeProps(domProps, {
102100
...interactions,
103101
type: 'radio',

packages/@react-aria/toggle/src/useToggle.ts

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -64,17 +64,14 @@ export function useToggle(props: AriaToggleProps, state: ToggleState, ref: RefOb
6464
console.warn('If you do not provide children, you must specify an aria-label for accessibility');
6565
}
6666

67-
// This handles focusing the input on pointer down, which Safari does not do by default.
67+
// Handle press state for keyboard interactions and cases where labelProps is not used.
6868
let {pressProps, isPressed} = usePress({
6969
isDisabled
7070
});
7171

72-
// iOS does not toggle checkboxes if you drag off and back onto the label, so handle it ourselves.
72+
// Handle press state on the label.
7373
let {pressProps: labelProps, isPressed: isLabelPressed} = usePress({
74-
isDisabled: isDisabled || isReadOnly,
75-
onPress() {
76-
state.toggle();
77-
}
74+
isDisabled: isDisabled || isReadOnly
7875
});
7976

8077
let {focusableProps} = useFocusable(props, ref);
@@ -84,7 +81,7 @@ export function useToggle(props: AriaToggleProps, state: ToggleState, ref: RefOb
8481
useFormReset(ref, state.isSelected, state.setSelected);
8582

8683
return {
87-
labelProps: mergeProps(labelProps, {onClick: e => e.preventDefault()}),
84+
labelProps,
8885
inputProps: mergeProps(domProps, {
8986
'aria-invalid': isInvalid || validationState === 'invalid' || undefined,
9087
'aria-errormessage': props['aria-errormessage'],

0 commit comments

Comments
 (0)