@react-aria/spinbutton 3.6.19 → 3.7.1

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.
@@ -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,CA8KhB","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"}
1
+ {"mappings":";;AAwBA,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,CA+PhB","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"}
@@ -29,13 +29,21 @@ $parcel$export(module.exports, "useSpinButton", () => $37bbd4c129023f61$export$e
29
29
 
30
30
 
31
31
 
32
+ const $37bbd4c129023f61$var$noop = ()=>{};
32
33
  function $37bbd4c129023f61$export$e908e06f4b8e3402(props) {
33
34
  const _async = (0, $2pZbw$react.useRef)(undefined);
34
35
  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
36
  const stringFormatter = (0, $2pZbw$reactariai18n.useLocalizedStringFormatter)((0, ($parcel$interopDefault($cb4b786159079747$exports))), '@react-aria/spinbutton');
36
- const clearAsync = ()=>clearTimeout(_async.current);
37
+ let isSpinning = (0, $2pZbw$react.useRef)(false);
38
+ const clearAsync = (0, $2pZbw$react.useCallback)(()=>{
39
+ clearTimeout(_async.current);
40
+ isSpinning.current = false;
41
+ }, []);
42
+ const clearAsyncEvent = (0, $2pZbw$reactariautils.useEffectEvent)(()=>{
43
+ clearAsync();
44
+ });
37
45
  (0, $2pZbw$react.useEffect)(()=>{
38
- return ()=>clearAsync();
46
+ return ()=>clearAsyncEvent();
39
47
  }, []);
40
48
  let onKeyDown = (e)=>{
41
49
  if (e.ctrlKey || e.metaKey || e.shiftKey || e.altKey || isReadOnly || e.nativeEvent.isComposing) return;
@@ -102,26 +110,61 @@ function $37bbd4c129023f61$export$e908e06f4b8e3402(props) {
102
110
  }, [
103
111
  ariaTextValue
104
112
  ]);
105
- const onIncrementPressStart = (0, $2pZbw$reactariautils.useEffectEvent)((initialStepDelay)=>{
113
+ // For touch users, if they move their finger like they're scrolling, we don't want to trigger a spin.
114
+ let onPointerCancel = (0, $2pZbw$react.useCallback)(()=>{
106
115
  clearAsync();
107
- onIncrement === null || onIncrement === void 0 ? void 0 : onIncrement();
116
+ }, [
117
+ clearAsync
118
+ ]);
119
+ const onIncrementEvent = (0, $2pZbw$reactariautils.useEffectEvent)(onIncrement !== null && onIncrement !== void 0 ? onIncrement : $37bbd4c129023f61$var$noop);
120
+ const onDecrementEvent = (0, $2pZbw$reactariautils.useEffectEvent)(onDecrement !== null && onDecrement !== void 0 ? onDecrement : $37bbd4c129023f61$var$noop);
121
+ const stepUpEvent = (0, $2pZbw$reactariautils.useEffectEvent)(()=>{
122
+ if (maxValue === undefined || isNaN(maxValue) || value === undefined || isNaN(value) || value < maxValue) {
123
+ onIncrementEvent();
124
+ onIncrementPressStartEvent(60);
125
+ }
126
+ });
127
+ const onIncrementPressStartEvent = (0, $2pZbw$reactariautils.useEffectEvent)((initialStepDelay)=>{
128
+ clearAsyncEvent();
129
+ isSpinning.current = true;
108
130
  // Start spinning after initial delay
109
- _async.current = window.setTimeout(()=>{
110
- if (maxValue === undefined || isNaN(maxValue) || value === undefined || isNaN(value) || value < maxValue) onIncrementPressStart(60);
111
- }, initialStepDelay);
131
+ _async.current = window.setTimeout(stepUpEvent, initialStepDelay);
112
132
  });
113
- const onDecrementPressStart = (0, $2pZbw$reactariautils.useEffectEvent)((initialStepDelay)=>{
114
- clearAsync();
115
- onDecrement === null || onDecrement === void 0 ? void 0 : onDecrement();
133
+ const stepDownEvent = (0, $2pZbw$reactariautils.useEffectEvent)(()=>{
134
+ if (minValue === undefined || isNaN(minValue) || value === undefined || isNaN(value) || value > minValue) {
135
+ onDecrementEvent();
136
+ onDecrementPressStartEvent(60);
137
+ }
138
+ });
139
+ const onDecrementPressStartEvent = (0, $2pZbw$reactariautils.useEffectEvent)((initialStepDelay)=>{
140
+ clearAsyncEvent();
141
+ isSpinning.current = true;
116
142
  // Start spinning after initial delay
117
- _async.current = window.setTimeout(()=>{
118
- if (minValue === undefined || isNaN(minValue) || value === undefined || isNaN(value) || value > minValue) onDecrementPressStart(60);
119
- }, initialStepDelay);
143
+ _async.current = window.setTimeout(stepDownEvent, initialStepDelay);
120
144
  });
121
145
  let cancelContextMenu = (e)=>{
122
146
  e.preventDefault();
123
147
  };
124
148
  let { addGlobalListener: addGlobalListener, removeAllGlobalListeners: removeAllGlobalListeners } = (0, $2pZbw$reactariautils.useGlobalListeners)();
149
+ // Tracks in touch if the press end event was preceded by a press up.
150
+ // If it wasn't, then we know the finger left the button while still in contact with the screen.
151
+ // This means that the user is trying to scroll or interact in some way that shouldn't trigger
152
+ // an increment or decrement.
153
+ let isUp = (0, $2pZbw$react.useRef)(false);
154
+ let [isIncrementPressed, setIsIncrementPressed] = (0, $2pZbw$react.useState)(null);
155
+ (0, $2pZbw$react.useEffect)(()=>{
156
+ if (isIncrementPressed === 'touch') onIncrementPressStartEvent(600);
157
+ else if (isIncrementPressed) onIncrementPressStartEvent(400);
158
+ }, [
159
+ isIncrementPressed
160
+ ]);
161
+ let [isDecrementPressed, setIsDecrementPressed] = (0, $2pZbw$react.useState)(null);
162
+ (0, $2pZbw$react.useEffect)(()=>{
163
+ if (isDecrementPressed === 'touch') onDecrementPressStartEvent(600);
164
+ else if (isDecrementPressed) onDecrementPressStartEvent(400);
165
+ }, [
166
+ isDecrementPressed
167
+ ]);
125
168
  return {
126
169
  spinButtonProps: {
127
170
  role: 'spinbutton',
@@ -137,25 +180,68 @@ function $37bbd4c129023f61$export$e908e06f4b8e3402(props) {
137
180
  onBlur: onBlur
138
181
  },
139
182
  incrementButtonProps: {
140
- onPressStart: ()=>{
141
- onIncrementPressStart(400);
183
+ onPressStart: (e)=>{
184
+ clearAsync();
185
+ if (e.pointerType !== 'touch') {
186
+ onIncrement === null || onIncrement === void 0 ? void 0 : onIncrement();
187
+ setIsIncrementPressed('mouse');
188
+ } else {
189
+ addGlobalListener(window, 'pointercancel', onPointerCancel, {
190
+ capture: true
191
+ });
192
+ isUp.current = false;
193
+ // For touch users, don't trigger a decrement on press start, we'll wait for the press end to trigger it if
194
+ // the control isn't spinning.
195
+ setIsIncrementPressed('touch');
196
+ }
142
197
  addGlobalListener(window, 'contextmenu', cancelContextMenu);
143
198
  },
144
- onPressEnd: ()=>{
199
+ onPressUp: (e)=>{
145
200
  clearAsync();
201
+ if (e.pointerType === 'touch') isUp.current = true;
146
202
  removeAllGlobalListeners();
203
+ setIsIncrementPressed(null);
204
+ },
205
+ onPressEnd: (e)=>{
206
+ clearAsync();
207
+ if (e.pointerType === 'touch') {
208
+ if (!isSpinning.current && isUp.current) onIncrement === null || onIncrement === void 0 ? void 0 : onIncrement();
209
+ }
210
+ isUp.current = false;
211
+ setIsIncrementPressed(null);
147
212
  },
148
213
  onFocus: onFocus,
149
214
  onBlur: onBlur
150
215
  },
151
216
  decrementButtonProps: {
152
- onPressStart: ()=>{
153
- onDecrementPressStart(400);
154
- addGlobalListener(window, 'contextmenu', cancelContextMenu);
217
+ onPressStart: (e)=>{
218
+ clearAsync();
219
+ if (e.pointerType !== 'touch') {
220
+ onDecrement === null || onDecrement === void 0 ? void 0 : onDecrement();
221
+ setIsDecrementPressed('mouse');
222
+ } else {
223
+ addGlobalListener(window, 'pointercancel', onPointerCancel, {
224
+ capture: true
225
+ });
226
+ isUp.current = false;
227
+ // For touch users, don't trigger a decrement on press start, we'll wait for the press end to trigger it if
228
+ // the control isn't spinning.
229
+ setIsDecrementPressed('touch');
230
+ }
155
231
  },
156
- onPressEnd: ()=>{
232
+ onPressUp: (e)=>{
157
233
  clearAsync();
234
+ if (e.pointerType === 'touch') isUp.current = true;
158
235
  removeAllGlobalListeners();
236
+ setIsDecrementPressed(null);
237
+ },
238
+ onPressEnd: (e)=>{
239
+ clearAsync();
240
+ if (e.pointerType === 'touch') {
241
+ if (!isSpinning.current && isUp.current) onDecrement === null || onDecrement === void 0 ? void 0 : onDecrement();
242
+ }
243
+ isUp.current = false;
244
+ setIsDecrementPressed(null);
159
245
  },
160
246
  onFocus: onFocus,
161
247
  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\nconst noop = () => {};\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 onIncrementEvent = useEffectEvent(onIncrement ?? noop);\n const onDecrementEvent = useEffectEvent(onDecrement ?? noop);\n\n const stepUpEvent = useEffectEvent(() => {\n if (maxValue === undefined || isNaN(maxValue) || value === undefined || isNaN(value) || value < maxValue) {\n onIncrementEvent();\n onIncrementPressStartEvent(60);\n }\n });\n\n const onIncrementPressStartEvent = useEffectEvent((initialStepDelay: number) => {\n clearAsyncEvent();\n isSpinning.current = true;\n // Start spinning after initial delay\n _async.current = window.setTimeout(stepUpEvent, initialStepDelay);\n });\n\n const stepDownEvent = useEffectEvent(() => {\n if (minValue === undefined || isNaN(minValue) || value === undefined || isNaN(value) || value > minValue) {\n onDecrementEvent();\n onDecrementPressStartEvent(60);\n }\n });\n\n const onDecrementPressStartEvent = useEffectEvent((initialStepDelay: number) => {\n clearAsyncEvent();\n isSpinning.current = true;\n // Start spinning after initial delay\n _async.current = window.setTimeout(stepDownEvent, initialStepDelay);\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 onIncrementPressStartEvent(600);\n } else if (isIncrementPressed) {\n onIncrementPressStartEvent(400);\n }\n }, [isIncrementPressed]);\n\n let [isDecrementPressed, setIsDecrementPressed] = useState<'touch' | 'mouse' | null>(null);\n useEffect(() => {\n if (isDecrementPressed === 'touch') {\n onDecrementPressStartEvent(600);\n } else if (isDecrementPressed) {\n onDecrementPressStartEvent(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 clearAsync();\n if (e.pointerType !== 'touch') {\n onIncrement?.();\n setIsIncrementPressed('mouse');\n } else {\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 setIsIncrementPressed('touch');\n }\n addGlobalListener(window, 'contextmenu', cancelContextMenu);\n },\n onPressUp: (e) => {\n clearAsync();\n if (e.pointerType === 'touch') {\n isUp.current = true;\n }\n removeAllGlobalListeners();\n setIsIncrementPressed(null);\n },\n onPressEnd: (e) => {\n clearAsync();\n if (e.pointerType === 'touch') {\n if (!isSpinning.current && isUp.current) {\n onIncrement?.();\n }\n }\n isUp.current = false;\n setIsIncrementPressed(null);\n },\n onFocus,\n onBlur\n },\n decrementButtonProps: {\n onPressStart: (e) => {\n clearAsync();\n if (e.pointerType !== 'touch') {\n onDecrement?.();\n setIsDecrementPressed('mouse');\n } else {\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 setIsDecrementPressed('touch');\n }\n },\n onPressUp: (e) => {\n clearAsync();\n if (e.pointerType === 'touch') {\n isUp.current = true;\n }\n removeAllGlobalListeners();\n setIsDecrementPressed(null);\n },\n onPressEnd: (e) => {\n clearAsync();\n if (e.pointerType === 'touch') {\n if (!isSpinning.current && isUp.current) {\n onDecrement?.();\n }\n }\n isUp.current = false;\n setIsDecrementPressed(null);\n },\n onFocus,\n onBlur\n }\n };\n}\n"],"names":[],"version":3,"file":"useSpinButton.main.js.map"}
@@ -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
 
@@ -23,13 +23,21 @@ function $parcel$interopDefault(a) {
23
23
 
24
24
 
25
25
 
26
+ const $d2e8511e6f209edf$var$noop = ()=>{};
26
27
  function $d2e8511e6f209edf$export$e908e06f4b8e3402(props) {
27
28
  const _async = (0, $5rwhf$useRef)(undefined);
28
29
  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
30
  const stringFormatter = (0, $5rwhf$useLocalizedStringFormatter)((0, ($parcel$interopDefault($5rwhf$intlStringsmodulejs))), '@react-aria/spinbutton');
30
- const clearAsync = ()=>clearTimeout(_async.current);
31
+ let isSpinning = (0, $5rwhf$useRef)(false);
32
+ const clearAsync = (0, $5rwhf$useCallback)(()=>{
33
+ clearTimeout(_async.current);
34
+ isSpinning.current = false;
35
+ }, []);
36
+ const clearAsyncEvent = (0, $5rwhf$useEffectEvent)(()=>{
37
+ clearAsync();
38
+ });
31
39
  (0, $5rwhf$useEffect)(()=>{
32
- return ()=>clearAsync();
40
+ return ()=>clearAsyncEvent();
33
41
  }, []);
34
42
  let onKeyDown = (e)=>{
35
43
  if (e.ctrlKey || e.metaKey || e.shiftKey || e.altKey || isReadOnly || e.nativeEvent.isComposing) return;
@@ -96,26 +104,61 @@ function $d2e8511e6f209edf$export$e908e06f4b8e3402(props) {
96
104
  }, [
97
105
  ariaTextValue
98
106
  ]);
99
- const onIncrementPressStart = (0, $5rwhf$useEffectEvent)((initialStepDelay)=>{
107
+ // For touch users, if they move their finger like they're scrolling, we don't want to trigger a spin.
108
+ let onPointerCancel = (0, $5rwhf$useCallback)(()=>{
100
109
  clearAsync();
101
- onIncrement === null || onIncrement === void 0 ? void 0 : onIncrement();
110
+ }, [
111
+ clearAsync
112
+ ]);
113
+ const onIncrementEvent = (0, $5rwhf$useEffectEvent)(onIncrement !== null && onIncrement !== void 0 ? onIncrement : $d2e8511e6f209edf$var$noop);
114
+ const onDecrementEvent = (0, $5rwhf$useEffectEvent)(onDecrement !== null && onDecrement !== void 0 ? onDecrement : $d2e8511e6f209edf$var$noop);
115
+ const stepUpEvent = (0, $5rwhf$useEffectEvent)(()=>{
116
+ if (maxValue === undefined || isNaN(maxValue) || value === undefined || isNaN(value) || value < maxValue) {
117
+ onIncrementEvent();
118
+ onIncrementPressStartEvent(60);
119
+ }
120
+ });
121
+ const onIncrementPressStartEvent = (0, $5rwhf$useEffectEvent)((initialStepDelay)=>{
122
+ clearAsyncEvent();
123
+ isSpinning.current = true;
102
124
  // Start spinning after initial delay
103
- _async.current = window.setTimeout(()=>{
104
- if (maxValue === undefined || isNaN(maxValue) || value === undefined || isNaN(value) || value < maxValue) onIncrementPressStart(60);
105
- }, initialStepDelay);
125
+ _async.current = window.setTimeout(stepUpEvent, initialStepDelay);
106
126
  });
107
- const onDecrementPressStart = (0, $5rwhf$useEffectEvent)((initialStepDelay)=>{
108
- clearAsync();
109
- onDecrement === null || onDecrement === void 0 ? void 0 : onDecrement();
127
+ const stepDownEvent = (0, $5rwhf$useEffectEvent)(()=>{
128
+ if (minValue === undefined || isNaN(minValue) || value === undefined || isNaN(value) || value > minValue) {
129
+ onDecrementEvent();
130
+ onDecrementPressStartEvent(60);
131
+ }
132
+ });
133
+ const onDecrementPressStartEvent = (0, $5rwhf$useEffectEvent)((initialStepDelay)=>{
134
+ clearAsyncEvent();
135
+ isSpinning.current = true;
110
136
  // Start spinning after initial delay
111
- _async.current = window.setTimeout(()=>{
112
- if (minValue === undefined || isNaN(minValue) || value === undefined || isNaN(value) || value > minValue) onDecrementPressStart(60);
113
- }, initialStepDelay);
137
+ _async.current = window.setTimeout(stepDownEvent, initialStepDelay);
114
138
  });
115
139
  let cancelContextMenu = (e)=>{
116
140
  e.preventDefault();
117
141
  };
118
142
  let { addGlobalListener: addGlobalListener, removeAllGlobalListeners: removeAllGlobalListeners } = (0, $5rwhf$useGlobalListeners)();
143
+ // Tracks in touch if the press end event was preceded by a press up.
144
+ // If it wasn't, then we know the finger left the button while still in contact with the screen.
145
+ // This means that the user is trying to scroll or interact in some way that shouldn't trigger
146
+ // an increment or decrement.
147
+ let isUp = (0, $5rwhf$useRef)(false);
148
+ let [isIncrementPressed, setIsIncrementPressed] = (0, $5rwhf$useState)(null);
149
+ (0, $5rwhf$useEffect)(()=>{
150
+ if (isIncrementPressed === 'touch') onIncrementPressStartEvent(600);
151
+ else if (isIncrementPressed) onIncrementPressStartEvent(400);
152
+ }, [
153
+ isIncrementPressed
154
+ ]);
155
+ let [isDecrementPressed, setIsDecrementPressed] = (0, $5rwhf$useState)(null);
156
+ (0, $5rwhf$useEffect)(()=>{
157
+ if (isDecrementPressed === 'touch') onDecrementPressStartEvent(600);
158
+ else if (isDecrementPressed) onDecrementPressStartEvent(400);
159
+ }, [
160
+ isDecrementPressed
161
+ ]);
119
162
  return {
120
163
  spinButtonProps: {
121
164
  role: 'spinbutton',
@@ -131,25 +174,68 @@ function $d2e8511e6f209edf$export$e908e06f4b8e3402(props) {
131
174
  onBlur: onBlur
132
175
  },
133
176
  incrementButtonProps: {
134
- onPressStart: ()=>{
135
- onIncrementPressStart(400);
177
+ onPressStart: (e)=>{
178
+ clearAsync();
179
+ if (e.pointerType !== 'touch') {
180
+ onIncrement === null || onIncrement === void 0 ? void 0 : onIncrement();
181
+ setIsIncrementPressed('mouse');
182
+ } else {
183
+ addGlobalListener(window, 'pointercancel', onPointerCancel, {
184
+ capture: true
185
+ });
186
+ isUp.current = false;
187
+ // For touch users, don't trigger a decrement on press start, we'll wait for the press end to trigger it if
188
+ // the control isn't spinning.
189
+ setIsIncrementPressed('touch');
190
+ }
136
191
  addGlobalListener(window, 'contextmenu', cancelContextMenu);
137
192
  },
138
- onPressEnd: ()=>{
193
+ onPressUp: (e)=>{
139
194
  clearAsync();
195
+ if (e.pointerType === 'touch') isUp.current = true;
140
196
  removeAllGlobalListeners();
197
+ setIsIncrementPressed(null);
198
+ },
199
+ onPressEnd: (e)=>{
200
+ clearAsync();
201
+ if (e.pointerType === 'touch') {
202
+ if (!isSpinning.current && isUp.current) onIncrement === null || onIncrement === void 0 ? void 0 : onIncrement();
203
+ }
204
+ isUp.current = false;
205
+ setIsIncrementPressed(null);
141
206
  },
142
207
  onFocus: onFocus,
143
208
  onBlur: onBlur
144
209
  },
145
210
  decrementButtonProps: {
146
- onPressStart: ()=>{
147
- onDecrementPressStart(400);
148
- addGlobalListener(window, 'contextmenu', cancelContextMenu);
211
+ onPressStart: (e)=>{
212
+ clearAsync();
213
+ if (e.pointerType !== 'touch') {
214
+ onDecrement === null || onDecrement === void 0 ? void 0 : onDecrement();
215
+ setIsDecrementPressed('mouse');
216
+ } else {
217
+ addGlobalListener(window, 'pointercancel', onPointerCancel, {
218
+ capture: true
219
+ });
220
+ isUp.current = false;
221
+ // For touch users, don't trigger a decrement on press start, we'll wait for the press end to trigger it if
222
+ // the control isn't spinning.
223
+ setIsDecrementPressed('touch');
224
+ }
149
225
  },
150
- onPressEnd: ()=>{
226
+ onPressUp: (e)=>{
151
227
  clearAsync();
228
+ if (e.pointerType === 'touch') isUp.current = true;
152
229
  removeAllGlobalListeners();
230
+ setIsDecrementPressed(null);
231
+ },
232
+ onPressEnd: (e)=>{
233
+ clearAsync();
234
+ if (e.pointerType === 'touch') {
235
+ if (!isSpinning.current && isUp.current) onDecrement === null || onDecrement === void 0 ? void 0 : onDecrement();
236
+ }
237
+ isUp.current = false;
238
+ setIsDecrementPressed(null);
153
239
  },
154
240
  onFocus: onFocus,
155
241
  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
 
@@ -23,13 +23,21 @@ function $parcel$interopDefault(a) {
23
23
 
24
24
 
25
25
 
26
+ const $d2e8511e6f209edf$var$noop = ()=>{};
26
27
  function $d2e8511e6f209edf$export$e908e06f4b8e3402(props) {
27
28
  const _async = (0, $5rwhf$useRef)(undefined);
28
29
  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
30
  const stringFormatter = (0, $5rwhf$useLocalizedStringFormatter)((0, ($parcel$interopDefault($5rwhf$intlStringsmodulejs))), '@react-aria/spinbutton');
30
- const clearAsync = ()=>clearTimeout(_async.current);
31
+ let isSpinning = (0, $5rwhf$useRef)(false);
32
+ const clearAsync = (0, $5rwhf$useCallback)(()=>{
33
+ clearTimeout(_async.current);
34
+ isSpinning.current = false;
35
+ }, []);
36
+ const clearAsyncEvent = (0, $5rwhf$useEffectEvent)(()=>{
37
+ clearAsync();
38
+ });
31
39
  (0, $5rwhf$useEffect)(()=>{
32
- return ()=>clearAsync();
40
+ return ()=>clearAsyncEvent();
33
41
  }, []);
34
42
  let onKeyDown = (e)=>{
35
43
  if (e.ctrlKey || e.metaKey || e.shiftKey || e.altKey || isReadOnly || e.nativeEvent.isComposing) return;
@@ -96,26 +104,61 @@ function $d2e8511e6f209edf$export$e908e06f4b8e3402(props) {
96
104
  }, [
97
105
  ariaTextValue
98
106
  ]);
99
- const onIncrementPressStart = (0, $5rwhf$useEffectEvent)((initialStepDelay)=>{
107
+ // For touch users, if they move their finger like they're scrolling, we don't want to trigger a spin.
108
+ let onPointerCancel = (0, $5rwhf$useCallback)(()=>{
100
109
  clearAsync();
101
- onIncrement === null || onIncrement === void 0 ? void 0 : onIncrement();
110
+ }, [
111
+ clearAsync
112
+ ]);
113
+ const onIncrementEvent = (0, $5rwhf$useEffectEvent)(onIncrement !== null && onIncrement !== void 0 ? onIncrement : $d2e8511e6f209edf$var$noop);
114
+ const onDecrementEvent = (0, $5rwhf$useEffectEvent)(onDecrement !== null && onDecrement !== void 0 ? onDecrement : $d2e8511e6f209edf$var$noop);
115
+ const stepUpEvent = (0, $5rwhf$useEffectEvent)(()=>{
116
+ if (maxValue === undefined || isNaN(maxValue) || value === undefined || isNaN(value) || value < maxValue) {
117
+ onIncrementEvent();
118
+ onIncrementPressStartEvent(60);
119
+ }
120
+ });
121
+ const onIncrementPressStartEvent = (0, $5rwhf$useEffectEvent)((initialStepDelay)=>{
122
+ clearAsyncEvent();
123
+ isSpinning.current = true;
102
124
  // Start spinning after initial delay
103
- _async.current = window.setTimeout(()=>{
104
- if (maxValue === undefined || isNaN(maxValue) || value === undefined || isNaN(value) || value < maxValue) onIncrementPressStart(60);
105
- }, initialStepDelay);
125
+ _async.current = window.setTimeout(stepUpEvent, initialStepDelay);
106
126
  });
107
- const onDecrementPressStart = (0, $5rwhf$useEffectEvent)((initialStepDelay)=>{
108
- clearAsync();
109
- onDecrement === null || onDecrement === void 0 ? void 0 : onDecrement();
127
+ const stepDownEvent = (0, $5rwhf$useEffectEvent)(()=>{
128
+ if (minValue === undefined || isNaN(minValue) || value === undefined || isNaN(value) || value > minValue) {
129
+ onDecrementEvent();
130
+ onDecrementPressStartEvent(60);
131
+ }
132
+ });
133
+ const onDecrementPressStartEvent = (0, $5rwhf$useEffectEvent)((initialStepDelay)=>{
134
+ clearAsyncEvent();
135
+ isSpinning.current = true;
110
136
  // Start spinning after initial delay
111
- _async.current = window.setTimeout(()=>{
112
- if (minValue === undefined || isNaN(minValue) || value === undefined || isNaN(value) || value > minValue) onDecrementPressStart(60);
113
- }, initialStepDelay);
137
+ _async.current = window.setTimeout(stepDownEvent, initialStepDelay);
114
138
  });
115
139
  let cancelContextMenu = (e)=>{
116
140
  e.preventDefault();
117
141
  };
118
142
  let { addGlobalListener: addGlobalListener, removeAllGlobalListeners: removeAllGlobalListeners } = (0, $5rwhf$useGlobalListeners)();
143
+ // Tracks in touch if the press end event was preceded by a press up.
144
+ // If it wasn't, then we know the finger left the button while still in contact with the screen.
145
+ // This means that the user is trying to scroll or interact in some way that shouldn't trigger
146
+ // an increment or decrement.
147
+ let isUp = (0, $5rwhf$useRef)(false);
148
+ let [isIncrementPressed, setIsIncrementPressed] = (0, $5rwhf$useState)(null);
149
+ (0, $5rwhf$useEffect)(()=>{
150
+ if (isIncrementPressed === 'touch') onIncrementPressStartEvent(600);
151
+ else if (isIncrementPressed) onIncrementPressStartEvent(400);
152
+ }, [
153
+ isIncrementPressed
154
+ ]);
155
+ let [isDecrementPressed, setIsDecrementPressed] = (0, $5rwhf$useState)(null);
156
+ (0, $5rwhf$useEffect)(()=>{
157
+ if (isDecrementPressed === 'touch') onDecrementPressStartEvent(600);
158
+ else if (isDecrementPressed) onDecrementPressStartEvent(400);
159
+ }, [
160
+ isDecrementPressed
161
+ ]);
119
162
  return {
120
163
  spinButtonProps: {
121
164
  role: 'spinbutton',
@@ -131,25 +174,68 @@ function $d2e8511e6f209edf$export$e908e06f4b8e3402(props) {
131
174
  onBlur: onBlur
132
175
  },
133
176
  incrementButtonProps: {
134
- onPressStart: ()=>{
135
- onIncrementPressStart(400);
177
+ onPressStart: (e)=>{
178
+ clearAsync();
179
+ if (e.pointerType !== 'touch') {
180
+ onIncrement === null || onIncrement === void 0 ? void 0 : onIncrement();
181
+ setIsIncrementPressed('mouse');
182
+ } else {
183
+ addGlobalListener(window, 'pointercancel', onPointerCancel, {
184
+ capture: true
185
+ });
186
+ isUp.current = false;
187
+ // For touch users, don't trigger a decrement on press start, we'll wait for the press end to trigger it if
188
+ // the control isn't spinning.
189
+ setIsIncrementPressed('touch');
190
+ }
136
191
  addGlobalListener(window, 'contextmenu', cancelContextMenu);
137
192
  },
138
- onPressEnd: ()=>{
193
+ onPressUp: (e)=>{
139
194
  clearAsync();
195
+ if (e.pointerType === 'touch') isUp.current = true;
140
196
  removeAllGlobalListeners();
197
+ setIsIncrementPressed(null);
198
+ },
199
+ onPressEnd: (e)=>{
200
+ clearAsync();
201
+ if (e.pointerType === 'touch') {
202
+ if (!isSpinning.current && isUp.current) onIncrement === null || onIncrement === void 0 ? void 0 : onIncrement();
203
+ }
204
+ isUp.current = false;
205
+ setIsIncrementPressed(null);
141
206
  },
142
207
  onFocus: onFocus,
143
208
  onBlur: onBlur
144
209
  },
145
210
  decrementButtonProps: {
146
- onPressStart: ()=>{
147
- onDecrementPressStart(400);
148
- addGlobalListener(window, 'contextmenu', cancelContextMenu);
211
+ onPressStart: (e)=>{
212
+ clearAsync();
213
+ if (e.pointerType !== 'touch') {
214
+ onDecrement === null || onDecrement === void 0 ? void 0 : onDecrement();
215
+ setIsDecrementPressed('mouse');
216
+ } else {
217
+ addGlobalListener(window, 'pointercancel', onPointerCancel, {
218
+ capture: true
219
+ });
220
+ isUp.current = false;
221
+ // For touch users, don't trigger a decrement on press start, we'll wait for the press end to trigger it if
222
+ // the control isn't spinning.
223
+ setIsDecrementPressed('touch');
224
+ }
149
225
  },
150
- onPressEnd: ()=>{
226
+ onPressUp: (e)=>{
151
227
  clearAsync();
228
+ if (e.pointerType === 'touch') isUp.current = true;
152
229
  removeAllGlobalListeners();
230
+ setIsDecrementPressed(null);
231
+ },
232
+ onPressEnd: (e)=>{
233
+ clearAsync();
234
+ if (e.pointerType === 'touch') {
235
+ if (!isSpinning.current && isUp.current) onDecrement === null || onDecrement === void 0 ? void 0 : onDecrement();
236
+ }
237
+ isUp.current = false;
238
+ setIsDecrementPressed(null);
153
239
  },
154
240
  onFocus: onFocus,
155
241
  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.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\nconst noop = () => {};\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 onIncrementEvent = useEffectEvent(onIncrement ?? noop);\n const onDecrementEvent = useEffectEvent(onDecrement ?? noop);\n\n const stepUpEvent = useEffectEvent(() => {\n if (maxValue === undefined || isNaN(maxValue) || value === undefined || isNaN(value) || value < maxValue) {\n onIncrementEvent();\n onIncrementPressStartEvent(60);\n }\n });\n\n const onIncrementPressStartEvent = useEffectEvent((initialStepDelay: number) => {\n clearAsyncEvent();\n isSpinning.current = true;\n // Start spinning after initial delay\n _async.current = window.setTimeout(stepUpEvent, initialStepDelay);\n });\n\n const stepDownEvent = useEffectEvent(() => {\n if (minValue === undefined || isNaN(minValue) || value === undefined || isNaN(value) || value > minValue) {\n onDecrementEvent();\n onDecrementPressStartEvent(60);\n }\n });\n\n const onDecrementPressStartEvent = useEffectEvent((initialStepDelay: number) => {\n clearAsyncEvent();\n isSpinning.current = true;\n // Start spinning after initial delay\n _async.current = window.setTimeout(stepDownEvent, initialStepDelay);\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 onIncrementPressStartEvent(600);\n } else if (isIncrementPressed) {\n onIncrementPressStartEvent(400);\n }\n }, [isIncrementPressed]);\n\n let [isDecrementPressed, setIsDecrementPressed] = useState<'touch' | 'mouse' | null>(null);\n useEffect(() => {\n if (isDecrementPressed === 'touch') {\n onDecrementPressStartEvent(600);\n } else if (isDecrementPressed) {\n onDecrementPressStartEvent(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 clearAsync();\n if (e.pointerType !== 'touch') {\n onIncrement?.();\n setIsIncrementPressed('mouse');\n } else {\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 setIsIncrementPressed('touch');\n }\n addGlobalListener(window, 'contextmenu', cancelContextMenu);\n },\n onPressUp: (e) => {\n clearAsync();\n if (e.pointerType === 'touch') {\n isUp.current = true;\n }\n removeAllGlobalListeners();\n setIsIncrementPressed(null);\n },\n onPressEnd: (e) => {\n clearAsync();\n if (e.pointerType === 'touch') {\n if (!isSpinning.current && isUp.current) {\n onIncrement?.();\n }\n }\n isUp.current = false;\n setIsIncrementPressed(null);\n },\n onFocus,\n onBlur\n },\n decrementButtonProps: {\n onPressStart: (e) => {\n clearAsync();\n if (e.pointerType !== 'touch') {\n onDecrement?.();\n setIsDecrementPressed('mouse');\n } else {\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 setIsDecrementPressed('touch');\n }\n },\n onPressUp: (e) => {\n clearAsync();\n if (e.pointerType === 'touch') {\n isUp.current = true;\n }\n removeAllGlobalListeners();\n setIsDecrementPressed(null);\n },\n onPressEnd: (e) => {\n clearAsync();\n if (e.pointerType === 'touch') {\n if (!isSpinning.current && isUp.current) {\n onDecrement?.();\n }\n }\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.6.19",
3
+ "version": "3.7.1",
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.13",
29
+ "@react-aria/i18n": "^3.12.15",
30
30
  "@react-aria/live-announcer": "^3.4.4",
31
- "@react-aria/utils": "^3.31.0",
32
- "@react-types/button": "^3.14.1",
33
- "@react-types/shared": "^3.32.1",
31
+ "@react-aria/utils": "^3.33.0",
32
+ "@react-types/button": "^3.15.0",
33
+ "@react-types/shared": "^3.33.0",
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": "0bda51183baa23306342af32a82012ea0fe0f2dc"
43
+ "gitHead": "66e51757606b43a89ed02c574ca24517323a2ab9"
44
44
  }
@@ -15,11 +15,13 @@ 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
 
22
22
 
23
+ const noop = () => {};
24
+
23
25
  export interface SpinButtonProps extends InputBase, Validation<number>, ValueBase<number>, RangeInputBase<number> {
24
26
  textValue?: string,
25
27
  onIncrement?: () => void,
@@ -57,11 +59,17 @@ export function useSpinButton(
57
59
  } = props;
58
60
  const stringFormatter = useLocalizedStringFormatter(intlMessages, '@react-aria/spinbutton');
59
61
 
60
- const clearAsync = () => clearTimeout(_async.current);
61
-
62
+ let isSpinning = useRef(false);
63
+ const clearAsync = useCallback(() => {
64
+ clearTimeout(_async.current);
65
+ isSpinning.current = false;
66
+ }, []);
67
+ const clearAsyncEvent = useEffectEvent(() => {
68
+ clearAsync();
69
+ });
62
70
 
63
71
  useEffect(() => {
64
- return () => clearAsync();
72
+ return () => clearAsyncEvent();
65
73
  }, []);
66
74
 
67
75
  let onKeyDown = (e) => {
@@ -135,37 +143,41 @@ export function useSpinButton(
135
143
  }
136
144
  }, [ariaTextValue]);
137
145
 
138
- const onIncrementPressStart = useEffectEvent(
139
- (initialStepDelay: number) => {
140
- clearAsync();
141
- onIncrement?.();
142
- // Start spinning after initial delay
143
- _async.current = window.setTimeout(
144
- () => {
145
- if ((maxValue === undefined || isNaN(maxValue)) || (value === undefined || isNaN(value)) || value < maxValue) {
146
- onIncrementPressStart(60);
147
- }
148
- },
149
- initialStepDelay
150
- );
146
+ // For touch users, if they move their finger like they're scrolling, we don't want to trigger a spin.
147
+ let onPointerCancel = useCallback(() => {
148
+ clearAsync();
149
+ }, [clearAsync]);
150
+
151
+ const onIncrementEvent = useEffectEvent(onIncrement ?? noop);
152
+ const onDecrementEvent = useEffectEvent(onDecrement ?? noop);
153
+
154
+ const stepUpEvent = useEffectEvent(() => {
155
+ if (maxValue === undefined || isNaN(maxValue) || value === undefined || isNaN(value) || value < maxValue) {
156
+ onIncrementEvent();
157
+ onIncrementPressStartEvent(60);
151
158
  }
152
- );
153
-
154
- const onDecrementPressStart = useEffectEvent(
155
- (initialStepDelay: number) => {
156
- clearAsync();
157
- onDecrement?.();
158
- // Start spinning after initial delay
159
- _async.current = window.setTimeout(
160
- () => {
161
- if ((minValue === undefined || isNaN(minValue)) || (value === undefined || isNaN(value)) || value > minValue) {
162
- onDecrementPressStart(60);
163
- }
164
- },
165
- initialStepDelay
166
- );
159
+ });
160
+
161
+ const onIncrementPressStartEvent = useEffectEvent((initialStepDelay: number) => {
162
+ clearAsyncEvent();
163
+ isSpinning.current = true;
164
+ // Start spinning after initial delay
165
+ _async.current = window.setTimeout(stepUpEvent, initialStepDelay);
166
+ });
167
+
168
+ const stepDownEvent = useEffectEvent(() => {
169
+ if (minValue === undefined || isNaN(minValue) || value === undefined || isNaN(value) || value > minValue) {
170
+ onDecrementEvent();
171
+ onDecrementPressStartEvent(60);
167
172
  }
168
- );
173
+ });
174
+
175
+ const onDecrementPressStartEvent = useEffectEvent((initialStepDelay: number) => {
176
+ clearAsyncEvent();
177
+ isSpinning.current = true;
178
+ // Start spinning after initial delay
179
+ _async.current = window.setTimeout(stepDownEvent, initialStepDelay);
180
+ });
169
181
 
170
182
  let cancelContextMenu = (e) => {
171
183
  e.preventDefault();
@@ -173,6 +185,30 @@ export function useSpinButton(
173
185
 
174
186
  let {addGlobalListener, removeAllGlobalListeners} = useGlobalListeners();
175
187
 
188
+ // Tracks in touch if the press end event was preceded by a press up.
189
+ // If it wasn't, then we know the finger left the button while still in contact with the screen.
190
+ // This means that the user is trying to scroll or interact in some way that shouldn't trigger
191
+ // an increment or decrement.
192
+ let isUp = useRef(false);
193
+
194
+ let [isIncrementPressed, setIsIncrementPressed] = useState<'touch' | 'mouse' | null>(null);
195
+ useEffect(() => {
196
+ if (isIncrementPressed === 'touch') {
197
+ onIncrementPressStartEvent(600);
198
+ } else if (isIncrementPressed) {
199
+ onIncrementPressStartEvent(400);
200
+ }
201
+ }, [isIncrementPressed]);
202
+
203
+ let [isDecrementPressed, setIsDecrementPressed] = useState<'touch' | 'mouse' | null>(null);
204
+ useEffect(() => {
205
+ if (isDecrementPressed === 'touch') {
206
+ onDecrementPressStartEvent(600);
207
+ } else if (isDecrementPressed) {
208
+ onDecrementPressStartEvent(400);
209
+ }
210
+ }, [isDecrementPressed]);
211
+
176
212
  return {
177
213
  spinButtonProps: {
178
214
  role: 'spinbutton',
@@ -188,25 +224,72 @@ export function useSpinButton(
188
224
  onBlur
189
225
  },
190
226
  incrementButtonProps: {
191
- onPressStart: () => {
192
- onIncrementPressStart(400);
227
+ onPressStart: (e) => {
228
+ clearAsync();
229
+ if (e.pointerType !== 'touch') {
230
+ onIncrement?.();
231
+ setIsIncrementPressed('mouse');
232
+ } else {
233
+ addGlobalListener(window, 'pointercancel', onPointerCancel, {capture: true});
234
+ isUp.current = false;
235
+ // For touch users, don't trigger a decrement on press start, we'll wait for the press end to trigger it if
236
+ // the control isn't spinning.
237
+ setIsIncrementPressed('touch');
238
+ }
193
239
  addGlobalListener(window, 'contextmenu', cancelContextMenu);
194
240
  },
195
- onPressEnd: () => {
241
+ onPressUp: (e) => {
196
242
  clearAsync();
243
+ if (e.pointerType === 'touch') {
244
+ isUp.current = true;
245
+ }
197
246
  removeAllGlobalListeners();
247
+ setIsIncrementPressed(null);
248
+ },
249
+ onPressEnd: (e) => {
250
+ clearAsync();
251
+ if (e.pointerType === 'touch') {
252
+ if (!isSpinning.current && isUp.current) {
253
+ onIncrement?.();
254
+ }
255
+ }
256
+ isUp.current = false;
257
+ setIsIncrementPressed(null);
198
258
  },
199
259
  onFocus,
200
260
  onBlur
201
261
  },
202
262
  decrementButtonProps: {
203
- onPressStart: () => {
204
- onDecrementPressStart(400);
205
- addGlobalListener(window, 'contextmenu', cancelContextMenu);
263
+ onPressStart: (e) => {
264
+ clearAsync();
265
+ if (e.pointerType !== 'touch') {
266
+ onDecrement?.();
267
+ setIsDecrementPressed('mouse');
268
+ } else {
269
+ addGlobalListener(window, 'pointercancel', onPointerCancel, {capture: true});
270
+ isUp.current = false;
271
+ // For touch users, don't trigger a decrement on press start, we'll wait for the press end to trigger it if
272
+ // the control isn't spinning.
273
+ setIsDecrementPressed('touch');
274
+ }
206
275
  },
207
- onPressEnd: () => {
276
+ onPressUp: (e) => {
208
277
  clearAsync();
278
+ if (e.pointerType === 'touch') {
279
+ isUp.current = true;
280
+ }
209
281
  removeAllGlobalListeners();
282
+ setIsDecrementPressed(null);
283
+ },
284
+ onPressEnd: (e) => {
285
+ clearAsync();
286
+ if (e.pointerType === 'touch') {
287
+ if (!isSpinning.current && isUp.current) {
288
+ onDecrement?.();
289
+ }
290
+ }
291
+ isUp.current = false;
292
+ setIsDecrementPressed(null);
210
293
  },
211
294
  onFocus,
212
295
  onBlur