@react-aria/spinbutton 3.6.18 → 3.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/types.d.ts.map +1 -1
- package/dist/useSpinButton.main.js +88 -11
- package/dist/useSpinButton.main.js.map +1 -1
- package/dist/useSpinButton.mjs +89 -12
- package/dist/useSpinButton.module.js +89 -12
- package/dist/useSpinButton.module.js.map +1 -1
- package/package.json +6 -6
- package/src/useSpinButton.ts +105 -13
package/dist/types.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"mappings":";;AAsBA,gCAAiC,SAAQ,SAAS,EAAE,WAAW,MAAM,CAAC,EAAE,UAAU,MAAM,CAAC,EAAE,eAAe,MAAM,CAAC;IAC/G,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,WAAW,CAAC,EAAE,MAAM,IAAI,CAAC;IACzB,eAAe,CAAC,EAAE,MAAM,IAAI,CAAC;IAC7B,WAAW,CAAC,EAAE,MAAM,IAAI,CAAC;IACzB,eAAe,CAAC,EAAE,MAAM,IAAI,CAAC;IAC7B,gBAAgB,CAAC,EAAE,MAAM,IAAI,CAAC;IAC9B,gBAAgB,CAAC,EAAE,MAAM,IAAI,CAAA;CAC9B;AAED;IACE,eAAe,EAAE,aAAa,CAAC;IAC/B,oBAAoB,EAAE,eAAe,CAAC;IACtC,oBAAoB,EAAE,eAAe,CAAA;CACtC;AAED,8BACE,KAAK,EAAE,eAAe,GACrB,cAAc,
|
|
1
|
+
{"mappings":";;AAsBA,gCAAiC,SAAQ,SAAS,EAAE,WAAW,MAAM,CAAC,EAAE,UAAU,MAAM,CAAC,EAAE,eAAe,MAAM,CAAC;IAC/G,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,WAAW,CAAC,EAAE,MAAM,IAAI,CAAC;IACzB,eAAe,CAAC,EAAE,MAAM,IAAI,CAAC;IAC7B,WAAW,CAAC,EAAE,MAAM,IAAI,CAAC;IACzB,eAAe,CAAC,EAAE,MAAM,IAAI,CAAC;IAC7B,gBAAgB,CAAC,EAAE,MAAM,IAAI,CAAC;IAC9B,gBAAgB,CAAC,EAAE,MAAM,IAAI,CAAA;CAC9B;AAED;IACE,eAAe,EAAE,aAAa,CAAC;IAC/B,oBAAoB,EAAE,eAAe,CAAC;IACtC,oBAAoB,EAAE,eAAe,CAAA;CACtC;AAED,8BACE,KAAK,EAAE,eAAe,GACrB,cAAc,CA0QhB","sources":["packages/@react-aria/spinbutton/src/packages/@react-aria/spinbutton/src/useSpinButton.ts","packages/@react-aria/spinbutton/src/packages/@react-aria/spinbutton/src/index.ts","packages/@react-aria/spinbutton/src/index.ts"],"sourcesContent":[null,null,"/*\n * Copyright 2020 Adobe. All rights reserved.\n * This file is licensed to you under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License. You may obtain a copy\n * of the License at http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software distributed under\n * the License is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS\n * OF ANY KIND, either express or implied. See the License for the specific language\n * governing permissions and limitations under the License.\n */\nexport type {SpinButtonProps, SpinbuttonAria} from './useSpinButton';\nexport {useSpinButton} from './useSpinButton';\n"],"names":[],"version":3,"file":"types.d.ts.map"}
|
|
@@ -33,9 +33,16 @@ function $37bbd4c129023f61$export$e908e06f4b8e3402(props) {
|
|
|
33
33
|
const _async = (0, $2pZbw$react.useRef)(undefined);
|
|
34
34
|
let { value: value, textValue: textValue, minValue: minValue, maxValue: maxValue, isDisabled: isDisabled, isReadOnly: isReadOnly, isRequired: isRequired, onIncrement: onIncrement, onIncrementPage: onIncrementPage, onDecrement: onDecrement, onDecrementPage: onDecrementPage, onDecrementToMin: onDecrementToMin, onIncrementToMax: onIncrementToMax } = props;
|
|
35
35
|
const stringFormatter = (0, $2pZbw$reactariai18n.useLocalizedStringFormatter)((0, ($parcel$interopDefault($cb4b786159079747$exports))), '@react-aria/spinbutton');
|
|
36
|
-
|
|
36
|
+
let isSpinning = (0, $2pZbw$react.useRef)(false);
|
|
37
|
+
const clearAsync = (0, $2pZbw$react.useCallback)(()=>{
|
|
38
|
+
clearTimeout(_async.current);
|
|
39
|
+
isSpinning.current = false;
|
|
40
|
+
}, []);
|
|
41
|
+
const clearAsyncEvent = (0, $2pZbw$reactariautils.useEffectEvent)(()=>{
|
|
42
|
+
clearAsync();
|
|
43
|
+
});
|
|
37
44
|
(0, $2pZbw$react.useEffect)(()=>{
|
|
38
|
-
return ()=>
|
|
45
|
+
return ()=>clearAsyncEvent();
|
|
39
46
|
}, []);
|
|
40
47
|
let onKeyDown = (e)=>{
|
|
41
48
|
if (e.ctrlKey || e.metaKey || e.shiftKey || e.altKey || isReadOnly || e.nativeEvent.isComposing) return;
|
|
@@ -102,8 +109,15 @@ function $37bbd4c129023f61$export$e908e06f4b8e3402(props) {
|
|
|
102
109
|
}, [
|
|
103
110
|
ariaTextValue
|
|
104
111
|
]);
|
|
105
|
-
|
|
112
|
+
// For touch users, if they move their finger like they're scrolling, we don't want to trigger a spin.
|
|
113
|
+
let onPointerCancel = (0, $2pZbw$react.useCallback)(()=>{
|
|
106
114
|
clearAsync();
|
|
115
|
+
}, [
|
|
116
|
+
clearAsync
|
|
117
|
+
]);
|
|
118
|
+
const onIncrementPressStart = (0, $2pZbw$reactariautils.useEffectEvent)((initialStepDelay)=>{
|
|
119
|
+
clearAsyncEvent();
|
|
120
|
+
isSpinning.current = true;
|
|
107
121
|
onIncrement === null || onIncrement === void 0 ? void 0 : onIncrement();
|
|
108
122
|
// Start spinning after initial delay
|
|
109
123
|
_async.current = window.setTimeout(()=>{
|
|
@@ -111,7 +125,8 @@ function $37bbd4c129023f61$export$e908e06f4b8e3402(props) {
|
|
|
111
125
|
}, initialStepDelay);
|
|
112
126
|
});
|
|
113
127
|
const onDecrementPressStart = (0, $2pZbw$reactariautils.useEffectEvent)((initialStepDelay)=>{
|
|
114
|
-
|
|
128
|
+
clearAsyncEvent();
|
|
129
|
+
isSpinning.current = true;
|
|
115
130
|
onDecrement === null || onDecrement === void 0 ? void 0 : onDecrement();
|
|
116
131
|
// Start spinning after initial delay
|
|
117
132
|
_async.current = window.setTimeout(()=>{
|
|
@@ -122,6 +137,25 @@ function $37bbd4c129023f61$export$e908e06f4b8e3402(props) {
|
|
|
122
137
|
e.preventDefault();
|
|
123
138
|
};
|
|
124
139
|
let { addGlobalListener: addGlobalListener, removeAllGlobalListeners: removeAllGlobalListeners } = (0, $2pZbw$reactariautils.useGlobalListeners)();
|
|
140
|
+
// Tracks in touch if the press end event was preceded by a press up.
|
|
141
|
+
// If it wasn't, then we know the finger left the button while still in contact with the screen.
|
|
142
|
+
// This means that the user is trying to scroll or interact in some way that shouldn't trigger
|
|
143
|
+
// an increment or decrement.
|
|
144
|
+
let isUp = (0, $2pZbw$react.useRef)(false);
|
|
145
|
+
let [isIncrementPressed, setIsIncrementPressed] = (0, $2pZbw$react.useState)(null);
|
|
146
|
+
(0, $2pZbw$react.useEffect)(()=>{
|
|
147
|
+
if (isIncrementPressed === 'touch') onIncrementPressStart(60);
|
|
148
|
+
else if (isIncrementPressed) onIncrementPressStart(400);
|
|
149
|
+
}, [
|
|
150
|
+
isIncrementPressed
|
|
151
|
+
]);
|
|
152
|
+
let [isDecrementPressed, setIsDecrementPressed] = (0, $2pZbw$react.useState)(null);
|
|
153
|
+
(0, $2pZbw$react.useEffect)(()=>{
|
|
154
|
+
if (isDecrementPressed === 'touch') onDecrementPressStart(60);
|
|
155
|
+
else if (isDecrementPressed) onDecrementPressStart(400);
|
|
156
|
+
}, [
|
|
157
|
+
isDecrementPressed
|
|
158
|
+
]);
|
|
125
159
|
return {
|
|
126
160
|
spinButtonProps: {
|
|
127
161
|
role: 'spinbutton',
|
|
@@ -137,25 +171,68 @@ function $37bbd4c129023f61$export$e908e06f4b8e3402(props) {
|
|
|
137
171
|
onBlur: onBlur
|
|
138
172
|
},
|
|
139
173
|
incrementButtonProps: {
|
|
140
|
-
onPressStart: ()=>{
|
|
141
|
-
|
|
174
|
+
onPressStart: (e)=>{
|
|
175
|
+
if (e.pointerType !== 'touch') setIsIncrementPressed('mouse');
|
|
176
|
+
else {
|
|
177
|
+
if (_async.current) clearAsync();
|
|
178
|
+
addGlobalListener(window, 'pointercancel', onPointerCancel, {
|
|
179
|
+
capture: true
|
|
180
|
+
});
|
|
181
|
+
isUp.current = false;
|
|
182
|
+
// For touch users, don't trigger a decrement on press start, we'll wait for the press end to trigger it if
|
|
183
|
+
// the control isn't spinning.
|
|
184
|
+
_async.current = window.setTimeout(()=>{
|
|
185
|
+
setIsIncrementPressed('touch');
|
|
186
|
+
}, 600);
|
|
187
|
+
}
|
|
142
188
|
addGlobalListener(window, 'contextmenu', cancelContextMenu);
|
|
143
189
|
},
|
|
144
|
-
|
|
190
|
+
onPressUp: (e)=>{
|
|
191
|
+
if (e.pointerType === 'touch') isUp.current = true;
|
|
145
192
|
clearAsync();
|
|
146
193
|
removeAllGlobalListeners();
|
|
194
|
+
setIsIncrementPressed(null);
|
|
195
|
+
},
|
|
196
|
+
onPressEnd: (e)=>{
|
|
197
|
+
if (e.pointerType === 'touch') {
|
|
198
|
+
if (!isSpinning.current && isUp.current) onIncrement === null || onIncrement === void 0 ? void 0 : onIncrement();
|
|
199
|
+
}
|
|
200
|
+
clearAsync();
|
|
201
|
+
isUp.current = false;
|
|
202
|
+
setIsIncrementPressed(null);
|
|
147
203
|
},
|
|
148
204
|
onFocus: onFocus,
|
|
149
205
|
onBlur: onBlur
|
|
150
206
|
},
|
|
151
207
|
decrementButtonProps: {
|
|
152
|
-
onPressStart: ()=>{
|
|
153
|
-
|
|
154
|
-
|
|
208
|
+
onPressStart: (e)=>{
|
|
209
|
+
if (e.pointerType !== 'touch') setIsDecrementPressed('mouse');
|
|
210
|
+
else {
|
|
211
|
+
if (_async.current) clearAsync();
|
|
212
|
+
addGlobalListener(window, 'pointercancel', onPointerCancel, {
|
|
213
|
+
capture: true
|
|
214
|
+
});
|
|
215
|
+
isUp.current = false;
|
|
216
|
+
// For touch users, don't trigger a decrement on press start, we'll wait for the press end to trigger it if
|
|
217
|
+
// the control isn't spinning.
|
|
218
|
+
_async.current = window.setTimeout(()=>{
|
|
219
|
+
setIsDecrementPressed('touch');
|
|
220
|
+
}, 600);
|
|
221
|
+
}
|
|
155
222
|
},
|
|
156
|
-
|
|
223
|
+
onPressUp: (e)=>{
|
|
224
|
+
if (e.pointerType === 'touch') isUp.current = true;
|
|
157
225
|
clearAsync();
|
|
158
226
|
removeAllGlobalListeners();
|
|
227
|
+
setIsDecrementPressed(null);
|
|
228
|
+
},
|
|
229
|
+
onPressEnd: (e)=>{
|
|
230
|
+
if (e.pointerType === 'touch') {
|
|
231
|
+
if (!isSpinning.current && isUp.current) onDecrement === null || onDecrement === void 0 ? void 0 : onDecrement();
|
|
232
|
+
}
|
|
233
|
+
clearAsync();
|
|
234
|
+
isUp.current = false;
|
|
235
|
+
setIsDecrementPressed(null);
|
|
159
236
|
},
|
|
160
237
|
onFocus: onFocus,
|
|
161
238
|
onBlur: onBlur
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"mappings":"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","sources":["packages/@react-aria/spinbutton/src/useSpinButton.ts"],"sourcesContent":["/*\n * Copyright 2020 Adobe. All rights reserved.\n * This file is licensed to you under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License. You may obtain a copy\n * of the License at http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software distributed under\n * the License is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS\n * OF ANY KIND, either express or implied. See the License for the specific language\n * governing permissions and limitations under the License.\n */\n\nimport {announce, clearAnnouncer} from '@react-aria/live-announcer';\nimport {AriaButtonProps} from '@react-types/button';\nimport {DOMAttributes, InputBase, RangeInputBase, Validation, ValueBase} from '@react-types/shared';\n// @ts-ignore\nimport intlMessages from '../intl/*.json';\nimport {useEffect, useRef} from 'react';\nimport {useEffectEvent, useGlobalListeners} from '@react-aria/utils';\nimport {useLocalizedStringFormatter} from '@react-aria/i18n';\n\n\nexport interface SpinButtonProps extends InputBase, Validation<number>, ValueBase<number>, RangeInputBase<number> {\n textValue?: string,\n onIncrement?: () => void,\n onIncrementPage?: () => void,\n onDecrement?: () => void,\n onDecrementPage?: () => void,\n onDecrementToMin?: () => void,\n onIncrementToMax?: () => void\n}\n\nexport interface SpinbuttonAria {\n spinButtonProps: DOMAttributes,\n incrementButtonProps: AriaButtonProps,\n decrementButtonProps: AriaButtonProps\n}\n\nexport function useSpinButton(\n props: SpinButtonProps\n): SpinbuttonAria {\n const _async = useRef<number>(undefined);\n let {\n value,\n textValue,\n minValue,\n maxValue,\n isDisabled,\n isReadOnly,\n isRequired,\n onIncrement,\n onIncrementPage,\n onDecrement,\n onDecrementPage,\n onDecrementToMin,\n onIncrementToMax\n } = props;\n const stringFormatter = useLocalizedStringFormatter(intlMessages, '@react-aria/spinbutton');\n\n const clearAsync = () => clearTimeout(_async.current);\n\n\n useEffect(() => {\n return () => clearAsync();\n }, []);\n\n let onKeyDown = (e) => {\n if (e.ctrlKey || e.metaKey || e.shiftKey || e.altKey || isReadOnly || e.nativeEvent.isComposing) {\n return;\n }\n\n switch (e.key) {\n case 'PageUp':\n if (onIncrementPage) {\n e.preventDefault();\n onIncrementPage?.();\n break;\n }\n // fallthrough!\n case 'ArrowUp':\n case 'Up':\n if (onIncrement) {\n e.preventDefault();\n onIncrement?.();\n }\n break;\n case 'PageDown':\n if (onDecrementPage) {\n e.preventDefault();\n onDecrementPage?.();\n break;\n }\n // fallthrough\n case 'ArrowDown':\n case 'Down':\n if (onDecrement) {\n e.preventDefault();\n onDecrement?.();\n }\n break;\n case 'Home':\n if (onDecrementToMin) {\n e.preventDefault();\n onDecrementToMin?.();\n }\n break;\n case 'End':\n if (onIncrementToMax) {\n e.preventDefault();\n onIncrementToMax?.();\n }\n break;\n }\n };\n\n let isFocused = useRef(false);\n let onFocus = () => {\n isFocused.current = true;\n };\n\n let onBlur = () => {\n isFocused.current = false;\n };\n\n // Replace Unicode hyphen-minus (U+002D) with minus sign (U+2212).\n // This ensures that macOS VoiceOver announces it as \"minus\" even with other characters between the minus sign\n // and the number (e.g. currency symbol). Otherwise it announces nothing because it assumes the character is a hyphen.\n // In addition, replace the empty string with the word \"Empty\" so that iOS VoiceOver does not read \"50%\" for an empty field.\n let ariaTextValue = textValue === '' ? stringFormatter.format('Empty') : (textValue || `${value}`).replace('-', '\\u2212');\n\n useEffect(() => {\n if (isFocused.current) {\n clearAnnouncer('assertive');\n announce(ariaTextValue, 'assertive');\n }\n }, [ariaTextValue]);\n\n const onIncrementPressStart = useEffectEvent(\n (initialStepDelay: number) => {\n clearAsync();\n onIncrement?.();\n // Start spinning after initial delay\n _async.current = window.setTimeout(\n () => {\n if ((maxValue === undefined || isNaN(maxValue)) || (value === undefined || isNaN(value)) || value < maxValue) {\n onIncrementPressStart(60);\n }\n },\n initialStepDelay\n );\n }\n );\n\n const onDecrementPressStart = useEffectEvent(\n (initialStepDelay: number) => {\n clearAsync();\n onDecrement?.();\n // Start spinning after initial delay\n _async.current = window.setTimeout(\n () => {\n if ((minValue === undefined || isNaN(minValue)) || (value === undefined || isNaN(value)) || value > minValue) {\n onDecrementPressStart(60);\n }\n },\n initialStepDelay\n );\n }\n );\n\n let cancelContextMenu = (e) => {\n e.preventDefault();\n };\n\n let {addGlobalListener, removeAllGlobalListeners} = useGlobalListeners();\n\n return {\n spinButtonProps: {\n role: 'spinbutton',\n 'aria-valuenow': value !== undefined && !isNaN(value) ? value : undefined,\n 'aria-valuetext': ariaTextValue,\n 'aria-valuemin': minValue,\n 'aria-valuemax': maxValue,\n 'aria-disabled': isDisabled || undefined,\n 'aria-readonly': isReadOnly || undefined,\n 'aria-required': isRequired || undefined,\n onKeyDown,\n onFocus,\n onBlur\n },\n incrementButtonProps: {\n onPressStart: () => {\n onIncrementPressStart(400);\n addGlobalListener(window, 'contextmenu', cancelContextMenu);\n },\n onPressEnd: () => {\n clearAsync();\n removeAllGlobalListeners();\n },\n onFocus,\n onBlur\n },\n decrementButtonProps: {\n onPressStart: () => {\n onDecrementPressStart(400);\n addGlobalListener(window, 'contextmenu', cancelContextMenu);\n },\n onPressEnd: () => {\n clearAsync();\n removeAllGlobalListeners();\n },\n onFocus,\n onBlur\n }\n };\n}\n"],"names":[],"version":3,"file":"useSpinButton.main.js.map"}
|
|
1
|
+
{"mappings":"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","sources":["packages/@react-aria/spinbutton/src/useSpinButton.ts"],"sourcesContent":["/*\n * Copyright 2020 Adobe. All rights reserved.\n * This file is licensed to you under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License. You may obtain a copy\n * of the License at http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software distributed under\n * the License is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS\n * OF ANY KIND, either express or implied. See the License for the specific language\n * governing permissions and limitations under the License.\n */\n\nimport {announce, clearAnnouncer} from '@react-aria/live-announcer';\nimport {AriaButtonProps} from '@react-types/button';\nimport {DOMAttributes, InputBase, RangeInputBase, Validation, ValueBase} from '@react-types/shared';\n// @ts-ignore\nimport intlMessages from '../intl/*.json';\nimport {useCallback, useEffect, useRef, useState} from 'react';\nimport {useEffectEvent, useGlobalListeners} from '@react-aria/utils';\nimport {useLocalizedStringFormatter} from '@react-aria/i18n';\n\n\nexport interface SpinButtonProps extends InputBase, Validation<number>, ValueBase<number>, RangeInputBase<number> {\n textValue?: string,\n onIncrement?: () => void,\n onIncrementPage?: () => void,\n onDecrement?: () => void,\n onDecrementPage?: () => void,\n onDecrementToMin?: () => void,\n onIncrementToMax?: () => void\n}\n\nexport interface SpinbuttonAria {\n spinButtonProps: DOMAttributes,\n incrementButtonProps: AriaButtonProps,\n decrementButtonProps: AriaButtonProps\n}\n\nexport function useSpinButton(\n props: SpinButtonProps\n): SpinbuttonAria {\n const _async = useRef<number>(undefined);\n let {\n value,\n textValue,\n minValue,\n maxValue,\n isDisabled,\n isReadOnly,\n isRequired,\n onIncrement,\n onIncrementPage,\n onDecrement,\n onDecrementPage,\n onDecrementToMin,\n onIncrementToMax\n } = props;\n const stringFormatter = useLocalizedStringFormatter(intlMessages, '@react-aria/spinbutton');\n\n let isSpinning = useRef(false);\n const clearAsync = useCallback(() => {\n clearTimeout(_async.current);\n isSpinning.current = false;\n }, []);\n const clearAsyncEvent = useEffectEvent(() => {\n clearAsync();\n });\n\n useEffect(() => {\n return () => clearAsyncEvent();\n }, []);\n\n let onKeyDown = (e) => {\n if (e.ctrlKey || e.metaKey || e.shiftKey || e.altKey || isReadOnly || e.nativeEvent.isComposing) {\n return;\n }\n\n switch (e.key) {\n case 'PageUp':\n if (onIncrementPage) {\n e.preventDefault();\n onIncrementPage?.();\n break;\n }\n // fallthrough!\n case 'ArrowUp':\n case 'Up':\n if (onIncrement) {\n e.preventDefault();\n onIncrement?.();\n }\n break;\n case 'PageDown':\n if (onDecrementPage) {\n e.preventDefault();\n onDecrementPage?.();\n break;\n }\n // fallthrough\n case 'ArrowDown':\n case 'Down':\n if (onDecrement) {\n e.preventDefault();\n onDecrement?.();\n }\n break;\n case 'Home':\n if (onDecrementToMin) {\n e.preventDefault();\n onDecrementToMin?.();\n }\n break;\n case 'End':\n if (onIncrementToMax) {\n e.preventDefault();\n onIncrementToMax?.();\n }\n break;\n }\n };\n\n let isFocused = useRef(false);\n let onFocus = () => {\n isFocused.current = true;\n };\n\n let onBlur = () => {\n isFocused.current = false;\n };\n\n // Replace Unicode hyphen-minus (U+002D) with minus sign (U+2212).\n // This ensures that macOS VoiceOver announces it as \"minus\" even with other characters between the minus sign\n // and the number (e.g. currency symbol). Otherwise it announces nothing because it assumes the character is a hyphen.\n // In addition, replace the empty string with the word \"Empty\" so that iOS VoiceOver does not read \"50%\" for an empty field.\n let ariaTextValue = textValue === '' ? stringFormatter.format('Empty') : (textValue || `${value}`).replace('-', '\\u2212');\n\n useEffect(() => {\n if (isFocused.current) {\n clearAnnouncer('assertive');\n announce(ariaTextValue, 'assertive');\n }\n }, [ariaTextValue]);\n\n // For touch users, if they move their finger like they're scrolling, we don't want to trigger a spin.\n let onPointerCancel = useCallback(() => {\n clearAsync();\n }, [clearAsync]);\n\n const onIncrementPressStart = useEffectEvent(\n (initialStepDelay: number) => {\n clearAsyncEvent();\n isSpinning.current = true;\n onIncrement?.();\n // Start spinning after initial delay\n _async.current = window.setTimeout(\n () => {\n if ((maxValue === undefined || isNaN(maxValue)) || (value === undefined || isNaN(value)) || value < maxValue) {\n onIncrementPressStart(60);\n }\n },\n initialStepDelay\n );\n }\n );\n\n const onDecrementPressStart = useEffectEvent(\n (initialStepDelay: number) => {\n clearAsyncEvent();\n isSpinning.current = true;\n onDecrement?.();\n // Start spinning after initial delay\n _async.current = window.setTimeout(\n () => {\n if ((minValue === undefined || isNaN(minValue)) || (value === undefined || isNaN(value)) || value > minValue) {\n onDecrementPressStart(60);\n }\n },\n initialStepDelay\n );\n }\n );\n\n let cancelContextMenu = (e) => {\n e.preventDefault();\n };\n\n let {addGlobalListener, removeAllGlobalListeners} = useGlobalListeners();\n\n // Tracks in touch if the press end event was preceded by a press up.\n // If it wasn't, then we know the finger left the button while still in contact with the screen.\n // This means that the user is trying to scroll or interact in some way that shouldn't trigger\n // an increment or decrement.\n let isUp = useRef(false);\n\n let [isIncrementPressed, setIsIncrementPressed] = useState<'touch' | 'mouse' | null>(null);\n useEffect(() => {\n if (isIncrementPressed === 'touch') {\n onIncrementPressStart(60);\n } else if (isIncrementPressed) {\n onIncrementPressStart(400);\n }\n }, [isIncrementPressed]);\n\n let [isDecrementPressed, setIsDecrementPressed] = useState<'touch' | 'mouse' | null>(null);\n useEffect(() => {\n if (isDecrementPressed === 'touch') {\n onDecrementPressStart(60);\n } else if (isDecrementPressed) {\n onDecrementPressStart(400);\n }\n }, [isDecrementPressed]);\n\n return {\n spinButtonProps: {\n role: 'spinbutton',\n 'aria-valuenow': value !== undefined && !isNaN(value) ? value : undefined,\n 'aria-valuetext': ariaTextValue,\n 'aria-valuemin': minValue,\n 'aria-valuemax': maxValue,\n 'aria-disabled': isDisabled || undefined,\n 'aria-readonly': isReadOnly || undefined,\n 'aria-required': isRequired || undefined,\n onKeyDown,\n onFocus,\n onBlur\n },\n incrementButtonProps: {\n onPressStart: (e) => {\n if (e.pointerType !== 'touch') {\n setIsIncrementPressed('mouse');\n } else {\n if (_async.current) {\n clearAsync();\n }\n\n addGlobalListener(window, 'pointercancel', onPointerCancel, {capture: true});\n isUp.current = false;\n // For touch users, don't trigger a decrement on press start, we'll wait for the press end to trigger it if\n // the control isn't spinning.\n _async.current = window.setTimeout(() => {\n setIsIncrementPressed('touch');\n }, 600);\n }\n addGlobalListener(window, 'contextmenu', cancelContextMenu);\n },\n onPressUp: (e) => {\n if (e.pointerType === 'touch') {\n isUp.current = true;\n }\n clearAsync();\n removeAllGlobalListeners();\n setIsIncrementPressed(null);\n },\n onPressEnd: (e) => {\n if (e.pointerType === 'touch') {\n if (!isSpinning.current && isUp.current) {\n onIncrement?.();\n }\n }\n clearAsync();\n isUp.current = false;\n setIsIncrementPressed(null);\n },\n onFocus,\n onBlur\n },\n decrementButtonProps: {\n onPressStart: (e) => {\n if (e.pointerType !== 'touch') {\n setIsDecrementPressed('mouse');\n } else {\n if (_async.current) {\n clearAsync();\n }\n\n addGlobalListener(window, 'pointercancel', onPointerCancel, {capture: true});\n isUp.current = false;\n // For touch users, don't trigger a decrement on press start, we'll wait for the press end to trigger it if\n // the control isn't spinning.\n _async.current = window.setTimeout(() => {\n setIsDecrementPressed('touch');\n }, 600);\n }\n },\n onPressUp: (e) => {\n if (e.pointerType === 'touch') {\n isUp.current = true;\n }\n clearAsync();\n removeAllGlobalListeners();\n setIsDecrementPressed(null);\n },\n onPressEnd: (e) => {\n if (e.pointerType === 'touch') {\n if (!isSpinning.current && isUp.current) {\n onDecrement?.();\n }\n }\n clearAsync();\n isUp.current = false;\n setIsDecrementPressed(null);\n },\n onFocus,\n onBlur\n }\n };\n}\n"],"names":[],"version":3,"file":"useSpinButton.main.js.map"}
|
package/dist/useSpinButton.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import $5rwhf$intlStringsmodulejs from "./intlStrings.mjs";
|
|
2
2
|
import {clearAnnouncer as $5rwhf$clearAnnouncer, announce as $5rwhf$announce} from "@react-aria/live-announcer";
|
|
3
|
-
import {useRef as $5rwhf$useRef, useEffect as $5rwhf$useEffect} from "react";
|
|
3
|
+
import {useRef as $5rwhf$useRef, useCallback as $5rwhf$useCallback, useEffect as $5rwhf$useEffect, useState as $5rwhf$useState} from "react";
|
|
4
4
|
import {useEffectEvent as $5rwhf$useEffectEvent, useGlobalListeners as $5rwhf$useGlobalListeners} from "@react-aria/utils";
|
|
5
5
|
import {useLocalizedStringFormatter as $5rwhf$useLocalizedStringFormatter} from "@react-aria/i18n";
|
|
6
6
|
|
|
@@ -27,9 +27,16 @@ function $d2e8511e6f209edf$export$e908e06f4b8e3402(props) {
|
|
|
27
27
|
const _async = (0, $5rwhf$useRef)(undefined);
|
|
28
28
|
let { value: value, textValue: textValue, minValue: minValue, maxValue: maxValue, isDisabled: isDisabled, isReadOnly: isReadOnly, isRequired: isRequired, onIncrement: onIncrement, onIncrementPage: onIncrementPage, onDecrement: onDecrement, onDecrementPage: onDecrementPage, onDecrementToMin: onDecrementToMin, onIncrementToMax: onIncrementToMax } = props;
|
|
29
29
|
const stringFormatter = (0, $5rwhf$useLocalizedStringFormatter)((0, ($parcel$interopDefault($5rwhf$intlStringsmodulejs))), '@react-aria/spinbutton');
|
|
30
|
-
|
|
30
|
+
let isSpinning = (0, $5rwhf$useRef)(false);
|
|
31
|
+
const clearAsync = (0, $5rwhf$useCallback)(()=>{
|
|
32
|
+
clearTimeout(_async.current);
|
|
33
|
+
isSpinning.current = false;
|
|
34
|
+
}, []);
|
|
35
|
+
const clearAsyncEvent = (0, $5rwhf$useEffectEvent)(()=>{
|
|
36
|
+
clearAsync();
|
|
37
|
+
});
|
|
31
38
|
(0, $5rwhf$useEffect)(()=>{
|
|
32
|
-
return ()=>
|
|
39
|
+
return ()=>clearAsyncEvent();
|
|
33
40
|
}, []);
|
|
34
41
|
let onKeyDown = (e)=>{
|
|
35
42
|
if (e.ctrlKey || e.metaKey || e.shiftKey || e.altKey || isReadOnly || e.nativeEvent.isComposing) return;
|
|
@@ -96,8 +103,15 @@ function $d2e8511e6f209edf$export$e908e06f4b8e3402(props) {
|
|
|
96
103
|
}, [
|
|
97
104
|
ariaTextValue
|
|
98
105
|
]);
|
|
99
|
-
|
|
106
|
+
// For touch users, if they move their finger like they're scrolling, we don't want to trigger a spin.
|
|
107
|
+
let onPointerCancel = (0, $5rwhf$useCallback)(()=>{
|
|
100
108
|
clearAsync();
|
|
109
|
+
}, [
|
|
110
|
+
clearAsync
|
|
111
|
+
]);
|
|
112
|
+
const onIncrementPressStart = (0, $5rwhf$useEffectEvent)((initialStepDelay)=>{
|
|
113
|
+
clearAsyncEvent();
|
|
114
|
+
isSpinning.current = true;
|
|
101
115
|
onIncrement === null || onIncrement === void 0 ? void 0 : onIncrement();
|
|
102
116
|
// Start spinning after initial delay
|
|
103
117
|
_async.current = window.setTimeout(()=>{
|
|
@@ -105,7 +119,8 @@ function $d2e8511e6f209edf$export$e908e06f4b8e3402(props) {
|
|
|
105
119
|
}, initialStepDelay);
|
|
106
120
|
});
|
|
107
121
|
const onDecrementPressStart = (0, $5rwhf$useEffectEvent)((initialStepDelay)=>{
|
|
108
|
-
|
|
122
|
+
clearAsyncEvent();
|
|
123
|
+
isSpinning.current = true;
|
|
109
124
|
onDecrement === null || onDecrement === void 0 ? void 0 : onDecrement();
|
|
110
125
|
// Start spinning after initial delay
|
|
111
126
|
_async.current = window.setTimeout(()=>{
|
|
@@ -116,6 +131,25 @@ function $d2e8511e6f209edf$export$e908e06f4b8e3402(props) {
|
|
|
116
131
|
e.preventDefault();
|
|
117
132
|
};
|
|
118
133
|
let { addGlobalListener: addGlobalListener, removeAllGlobalListeners: removeAllGlobalListeners } = (0, $5rwhf$useGlobalListeners)();
|
|
134
|
+
// Tracks in touch if the press end event was preceded by a press up.
|
|
135
|
+
// If it wasn't, then we know the finger left the button while still in contact with the screen.
|
|
136
|
+
// This means that the user is trying to scroll or interact in some way that shouldn't trigger
|
|
137
|
+
// an increment or decrement.
|
|
138
|
+
let isUp = (0, $5rwhf$useRef)(false);
|
|
139
|
+
let [isIncrementPressed, setIsIncrementPressed] = (0, $5rwhf$useState)(null);
|
|
140
|
+
(0, $5rwhf$useEffect)(()=>{
|
|
141
|
+
if (isIncrementPressed === 'touch') onIncrementPressStart(60);
|
|
142
|
+
else if (isIncrementPressed) onIncrementPressStart(400);
|
|
143
|
+
}, [
|
|
144
|
+
isIncrementPressed
|
|
145
|
+
]);
|
|
146
|
+
let [isDecrementPressed, setIsDecrementPressed] = (0, $5rwhf$useState)(null);
|
|
147
|
+
(0, $5rwhf$useEffect)(()=>{
|
|
148
|
+
if (isDecrementPressed === 'touch') onDecrementPressStart(60);
|
|
149
|
+
else if (isDecrementPressed) onDecrementPressStart(400);
|
|
150
|
+
}, [
|
|
151
|
+
isDecrementPressed
|
|
152
|
+
]);
|
|
119
153
|
return {
|
|
120
154
|
spinButtonProps: {
|
|
121
155
|
role: 'spinbutton',
|
|
@@ -131,25 +165,68 @@ function $d2e8511e6f209edf$export$e908e06f4b8e3402(props) {
|
|
|
131
165
|
onBlur: onBlur
|
|
132
166
|
},
|
|
133
167
|
incrementButtonProps: {
|
|
134
|
-
onPressStart: ()=>{
|
|
135
|
-
|
|
168
|
+
onPressStart: (e)=>{
|
|
169
|
+
if (e.pointerType !== 'touch') setIsIncrementPressed('mouse');
|
|
170
|
+
else {
|
|
171
|
+
if (_async.current) clearAsync();
|
|
172
|
+
addGlobalListener(window, 'pointercancel', onPointerCancel, {
|
|
173
|
+
capture: true
|
|
174
|
+
});
|
|
175
|
+
isUp.current = false;
|
|
176
|
+
// For touch users, don't trigger a decrement on press start, we'll wait for the press end to trigger it if
|
|
177
|
+
// the control isn't spinning.
|
|
178
|
+
_async.current = window.setTimeout(()=>{
|
|
179
|
+
setIsIncrementPressed('touch');
|
|
180
|
+
}, 600);
|
|
181
|
+
}
|
|
136
182
|
addGlobalListener(window, 'contextmenu', cancelContextMenu);
|
|
137
183
|
},
|
|
138
|
-
|
|
184
|
+
onPressUp: (e)=>{
|
|
185
|
+
if (e.pointerType === 'touch') isUp.current = true;
|
|
139
186
|
clearAsync();
|
|
140
187
|
removeAllGlobalListeners();
|
|
188
|
+
setIsIncrementPressed(null);
|
|
189
|
+
},
|
|
190
|
+
onPressEnd: (e)=>{
|
|
191
|
+
if (e.pointerType === 'touch') {
|
|
192
|
+
if (!isSpinning.current && isUp.current) onIncrement === null || onIncrement === void 0 ? void 0 : onIncrement();
|
|
193
|
+
}
|
|
194
|
+
clearAsync();
|
|
195
|
+
isUp.current = false;
|
|
196
|
+
setIsIncrementPressed(null);
|
|
141
197
|
},
|
|
142
198
|
onFocus: onFocus,
|
|
143
199
|
onBlur: onBlur
|
|
144
200
|
},
|
|
145
201
|
decrementButtonProps: {
|
|
146
|
-
onPressStart: ()=>{
|
|
147
|
-
|
|
148
|
-
|
|
202
|
+
onPressStart: (e)=>{
|
|
203
|
+
if (e.pointerType !== 'touch') setIsDecrementPressed('mouse');
|
|
204
|
+
else {
|
|
205
|
+
if (_async.current) clearAsync();
|
|
206
|
+
addGlobalListener(window, 'pointercancel', onPointerCancel, {
|
|
207
|
+
capture: true
|
|
208
|
+
});
|
|
209
|
+
isUp.current = false;
|
|
210
|
+
// For touch users, don't trigger a decrement on press start, we'll wait for the press end to trigger it if
|
|
211
|
+
// the control isn't spinning.
|
|
212
|
+
_async.current = window.setTimeout(()=>{
|
|
213
|
+
setIsDecrementPressed('touch');
|
|
214
|
+
}, 600);
|
|
215
|
+
}
|
|
149
216
|
},
|
|
150
|
-
|
|
217
|
+
onPressUp: (e)=>{
|
|
218
|
+
if (e.pointerType === 'touch') isUp.current = true;
|
|
151
219
|
clearAsync();
|
|
152
220
|
removeAllGlobalListeners();
|
|
221
|
+
setIsDecrementPressed(null);
|
|
222
|
+
},
|
|
223
|
+
onPressEnd: (e)=>{
|
|
224
|
+
if (e.pointerType === 'touch') {
|
|
225
|
+
if (!isSpinning.current && isUp.current) onDecrement === null || onDecrement === void 0 ? void 0 : onDecrement();
|
|
226
|
+
}
|
|
227
|
+
clearAsync();
|
|
228
|
+
isUp.current = false;
|
|
229
|
+
setIsDecrementPressed(null);
|
|
153
230
|
},
|
|
154
231
|
onFocus: onFocus,
|
|
155
232
|
onBlur: onBlur
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import $5rwhf$intlStringsmodulejs from "./intlStrings.module.js";
|
|
2
2
|
import {clearAnnouncer as $5rwhf$clearAnnouncer, announce as $5rwhf$announce} from "@react-aria/live-announcer";
|
|
3
|
-
import {useRef as $5rwhf$useRef, useEffect as $5rwhf$useEffect} from "react";
|
|
3
|
+
import {useRef as $5rwhf$useRef, useCallback as $5rwhf$useCallback, useEffect as $5rwhf$useEffect, useState as $5rwhf$useState} from "react";
|
|
4
4
|
import {useEffectEvent as $5rwhf$useEffectEvent, useGlobalListeners as $5rwhf$useGlobalListeners} from "@react-aria/utils";
|
|
5
5
|
import {useLocalizedStringFormatter as $5rwhf$useLocalizedStringFormatter} from "@react-aria/i18n";
|
|
6
6
|
|
|
@@ -27,9 +27,16 @@ function $d2e8511e6f209edf$export$e908e06f4b8e3402(props) {
|
|
|
27
27
|
const _async = (0, $5rwhf$useRef)(undefined);
|
|
28
28
|
let { value: value, textValue: textValue, minValue: minValue, maxValue: maxValue, isDisabled: isDisabled, isReadOnly: isReadOnly, isRequired: isRequired, onIncrement: onIncrement, onIncrementPage: onIncrementPage, onDecrement: onDecrement, onDecrementPage: onDecrementPage, onDecrementToMin: onDecrementToMin, onIncrementToMax: onIncrementToMax } = props;
|
|
29
29
|
const stringFormatter = (0, $5rwhf$useLocalizedStringFormatter)((0, ($parcel$interopDefault($5rwhf$intlStringsmodulejs))), '@react-aria/spinbutton');
|
|
30
|
-
|
|
30
|
+
let isSpinning = (0, $5rwhf$useRef)(false);
|
|
31
|
+
const clearAsync = (0, $5rwhf$useCallback)(()=>{
|
|
32
|
+
clearTimeout(_async.current);
|
|
33
|
+
isSpinning.current = false;
|
|
34
|
+
}, []);
|
|
35
|
+
const clearAsyncEvent = (0, $5rwhf$useEffectEvent)(()=>{
|
|
36
|
+
clearAsync();
|
|
37
|
+
});
|
|
31
38
|
(0, $5rwhf$useEffect)(()=>{
|
|
32
|
-
return ()=>
|
|
39
|
+
return ()=>clearAsyncEvent();
|
|
33
40
|
}, []);
|
|
34
41
|
let onKeyDown = (e)=>{
|
|
35
42
|
if (e.ctrlKey || e.metaKey || e.shiftKey || e.altKey || isReadOnly || e.nativeEvent.isComposing) return;
|
|
@@ -96,8 +103,15 @@ function $d2e8511e6f209edf$export$e908e06f4b8e3402(props) {
|
|
|
96
103
|
}, [
|
|
97
104
|
ariaTextValue
|
|
98
105
|
]);
|
|
99
|
-
|
|
106
|
+
// For touch users, if they move their finger like they're scrolling, we don't want to trigger a spin.
|
|
107
|
+
let onPointerCancel = (0, $5rwhf$useCallback)(()=>{
|
|
100
108
|
clearAsync();
|
|
109
|
+
}, [
|
|
110
|
+
clearAsync
|
|
111
|
+
]);
|
|
112
|
+
const onIncrementPressStart = (0, $5rwhf$useEffectEvent)((initialStepDelay)=>{
|
|
113
|
+
clearAsyncEvent();
|
|
114
|
+
isSpinning.current = true;
|
|
101
115
|
onIncrement === null || onIncrement === void 0 ? void 0 : onIncrement();
|
|
102
116
|
// Start spinning after initial delay
|
|
103
117
|
_async.current = window.setTimeout(()=>{
|
|
@@ -105,7 +119,8 @@ function $d2e8511e6f209edf$export$e908e06f4b8e3402(props) {
|
|
|
105
119
|
}, initialStepDelay);
|
|
106
120
|
});
|
|
107
121
|
const onDecrementPressStart = (0, $5rwhf$useEffectEvent)((initialStepDelay)=>{
|
|
108
|
-
|
|
122
|
+
clearAsyncEvent();
|
|
123
|
+
isSpinning.current = true;
|
|
109
124
|
onDecrement === null || onDecrement === void 0 ? void 0 : onDecrement();
|
|
110
125
|
// Start spinning after initial delay
|
|
111
126
|
_async.current = window.setTimeout(()=>{
|
|
@@ -116,6 +131,25 @@ function $d2e8511e6f209edf$export$e908e06f4b8e3402(props) {
|
|
|
116
131
|
e.preventDefault();
|
|
117
132
|
};
|
|
118
133
|
let { addGlobalListener: addGlobalListener, removeAllGlobalListeners: removeAllGlobalListeners } = (0, $5rwhf$useGlobalListeners)();
|
|
134
|
+
// Tracks in touch if the press end event was preceded by a press up.
|
|
135
|
+
// If it wasn't, then we know the finger left the button while still in contact with the screen.
|
|
136
|
+
// This means that the user is trying to scroll or interact in some way that shouldn't trigger
|
|
137
|
+
// an increment or decrement.
|
|
138
|
+
let isUp = (0, $5rwhf$useRef)(false);
|
|
139
|
+
let [isIncrementPressed, setIsIncrementPressed] = (0, $5rwhf$useState)(null);
|
|
140
|
+
(0, $5rwhf$useEffect)(()=>{
|
|
141
|
+
if (isIncrementPressed === 'touch') onIncrementPressStart(60);
|
|
142
|
+
else if (isIncrementPressed) onIncrementPressStart(400);
|
|
143
|
+
}, [
|
|
144
|
+
isIncrementPressed
|
|
145
|
+
]);
|
|
146
|
+
let [isDecrementPressed, setIsDecrementPressed] = (0, $5rwhf$useState)(null);
|
|
147
|
+
(0, $5rwhf$useEffect)(()=>{
|
|
148
|
+
if (isDecrementPressed === 'touch') onDecrementPressStart(60);
|
|
149
|
+
else if (isDecrementPressed) onDecrementPressStart(400);
|
|
150
|
+
}, [
|
|
151
|
+
isDecrementPressed
|
|
152
|
+
]);
|
|
119
153
|
return {
|
|
120
154
|
spinButtonProps: {
|
|
121
155
|
role: 'spinbutton',
|
|
@@ -131,25 +165,68 @@ function $d2e8511e6f209edf$export$e908e06f4b8e3402(props) {
|
|
|
131
165
|
onBlur: onBlur
|
|
132
166
|
},
|
|
133
167
|
incrementButtonProps: {
|
|
134
|
-
onPressStart: ()=>{
|
|
135
|
-
|
|
168
|
+
onPressStart: (e)=>{
|
|
169
|
+
if (e.pointerType !== 'touch') setIsIncrementPressed('mouse');
|
|
170
|
+
else {
|
|
171
|
+
if (_async.current) clearAsync();
|
|
172
|
+
addGlobalListener(window, 'pointercancel', onPointerCancel, {
|
|
173
|
+
capture: true
|
|
174
|
+
});
|
|
175
|
+
isUp.current = false;
|
|
176
|
+
// For touch users, don't trigger a decrement on press start, we'll wait for the press end to trigger it if
|
|
177
|
+
// the control isn't spinning.
|
|
178
|
+
_async.current = window.setTimeout(()=>{
|
|
179
|
+
setIsIncrementPressed('touch');
|
|
180
|
+
}, 600);
|
|
181
|
+
}
|
|
136
182
|
addGlobalListener(window, 'contextmenu', cancelContextMenu);
|
|
137
183
|
},
|
|
138
|
-
|
|
184
|
+
onPressUp: (e)=>{
|
|
185
|
+
if (e.pointerType === 'touch') isUp.current = true;
|
|
139
186
|
clearAsync();
|
|
140
187
|
removeAllGlobalListeners();
|
|
188
|
+
setIsIncrementPressed(null);
|
|
189
|
+
},
|
|
190
|
+
onPressEnd: (e)=>{
|
|
191
|
+
if (e.pointerType === 'touch') {
|
|
192
|
+
if (!isSpinning.current && isUp.current) onIncrement === null || onIncrement === void 0 ? void 0 : onIncrement();
|
|
193
|
+
}
|
|
194
|
+
clearAsync();
|
|
195
|
+
isUp.current = false;
|
|
196
|
+
setIsIncrementPressed(null);
|
|
141
197
|
},
|
|
142
198
|
onFocus: onFocus,
|
|
143
199
|
onBlur: onBlur
|
|
144
200
|
},
|
|
145
201
|
decrementButtonProps: {
|
|
146
|
-
onPressStart: ()=>{
|
|
147
|
-
|
|
148
|
-
|
|
202
|
+
onPressStart: (e)=>{
|
|
203
|
+
if (e.pointerType !== 'touch') setIsDecrementPressed('mouse');
|
|
204
|
+
else {
|
|
205
|
+
if (_async.current) clearAsync();
|
|
206
|
+
addGlobalListener(window, 'pointercancel', onPointerCancel, {
|
|
207
|
+
capture: true
|
|
208
|
+
});
|
|
209
|
+
isUp.current = false;
|
|
210
|
+
// For touch users, don't trigger a decrement on press start, we'll wait for the press end to trigger it if
|
|
211
|
+
// the control isn't spinning.
|
|
212
|
+
_async.current = window.setTimeout(()=>{
|
|
213
|
+
setIsDecrementPressed('touch');
|
|
214
|
+
}, 600);
|
|
215
|
+
}
|
|
149
216
|
},
|
|
150
|
-
|
|
217
|
+
onPressUp: (e)=>{
|
|
218
|
+
if (e.pointerType === 'touch') isUp.current = true;
|
|
151
219
|
clearAsync();
|
|
152
220
|
removeAllGlobalListeners();
|
|
221
|
+
setIsDecrementPressed(null);
|
|
222
|
+
},
|
|
223
|
+
onPressEnd: (e)=>{
|
|
224
|
+
if (e.pointerType === 'touch') {
|
|
225
|
+
if (!isSpinning.current && isUp.current) onDecrement === null || onDecrement === void 0 ? void 0 : onDecrement();
|
|
226
|
+
}
|
|
227
|
+
clearAsync();
|
|
228
|
+
isUp.current = false;
|
|
229
|
+
setIsDecrementPressed(null);
|
|
153
230
|
},
|
|
154
231
|
onFocus: onFocus,
|
|
155
232
|
onBlur: onBlur
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"mappings":";;;;;;;;;;AAAA;;;;;;;;;;CAUC;;;;;AA4BM,SAAS,0CACd,KAAsB;IAEtB,MAAM,SAAS,CAAA,GAAA,aAAK,EAAU;IAC9B,IAAI,SACF,KAAK,aACL,SAAS,YACT,QAAQ,YACR,QAAQ,cACR,UAAU,cACV,UAAU,cACV,UAAU,eACV,WAAW,mBACX,eAAe,eACf,WAAW,mBACX,eAAe,oBACf,gBAAgB,oBAChB,gBAAgB,EACjB,GAAG;IACJ,MAAM,kBAAkB,CAAA,GAAA,kCAA0B,EAAE,CAAA,GAAA,oDAAW,GAAG;IAElE,MAAM,aAAa,IAAM,aAAa,OAAO,OAAO;IAGpD,CAAA,GAAA,gBAAQ,EAAE;QACR,OAAO,IAAM;IACf,GAAG,EAAE;IAEL,IAAI,YAAY,CAAC;QACf,IAAI,EAAE,OAAO,IAAI,EAAE,OAAO,IAAI,EAAE,QAAQ,IAAI,EAAE,MAAM,IAAI,cAAc,EAAE,WAAW,CAAC,WAAW,EAC7F;QAGF,OAAQ,EAAE,GAAG;YACX,KAAK;gBACH,IAAI,iBAAiB;oBACnB,EAAE,cAAc;oBAChB,4BAAA,sCAAA;oBACA;gBACF;YACF,eAAe;YACf,KAAK;YACL,KAAK;gBACH,IAAI,aAAa;oBACf,EAAE,cAAc;oBAChB,wBAAA,kCAAA;gBACF;gBACA;YACF,KAAK;gBACH,IAAI,iBAAiB;oBACnB,EAAE,cAAc;oBAChB,4BAAA,sCAAA;oBACA;gBACF;YACF,cAAc;YACd,KAAK;YACL,KAAK;gBACH,IAAI,aAAa;oBACf,EAAE,cAAc;oBAChB,wBAAA,kCAAA;gBACF;gBACA;YACF,KAAK;gBACH,IAAI,kBAAkB;oBACpB,EAAE,cAAc;oBAChB,6BAAA,uCAAA;gBACF;gBACA;YACF,KAAK;gBACH,IAAI,kBAAkB;oBACpB,EAAE,cAAc;oBAChB,6BAAA,uCAAA;gBACF;gBACA;QACJ;IACF;IAEA,IAAI,YAAY,CAAA,GAAA,aAAK,EAAE;IACvB,IAAI,UAAU;QACZ,UAAU,OAAO,GAAG;IACtB;IAEA,IAAI,SAAS;QACX,UAAU,OAAO,GAAG;IACtB;IAEA,kEAAkE;IAClE,8GAA8G;IAC9G,sHAAsH;IACtH,4HAA4H;IAC5H,IAAI,gBAAgB,cAAc,KAAK,gBAAgB,MAAM,CAAC,WAAW,AAAC,CAAA,aAAa,GAAG,OAAO,AAAD,EAAG,OAAO,CAAC,KAAK;IAEhH,CAAA,GAAA,gBAAQ,EAAE;QACR,IAAI,UAAU,OAAO,EAAE;YACrB,CAAA,GAAA,qBAAa,EAAE;YACf,CAAA,GAAA,eAAO,EAAE,eAAe;QAC1B;IACF,GAAG;QAAC;KAAc;IAElB,MAAM,wBAAwB,CAAA,GAAA,qBAAa,EACzC,CAAC;QACC;QACA,wBAAA,kCAAA;QACA,qCAAqC;QACrC,OAAO,OAAO,GAAG,OAAO,UAAU,CAChC;YACE,IAAI,AAAC,aAAa,aAAa,MAAM,aAAe,UAAU,aAAa,MAAM,UAAW,QAAQ,UAClG,sBAAsB;QAE1B,GACA;IAEJ;IAGF,MAAM,wBAAwB,CAAA,GAAA,qBAAa,EACzC,CAAC;QACC;QACA,wBAAA,kCAAA;QACA,qCAAqC;QACrC,OAAO,OAAO,GAAG,OAAO,UAAU,CAChC;YACE,IAAI,AAAC,aAAa,aAAa,MAAM,aAAe,UAAU,aAAa,MAAM,UAAW,QAAQ,UAClG,sBAAsB;QAE1B,GACA;IAEJ;IAGF,IAAI,oBAAoB,CAAC;QACvB,EAAE,cAAc;IAClB;IAEA,IAAI,qBAAC,iBAAiB,4BAAE,wBAAwB,EAAC,GAAG,CAAA,GAAA,yBAAiB;IAErE,OAAO;QACL,iBAAiB;YACf,MAAM;YACN,iBAAiB,UAAU,aAAa,CAAC,MAAM,SAAS,QAAQ;YAChE,kBAAkB;YAClB,iBAAiB;YACjB,iBAAiB;YACjB,iBAAiB,cAAc;YAC/B,iBAAiB,cAAc;YAC/B,iBAAiB,cAAc;uBAC/B;qBACA;oBACA;QACF;QACA,sBAAsB;YACpB,cAAc;gBACZ,sBAAsB;gBACtB,kBAAkB,QAAQ,eAAe;YAC3C;YACA,YAAY;gBACV;gBACA;YACF;qBACA;oBACA;QACF;QACA,sBAAsB;YACpB,cAAc;gBACZ,sBAAsB;gBACtB,kBAAkB,QAAQ,eAAe;YAC3C;YACA,YAAY;gBACV;gBACA;YACF;qBACA;oBACA;QACF;IACF;AACF","sources":["packages/@react-aria/spinbutton/src/useSpinButton.ts"],"sourcesContent":["/*\n * Copyright 2020 Adobe. All rights reserved.\n * This file is licensed to you under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License. You may obtain a copy\n * of the License at http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software distributed under\n * the License is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS\n * OF ANY KIND, either express or implied. See the License for the specific language\n * governing permissions and limitations under the License.\n */\n\nimport {announce, clearAnnouncer} from '@react-aria/live-announcer';\nimport {AriaButtonProps} from '@react-types/button';\nimport {DOMAttributes, InputBase, RangeInputBase, Validation, ValueBase} from '@react-types/shared';\n// @ts-ignore\nimport intlMessages from '../intl/*.json';\nimport {useEffect, useRef} from 'react';\nimport {useEffectEvent, useGlobalListeners} from '@react-aria/utils';\nimport {useLocalizedStringFormatter} from '@react-aria/i18n';\n\n\nexport interface SpinButtonProps extends InputBase, Validation<number>, ValueBase<number>, RangeInputBase<number> {\n textValue?: string,\n onIncrement?: () => void,\n onIncrementPage?: () => void,\n onDecrement?: () => void,\n onDecrementPage?: () => void,\n onDecrementToMin?: () => void,\n onIncrementToMax?: () => void\n}\n\nexport interface SpinbuttonAria {\n spinButtonProps: DOMAttributes,\n incrementButtonProps: AriaButtonProps,\n decrementButtonProps: AriaButtonProps\n}\n\nexport function useSpinButton(\n props: SpinButtonProps\n): SpinbuttonAria {\n const _async = useRef<number>(undefined);\n let {\n value,\n textValue,\n minValue,\n maxValue,\n isDisabled,\n isReadOnly,\n isRequired,\n onIncrement,\n onIncrementPage,\n onDecrement,\n onDecrementPage,\n onDecrementToMin,\n onIncrementToMax\n } = props;\n const stringFormatter = useLocalizedStringFormatter(intlMessages, '@react-aria/spinbutton');\n\n const clearAsync = () => clearTimeout(_async.current);\n\n\n useEffect(() => {\n return () => clearAsync();\n }, []);\n\n let onKeyDown = (e) => {\n if (e.ctrlKey || e.metaKey || e.shiftKey || e.altKey || isReadOnly || e.nativeEvent.isComposing) {\n return;\n }\n\n switch (e.key) {\n case 'PageUp':\n if (onIncrementPage) {\n e.preventDefault();\n onIncrementPage?.();\n break;\n }\n // fallthrough!\n case 'ArrowUp':\n case 'Up':\n if (onIncrement) {\n e.preventDefault();\n onIncrement?.();\n }\n break;\n case 'PageDown':\n if (onDecrementPage) {\n e.preventDefault();\n onDecrementPage?.();\n break;\n }\n // fallthrough\n case 'ArrowDown':\n case 'Down':\n if (onDecrement) {\n e.preventDefault();\n onDecrement?.();\n }\n break;\n case 'Home':\n if (onDecrementToMin) {\n e.preventDefault();\n onDecrementToMin?.();\n }\n break;\n case 'End':\n if (onIncrementToMax) {\n e.preventDefault();\n onIncrementToMax?.();\n }\n break;\n }\n };\n\n let isFocused = useRef(false);\n let onFocus = () => {\n isFocused.current = true;\n };\n\n let onBlur = () => {\n isFocused.current = false;\n };\n\n // Replace Unicode hyphen-minus (U+002D) with minus sign (U+2212).\n // This ensures that macOS VoiceOver announces it as \"minus\" even with other characters between the minus sign\n // and the number (e.g. currency symbol). Otherwise it announces nothing because it assumes the character is a hyphen.\n // In addition, replace the empty string with the word \"Empty\" so that iOS VoiceOver does not read \"50%\" for an empty field.\n let ariaTextValue = textValue === '' ? stringFormatter.format('Empty') : (textValue || `${value}`).replace('-', '\\u2212');\n\n useEffect(() => {\n if (isFocused.current) {\n clearAnnouncer('assertive');\n announce(ariaTextValue, 'assertive');\n }\n }, [ariaTextValue]);\n\n const onIncrementPressStart = useEffectEvent(\n (initialStepDelay: number) => {\n clearAsync();\n onIncrement?.();\n // Start spinning after initial delay\n _async.current = window.setTimeout(\n () => {\n if ((maxValue === undefined || isNaN(maxValue)) || (value === undefined || isNaN(value)) || value < maxValue) {\n onIncrementPressStart(60);\n }\n },\n initialStepDelay\n );\n }\n );\n\n const onDecrementPressStart = useEffectEvent(\n (initialStepDelay: number) => {\n clearAsync();\n onDecrement?.();\n // Start spinning after initial delay\n _async.current = window.setTimeout(\n () => {\n if ((minValue === undefined || isNaN(minValue)) || (value === undefined || isNaN(value)) || value > minValue) {\n onDecrementPressStart(60);\n }\n },\n initialStepDelay\n );\n }\n );\n\n let cancelContextMenu = (e) => {\n e.preventDefault();\n };\n\n let {addGlobalListener, removeAllGlobalListeners} = useGlobalListeners();\n\n return {\n spinButtonProps: {\n role: 'spinbutton',\n 'aria-valuenow': value !== undefined && !isNaN(value) ? value : undefined,\n 'aria-valuetext': ariaTextValue,\n 'aria-valuemin': minValue,\n 'aria-valuemax': maxValue,\n 'aria-disabled': isDisabled || undefined,\n 'aria-readonly': isReadOnly || undefined,\n 'aria-required': isRequired || undefined,\n onKeyDown,\n onFocus,\n onBlur\n },\n incrementButtonProps: {\n onPressStart: () => {\n onIncrementPressStart(400);\n addGlobalListener(window, 'contextmenu', cancelContextMenu);\n },\n onPressEnd: () => {\n clearAsync();\n removeAllGlobalListeners();\n },\n onFocus,\n onBlur\n },\n decrementButtonProps: {\n onPressStart: () => {\n onDecrementPressStart(400);\n addGlobalListener(window, 'contextmenu', cancelContextMenu);\n },\n onPressEnd: () => {\n clearAsync();\n removeAllGlobalListeners();\n },\n onFocus,\n onBlur\n }\n };\n}\n"],"names":[],"version":3,"file":"useSpinButton.module.js.map"}
|
|
1
|
+
{"mappings":"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","sources":["packages/@react-aria/spinbutton/src/useSpinButton.ts"],"sourcesContent":["/*\n * Copyright 2020 Adobe. All rights reserved.\n * This file is licensed to you under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License. You may obtain a copy\n * of the License at http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software distributed under\n * the License is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS\n * OF ANY KIND, either express or implied. See the License for the specific language\n * governing permissions and limitations under the License.\n */\n\nimport {announce, clearAnnouncer} from '@react-aria/live-announcer';\nimport {AriaButtonProps} from '@react-types/button';\nimport {DOMAttributes, InputBase, RangeInputBase, Validation, ValueBase} from '@react-types/shared';\n// @ts-ignore\nimport intlMessages from '../intl/*.json';\nimport {useCallback, useEffect, useRef, useState} from 'react';\nimport {useEffectEvent, useGlobalListeners} from '@react-aria/utils';\nimport {useLocalizedStringFormatter} from '@react-aria/i18n';\n\n\nexport interface SpinButtonProps extends InputBase, Validation<number>, ValueBase<number>, RangeInputBase<number> {\n textValue?: string,\n onIncrement?: () => void,\n onIncrementPage?: () => void,\n onDecrement?: () => void,\n onDecrementPage?: () => void,\n onDecrementToMin?: () => void,\n onIncrementToMax?: () => void\n}\n\nexport interface SpinbuttonAria {\n spinButtonProps: DOMAttributes,\n incrementButtonProps: AriaButtonProps,\n decrementButtonProps: AriaButtonProps\n}\n\nexport function useSpinButton(\n props: SpinButtonProps\n): SpinbuttonAria {\n const _async = useRef<number>(undefined);\n let {\n value,\n textValue,\n minValue,\n maxValue,\n isDisabled,\n isReadOnly,\n isRequired,\n onIncrement,\n onIncrementPage,\n onDecrement,\n onDecrementPage,\n onDecrementToMin,\n onIncrementToMax\n } = props;\n const stringFormatter = useLocalizedStringFormatter(intlMessages, '@react-aria/spinbutton');\n\n let isSpinning = useRef(false);\n const clearAsync = useCallback(() => {\n clearTimeout(_async.current);\n isSpinning.current = false;\n }, []);\n const clearAsyncEvent = useEffectEvent(() => {\n clearAsync();\n });\n\n useEffect(() => {\n return () => clearAsyncEvent();\n }, []);\n\n let onKeyDown = (e) => {\n if (e.ctrlKey || e.metaKey || e.shiftKey || e.altKey || isReadOnly || e.nativeEvent.isComposing) {\n return;\n }\n\n switch (e.key) {\n case 'PageUp':\n if (onIncrementPage) {\n e.preventDefault();\n onIncrementPage?.();\n break;\n }\n // fallthrough!\n case 'ArrowUp':\n case 'Up':\n if (onIncrement) {\n e.preventDefault();\n onIncrement?.();\n }\n break;\n case 'PageDown':\n if (onDecrementPage) {\n e.preventDefault();\n onDecrementPage?.();\n break;\n }\n // fallthrough\n case 'ArrowDown':\n case 'Down':\n if (onDecrement) {\n e.preventDefault();\n onDecrement?.();\n }\n break;\n case 'Home':\n if (onDecrementToMin) {\n e.preventDefault();\n onDecrementToMin?.();\n }\n break;\n case 'End':\n if (onIncrementToMax) {\n e.preventDefault();\n onIncrementToMax?.();\n }\n break;\n }\n };\n\n let isFocused = useRef(false);\n let onFocus = () => {\n isFocused.current = true;\n };\n\n let onBlur = () => {\n isFocused.current = false;\n };\n\n // Replace Unicode hyphen-minus (U+002D) with minus sign (U+2212).\n // This ensures that macOS VoiceOver announces it as \"minus\" even with other characters between the minus sign\n // and the number (e.g. currency symbol). Otherwise it announces nothing because it assumes the character is a hyphen.\n // In addition, replace the empty string with the word \"Empty\" so that iOS VoiceOver does not read \"50%\" for an empty field.\n let ariaTextValue = textValue === '' ? stringFormatter.format('Empty') : (textValue || `${value}`).replace('-', '\\u2212');\n\n useEffect(() => {\n if (isFocused.current) {\n clearAnnouncer('assertive');\n announce(ariaTextValue, 'assertive');\n }\n }, [ariaTextValue]);\n\n // For touch users, if they move their finger like they're scrolling, we don't want to trigger a spin.\n let onPointerCancel = useCallback(() => {\n clearAsync();\n }, [clearAsync]);\n\n const onIncrementPressStart = useEffectEvent(\n (initialStepDelay: number) => {\n clearAsyncEvent();\n isSpinning.current = true;\n onIncrement?.();\n // Start spinning after initial delay\n _async.current = window.setTimeout(\n () => {\n if ((maxValue === undefined || isNaN(maxValue)) || (value === undefined || isNaN(value)) || value < maxValue) {\n onIncrementPressStart(60);\n }\n },\n initialStepDelay\n );\n }\n );\n\n const onDecrementPressStart = useEffectEvent(\n (initialStepDelay: number) => {\n clearAsyncEvent();\n isSpinning.current = true;\n onDecrement?.();\n // Start spinning after initial delay\n _async.current = window.setTimeout(\n () => {\n if ((minValue === undefined || isNaN(minValue)) || (value === undefined || isNaN(value)) || value > minValue) {\n onDecrementPressStart(60);\n }\n },\n initialStepDelay\n );\n }\n );\n\n let cancelContextMenu = (e) => {\n e.preventDefault();\n };\n\n let {addGlobalListener, removeAllGlobalListeners} = useGlobalListeners();\n\n // Tracks in touch if the press end event was preceded by a press up.\n // If it wasn't, then we know the finger left the button while still in contact with the screen.\n // This means that the user is trying to scroll or interact in some way that shouldn't trigger\n // an increment or decrement.\n let isUp = useRef(false);\n\n let [isIncrementPressed, setIsIncrementPressed] = useState<'touch' | 'mouse' | null>(null);\n useEffect(() => {\n if (isIncrementPressed === 'touch') {\n onIncrementPressStart(60);\n } else if (isIncrementPressed) {\n onIncrementPressStart(400);\n }\n }, [isIncrementPressed]);\n\n let [isDecrementPressed, setIsDecrementPressed] = useState<'touch' | 'mouse' | null>(null);\n useEffect(() => {\n if (isDecrementPressed === 'touch') {\n onDecrementPressStart(60);\n } else if (isDecrementPressed) {\n onDecrementPressStart(400);\n }\n }, [isDecrementPressed]);\n\n return {\n spinButtonProps: {\n role: 'spinbutton',\n 'aria-valuenow': value !== undefined && !isNaN(value) ? value : undefined,\n 'aria-valuetext': ariaTextValue,\n 'aria-valuemin': minValue,\n 'aria-valuemax': maxValue,\n 'aria-disabled': isDisabled || undefined,\n 'aria-readonly': isReadOnly || undefined,\n 'aria-required': isRequired || undefined,\n onKeyDown,\n onFocus,\n onBlur\n },\n incrementButtonProps: {\n onPressStart: (e) => {\n if (e.pointerType !== 'touch') {\n setIsIncrementPressed('mouse');\n } else {\n if (_async.current) {\n clearAsync();\n }\n\n addGlobalListener(window, 'pointercancel', onPointerCancel, {capture: true});\n isUp.current = false;\n // For touch users, don't trigger a decrement on press start, we'll wait for the press end to trigger it if\n // the control isn't spinning.\n _async.current = window.setTimeout(() => {\n setIsIncrementPressed('touch');\n }, 600);\n }\n addGlobalListener(window, 'contextmenu', cancelContextMenu);\n },\n onPressUp: (e) => {\n if (e.pointerType === 'touch') {\n isUp.current = true;\n }\n clearAsync();\n removeAllGlobalListeners();\n setIsIncrementPressed(null);\n },\n onPressEnd: (e) => {\n if (e.pointerType === 'touch') {\n if (!isSpinning.current && isUp.current) {\n onIncrement?.();\n }\n }\n clearAsync();\n isUp.current = false;\n setIsIncrementPressed(null);\n },\n onFocus,\n onBlur\n },\n decrementButtonProps: {\n onPressStart: (e) => {\n if (e.pointerType !== 'touch') {\n setIsDecrementPressed('mouse');\n } else {\n if (_async.current) {\n clearAsync();\n }\n\n addGlobalListener(window, 'pointercancel', onPointerCancel, {capture: true});\n isUp.current = false;\n // For touch users, don't trigger a decrement on press start, we'll wait for the press end to trigger it if\n // the control isn't spinning.\n _async.current = window.setTimeout(() => {\n setIsDecrementPressed('touch');\n }, 600);\n }\n },\n onPressUp: (e) => {\n if (e.pointerType === 'touch') {\n isUp.current = true;\n }\n clearAsync();\n removeAllGlobalListeners();\n setIsDecrementPressed(null);\n },\n onPressEnd: (e) => {\n if (e.pointerType === 'touch') {\n if (!isSpinning.current && isUp.current) {\n onDecrement?.();\n }\n }\n clearAsync();\n isUp.current = false;\n setIsDecrementPressed(null);\n },\n onFocus,\n onBlur\n }\n };\n}\n"],"names":[],"version":3,"file":"useSpinButton.module.js.map"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@react-aria/spinbutton",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.7.0",
|
|
4
4
|
"description": "Spectrum UI components in React",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"main": "dist/main.js",
|
|
@@ -26,11 +26,11 @@
|
|
|
26
26
|
"url": "https://github.com/adobe/react-spectrum"
|
|
27
27
|
},
|
|
28
28
|
"dependencies": {
|
|
29
|
-
"@react-aria/i18n": "^3.12.
|
|
29
|
+
"@react-aria/i18n": "^3.12.14",
|
|
30
30
|
"@react-aria/live-announcer": "^3.4.4",
|
|
31
|
-
"@react-aria/utils": "^3.
|
|
32
|
-
"@react-types/button": "^3.14.
|
|
33
|
-
"@react-types/shared": "^3.32.
|
|
31
|
+
"@react-aria/utils": "^3.32.0",
|
|
32
|
+
"@react-types/button": "^3.14.1",
|
|
33
|
+
"@react-types/shared": "^3.32.1",
|
|
34
34
|
"@swc/helpers": "^0.5.0"
|
|
35
35
|
},
|
|
36
36
|
"peerDependencies": {
|
|
@@ -40,5 +40,5 @@
|
|
|
40
40
|
"publishConfig": {
|
|
41
41
|
"access": "public"
|
|
42
42
|
},
|
|
43
|
-
"gitHead": "
|
|
43
|
+
"gitHead": "4d838da5bfe36abb35aed166995a9ef63825370f"
|
|
44
44
|
}
|
package/src/useSpinButton.ts
CHANGED
|
@@ -15,7 +15,7 @@ import {AriaButtonProps} from '@react-types/button';
|
|
|
15
15
|
import {DOMAttributes, InputBase, RangeInputBase, Validation, ValueBase} from '@react-types/shared';
|
|
16
16
|
// @ts-ignore
|
|
17
17
|
import intlMessages from '../intl/*.json';
|
|
18
|
-
import {useEffect, useRef} from 'react';
|
|
18
|
+
import {useCallback, useEffect, useRef, useState} from 'react';
|
|
19
19
|
import {useEffectEvent, useGlobalListeners} from '@react-aria/utils';
|
|
20
20
|
import {useLocalizedStringFormatter} from '@react-aria/i18n';
|
|
21
21
|
|
|
@@ -57,11 +57,17 @@ export function useSpinButton(
|
|
|
57
57
|
} = props;
|
|
58
58
|
const stringFormatter = useLocalizedStringFormatter(intlMessages, '@react-aria/spinbutton');
|
|
59
59
|
|
|
60
|
-
|
|
61
|
-
|
|
60
|
+
let isSpinning = useRef(false);
|
|
61
|
+
const clearAsync = useCallback(() => {
|
|
62
|
+
clearTimeout(_async.current);
|
|
63
|
+
isSpinning.current = false;
|
|
64
|
+
}, []);
|
|
65
|
+
const clearAsyncEvent = useEffectEvent(() => {
|
|
66
|
+
clearAsync();
|
|
67
|
+
});
|
|
62
68
|
|
|
63
69
|
useEffect(() => {
|
|
64
|
-
return () =>
|
|
70
|
+
return () => clearAsyncEvent();
|
|
65
71
|
}, []);
|
|
66
72
|
|
|
67
73
|
let onKeyDown = (e) => {
|
|
@@ -135,9 +141,15 @@ export function useSpinButton(
|
|
|
135
141
|
}
|
|
136
142
|
}, [ariaTextValue]);
|
|
137
143
|
|
|
144
|
+
// For touch users, if they move their finger like they're scrolling, we don't want to trigger a spin.
|
|
145
|
+
let onPointerCancel = useCallback(() => {
|
|
146
|
+
clearAsync();
|
|
147
|
+
}, [clearAsync]);
|
|
148
|
+
|
|
138
149
|
const onIncrementPressStart = useEffectEvent(
|
|
139
150
|
(initialStepDelay: number) => {
|
|
140
|
-
|
|
151
|
+
clearAsyncEvent();
|
|
152
|
+
isSpinning.current = true;
|
|
141
153
|
onIncrement?.();
|
|
142
154
|
// Start spinning after initial delay
|
|
143
155
|
_async.current = window.setTimeout(
|
|
@@ -153,7 +165,8 @@ export function useSpinButton(
|
|
|
153
165
|
|
|
154
166
|
const onDecrementPressStart = useEffectEvent(
|
|
155
167
|
(initialStepDelay: number) => {
|
|
156
|
-
|
|
168
|
+
clearAsyncEvent();
|
|
169
|
+
isSpinning.current = true;
|
|
157
170
|
onDecrement?.();
|
|
158
171
|
// Start spinning after initial delay
|
|
159
172
|
_async.current = window.setTimeout(
|
|
@@ -173,6 +186,30 @@ export function useSpinButton(
|
|
|
173
186
|
|
|
174
187
|
let {addGlobalListener, removeAllGlobalListeners} = useGlobalListeners();
|
|
175
188
|
|
|
189
|
+
// Tracks in touch if the press end event was preceded by a press up.
|
|
190
|
+
// If it wasn't, then we know the finger left the button while still in contact with the screen.
|
|
191
|
+
// This means that the user is trying to scroll or interact in some way that shouldn't trigger
|
|
192
|
+
// an increment or decrement.
|
|
193
|
+
let isUp = useRef(false);
|
|
194
|
+
|
|
195
|
+
let [isIncrementPressed, setIsIncrementPressed] = useState<'touch' | 'mouse' | null>(null);
|
|
196
|
+
useEffect(() => {
|
|
197
|
+
if (isIncrementPressed === 'touch') {
|
|
198
|
+
onIncrementPressStart(60);
|
|
199
|
+
} else if (isIncrementPressed) {
|
|
200
|
+
onIncrementPressStart(400);
|
|
201
|
+
}
|
|
202
|
+
}, [isIncrementPressed]);
|
|
203
|
+
|
|
204
|
+
let [isDecrementPressed, setIsDecrementPressed] = useState<'touch' | 'mouse' | null>(null);
|
|
205
|
+
useEffect(() => {
|
|
206
|
+
if (isDecrementPressed === 'touch') {
|
|
207
|
+
onDecrementPressStart(60);
|
|
208
|
+
} else if (isDecrementPressed) {
|
|
209
|
+
onDecrementPressStart(400);
|
|
210
|
+
}
|
|
211
|
+
}, [isDecrementPressed]);
|
|
212
|
+
|
|
176
213
|
return {
|
|
177
214
|
spinButtonProps: {
|
|
178
215
|
role: 'spinbutton',
|
|
@@ -188,25 +225,80 @@ export function useSpinButton(
|
|
|
188
225
|
onBlur
|
|
189
226
|
},
|
|
190
227
|
incrementButtonProps: {
|
|
191
|
-
onPressStart: () => {
|
|
192
|
-
|
|
228
|
+
onPressStart: (e) => {
|
|
229
|
+
if (e.pointerType !== 'touch') {
|
|
230
|
+
setIsIncrementPressed('mouse');
|
|
231
|
+
} else {
|
|
232
|
+
if (_async.current) {
|
|
233
|
+
clearAsync();
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
addGlobalListener(window, 'pointercancel', onPointerCancel, {capture: true});
|
|
237
|
+
isUp.current = false;
|
|
238
|
+
// For touch users, don't trigger a decrement on press start, we'll wait for the press end to trigger it if
|
|
239
|
+
// the control isn't spinning.
|
|
240
|
+
_async.current = window.setTimeout(() => {
|
|
241
|
+
setIsIncrementPressed('touch');
|
|
242
|
+
}, 600);
|
|
243
|
+
}
|
|
193
244
|
addGlobalListener(window, 'contextmenu', cancelContextMenu);
|
|
194
245
|
},
|
|
195
|
-
|
|
246
|
+
onPressUp: (e) => {
|
|
247
|
+
if (e.pointerType === 'touch') {
|
|
248
|
+
isUp.current = true;
|
|
249
|
+
}
|
|
196
250
|
clearAsync();
|
|
197
251
|
removeAllGlobalListeners();
|
|
252
|
+
setIsIncrementPressed(null);
|
|
253
|
+
},
|
|
254
|
+
onPressEnd: (e) => {
|
|
255
|
+
if (e.pointerType === 'touch') {
|
|
256
|
+
if (!isSpinning.current && isUp.current) {
|
|
257
|
+
onIncrement?.();
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
clearAsync();
|
|
261
|
+
isUp.current = false;
|
|
262
|
+
setIsIncrementPressed(null);
|
|
198
263
|
},
|
|
199
264
|
onFocus,
|
|
200
265
|
onBlur
|
|
201
266
|
},
|
|
202
267
|
decrementButtonProps: {
|
|
203
|
-
onPressStart: () => {
|
|
204
|
-
|
|
205
|
-
|
|
268
|
+
onPressStart: (e) => {
|
|
269
|
+
if (e.pointerType !== 'touch') {
|
|
270
|
+
setIsDecrementPressed('mouse');
|
|
271
|
+
} else {
|
|
272
|
+
if (_async.current) {
|
|
273
|
+
clearAsync();
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
addGlobalListener(window, 'pointercancel', onPointerCancel, {capture: true});
|
|
277
|
+
isUp.current = false;
|
|
278
|
+
// For touch users, don't trigger a decrement on press start, we'll wait for the press end to trigger it if
|
|
279
|
+
// the control isn't spinning.
|
|
280
|
+
_async.current = window.setTimeout(() => {
|
|
281
|
+
setIsDecrementPressed('touch');
|
|
282
|
+
}, 600);
|
|
283
|
+
}
|
|
206
284
|
},
|
|
207
|
-
|
|
285
|
+
onPressUp: (e) => {
|
|
286
|
+
if (e.pointerType === 'touch') {
|
|
287
|
+
isUp.current = true;
|
|
288
|
+
}
|
|
208
289
|
clearAsync();
|
|
209
290
|
removeAllGlobalListeners();
|
|
291
|
+
setIsDecrementPressed(null);
|
|
292
|
+
},
|
|
293
|
+
onPressEnd: (e) => {
|
|
294
|
+
if (e.pointerType === 'touch') {
|
|
295
|
+
if (!isSpinning.current && isUp.current) {
|
|
296
|
+
onDecrement?.();
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
clearAsync();
|
|
300
|
+
isUp.current = false;
|
|
301
|
+
setIsDecrementPressed(null);
|
|
210
302
|
},
|
|
211
303
|
onFocus,
|
|
212
304
|
onBlur
|