-
Notifications
You must be signed in to change notification settings - Fork 1.4k
Fix React StrictMode NumberField, ColorField, ActionGroup #4005
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
71e7833
6c43ef8
afbe8ff
1d16299
2856060
10d22f8
cb49b49
533a1b8
83d52da
b9a7acc
5fee650
3c49b6d
87270e2
087f8c1
3ff832f
aaeb7a2
ab3b737
4e4c654
c2334fe
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -15,8 +15,8 @@ import {AriaButtonProps} from '@react-types/button'; | |
| import {DOMAttributes, InputBase, RangeInputBase, Validation, ValueBase} from '@react-types/shared'; | ||
| // @ts-ignore | ||
| import intlMessages from '../intl/*.json'; | ||
| import {useCallback, useEffect, useRef} from 'react'; | ||
| import {useGlobalListeners} from '@react-aria/utils'; | ||
| import {useCallback, useEffect, useMemo, useRef} from 'react'; | ||
| import {useEffectEvent, useGlobalListeners} from '@react-aria/utils'; | ||
| import {useLocalizedStringFormatter} from '@react-aria/i18n'; | ||
|
|
||
|
|
||
|
|
@@ -39,7 +39,6 @@ export interface SpinbuttonAria { | |
| export function useSpinButton( | ||
| props: SpinButtonProps | ||
| ): SpinbuttonAria { | ||
| const _async = useRef<number>(); | ||
| let { | ||
| value, | ||
| textValue, | ||
|
|
@@ -55,18 +54,17 @@ export function useSpinButton( | |
| onDecrementToMin, | ||
| onIncrementToMax | ||
| } = props; | ||
| const stringFormatter = useLocalizedStringFormatter(intlMessages); | ||
| const propsRef = useRef(props); | ||
| propsRef.current = props; | ||
|
|
||
| const clearAsync = () => clearTimeout(_async.current); | ||
| const stringFormatter = useLocalizedStringFormatter(intlMessages); | ||
|
|
||
| // eslint-disable-next-line arrow-body-style | ||
| const _async = useRef<number>(); | ||
| const clearAsync = useCallback(() => clearTimeout(_async.current), [_async]); | ||
| // only run on unmount | ||
| useEffect(() => { | ||
| return () => clearAsync(); | ||
| }, []); | ||
| }, [clearAsync]); | ||
|
|
||
| let onKeyDown = (e) => { | ||
| let onKeyDown = useCallback((e) => { | ||
| if (e.ctrlKey || e.metaKey || e.shiftKey || e.altKey || isReadOnly) { | ||
| return; | ||
| } | ||
|
|
@@ -113,22 +111,24 @@ export function useSpinButton( | |
| } | ||
| break; | ||
| } | ||
| }; | ||
| }, [isReadOnly, onIncrementPage, onIncrement, onDecrementPage, onDecrement, onDecrementToMin, onIncrementToMax]); | ||
|
|
||
| let isFocused = useRef(false); | ||
| let onFocus = () => { | ||
| let onFocus = useCallback(() => { | ||
| isFocused.current = true; | ||
| }; | ||
| }, [isFocused]); | ||
|
|
||
| let onBlur = () => { | ||
| let onBlur = useCallback(() => { | ||
| isFocused.current = false; | ||
| }; | ||
| }, [isFocused]); | ||
|
|
||
| // Replace Unicode hyphen-minus (U+002D) with minus sign (U+2212). | ||
| // This ensures that macOS VoiceOver announces it as "minus" even with other characters between the minus sign | ||
| // and the number (e.g. currency symbol). Otherwise it announces nothing because it assumes the character is a hyphen. | ||
| // In addition, replace the empty string with the word "Empty" so that iOS VoiceOver does not read "50%" for an empty field. | ||
| textValue = textValue === '' ? stringFormatter.format('Empty') : (textValue || `${value}`).replace('-', '\u2212'); | ||
| textValue = useMemo( | ||
| () => textValue === '' ? stringFormatter.format('Empty') : (textValue || `${value}`).replace('-', '\u2212') | ||
| , [textValue, stringFormatter, value]); | ||
|
|
||
| useEffect(() => { | ||
| if (isFocused.current) { | ||
|
|
@@ -137,45 +137,37 @@ export function useSpinButton( | |
| } | ||
| }, [textValue]); | ||
|
|
||
| const onIncrementPressStart = useCallback( | ||
| (initialStepDelay: number) => { | ||
| clearAsync(); | ||
| propsRef.current.onIncrement(); | ||
| // Start spinning after initial delay | ||
| _async.current = window.setTimeout( | ||
| () => { | ||
| if (isNaN(maxValue) || isNaN(value) || value < maxValue) { | ||
| onIncrementPressStart(60); | ||
| } | ||
| }, | ||
| initialStepDelay | ||
| ); | ||
| }, | ||
| // eslint-disable-next-line react-hooks/exhaustive-deps | ||
| [onIncrement, maxValue, value] | ||
| ); | ||
|
|
||
| const onDecrementPressStart = useCallback( | ||
| (initialStepDelay: number) => { | ||
| clearAsync(); | ||
| propsRef.current.onDecrement(); | ||
| // Start spinning after initial delay | ||
| _async.current = window.setTimeout( | ||
| () => { | ||
| if (isNaN(minValue) || isNaN(value) || value > minValue) { | ||
| onDecrementPressStart(60); | ||
| } | ||
| }, | ||
| initialStepDelay | ||
| ); | ||
| }, | ||
| // eslint-disable-next-line react-hooks/exhaustive-deps | ||
| [onDecrement, minValue, value] | ||
| ); | ||
| let onIncrementPressStart = useEffectEvent((initialStepDelay: number) => { | ||
| clearAsync(); | ||
| onIncrement(); | ||
| // Start spinning after initial delay | ||
| _async.current = window.setTimeout( | ||
| () => { | ||
| if (isNaN(maxValue) || isNaN(value) || value < maxValue) { | ||
| onIncrementPressStart(60); | ||
| } | ||
| }, | ||
| initialStepDelay | ||
| ); | ||
| }); | ||
|
|
||
| let onDecrementPressStart = useEffectEvent((initialStepDelay: number) => { | ||
| clearAsync(); | ||
| onDecrement(); | ||
| // Start spinning after initial delay | ||
| _async.current = window.setTimeout( | ||
| () => { | ||
| if (isNaN(minValue) || isNaN(value) || value > minValue) { | ||
| onDecrementPressStart(60); | ||
| } | ||
| }, | ||
| initialStepDelay | ||
| ); | ||
| }); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think the only changes we needed in this file were converting these to
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. got carried away optimizing it would look like, I think your assessment is correct. |
||
|
|
||
| let cancelContextMenu = (e) => { | ||
| let cancelContextMenu = useCallback((e) => { | ||
| e.preventDefault(); | ||
| }; | ||
| }, []); | ||
|
|
||
| let {addGlobalListener, removeAllGlobalListeners} = useGlobalListeners(); | ||
|
|
||
|
|
@@ -194,26 +186,26 @@ export function useSpinButton( | |
| onBlur | ||
| }, | ||
| incrementButtonProps: { | ||
| onPressStart: () => { | ||
| onPressStart: useCallback(() => { | ||
| onIncrementPressStart(400); | ||
| addGlobalListener(window, 'contextmenu', cancelContextMenu); | ||
| }, | ||
| onPressEnd: () => { | ||
| }, [onIncrementPressStart, addGlobalListener, cancelContextMenu]), | ||
| onPressEnd: useCallback(() => { | ||
| clearAsync(); | ||
| removeAllGlobalListeners(); | ||
| }, | ||
| }, [clearAsync, removeAllGlobalListeners]), | ||
| onFocus, | ||
| onBlur | ||
| }, | ||
| decrementButtonProps: { | ||
| onPressStart: () => { | ||
| onPressStart: useCallback(() => { | ||
| onDecrementPressStart(400); | ||
| addGlobalListener(window, 'contextmenu', cancelContextMenu); | ||
| }, | ||
| onPressEnd: () => { | ||
| }, [onDecrementPressStart, addGlobalListener, cancelContextMenu]), | ||
| onPressEnd: useCallback(() => { | ||
| clearAsync(); | ||
| removeAllGlobalListeners(); | ||
| }, | ||
| }, [clearAsync, removeAllGlobalListeners]), | ||
| onFocus, | ||
| onBlur | ||
| } | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -280,6 +280,7 @@ describe('NumberField', function () { | |
| act(() => {textField.blur();}); | ||
| expect(onChangeSpy).toHaveBeenLastCalledWith(5); | ||
| expect(onChangeSpy).toHaveBeenCalledTimes(2); | ||
| expect(textField).toHaveAttribute('value', '5'); | ||
| triggerPress(incrementButton); | ||
| expect(onChangeSpy).toHaveBeenLastCalledWith(10); | ||
| expect(onChangeSpy).toHaveBeenCalledTimes(3); | ||
|
|
@@ -881,41 +882,29 @@ describe('NumberField', function () { | |
| act(() => {textField.focus();}); | ||
| textField.setSelectionRange(2, 3); | ||
| userEvent.type(textField, '{backspace}'); | ||
| expect(announce).toHaveBeenCalledTimes(2); | ||
| expect(announce).toHaveBeenLastCalledWith('−$0.00', 'assertive'); | ||
| textField.setSelectionRange(2, 2); | ||
| typeText(textField, '1'); | ||
| expect(announce).toHaveBeenCalledTimes(3); | ||
| expect(announce).toHaveBeenLastCalledWith('−$1.00', 'assertive'); | ||
| textField.setSelectionRange(3, 3); | ||
| typeText(textField, '8'); | ||
| expect(textField).toHaveAttribute('value', '($18.00)'); | ||
| act(() => {textField.blur();}); | ||
| expect(textField).toHaveAttribute('value', '($18.00)'); | ||
| expect(onChangeSpy).toHaveBeenCalledTimes(2); | ||
| expect(onChangeSpy).toHaveBeenLastCalledWith(-18); | ||
| expect(announce).toHaveBeenCalledTimes(4); | ||
| expect(announce).toHaveBeenLastCalledWith('−$18.00', 'assertive'); | ||
|
|
||
| act(() => {textField.focus();}); | ||
| textField.setSelectionRange(7, 8); | ||
| userEvent.type(textField, '{backspace}'); | ||
| expect(textField).toHaveAttribute('value', '($18.00'); | ||
| expect(announce).toHaveBeenCalledTimes(5); | ||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. these were just wrong. it should only announce during spinning
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Are you sure? The useEffect in useSpinButton seems to always announce if the text field is focused.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. you are right, these appear to have come from a bad merge |
||
| expect(announce).toHaveBeenLastCalledWith('$18.00', 'assertive'); | ||
| act(() => {textField.blur();}); | ||
| expect(textField).toHaveAttribute('value', '$18.00'); | ||
| expect(onChangeSpy).toHaveBeenCalledTimes(3); | ||
| expect(onChangeSpy).toHaveBeenLastCalledWith(18); | ||
|
|
||
| act(() => {textField.focus();}); | ||
| userEvent.clear(textField); | ||
| expect(announce).toHaveBeenCalledTimes(6); | ||
| expect(announce).toHaveBeenLastCalledWith('Empty', 'assertive'); | ||
| typeText(textField, '($32)'); | ||
| expect(textField).toHaveAttribute('value', '($32)'); | ||
| expect(announce).toHaveBeenCalledTimes(9); | ||
| expect(announce).toHaveBeenLastCalledWith('−$32.00', 'assertive'); | ||
| act(() => {textField.blur();}); | ||
| expect(textField).toHaveAttribute('value', '($32.00)'); | ||
| expect(onChangeSpy).toHaveBeenCalledTimes(4); | ||
|
|
@@ -929,10 +918,9 @@ describe('NumberField', function () { | |
| let {textField} = renderNumberField({onChange: onChangeSpy, formatOptions: {style: 'currency', currency: 'USD', currencySign: 'accounting'}}, {locale: 'ar-AE'}); | ||
|
|
||
| act(() => {textField.focus();}); | ||
| userEvent.type(textField, '(10)'); | ||
| typeText(textField, '(10)'); | ||
| expect(textField).toHaveAttribute('value', '(10)'); | ||
| expect(announce).toHaveBeenCalledTimes(3); | ||
| expect(announce).toHaveBeenLastCalledWith('−١٠٫٠٠ US$', 'assertive'); | ||
| expect(announce).toHaveBeenCalledTimes(0); | ||
| act(() => {textField.blur();}); | ||
| expect(textField).toHaveAttribute('value', '(US$10.00)'); | ||
| expect(onChangeSpy).toHaveBeenCalledTimes(1); | ||
|
|
@@ -962,17 +950,11 @@ describe('NumberField', function () { | |
| }); | ||
| textField.setSelectionRange(1, 2); | ||
| userEvent.type(textField, '{backspace}'); | ||
| expect(announce).toHaveBeenCalledTimes(2); | ||
| expect(announce).toHaveBeenLastCalledWith('−0,00 $', 'assertive'); | ||
| textField.setSelectionRange(1, 1); | ||
| typeText(textField, '1'); | ||
| expect(announce).toHaveBeenCalledTimes(3); | ||
| expect(announce).toHaveBeenLastCalledWith('−1,00 $', 'assertive'); | ||
| textField.setSelectionRange(2, 2); | ||
| typeText(textField, '8'); | ||
| expect(textField).toHaveAttribute('value', '-18,00 $'); | ||
| expect(announce).toHaveBeenCalledTimes(4); | ||
| expect(announce).toHaveBeenLastCalledWith('−18,00 $', 'assertive'); | ||
| act(() => { | ||
| textField.blur(); | ||
| }); | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this was unneeded it would seem