@redsift/design-system 11.6.0-muiv5-alpha.5 → 11.6.0-muiv5-alpha.7
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/_internal/Alert2.js +182 -39
- package/_internal/AppBar.js +240 -29
- package/_internal/AppContainer.js +132 -86
- package/_internal/AppContent.js +84 -17
- package/_internal/Badge2.js +137 -4
- package/_internal/BreadcrumbItem.js +85 -3
- package/_internal/Breadcrumbs2.js +86 -21
- package/_internal/Button2.js +81 -20
- package/_internal/ButtonGroup.js +165 -25
- package/_internal/ButtonLink.js +74 -18
- package/_internal/Card2.js +151 -29
- package/_internal/CardActions.js +38 -3
- package/_internal/CardBody.js +36 -3
- package/_internal/CardHeader.js +77 -3
- package/_internal/Checkbox2.js +234 -58
- package/_internal/CheckboxGroup.js +182 -4
- package/_internal/ConditionalWrapper.js +11 -12
- package/_internal/DetailedCard.js +6912 -48
- package/_internal/DetailedCardCollapsibleSectionItems.js +58 -3
- package/_internal/DetailedCardHeader.js +61 -3
- package/_internal/DetailedCardSection.js +166 -3
- package/_internal/DetailedCardSectionItem.js +88 -3
- package/_internal/Flexbox2.js +85 -22
- package/_internal/Grid2.js +87 -24
- package/_internal/GridItem.js +34 -3
- package/_internal/Heading2.js +107 -3
- package/_internal/Icon2.js +206 -5
- package/_internal/IconButton.js +71 -3
- package/_internal/IconButtonLink.js +65 -18
- package/_internal/Item2.js +390 -73
- package/_internal/Link2.js +56 -15
- package/_internal/LinkButton.js +56 -13
- package/_internal/Number2.js +103 -61
- package/_internal/NumberField.js +3959 -65
- package/_internal/Pill2.js +400 -4
- package/_internal/ProgressBar.js +61 -18
- package/_internal/Radio2.js +227 -56
- package/_internal/RadioGroup.js +170 -4
- package/_internal/Shield2.js +220 -4
- package/_internal/SideNavigationMenu.js +586 -4
- package/_internal/SideNavigationMenuItem.js +299 -4
- package/_internal/Skeleton2.js +36 -9
- package/_internal/SkeletonCircle.js +52 -3
- package/_internal/SkeletonText.js +71 -3
- package/_internal/Spinner2.js +319 -29
- package/_internal/Switch2.js +310 -56
- package/_internal/SwitchGroup.js +182 -4
- package/_internal/Text2.js +45 -3
- package/_internal/TextArea.js +430 -20
- package/_internal/TextField.js +463 -19
- package/_internal/alert.js +2 -5
- package/_internal/app-bar.js +2 -8
- package/_internal/app-container.js +3 -9
- package/_internal/app-content.js +2 -5
- package/_internal/app-side-panel.js +3 -11
- package/_internal/badge.js +2 -6
- package/_internal/breadcrumb-item.js +1 -4
- package/_internal/breadcrumbs.js +2 -6
- package/_internal/button-group.js +2 -5
- package/_internal/button-link.js +2 -8
- package/_internal/button.js +3 -8
- package/_internal/card-actions.js +1 -4
- package/_internal/card-body.js +1 -4
- package/_internal/card-header.js +1 -8
- package/_internal/card.js +2 -11
- package/_internal/checkbox-group.js +2 -6
- package/_internal/checkbox.js +2 -6
- package/_internal/colors.js +87 -91
- package/_internal/conditional-wrapper.js +2 -2
- package/_internal/detailed-card-collapsible-section-items.js +1 -3
- package/_internal/detailed-card-header.js +1 -7
- package/_internal/detailed-card-section-item.js +1 -10
- package/_internal/detailed-card-section.js +1 -6
- package/_internal/detailed-card.js +2 -16
- package/_internal/flexbox.js +2 -5
- package/_internal/focus-within-group.js +3 -3
- package/_internal/fonts.js +4 -6
- package/_internal/gradient-border.js +35 -16
- package/_internal/grid-item.js +1 -4
- package/_internal/grid.js +2 -6
- package/_internal/heading.js +2 -6
- package/_internal/icon-button-link.js +2 -8
- package/_internal/icon-button.js +2 -6
- package/_internal/icon.js +2 -6
- package/_internal/item.js +2 -8
- package/_internal/link-button.js +2 -8
- package/_internal/link.js +3 -8
- package/_internal/listbox.js +3 -6
- package/_internal/number-field.js +2 -9
- package/_internal/number.js +2 -7
- package/_internal/pill.js +2 -6
- package/_internal/progress-bar.js +2 -5
- package/_internal/radio-group.js +2 -6
- package/_internal/radio.js +2 -6
- package/_internal/shared.js +2 -5
- package/_internal/shield.js +2 -6
- package/_internal/side-navigation-menu-bar.js +3 -9
- package/_internal/side-navigation-menu-item.js +2 -8
- package/_internal/side-navigation-menu.js +2 -8
- package/_internal/skeleton-circle.js +1 -6
- package/_internal/skeleton-text.js +2 -6
- package/_internal/skeleton.js +1 -7
- package/_internal/spinner.js +2 -5
- package/_internal/styles.js +235 -17
- package/_internal/styles2.js +44 -280
- package/_internal/switch-group.js +2 -6
- package/_internal/switch.js +2 -6
- package/_internal/text-area.js +2 -9
- package/_internal/text-field.js +2 -10
- package/_internal/text.js +2 -6
- package/_internal/theme.js +1 -3
- package/_internal/types.js +7 -31
- package/_internal/types2.js +18 -29
- package/_internal/types3.js +15 -18
- package/_internal/useAppSidePanel.js +331 -6
- package/_internal/useFocusOnList.js +502 -44
- package/_internal/useListboxItem.js +120 -23
- package/_internal/useSideNavigationMenuBar.js +371 -7
- package/_internal/useTheme.js +10 -8
- package/index.d.ts +4667 -0
- package/index.js +523 -1674
- package/package.json +2 -2
- package/_internal/SideNavigationMenuBar.js +0 -9
- package/_internal/helpers.js +0 -23
- package/_internal/types10.js +0 -20
- package/_internal/types11.js +0 -27
- package/_internal/types12.js +0 -35
- package/_internal/types13.js +0 -143
- package/_internal/types14.js +0 -11
- package/_internal/types15.js +0 -62
- package/_internal/types16.js +0 -56
- package/_internal/types17.js +0 -57
- package/_internal/types18.js +0 -40
- package/_internal/types19.js +0 -101
- package/_internal/types20.js +0 -47
- package/_internal/types21.js +0 -68
- package/_internal/types22.js +0 -52
- package/_internal/types23.js +0 -174
- package/_internal/types24.js +0 -18
- package/_internal/types25.js +0 -12
- package/_internal/types26.js +0 -36
- package/_internal/types27.js +0 -72
- package/_internal/types28.js +0 -73
- package/_internal/types29.js +0 -99
- package/_internal/types4.js +0 -67
- package/_internal/types5.js +0 -11
- package/_internal/types6.js +0 -11
- package/_internal/types7.js +0 -28
- package/_internal/types8.js +0 -72
- package/_internal/types9.js +0 -16
package/_internal/NumberField.js
CHANGED
|
@@ -1,80 +1,3974 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import
|
|
1
|
+
import { _ as _objectSpread2, a as _defineProperty, b as _objectWithoutProperties, d as _classPrivateFieldInitSpec, e as _classPrivateFieldSet, f as _classPrivateFieldGet, c as _extends } from './_rollupPluginBabelHelpers.js';
|
|
2
|
+
import React__default, { useState, useRef, useCallback, useEffect, useMemo, useContext, createContext, forwardRef } from 'react';
|
|
3
|
+
import classNames from 'classnames';
|
|
4
|
+
import { w as warnIfNoAccessibleLabelFound } from './warnIfNoAccessibleLabelFound.js';
|
|
5
|
+
import styled, { css } from 'styled-components';
|
|
6
|
+
import { i as baseStyling } from './styles4.js';
|
|
7
|
+
import { T as Theme } from './colors.js';
|
|
8
|
+
import { mdiMenuUp, mdiMenuDown } from '@redsift/icons';
|
|
9
|
+
import { S as StyledIconButton } from './styles2.js';
|
|
10
|
+
import { a as useSSRSafeId, u as useLocale } from './context2.js';
|
|
11
|
+
import { N as NumberFormatter, u as useNumberFormatter } from './useNumberFormatter.js';
|
|
12
|
+
import { u as useMessageFormatter } from './useMessageFormatter.js';
|
|
13
|
+
import { b as useLayoutEffect, i as isFirefox, c as isMac, d as isWebKit, e as isIPad, f as useEffectEvent, g as isIOS, h as getOwnerDocument, j as isVirtualPointerEvent, k as isVirtualClick, l as getOwnerWindow, m as getInteractionModality, n as useFocus, s as setInteractionModality, a as useFocusWithin, o as isIPhone, p as isAndroid, u as useFocusRing } from './useFocusRing.js';
|
|
14
|
+
import c from 'clsx';
|
|
15
|
+
import { u as useTheme } from './useTheme.js';
|
|
16
|
+
import { a as Icon } from './Icon2.js';
|
|
17
|
+
import { F as Flexbox } from './Flexbox2.js';
|
|
18
|
+
|
|
19
|
+
/* eslint-disable prefer-const */
|
|
20
|
+
|
|
21
|
+
// copied from SSRProvider.tsx to reduce exports, if needed again, consider sharing
|
|
22
|
+
let canUseDOM = Boolean(typeof window !== 'undefined' && window.document && window.document.createElement);
|
|
23
|
+
let idsUpdaterMap = new Map();
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* If a default is not provided, generate an id.
|
|
27
|
+
* @param defaultId - Default component id.
|
|
28
|
+
*/
|
|
29
|
+
function useId(defaultId) {
|
|
30
|
+
let [value, setValue] = useState(defaultId);
|
|
31
|
+
let nextId = useRef(null);
|
|
32
|
+
let res = useSSRSafeId(value);
|
|
33
|
+
let updateValue = useCallback(val => {
|
|
34
|
+
nextId.current = val;
|
|
35
|
+
}, []);
|
|
36
|
+
if (canUseDOM) {
|
|
37
|
+
idsUpdaterMap.set(res, updateValue);
|
|
38
|
+
}
|
|
39
|
+
useLayoutEffect(() => {
|
|
40
|
+
let r = res;
|
|
41
|
+
return () => {
|
|
42
|
+
idsUpdaterMap.delete(r);
|
|
43
|
+
};
|
|
44
|
+
}, [res]);
|
|
45
|
+
|
|
46
|
+
// This cannot cause an infinite loop because the ref is updated first.
|
|
47
|
+
// eslint-disable-next-line
|
|
48
|
+
useEffect(() => {
|
|
49
|
+
let newId = nextId.current;
|
|
50
|
+
if (newId) {
|
|
51
|
+
nextId.current = null;
|
|
52
|
+
setValue(newId);
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
return res;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Merges two ids.
|
|
60
|
+
* Different ids will trigger a side-effect and re-render components hooked up with `useId`.
|
|
61
|
+
*/
|
|
62
|
+
function mergeIds(idA, idB) {
|
|
63
|
+
if (idA === idB) {
|
|
64
|
+
return idA;
|
|
65
|
+
}
|
|
66
|
+
let setIdA = idsUpdaterMap.get(idA);
|
|
67
|
+
if (setIdA) {
|
|
68
|
+
setIdA(idB);
|
|
69
|
+
return idB;
|
|
70
|
+
}
|
|
71
|
+
let setIdB = idsUpdaterMap.get(idB);
|
|
72
|
+
if (setIdB) {
|
|
73
|
+
setIdB(idA);
|
|
74
|
+
return idA;
|
|
75
|
+
}
|
|
76
|
+
return idB;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Used to generate an id, and after render, check if that id is rendered so we know
|
|
81
|
+
* if we can use it in places such as labelledby.
|
|
82
|
+
* @param depArray - When to recalculate if the id is in the DOM.
|
|
83
|
+
*/
|
|
84
|
+
function useSlotId() {
|
|
85
|
+
let depArray = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : [];
|
|
86
|
+
let id = useId();
|
|
87
|
+
let [resolvedId, setResolvedId] = useValueEffect(id);
|
|
88
|
+
let updateId = useCallback(() => {
|
|
89
|
+
setResolvedId(function* () {
|
|
90
|
+
yield id;
|
|
91
|
+
yield document.getElementById(id) ? id : undefined;
|
|
92
|
+
});
|
|
93
|
+
}, [id, setResolvedId]);
|
|
94
|
+
useLayoutEffect(updateId, [id, updateId, ...depArray]);
|
|
95
|
+
return resolvedId;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/* eslint-disable prefer-const */
|
|
99
|
+
// @ts-nocheck
|
|
100
|
+
|
|
101
|
+
/*
|
|
102
|
+
* Copyright 2020 Adobe. All rights reserved.
|
|
103
|
+
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
|
|
104
|
+
* you may not use this file except in compliance with the License. You may obtain a copy
|
|
105
|
+
* of the License at http://www.apache.org/licenses/LICENSE-2.0
|
|
106
|
+
*
|
|
107
|
+
* Unless required by applicable law or agreed to in writing, software distributed under
|
|
108
|
+
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
|
|
109
|
+
* OF ANY KIND, either express or implied. See the License for the specific language
|
|
110
|
+
* governing permissions and limitations under the License.
|
|
111
|
+
*/
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Calls all functions in the order they were chained with the same arguments.
|
|
115
|
+
*/
|
|
116
|
+
function chain() {
|
|
117
|
+
for (var _len = arguments.length, callbacks = new Array(_len), _key = 0; _key < _len; _key++) {
|
|
118
|
+
callbacks[_key] = arguments[_key];
|
|
119
|
+
}
|
|
120
|
+
return function () {
|
|
121
|
+
for (let callback of callbacks) {
|
|
122
|
+
if (typeof callback === 'function') {
|
|
123
|
+
callback(...arguments);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// taken from: https://stackoverflow.com/questions/51603250/typescript-3-parameter-list-intersection-type/51604379#51604379
|
|
130
|
+
|
|
131
|
+
// eslint-disable-next-line no-undef, @typescript-eslint/no-unused-vars
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Merges multiple props objects together. Event handlers are chained,
|
|
135
|
+
* classNames are combined, and ids are deduplicated - different ids
|
|
136
|
+
* will trigger a side-effect and re-render components hooked up with `useId`.
|
|
137
|
+
* For all other props, the last prop object overrides all previous ones.
|
|
138
|
+
* @param args - Multiple sets of props to merge together.
|
|
139
|
+
*/
|
|
140
|
+
function mergeProps() {
|
|
141
|
+
// Start with a base clone of the first argument. This is a lot faster than starting
|
|
142
|
+
// with an empty object and adding properties as we go.
|
|
143
|
+
let result = _objectSpread2({}, arguments.length <= 0 ? undefined : arguments[0]);
|
|
144
|
+
for (let i = 1; i < arguments.length; i++) {
|
|
145
|
+
let props = i < 0 || arguments.length <= i ? undefined : arguments[i];
|
|
146
|
+
for (let key in props) {
|
|
147
|
+
let a = result[key];
|
|
148
|
+
let b = props[key];
|
|
149
|
+
|
|
150
|
+
// Chain events
|
|
151
|
+
if (typeof a === 'function' && typeof b === 'function' &&
|
|
152
|
+
// This is a lot faster than a regex.
|
|
153
|
+
key[0] === 'o' && key[1] === 'n' && key.charCodeAt(2) >= /* 'A' */65 && key.charCodeAt(2) <= /* 'Z' */90) {
|
|
154
|
+
result[key] = chain(a, b);
|
|
155
|
+
|
|
156
|
+
// Merge classnames, sometimes classNames are empty string which eval to false, so we just need to do a type check
|
|
157
|
+
} else if ((key === 'className' || key === 'UNSAFE_className') && typeof a === 'string' && typeof b === 'string') {
|
|
158
|
+
result[key] = c(a, b);
|
|
159
|
+
} else if (key === 'id' && a && b) {
|
|
160
|
+
result.id = mergeIds(a, b);
|
|
161
|
+
// Override others
|
|
162
|
+
} else {
|
|
163
|
+
result[key] = b !== undefined ? b : a;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
return result;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/* eslint-disable prefer-const */
|
|
171
|
+
// @ts-nocheck
|
|
172
|
+
|
|
173
|
+
/*
|
|
174
|
+
* Copyright 2020 Adobe. All rights reserved.
|
|
175
|
+
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
|
|
176
|
+
* you may not use this file except in compliance with the License. You may obtain a copy
|
|
177
|
+
* of the License at http://www.apache.org/licenses/LICENSE-2.0
|
|
178
|
+
*
|
|
179
|
+
* Unless required by applicable law or agreed to in writing, software distributed under
|
|
180
|
+
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
|
|
181
|
+
* OF ANY KIND, either express or implied. See the License for the specific language
|
|
182
|
+
* governing permissions and limitations under the License.
|
|
183
|
+
*/
|
|
184
|
+
|
|
185
|
+
const DOMPropNames = new Set(['id']);
|
|
186
|
+
const labelablePropNames = new Set(['aria-label', 'aria-labelledby', 'aria-describedby', 'aria-details']);
|
|
187
|
+
|
|
188
|
+
// See LinkDOMProps in dom.d.ts.
|
|
189
|
+
const linkPropNames = new Set(['href', 'hrefLang', 'target', 'rel', 'download', 'ping', 'referrerPolicy']);
|
|
190
|
+
const propRe = /^(data-.*)$/;
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Filters out all props that aren't valid DOM props or defined via override prop obj.
|
|
194
|
+
* @param props - The component props to be filtered.
|
|
195
|
+
* @param opts - Props to override.
|
|
196
|
+
*/
|
|
197
|
+
function filterDOMProps(props) {
|
|
198
|
+
let opts = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
|
|
199
|
+
let {
|
|
200
|
+
labelable,
|
|
201
|
+
isLink,
|
|
202
|
+
propNames
|
|
203
|
+
} = opts;
|
|
204
|
+
let filteredProps = {};
|
|
205
|
+
for (const prop in props) {
|
|
206
|
+
if (Object.prototype.hasOwnProperty.call(props, prop) && (DOMPropNames.has(prop) || labelable && labelablePropNames.has(prop) || isLink && linkPropNames.has(prop) || propNames !== null && propNames !== void 0 && propNames.has(prop) || propRe.test(prop))) {
|
|
207
|
+
filteredProps[prop] = props[prop];
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
return filteredProps;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/* eslint-disable prefer-const */
|
|
214
|
+
// @ts-nocheck
|
|
215
|
+
/*
|
|
216
|
+
* Copyright 2020 Adobe. All rights reserved.
|
|
217
|
+
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
|
|
218
|
+
* you may not use this file except in compliance with the License. You may obtain a copy
|
|
219
|
+
* of the License at http://www.apache.org/licenses/LICENSE-2.0
|
|
220
|
+
*
|
|
221
|
+
* Unless required by applicable law or agreed to in writing, software distributed under
|
|
222
|
+
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
|
|
223
|
+
* OF ANY KIND, either express or implied. See the License for the specific language
|
|
224
|
+
* governing permissions and limitations under the License.
|
|
225
|
+
*/
|
|
226
|
+
|
|
227
|
+
// This is a polyfill for element.focus({preventScroll: true});
|
|
228
|
+
// Currently necessary for Safari and old Edge:
|
|
229
|
+
// https://caniuse.com/#feat=mdn-api_htmlelement_focus_preventscroll_option
|
|
230
|
+
// See https://bugs.webkit.org/show_bug.cgi?id=178583
|
|
231
|
+
//
|
|
232
|
+
|
|
233
|
+
// Original licensing for the following methods can be found in the
|
|
234
|
+
// NOTICE file in the root directory of this source tree.
|
|
235
|
+
// See https://github.com/calvellido/focus-options-polyfill
|
|
236
|
+
function focusWithoutScrolling(element) {
|
|
237
|
+
if (supportsPreventScroll()) {
|
|
238
|
+
element.focus({
|
|
239
|
+
preventScroll: true
|
|
240
|
+
});
|
|
241
|
+
} else {
|
|
242
|
+
let scrollableElements = getScrollableElements(element);
|
|
243
|
+
element.focus();
|
|
244
|
+
restoreScrollPosition(scrollableElements);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
let supportsPreventScrollCached = null;
|
|
248
|
+
function supportsPreventScroll() {
|
|
249
|
+
if (supportsPreventScrollCached == null) {
|
|
250
|
+
supportsPreventScrollCached = false;
|
|
251
|
+
try {
|
|
252
|
+
let focusElem = document.createElement('div');
|
|
253
|
+
focusElem.focus({
|
|
254
|
+
get preventScroll() {
|
|
255
|
+
supportsPreventScrollCached = true;
|
|
256
|
+
return true;
|
|
257
|
+
}
|
|
258
|
+
});
|
|
259
|
+
} catch (e) {
|
|
260
|
+
// Ignore
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
return supportsPreventScrollCached;
|
|
264
|
+
}
|
|
265
|
+
function getScrollableElements(element) {
|
|
266
|
+
let parent = element.parentNode;
|
|
267
|
+
let scrollableElements = [];
|
|
268
|
+
let rootScrollingElement = document.scrollingElement || document.documentElement;
|
|
269
|
+
while (parent instanceof HTMLElement && parent !== rootScrollingElement) {
|
|
270
|
+
if (parent.offsetHeight < parent.scrollHeight || parent.offsetWidth < parent.scrollWidth) {
|
|
271
|
+
scrollableElements.push({
|
|
272
|
+
element: parent,
|
|
273
|
+
scrollTop: parent.scrollTop,
|
|
274
|
+
scrollLeft: parent.scrollLeft
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
parent = parent.parentNode;
|
|
278
|
+
}
|
|
279
|
+
if (rootScrollingElement instanceof HTMLElement) {
|
|
280
|
+
scrollableElements.push({
|
|
281
|
+
element: rootScrollingElement,
|
|
282
|
+
scrollTop: rootScrollingElement.scrollTop,
|
|
283
|
+
scrollLeft: rootScrollingElement.scrollLeft
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
return scrollableElements;
|
|
287
|
+
}
|
|
288
|
+
function restoreScrollPosition(scrollableElements) {
|
|
289
|
+
for (let {
|
|
290
|
+
element,
|
|
291
|
+
scrollTop,
|
|
292
|
+
scrollLeft
|
|
293
|
+
} of scrollableElements) {
|
|
294
|
+
element.scrollTop = scrollTop;
|
|
295
|
+
element.scrollLeft = scrollLeft;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/* eslint-disable prefer-const */
|
|
300
|
+
function openLink(target, modifiers) {
|
|
301
|
+
var _window$event, _window$event$type;
|
|
302
|
+
let setOpening = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : true;
|
|
303
|
+
let {
|
|
304
|
+
metaKey,
|
|
305
|
+
ctrlKey,
|
|
306
|
+
altKey,
|
|
307
|
+
shiftKey
|
|
308
|
+
} = modifiers;
|
|
309
|
+
|
|
310
|
+
// Firefox does not recognize keyboard events as a user action by default, and the popup blocker
|
|
311
|
+
// will prevent links with target="_blank" from opening. However, it does allow the event if the
|
|
312
|
+
// Command/Control key is held, which opens the link in a background tab. This seems like the best we can do.
|
|
313
|
+
// See https://bugzilla.mozilla.org/show_bug.cgi?id=257870 and https://bugzilla.mozilla.org/show_bug.cgi?id=746640.
|
|
314
|
+
if (isFirefox() && (_window$event = window.event) !== null && _window$event !== void 0 && (_window$event$type = _window$event.type) !== null && _window$event$type !== void 0 && _window$event$type.startsWith('key') && target.target === '_blank') {
|
|
315
|
+
if (isMac()) {
|
|
316
|
+
metaKey = true;
|
|
317
|
+
} else {
|
|
318
|
+
ctrlKey = true;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// WebKit does not support firing click events with modifier keys, but does support keyboard events.
|
|
323
|
+
// https://github.com/WebKit/WebKit/blob/c03d0ac6e6db178f90923a0a63080b5ca210d25f/Source/WebCore/html/HTMLAnchorElement.cpp#L184
|
|
324
|
+
let event = isWebKit() && isMac() && !isIPad() && process.env.NODE_ENV !== 'test' ?
|
|
325
|
+
// @ts-ignore - keyIdentifier is a non-standard property, but it's what webkit expects
|
|
326
|
+
new KeyboardEvent('keydown', {
|
|
327
|
+
keyIdentifier: 'Enter',
|
|
328
|
+
metaKey,
|
|
329
|
+
ctrlKey,
|
|
330
|
+
altKey,
|
|
331
|
+
shiftKey
|
|
332
|
+
}) : new MouseEvent('click', {
|
|
333
|
+
metaKey,
|
|
334
|
+
ctrlKey,
|
|
335
|
+
altKey,
|
|
336
|
+
shiftKey,
|
|
337
|
+
bubbles: true,
|
|
338
|
+
cancelable: true
|
|
339
|
+
});
|
|
340
|
+
openLink.isOpening = setOpening;
|
|
341
|
+
focusWithoutScrolling(target);
|
|
342
|
+
target.dispatchEvent(event);
|
|
343
|
+
openLink.isOpening = false;
|
|
344
|
+
}
|
|
345
|
+
// https://github.com/parcel-bundler/parcel/issues/8724
|
|
346
|
+
openLink.isOpening = false;
|
|
347
|
+
|
|
348
|
+
/* eslint-disable prefer-const */
|
|
349
|
+
// @ts-nocheck
|
|
350
|
+
/*
|
|
351
|
+
* Copyright 2020 Adobe. All rights reserved.
|
|
352
|
+
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
|
|
353
|
+
* you may not use this file except in compliance with the License. You may obtain a copy
|
|
354
|
+
* of the License at http://www.apache.org/licenses/LICENSE-2.0
|
|
355
|
+
*
|
|
356
|
+
* Unless required by applicable law or agreed to in writing, software distributed under
|
|
357
|
+
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
|
|
358
|
+
* OF ANY KIND, either express or implied. See the License for the specific language
|
|
359
|
+
* governing permissions and limitations under the License.
|
|
360
|
+
*/
|
|
361
|
+
|
|
362
|
+
// We store a global list of elements that are currently transitioning,
|
|
363
|
+
// mapped to a set of CSS properties that are transitioning for that element.
|
|
364
|
+
// This is necessary rather than a simple count of transitions because of browser
|
|
365
|
+
// bugs, e.g. Chrome sometimes fires both transitionend and transitioncancel rather
|
|
366
|
+
// than one or the other. So we need to track what's actually transitioning so that
|
|
367
|
+
// we can ignore these duplicate events.
|
|
368
|
+
let transitionsByElement = new Map();
|
|
369
|
+
|
|
370
|
+
// A list of callbacks to call once there are no transitioning elements.
|
|
371
|
+
let transitionCallbacks = new Set();
|
|
372
|
+
function setupGlobalEvents() {
|
|
373
|
+
if (typeof window === 'undefined') {
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
376
|
+
function isTransitionEvent(event) {
|
|
377
|
+
return 'propertyName' in event;
|
|
378
|
+
}
|
|
379
|
+
let onTransitionStart = e => {
|
|
380
|
+
if (!isTransitionEvent(e) || !e.target) {
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
383
|
+
// Add the transitioning property to the list for this element.
|
|
384
|
+
let transitions = transitionsByElement.get(e.target);
|
|
385
|
+
if (!transitions) {
|
|
386
|
+
transitions = new Set();
|
|
387
|
+
transitionsByElement.set(e.target, transitions);
|
|
388
|
+
|
|
389
|
+
// The transitioncancel event must be registered on the element itself, rather than as a global
|
|
390
|
+
// event. This enables us to handle when the node is deleted from the document while it is transitioning.
|
|
391
|
+
// In that case, the cancel event would have nowhere to bubble to so we need to handle it directly.
|
|
392
|
+
e.target.addEventListener('transitioncancel', onTransitionEnd, {
|
|
393
|
+
once: true
|
|
394
|
+
});
|
|
395
|
+
}
|
|
396
|
+
transitions.add(e.propertyName);
|
|
397
|
+
};
|
|
398
|
+
let onTransitionEnd = e => {
|
|
399
|
+
if (!isTransitionEvent(e) || !e.target) {
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
402
|
+
// Remove property from list of transitioning properties.
|
|
403
|
+
let properties = transitionsByElement.get(e.target);
|
|
404
|
+
if (!properties) {
|
|
405
|
+
return;
|
|
406
|
+
}
|
|
407
|
+
properties.delete(e.propertyName);
|
|
408
|
+
|
|
409
|
+
// If empty, remove transitioncancel event, and remove the element from the list of transitioning elements.
|
|
410
|
+
if (properties.size === 0) {
|
|
411
|
+
e.target.removeEventListener('transitioncancel', onTransitionEnd);
|
|
412
|
+
transitionsByElement.delete(e.target);
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// If no transitioning elements, call all of the queued callbacks.
|
|
416
|
+
if (transitionsByElement.size === 0) {
|
|
417
|
+
for (let cb of transitionCallbacks) {
|
|
418
|
+
cb();
|
|
419
|
+
}
|
|
420
|
+
transitionCallbacks.clear();
|
|
421
|
+
}
|
|
422
|
+
};
|
|
423
|
+
document.body.addEventListener('transitionrun', onTransitionStart);
|
|
424
|
+
document.body.addEventListener('transitionend', onTransitionEnd);
|
|
425
|
+
}
|
|
426
|
+
if (typeof document !== 'undefined') {
|
|
427
|
+
if (document.readyState !== 'loading') {
|
|
428
|
+
setupGlobalEvents();
|
|
429
|
+
} else {
|
|
430
|
+
document.addEventListener('DOMContentLoaded', setupGlobalEvents);
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
function runAfterTransition(fn) {
|
|
434
|
+
// Wait one frame to see if an animation starts, e.g. a transition on mount.
|
|
435
|
+
requestAnimationFrame(() => {
|
|
436
|
+
// If no transitions are running, call the function immediately.
|
|
437
|
+
// Otherwise, add it to a list of callbacks to run at the end of the animation.
|
|
438
|
+
if (transitionsByElement.size === 0) {
|
|
439
|
+
fn();
|
|
440
|
+
} else {
|
|
441
|
+
transitionCallbacks.add(fn);
|
|
442
|
+
}
|
|
443
|
+
});
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
/* eslint-disable prefer-const */
|
|
447
|
+
function useGlobalListeners() {
|
|
448
|
+
let globalListeners = useRef(new Map());
|
|
449
|
+
let addGlobalListener = useCallback((eventTarget, type, listener, options) => {
|
|
450
|
+
// Make sure we remove the listener after it is called with the `once` option.
|
|
451
|
+
let fn = options !== null && options !== void 0 && options.once ? function () {
|
|
452
|
+
globalListeners.current.delete(listener);
|
|
453
|
+
listener(...arguments);
|
|
454
|
+
} : listener;
|
|
455
|
+
globalListeners.current.set(listener, {
|
|
456
|
+
type,
|
|
457
|
+
eventTarget,
|
|
458
|
+
fn,
|
|
459
|
+
options
|
|
460
|
+
});
|
|
461
|
+
eventTarget.addEventListener(type, listener, options);
|
|
462
|
+
}, []);
|
|
463
|
+
let removeGlobalListener = useCallback((eventTarget, type, listener, options) => {
|
|
464
|
+
var _globalListeners$curr;
|
|
465
|
+
let fn = ((_globalListeners$curr = globalListeners.current.get(listener)) === null || _globalListeners$curr === void 0 ? void 0 : _globalListeners$curr.fn) || listener;
|
|
466
|
+
eventTarget.removeEventListener(type, fn, options);
|
|
467
|
+
globalListeners.current.delete(listener);
|
|
468
|
+
}, []);
|
|
469
|
+
let removeAllGlobalListeners = useCallback(() => {
|
|
470
|
+
globalListeners.current.forEach((value, key) => {
|
|
471
|
+
removeGlobalListener(value.eventTarget, value.type, key, value.options);
|
|
472
|
+
});
|
|
473
|
+
}, [removeGlobalListener]);
|
|
474
|
+
|
|
475
|
+
// eslint-disable-next-line arrow-body-style
|
|
476
|
+
useEffect(() => {
|
|
477
|
+
return removeAllGlobalListeners;
|
|
478
|
+
}, [removeAllGlobalListeners]);
|
|
479
|
+
return {
|
|
480
|
+
addGlobalListener,
|
|
481
|
+
removeGlobalListener,
|
|
482
|
+
removeAllGlobalListeners
|
|
483
|
+
};
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
/* eslint-disable prefer-const */
|
|
487
|
+
|
|
488
|
+
/**
|
|
489
|
+
* Merges aria-label and aria-labelledby into aria-labelledby when both exist.
|
|
490
|
+
* @param props - Aria label props.
|
|
491
|
+
* @param defaultLabel - Default value for aria-label when not present.
|
|
492
|
+
*/
|
|
493
|
+
function useLabels(props, defaultLabel) {
|
|
494
|
+
let {
|
|
495
|
+
id,
|
|
496
|
+
'aria-label': label,
|
|
497
|
+
'aria-labelledby': labelledBy
|
|
498
|
+
} = props;
|
|
499
|
+
|
|
500
|
+
// If there is both an aria-label and aria-labelledby,
|
|
501
|
+
// combine them by pointing to the element itself.
|
|
502
|
+
id = useId(id);
|
|
503
|
+
if (labelledBy && label) {
|
|
504
|
+
let ids = new Set([id, ...labelledBy.trim().split(/\s+/)]);
|
|
505
|
+
labelledBy = [...ids].join(' ');
|
|
506
|
+
} else if (labelledBy) {
|
|
507
|
+
labelledBy = labelledBy.trim().split(/\s+/).join(' ');
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
// If no labels are provided, use the default
|
|
511
|
+
if (!label && !labelledBy && defaultLabel) {
|
|
512
|
+
label = defaultLabel;
|
|
513
|
+
}
|
|
514
|
+
return {
|
|
515
|
+
id,
|
|
516
|
+
'aria-label': label,
|
|
517
|
+
'aria-labelledby': labelledBy
|
|
518
|
+
};
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
/*
|
|
522
|
+
* Copyright 2020 Adobe. All rights reserved.
|
|
523
|
+
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
|
|
524
|
+
* you may not use this file except in compliance with the License. You may obtain a copy
|
|
525
|
+
* of the License at http://www.apache.org/licenses/LICENSE-2.0
|
|
526
|
+
*
|
|
527
|
+
* Unless required by applicable law or agreed to in writing, software distributed under
|
|
528
|
+
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
|
|
529
|
+
* OF ANY KIND, either express or implied. See the License for the specific language
|
|
530
|
+
* governing permissions and limitations under the License.
|
|
531
|
+
*/
|
|
532
|
+
// Syncs ref from context with ref passed to hook
|
|
533
|
+
function useSyncRef(context, ref) {
|
|
534
|
+
useLayoutEffect(() => {
|
|
535
|
+
if (context && context.ref && ref) {
|
|
536
|
+
context.ref.current = ref.current;
|
|
537
|
+
return () => {
|
|
538
|
+
if (context.ref) {
|
|
539
|
+
context.ref.current = null;
|
|
540
|
+
}
|
|
541
|
+
};
|
|
542
|
+
}
|
|
543
|
+
});
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
/* eslint-disable prefer-const */
|
|
547
|
+
function useEvent(ref, event, handler, options) {
|
|
548
|
+
let handleEvent = useEffectEvent(handler);
|
|
549
|
+
let isDisabled = handler == null;
|
|
550
|
+
useEffect(() => {
|
|
551
|
+
if (isDisabled || !ref.current) {
|
|
552
|
+
return;
|
|
553
|
+
}
|
|
554
|
+
let element = ref.current;
|
|
555
|
+
element.addEventListener(event, handleEvent, options);
|
|
556
|
+
return () => {
|
|
557
|
+
element.removeEventListener(event, handleEvent, options);
|
|
558
|
+
};
|
|
559
|
+
}, [ref, event, options, isDisabled, handleEvent]);
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
/* eslint-disable prefer-const */
|
|
563
|
+
// This hook works like `useState`, but when setting the value, you pass a generator function
|
|
564
|
+
// that can yield multiple values. Each yielded value updates the state and waits for the next
|
|
565
|
+
// layout effect, then continues the generator. This allows sequential updates to state to be
|
|
566
|
+
// written linearly.
|
|
567
|
+
function useValueEffect(defaultValue) {
|
|
568
|
+
let [value, setValue] = useState(defaultValue);
|
|
569
|
+
let effect = useRef(null);
|
|
570
|
+
|
|
571
|
+
// Store the function in a ref so we can always access the current version
|
|
572
|
+
// which has the proper `value` in scope.
|
|
573
|
+
let nextRef = useEffectEvent(() => {
|
|
574
|
+
if (!effect.current) {
|
|
575
|
+
return;
|
|
576
|
+
}
|
|
577
|
+
// Run the generator to the next yield.
|
|
578
|
+
let newValue = effect.current.next();
|
|
579
|
+
|
|
580
|
+
// If the generator is done, reset the effect.
|
|
581
|
+
if (newValue.done) {
|
|
582
|
+
effect.current = null;
|
|
583
|
+
return;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
// If the value is the same as the current value,
|
|
587
|
+
// then continue to the next yield. Otherwise,
|
|
588
|
+
// set the value in state and wait for the next layout effect.
|
|
589
|
+
if (value === newValue.value) {
|
|
590
|
+
nextRef();
|
|
591
|
+
} else {
|
|
592
|
+
setValue(newValue.value);
|
|
593
|
+
}
|
|
594
|
+
});
|
|
595
|
+
useLayoutEffect(() => {
|
|
596
|
+
// If there is an effect currently running, continue to the next yield.
|
|
597
|
+
if (effect.current) {
|
|
598
|
+
nextRef();
|
|
599
|
+
}
|
|
600
|
+
});
|
|
601
|
+
let queue = useEffectEvent(fn => {
|
|
602
|
+
effect.current = fn(value);
|
|
603
|
+
nextRef();
|
|
604
|
+
});
|
|
605
|
+
return [value, queue];
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
/* eslint-disable prefer-const */
|
|
609
|
+
function useControlledState(value, defaultValue, onChange) {
|
|
610
|
+
let [stateValue, setStateValue] = useState(value || defaultValue);
|
|
611
|
+
let isControlledRef = useRef(value !== undefined);
|
|
612
|
+
let isControlled = value !== undefined;
|
|
613
|
+
useEffect(() => {
|
|
614
|
+
let wasControlled = isControlledRef.current;
|
|
615
|
+
if (wasControlled !== isControlled) {
|
|
616
|
+
console.warn(`WARN: A component changed from ${wasControlled ? 'controlled' : 'uncontrolled'} to ${isControlled ? 'controlled' : 'uncontrolled'}.`);
|
|
617
|
+
}
|
|
618
|
+
isControlledRef.current = isControlled;
|
|
619
|
+
}, [isControlled]);
|
|
620
|
+
let currentValue = isControlled ? value : stateValue;
|
|
621
|
+
let setValue = useCallback(function (value) {
|
|
622
|
+
for (var _len = arguments.length, args = new Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) {
|
|
623
|
+
args[_key - 1] = arguments[_key];
|
|
624
|
+
}
|
|
625
|
+
let onChangeCaller = function (value) {
|
|
626
|
+
if (onChange) {
|
|
627
|
+
if (!Object.is(currentValue, value)) {
|
|
628
|
+
for (var _len2 = arguments.length, onChangeArgs = new Array(_len2 > 1 ? _len2 - 1 : 0), _key2 = 1; _key2 < _len2; _key2++) {
|
|
629
|
+
onChangeArgs[_key2 - 1] = arguments[_key2];
|
|
630
|
+
}
|
|
631
|
+
onChange(value, ...onChangeArgs);
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
if (!isControlled) {
|
|
635
|
+
// If uncontrolled, mutate the currentValue local variable so that
|
|
636
|
+
// calling setState multiple times with the same value only emits onChange once.
|
|
637
|
+
// We do not use a ref for this because we specifically _do_ want the value to
|
|
638
|
+
// reset every render, and assigning to a ref in render breaks aborted suspended renders.
|
|
639
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
640
|
+
currentValue = value;
|
|
641
|
+
}
|
|
642
|
+
};
|
|
643
|
+
if (typeof value === 'function') {
|
|
644
|
+
console.warn('We can not support a function callback. See Github Issues for details https://github.com/adobe/react-spectrum/issues/2320');
|
|
645
|
+
// this supports functional updates https://reactjs.org/docs/hooks-reference.html#functional-updates
|
|
646
|
+
// when someone using useControlledState calls setControlledState(myFunc)
|
|
647
|
+
// this will call our useState setState with a function as well which invokes myFunc and calls onChange with the value from myFunc
|
|
648
|
+
// if we're in an uncontrolled state, then we also return the value of myFunc which to setState looks as though it was just called with myFunc from the beginning
|
|
649
|
+
// otherwise we just return the controlled value, which won't cause a rerender because React knows to bail out when the value is the same
|
|
650
|
+
let updateFunction = function (oldValue) {
|
|
651
|
+
for (var _len3 = arguments.length, functionArgs = new Array(_len3 > 1 ? _len3 - 1 : 0), _key3 = 1; _key3 < _len3; _key3++) {
|
|
652
|
+
functionArgs[_key3 - 1] = arguments[_key3];
|
|
653
|
+
}
|
|
654
|
+
let interceptedValue = value(isControlled ? currentValue : oldValue, ...functionArgs);
|
|
655
|
+
onChangeCaller(interceptedValue, ...args);
|
|
656
|
+
if (!isControlled) {
|
|
657
|
+
return interceptedValue;
|
|
658
|
+
}
|
|
659
|
+
return oldValue;
|
|
660
|
+
};
|
|
661
|
+
setStateValue(updateFunction);
|
|
662
|
+
} else {
|
|
663
|
+
if (!isControlled) {
|
|
664
|
+
setStateValue(value);
|
|
665
|
+
}
|
|
666
|
+
onChangeCaller(value, ...args);
|
|
667
|
+
}
|
|
668
|
+
}, [isControlled, currentValue, onChange]);
|
|
669
|
+
return [currentValue, setValue];
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
/* eslint-disable prefer-const */
|
|
673
|
+
// @ts-nocheck
|
|
674
|
+
/*
|
|
675
|
+
* Copyright 2020 Adobe. All rights reserved.
|
|
676
|
+
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
|
|
677
|
+
* you may not use this file except in compliance with the License. You may obtain a copy
|
|
678
|
+
* of the License at http://www.apache.org/licenses/LICENSE-2.0
|
|
679
|
+
*
|
|
680
|
+
* Unless required by applicable law or agreed to in writing, software distributed under
|
|
681
|
+
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
|
|
682
|
+
* OF ANY KIND, either express or implied. See the License for the specific language
|
|
683
|
+
* governing permissions and limitations under the License.
|
|
684
|
+
*/
|
|
685
|
+
|
|
686
|
+
/**
|
|
687
|
+
* Takes a value and forces it to the closest min/max if it's outside. Also forces it to the closest valid step.
|
|
688
|
+
*/
|
|
689
|
+
function clamp(value) {
|
|
690
|
+
let min = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : -Infinity;
|
|
691
|
+
let max = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : Infinity;
|
|
692
|
+
let newValue = Math.min(Math.max(value, min), max);
|
|
693
|
+
return newValue;
|
|
694
|
+
}
|
|
695
|
+
function roundToStepPrecision(value, step) {
|
|
696
|
+
let roundedValue = value;
|
|
697
|
+
let stepString = step.toString();
|
|
698
|
+
let pointIndex = stepString.indexOf('.');
|
|
699
|
+
let precision = pointIndex >= 0 ? stepString.length - pointIndex : 0;
|
|
700
|
+
if (precision > 0) {
|
|
701
|
+
let pow = Math.pow(10, precision);
|
|
702
|
+
roundedValue = Math.round(roundedValue * pow) / pow;
|
|
703
|
+
}
|
|
704
|
+
return roundedValue;
|
|
705
|
+
}
|
|
706
|
+
function snapValueToStep(value, min, max, step) {
|
|
707
|
+
min = Number(min);
|
|
708
|
+
max = Number(max);
|
|
709
|
+
let remainder = (value - (isNaN(min) ? 0 : min)) % step;
|
|
710
|
+
let snappedValue = roundToStepPrecision(Math.abs(remainder) * 2 >= step ? value + Math.sign(remainder) * (step - Math.abs(remainder)) : value - remainder, step);
|
|
711
|
+
if (!isNaN(min)) {
|
|
712
|
+
if (snappedValue < min) {
|
|
713
|
+
snappedValue = min;
|
|
714
|
+
} else if (!isNaN(max) && snappedValue > max) {
|
|
715
|
+
snappedValue = min + Math.floor(roundToStepPrecision((max - min) / step, step)) * step;
|
|
716
|
+
}
|
|
717
|
+
} else if (!isNaN(max) && snappedValue > max) {
|
|
718
|
+
snappedValue = Math.floor(roundToStepPrecision(max / step, step)) * step;
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
// correct floating point behavior by rounding to step precision
|
|
722
|
+
snappedValue = roundToStepPrecision(snappedValue, step);
|
|
723
|
+
return snappedValue;
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
/* eslint-disable prefer-const */
|
|
727
|
+
function useFormReset(ref, initialValue, onReset) {
|
|
728
|
+
let resetValue = useRef(initialValue);
|
|
729
|
+
let handleReset = useEffectEvent(() => {
|
|
730
|
+
if (onReset) {
|
|
731
|
+
onReset(resetValue.current);
|
|
732
|
+
}
|
|
733
|
+
});
|
|
734
|
+
useEffect(() => {
|
|
735
|
+
var _ref$current;
|
|
736
|
+
let form = ref === null || ref === void 0 ? void 0 : (_ref$current = ref.current) === null || _ref$current === void 0 ? void 0 : _ref$current.form;
|
|
737
|
+
form === null || form === void 0 ? void 0 : form.addEventListener('reset', handleReset);
|
|
738
|
+
return () => {
|
|
739
|
+
form === null || form === void 0 ? void 0 : form.removeEventListener('reset', handleReset);
|
|
740
|
+
};
|
|
741
|
+
}, [ref, handleReset]);
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
const CURRENCY_SIGN_REGEX = new RegExp('^.*\\(.*\\).*$');
|
|
745
|
+
const NUMBERING_SYSTEMS = ['latn', 'arab', 'hanidec'];
|
|
746
|
+
|
|
747
|
+
/**
|
|
748
|
+
* A NumberParser can be used to perform locale-aware parsing of numbers from Unicode strings,
|
|
749
|
+
* as well as validation of partial user input. It automatically detects the numbering system
|
|
750
|
+
* used in the input, and supports parsing decimals, percentages, currency values, and units
|
|
751
|
+
* according to the locale.
|
|
752
|
+
*/
|
|
753
|
+
class NumberParser {
|
|
754
|
+
constructor(locale) {
|
|
755
|
+
let options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
|
|
756
|
+
_defineProperty(this, "locale", void 0);
|
|
757
|
+
_defineProperty(this, "options", void 0);
|
|
758
|
+
this.locale = locale;
|
|
759
|
+
this.options = options;
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
/**
|
|
763
|
+
* Parses the given string to a number. Returns NaN if a valid number could not be parsed.
|
|
764
|
+
*/
|
|
765
|
+
parse(value) {
|
|
766
|
+
return getNumberParserImpl(this.locale, this.options, value).parse(value);
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
/**
|
|
770
|
+
* Returns whether the given string could potentially be a valid number. This should be used to
|
|
771
|
+
* validate user input as the user types. If a `minValue` or `maxValue` is provided, the validity
|
|
772
|
+
* of the minus/plus sign characters can be checked.
|
|
773
|
+
*/
|
|
774
|
+
isValidPartialNumber(value, minValue, maxValue) {
|
|
775
|
+
return getNumberParserImpl(this.locale, this.options, value).isValidPartialNumber(value, minValue, maxValue);
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
/**
|
|
779
|
+
* Returns a numbering system for which the given string is valid in the current locale.
|
|
780
|
+
* If no numbering system could be detected, the default numbering system for the current
|
|
781
|
+
* locale is returned.
|
|
782
|
+
*/
|
|
783
|
+
getNumberingSystem(value) {
|
|
784
|
+
return getNumberParserImpl(this.locale, this.options, value).options.numberingSystem;
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
const numberParserCache = new Map();
|
|
788
|
+
function getNumberParserImpl(locale, options, value) {
|
|
789
|
+
// First try the default numbering system for the provided locale
|
|
790
|
+
let defaultParser = getCachedNumberParser(locale, options);
|
|
791
|
+
|
|
792
|
+
// If that doesn't match, and the locale doesn't include a hard coded numbering system,
|
|
793
|
+
// try each of the other supported numbering systems until we find one that matches.
|
|
794
|
+
if (!locale.includes('-nu-') && !defaultParser.isValidPartialNumber(value)) {
|
|
795
|
+
for (let numberingSystem of NUMBERING_SYSTEMS) {
|
|
796
|
+
if (numberingSystem !== defaultParser.options.numberingSystem) {
|
|
797
|
+
let parser = getCachedNumberParser(locale + (locale.includes('-u-') ? '-nu-' : '-u-nu-') + numberingSystem, options);
|
|
798
|
+
if (parser.isValidPartialNumber(value)) {
|
|
799
|
+
return parser;
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
return defaultParser;
|
|
805
|
+
}
|
|
806
|
+
function getCachedNumberParser(locale, options) {
|
|
807
|
+
let cacheKey = locale + (options ? Object.entries(options).sort((a, b) => a[0] < b[0] ? -1 : 1).join() : '');
|
|
808
|
+
let parser = numberParserCache.get(cacheKey);
|
|
809
|
+
if (!parser) {
|
|
810
|
+
parser = new NumberParserImpl(locale, options);
|
|
811
|
+
numberParserCache.set(cacheKey, parser);
|
|
812
|
+
}
|
|
813
|
+
return parser;
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
// The actual number parser implementation. Instances of this class are cached
|
|
817
|
+
// based on the locale, options, and detected numbering system.
|
|
818
|
+
class NumberParserImpl {
|
|
819
|
+
constructor(locale) {
|
|
820
|
+
var _this$options$minimum, _this$options$maximum;
|
|
821
|
+
let options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
|
|
822
|
+
_defineProperty(this, "formatter", void 0);
|
|
823
|
+
_defineProperty(this, "options", void 0);
|
|
824
|
+
_defineProperty(this, "symbols", void 0);
|
|
825
|
+
_defineProperty(this, "locale", void 0);
|
|
826
|
+
this.locale = locale;
|
|
827
|
+
this.formatter = new Intl.NumberFormat(locale, options);
|
|
828
|
+
this.options = this.formatter.resolvedOptions();
|
|
829
|
+
this.symbols = getSymbols(locale, this.formatter, this.options, options);
|
|
830
|
+
if (this.options.style === 'percent' && (((_this$options$minimum = this.options.minimumFractionDigits) !== null && _this$options$minimum !== void 0 ? _this$options$minimum : 0) > 18 || ((_this$options$maximum = this.options.maximumFractionDigits) !== null && _this$options$maximum !== void 0 ? _this$options$maximum : 0) > 18)) {
|
|
831
|
+
console.warn('NumberParser cannot handle percentages with greater than 18 decimal places, please reduce the number in your options.');
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
parse(value) {
|
|
835
|
+
// to parse the number, we need to remove anything that isn't actually part of the number, for example we want '-10.40' not '-10.40 USD'
|
|
836
|
+
let fullySanitizedValue = this.sanitize(value);
|
|
837
|
+
if (this.symbols.group) {
|
|
838
|
+
// Remove group characters, and replace decimal points and numerals with ASCII values.
|
|
839
|
+
fullySanitizedValue = replaceAll(fullySanitizedValue, this.symbols.group, '');
|
|
840
|
+
}
|
|
841
|
+
if (this.symbols.decimal) {
|
|
842
|
+
fullySanitizedValue = fullySanitizedValue.replace(this.symbols.decimal, '.');
|
|
843
|
+
}
|
|
844
|
+
if (this.symbols.minusSign) {
|
|
845
|
+
fullySanitizedValue = fullySanitizedValue.replace(this.symbols.minusSign, '-');
|
|
846
|
+
}
|
|
847
|
+
fullySanitizedValue = fullySanitizedValue.replace(this.symbols.numeral, this.symbols.index);
|
|
848
|
+
if (this.options.style === 'percent') {
|
|
849
|
+
// javascript is bad at dividing by 100 and maintaining the same significant figures, so perform it on the string before parsing
|
|
850
|
+
let isNegative = fullySanitizedValue.indexOf('-');
|
|
851
|
+
fullySanitizedValue = fullySanitizedValue.replace('-', '');
|
|
852
|
+
let index = fullySanitizedValue.indexOf('.');
|
|
853
|
+
if (index === -1) {
|
|
854
|
+
index = fullySanitizedValue.length;
|
|
855
|
+
}
|
|
856
|
+
fullySanitizedValue = fullySanitizedValue.replace('.', '');
|
|
857
|
+
if (index - 2 === 0) {
|
|
858
|
+
fullySanitizedValue = `0.${fullySanitizedValue}`;
|
|
859
|
+
} else if (index - 2 === -1) {
|
|
860
|
+
fullySanitizedValue = `0.0${fullySanitizedValue}`;
|
|
861
|
+
} else if (index - 2 === -2) {
|
|
862
|
+
fullySanitizedValue = '0.00';
|
|
863
|
+
} else {
|
|
864
|
+
fullySanitizedValue = `${fullySanitizedValue.slice(0, index - 2)}.${fullySanitizedValue.slice(index - 2)}`;
|
|
865
|
+
}
|
|
866
|
+
if (isNegative > -1) {
|
|
867
|
+
fullySanitizedValue = `-${fullySanitizedValue}`;
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
let newValue = fullySanitizedValue ? +fullySanitizedValue : NaN;
|
|
871
|
+
if (isNaN(newValue)) {
|
|
872
|
+
return NaN;
|
|
873
|
+
}
|
|
874
|
+
if (this.options.style === 'percent') {
|
|
875
|
+
var _this$options$minimum2, _this$options$maximum2;
|
|
876
|
+
// extra step for rounding percents to what our formatter would output
|
|
877
|
+
let options = _objectSpread2(_objectSpread2({}, this.options), {}, {
|
|
878
|
+
style: 'decimal',
|
|
879
|
+
minimumFractionDigits: Math.min(((_this$options$minimum2 = this.options.minimumFractionDigits) !== null && _this$options$minimum2 !== void 0 ? _this$options$minimum2 : 0) + 2, 20),
|
|
880
|
+
maximumFractionDigits: Math.min(((_this$options$maximum2 = this.options.maximumFractionDigits) !== null && _this$options$maximum2 !== void 0 ? _this$options$maximum2 : 0) + 2, 20)
|
|
881
|
+
});
|
|
882
|
+
return new NumberParser(this.locale, options).parse(new NumberFormatter(this.locale, options).format(newValue));
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
// accounting will always be stripped to a positive number, so if it's accounting and has a () around everything, then we need to make it negative again
|
|
886
|
+
if (this.options.currencySign === 'accounting' && CURRENCY_SIGN_REGEX.test(value)) {
|
|
887
|
+
newValue = -1 * newValue;
|
|
888
|
+
}
|
|
889
|
+
return newValue;
|
|
890
|
+
}
|
|
891
|
+
sanitize(value) {
|
|
892
|
+
// Remove literals and whitespace, which are allowed anywhere in the string
|
|
893
|
+
value = value.replace(this.symbols.literals, '');
|
|
894
|
+
|
|
895
|
+
// Replace the ASCII minus sign with the minus sign used in the current locale
|
|
896
|
+
// so that both are allowed in case the user's keyboard doesn't have the locale's minus sign.
|
|
897
|
+
if (this.symbols.minusSign) {
|
|
898
|
+
value = value.replace('-', this.symbols.minusSign);
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
// In arab numeral system, their decimal character is 1643, but most keyboards don't type that
|
|
902
|
+
// instead they use the , (44) character or apparently the (1548) character.
|
|
903
|
+
if (this.options.numberingSystem === 'arab') {
|
|
904
|
+
if (this.symbols.decimal) {
|
|
905
|
+
value = value.replace(',', this.symbols.decimal);
|
|
906
|
+
value = value.replace(String.fromCharCode(1548), this.symbols.decimal);
|
|
907
|
+
}
|
|
908
|
+
if (this.symbols.group) {
|
|
909
|
+
value = replaceAll(value, '.', this.symbols.group);
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
// fr-FR group character is char code 8239, but that's not a key on the french keyboard,
|
|
914
|
+
// so allow 'period' as a group char and replace it with a space
|
|
915
|
+
if (this.options.locale === 'fr-FR') {
|
|
916
|
+
value = replaceAll(value, '.', String.fromCharCode(8239));
|
|
917
|
+
}
|
|
918
|
+
return value;
|
|
919
|
+
}
|
|
920
|
+
isValidPartialNumber(value) {
|
|
921
|
+
let minValue = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : -Infinity;
|
|
922
|
+
let maxValue = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : Infinity;
|
|
923
|
+
value = this.sanitize(value);
|
|
924
|
+
|
|
925
|
+
// Remove minus or plus sign, which must be at the start of the string.
|
|
926
|
+
if (this.symbols.minusSign && value.startsWith(this.symbols.minusSign) && minValue < 0) {
|
|
927
|
+
value = value.slice(this.symbols.minusSign.length);
|
|
928
|
+
} else if (this.symbols.plusSign && value.startsWith(this.symbols.plusSign) && maxValue > 0) {
|
|
929
|
+
value = value.slice(this.symbols.plusSign.length);
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
// Numbers cannot start with a group separator
|
|
933
|
+
if (this.symbols.group && value.startsWith(this.symbols.group)) {
|
|
934
|
+
return false;
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
// Numbers that can't have any decimal values fail if a decimal character is typed
|
|
938
|
+
if (this.symbols.decimal && value.indexOf(this.symbols.decimal) > -1 && this.options.maximumFractionDigits === 0) {
|
|
939
|
+
return false;
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
// Remove numerals, groups, and decimals
|
|
943
|
+
if (this.symbols.group) {
|
|
944
|
+
value = replaceAll(value, this.symbols.group, '');
|
|
945
|
+
}
|
|
946
|
+
value = value.replace(this.symbols.numeral, '');
|
|
947
|
+
if (this.symbols.decimal) {
|
|
948
|
+
value = value.replace(this.symbols.decimal, '');
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
// The number is valid if there are no remaining characters
|
|
952
|
+
return value.length === 0;
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
const nonLiteralParts = new Set(['decimal', 'fraction', 'integer', 'minusSign', 'plusSign', 'group']);
|
|
956
|
+
|
|
957
|
+
// This list is derived from https://www.unicode.org/cldr/charts/43/supplemental/language_plural_rules.html#comparison and includes
|
|
958
|
+
// all unique numbers which we need to check in order to determine all the plural forms for a given locale.
|
|
959
|
+
// See: https://github.com/adobe/react-spectrum/pull/5134/files#r1337037855 for used script
|
|
960
|
+
const pluralNumbers = [0, 4, 2, 1, 11, 20, 3, 7, 100, 21, 0.1, 1.1];
|
|
961
|
+
function getSymbols(locale, formatter, intlOptions, originalOptions) {
|
|
962
|
+
var _allParts$find$value, _allParts$find, _posAllParts$find, _decimalParts$find, _allParts$find2;
|
|
963
|
+
// formatter needs access to all decimal places in order to generate the correct literal strings for the plural set
|
|
964
|
+
let symbolFormatter = new Intl.NumberFormat(locale, _objectSpread2(_objectSpread2({}, intlOptions), {}, {
|
|
965
|
+
minimumSignificantDigits: 1,
|
|
966
|
+
maximumSignificantDigits: 21
|
|
967
|
+
}));
|
|
968
|
+
// Note: some locale's don't add a group symbol until there is a ten thousands place
|
|
969
|
+
let allParts = symbolFormatter.formatToParts(-10000.111);
|
|
970
|
+
let posAllParts = symbolFormatter.formatToParts(10000.111);
|
|
971
|
+
let pluralParts = pluralNumbers.map(n => symbolFormatter.formatToParts(n));
|
|
972
|
+
let minusSign = (_allParts$find$value = (_allParts$find = allParts.find(p => p.type === 'minusSign')) === null || _allParts$find === void 0 ? void 0 : _allParts$find.value) !== null && _allParts$find$value !== void 0 ? _allParts$find$value : '-';
|
|
973
|
+
let plusSign = (_posAllParts$find = posAllParts.find(p => p.type === 'plusSign')) === null || _posAllParts$find === void 0 ? void 0 : _posAllParts$find.value;
|
|
974
|
+
|
|
975
|
+
// Safari does not support the signDisplay option, but our number parser polyfills it.
|
|
976
|
+
// If no plus sign was returned, but the original options contained signDisplay, default to the '+' character.
|
|
977
|
+
// @ts-ignore
|
|
978
|
+
if (!plusSign && ((originalOptions === null || originalOptions === void 0 ? void 0 : originalOptions.signDisplay) === 'exceptZero' || (originalOptions === null || originalOptions === void 0 ? void 0 : originalOptions.signDisplay) === 'always')) {
|
|
979
|
+
plusSign = '+';
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
// If maximumSignificantDigits is 1 (the minimum) then we won't get decimal characters out of the above formatters
|
|
983
|
+
// Percent also defaults to 0 fractionDigits, so we need to make a new one that isn't percent to get an accurate decimal
|
|
984
|
+
let decimalParts = new Intl.NumberFormat(locale, _objectSpread2(_objectSpread2({}, intlOptions), {}, {
|
|
985
|
+
minimumFractionDigits: 2,
|
|
986
|
+
maximumFractionDigits: 2
|
|
987
|
+
})).formatToParts(0.001);
|
|
988
|
+
let decimal = (_decimalParts$find = decimalParts.find(p => p.type === 'decimal')) === null || _decimalParts$find === void 0 ? void 0 : _decimalParts$find.value;
|
|
989
|
+
let group = (_allParts$find2 = allParts.find(p => p.type === 'group')) === null || _allParts$find2 === void 0 ? void 0 : _allParts$find2.value;
|
|
990
|
+
|
|
991
|
+
// this set is also for a regex, it's all literals that might be in the string we want to eventually parse that
|
|
992
|
+
// don't contribute to the numerical value
|
|
993
|
+
let allPartsLiterals = allParts.filter(p => !nonLiteralParts.has(p.type)).map(p => escapeRegex(p.value));
|
|
994
|
+
let pluralPartsLiterals = pluralParts.flatMap(p => p.filter(p => !nonLiteralParts.has(p.type)).map(p => escapeRegex(p.value)));
|
|
995
|
+
let sortedLiterals = [...new Set([...allPartsLiterals, ...pluralPartsLiterals])].sort((a, b) => b.length - a.length);
|
|
996
|
+
let literals = sortedLiterals.length === 0 ? new RegExp('[\\p{White_Space}]', 'gu') : new RegExp(`${sortedLiterals.join('|')}|[\\p{White_Space}]`, 'gu');
|
|
997
|
+
|
|
998
|
+
// These are for replacing non-latn characters with the latn equivalent
|
|
999
|
+
let numerals = [...new Intl.NumberFormat(intlOptions.locale, {
|
|
1000
|
+
useGrouping: false
|
|
1001
|
+
}).format(9876543210)].reverse();
|
|
1002
|
+
let indexes = new Map(numerals.map((d, i) => [d, i]));
|
|
1003
|
+
let numeral = new RegExp(`[${numerals.join('')}]`, 'g');
|
|
1004
|
+
let index = d => String(indexes.get(d));
|
|
1005
|
+
return {
|
|
1006
|
+
minusSign,
|
|
1007
|
+
plusSign,
|
|
1008
|
+
decimal,
|
|
1009
|
+
group,
|
|
1010
|
+
literals,
|
|
1011
|
+
numeral,
|
|
1012
|
+
index
|
|
1013
|
+
};
|
|
1014
|
+
}
|
|
1015
|
+
function replaceAll(str, find, replace) {
|
|
1016
|
+
// @ts-ignore
|
|
1017
|
+
if (str.replaceAll) {
|
|
1018
|
+
// @ts-ignore
|
|
1019
|
+
return str.replaceAll(find, replace);
|
|
1020
|
+
}
|
|
1021
|
+
return str.split(find).join(replace);
|
|
1022
|
+
}
|
|
1023
|
+
function escapeRegex(string) {
|
|
1024
|
+
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
/* eslint-disable prefer-const */
|
|
1028
|
+
|
|
1029
|
+
// Safari on iOS starts selecting text on long press. The only way to avoid this, it seems,
|
|
1030
|
+
// is to add user-select: none to the entire page. Adding it to the pressable element prevents
|
|
1031
|
+
// that element from being selected, but nearby elements may still receive selection. We add
|
|
1032
|
+
// user-select: none on touch start, and remove it again on touch end to prevent this.
|
|
1033
|
+
// This must be implemented using global state to avoid race conditions between multiple elements.
|
|
1034
|
+
|
|
1035
|
+
// There are three possible states due to the delay before removing user-select: none after
|
|
1036
|
+
// pointer up. The 'default' state always transitions to the 'disabled' state, which transitions
|
|
1037
|
+
// to 'restoring'. The 'restoring' state can either transition back to 'disabled' or 'default'.
|
|
1038
|
+
|
|
1039
|
+
// For non-iOS devices, we apply user-select: none to the pressed element instead to avoid possible
|
|
1040
|
+
// performance issues that arise from applying and removing user-select: none to the entire page
|
|
1041
|
+
// (see https://github.com/adobe/react-spectrum/issues/1609).
|
|
1042
|
+
// Note that state only matters here for iOS. Non-iOS gets user-select: none applied to the target element
|
|
1043
|
+
// rather than at the document level so we just need to apply/remove user-select: none for each pressed element individually
|
|
1044
|
+
let state = 'default';
|
|
1045
|
+
let savedUserSelect = '';
|
|
1046
|
+
let modifiedElementMap = new WeakMap();
|
|
1047
|
+
function disableTextSelection(target) {
|
|
1048
|
+
if (isIOS()) {
|
|
1049
|
+
if (state === 'default') {
|
|
1050
|
+
// eslint-disable-next-line no-restricted-globals
|
|
1051
|
+
const documentObject = getOwnerDocument(target);
|
|
1052
|
+
savedUserSelect = documentObject.documentElement.style.webkitUserSelect;
|
|
1053
|
+
documentObject.documentElement.style.webkitUserSelect = 'none';
|
|
1054
|
+
}
|
|
1055
|
+
state = 'disabled';
|
|
1056
|
+
} else if (target instanceof HTMLElement || target instanceof SVGElement) {
|
|
1057
|
+
// If not iOS, store the target's original user-select and change to user-select: none
|
|
1058
|
+
// Ignore state since it doesn't apply for non iOS
|
|
1059
|
+
modifiedElementMap.set(target, target.style.userSelect);
|
|
1060
|
+
target.style.userSelect = 'none';
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
1063
|
+
function restoreTextSelection(target) {
|
|
1064
|
+
if (isIOS()) {
|
|
1065
|
+
// If the state is already default, there's nothing to do.
|
|
1066
|
+
// If it is restoring, then there's no need to queue a second restore.
|
|
1067
|
+
if (state !== 'disabled') {
|
|
1068
|
+
return;
|
|
1069
|
+
}
|
|
1070
|
+
state = 'restoring';
|
|
1071
|
+
|
|
1072
|
+
// There appears to be a delay on iOS where selection still might occur
|
|
1073
|
+
// after pointer up, so wait a bit before removing user-select.
|
|
1074
|
+
setTimeout(() => {
|
|
1075
|
+
// Wait for any CSS transitions to complete so we don't recompute style
|
|
1076
|
+
// for the whole page in the middle of the animation and cause jank.
|
|
1077
|
+
runAfterTransition(() => {
|
|
1078
|
+
// Avoid race conditions
|
|
1079
|
+
if (state === 'restoring') {
|
|
1080
|
+
// eslint-disable-next-line no-restricted-globals
|
|
1081
|
+
const documentObject = getOwnerDocument(target);
|
|
1082
|
+
if (documentObject.documentElement.style.webkitUserSelect === 'none') {
|
|
1083
|
+
documentObject.documentElement.style.webkitUserSelect = savedUserSelect || '';
|
|
1084
|
+
}
|
|
1085
|
+
savedUserSelect = '';
|
|
1086
|
+
state = 'default';
|
|
1087
|
+
}
|
|
1088
|
+
});
|
|
1089
|
+
}, 300);
|
|
1090
|
+
} else if (target instanceof HTMLElement || target instanceof SVGElement) {
|
|
1091
|
+
// If not iOS, restore the target's original user-select if any
|
|
1092
|
+
// Ignore state since it doesn't apply for non iOS
|
|
1093
|
+
if (target && modifiedElementMap.has(target)) {
|
|
1094
|
+
let targetOldUserSelect = modifiedElementMap.get(target);
|
|
1095
|
+
if (target.style.userSelect === 'none') {
|
|
1096
|
+
target.style.userSelect = targetOldUserSelect;
|
|
1097
|
+
}
|
|
1098
|
+
if (target.getAttribute('style') === '') {
|
|
1099
|
+
target.removeAttribute('style');
|
|
1100
|
+
}
|
|
1101
|
+
modifiedElementMap.delete(target);
|
|
1102
|
+
}
|
|
1103
|
+
}
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
/*
|
|
1107
|
+
* Copyright 2020 Adobe. All rights reserved.
|
|
1108
|
+
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
|
|
1109
|
+
* you may not use this file except in compliance with the License. You may obtain a copy
|
|
1110
|
+
* of the License at http://www.apache.org/licenses/LICENSE-2.0
|
|
1111
|
+
*
|
|
1112
|
+
* Unless required by applicable law or agreed to in writing, software distributed under
|
|
1113
|
+
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
|
|
1114
|
+
* OF ANY KIND, either express or implied. See the License for the specific language
|
|
1115
|
+
* governing permissions and limitations under the License.
|
|
1116
|
+
*/
|
|
1117
|
+
const PressResponderContext = /*#__PURE__*/React__default.createContext({
|
|
1118
|
+
register: () => {}
|
|
1119
|
+
});
|
|
1120
|
+
PressResponderContext.displayName = 'PressResponderContext';
|
|
1121
|
+
|
|
1122
|
+
const _excluded$4 = ["register"],
|
|
1123
|
+
_excluded2 = ["onPress", "onPressChange", "onPressStart", "onPressEnd", "onPressUp", "isDisabled", "isPressed", "preventFocusOnPress", "shouldCancelOnPointerExit", "allowTextSelectionOnPress", "ref"];
|
|
1124
|
+
function usePressResponderContext(props) {
|
|
1125
|
+
// Consume context from <PressResponder> and merge with props.
|
|
1126
|
+
let context = useContext(PressResponderContext);
|
|
1127
|
+
if (context) {
|
|
1128
|
+
let {
|
|
1129
|
+
register
|
|
1130
|
+
} = context,
|
|
1131
|
+
contextProps = _objectWithoutProperties(context, _excluded$4);
|
|
1132
|
+
props = mergeProps(contextProps, props);
|
|
1133
|
+
register();
|
|
1134
|
+
}
|
|
1135
|
+
useSyncRef(context, props.ref);
|
|
1136
|
+
return props;
|
|
1137
|
+
}
|
|
1138
|
+
var _shouldStopPropagation = /*#__PURE__*/new WeakMap();
|
|
1139
|
+
class PressEvent {
|
|
1140
|
+
constructor(type, pointerType, originalEvent, state) {
|
|
1141
|
+
var _state$target;
|
|
1142
|
+
_defineProperty(this, "type", void 0);
|
|
1143
|
+
_defineProperty(this, "pointerType", void 0);
|
|
1144
|
+
_defineProperty(this, "target", void 0);
|
|
1145
|
+
_defineProperty(this, "shiftKey", void 0);
|
|
1146
|
+
_defineProperty(this, "ctrlKey", void 0);
|
|
1147
|
+
_defineProperty(this, "metaKey", void 0);
|
|
1148
|
+
_defineProperty(this, "altKey", void 0);
|
|
1149
|
+
_defineProperty(this, "x", void 0);
|
|
1150
|
+
_defineProperty(this, "y", void 0);
|
|
1151
|
+
_classPrivateFieldInitSpec(this, _shouldStopPropagation, {
|
|
1152
|
+
writable: true,
|
|
1153
|
+
value: true
|
|
1154
|
+
});
|
|
1155
|
+
let currentTarget = (_state$target = state === null || state === void 0 ? void 0 : state.target) !== null && _state$target !== void 0 ? _state$target : originalEvent.currentTarget;
|
|
1156
|
+
const rect = currentTarget === null || currentTarget === void 0 ? void 0 : currentTarget.getBoundingClientRect();
|
|
1157
|
+
let x,
|
|
1158
|
+
y = 0;
|
|
1159
|
+
let clientX,
|
|
1160
|
+
clientY = null;
|
|
1161
|
+
if (originalEvent.clientX != null && originalEvent.clientY != null) {
|
|
1162
|
+
clientX = originalEvent.clientX;
|
|
1163
|
+
clientY = originalEvent.clientY;
|
|
1164
|
+
}
|
|
1165
|
+
if (rect) {
|
|
1166
|
+
if (clientX != null && clientY != null) {
|
|
1167
|
+
x = clientX - rect.left;
|
|
1168
|
+
y = clientY - rect.top;
|
|
1169
|
+
} else {
|
|
1170
|
+
x = rect.width / 2;
|
|
1171
|
+
y = rect.height / 2;
|
|
1172
|
+
}
|
|
1173
|
+
}
|
|
1174
|
+
this.type = type;
|
|
1175
|
+
this.pointerType = pointerType;
|
|
1176
|
+
this.target = originalEvent.currentTarget;
|
|
1177
|
+
this.shiftKey = originalEvent.shiftKey;
|
|
1178
|
+
this.metaKey = originalEvent.metaKey;
|
|
1179
|
+
this.ctrlKey = originalEvent.ctrlKey;
|
|
1180
|
+
this.altKey = originalEvent.altKey;
|
|
1181
|
+
this.x = x;
|
|
1182
|
+
this.y = y;
|
|
1183
|
+
}
|
|
1184
|
+
continuePropagation() {
|
|
1185
|
+
_classPrivateFieldSet(this, _shouldStopPropagation, false);
|
|
1186
|
+
}
|
|
1187
|
+
get shouldStopPropagation() {
|
|
1188
|
+
return _classPrivateFieldGet(this, _shouldStopPropagation);
|
|
1189
|
+
}
|
|
1190
|
+
}
|
|
1191
|
+
const LINK_CLICKED = Symbol('linkClicked');
|
|
1192
|
+
|
|
1193
|
+
/**
|
|
1194
|
+
* Handles press interactions across mouse, touch, keyboard, and screen readers.
|
|
1195
|
+
* It normalizes behavior across browsers and platforms, and handles many nuances
|
|
1196
|
+
* of dealing with pointer and keyboard events.
|
|
1197
|
+
*/
|
|
1198
|
+
function usePress(props) {
|
|
1199
|
+
let _usePressResponderCon = usePressResponderContext(props),
|
|
1200
|
+
{
|
|
1201
|
+
onPress,
|
|
1202
|
+
onPressChange,
|
|
1203
|
+
onPressStart,
|
|
1204
|
+
onPressEnd,
|
|
1205
|
+
onPressUp,
|
|
1206
|
+
isDisabled,
|
|
1207
|
+
isPressed: isPressedProp,
|
|
1208
|
+
preventFocusOnPress,
|
|
1209
|
+
shouldCancelOnPointerExit,
|
|
1210
|
+
allowTextSelectionOnPress,
|
|
1211
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
1212
|
+
ref: _ // Removing `ref` from `domProps` because TypeScript is dumb
|
|
1213
|
+
} = _usePressResponderCon,
|
|
1214
|
+
domProps = _objectWithoutProperties(_usePressResponderCon, _excluded2);
|
|
1215
|
+
let [isPressed, setPressed] = useState(false);
|
|
1216
|
+
let ref = useRef({
|
|
1217
|
+
isPressed: false,
|
|
1218
|
+
ignoreEmulatedMouseEvents: false,
|
|
1219
|
+
ignoreClickAfterPress: false,
|
|
1220
|
+
didFirePressStart: false,
|
|
1221
|
+
isTriggeringEvent: false,
|
|
1222
|
+
activePointerId: null,
|
|
1223
|
+
target: null,
|
|
1224
|
+
isOverTarget: false,
|
|
1225
|
+
pointerType: null
|
|
1226
|
+
});
|
|
1227
|
+
let {
|
|
1228
|
+
addGlobalListener,
|
|
1229
|
+
removeAllGlobalListeners
|
|
1230
|
+
} = useGlobalListeners();
|
|
1231
|
+
let triggerPressStart = useEffectEvent((originalEvent, pointerType) => {
|
|
1232
|
+
let state = ref.current;
|
|
1233
|
+
if (isDisabled || state.didFirePressStart) {
|
|
1234
|
+
return false;
|
|
1235
|
+
}
|
|
1236
|
+
let shouldStopPropagation = true;
|
|
1237
|
+
state.isTriggeringEvent = true;
|
|
1238
|
+
if (onPressStart) {
|
|
1239
|
+
let event = new PressEvent('pressstart', pointerType, originalEvent);
|
|
1240
|
+
onPressStart(event);
|
|
1241
|
+
shouldStopPropagation = event.shouldStopPropagation;
|
|
1242
|
+
}
|
|
1243
|
+
if (onPressChange) {
|
|
1244
|
+
onPressChange(true);
|
|
1245
|
+
}
|
|
1246
|
+
state.isTriggeringEvent = false;
|
|
1247
|
+
state.didFirePressStart = true;
|
|
1248
|
+
setPressed(true);
|
|
1249
|
+
return shouldStopPropagation;
|
|
1250
|
+
});
|
|
1251
|
+
let triggerPressEnd = useEffectEvent(function (originalEvent, pointerType) {
|
|
1252
|
+
let wasPressed = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : true;
|
|
1253
|
+
let state = ref.current;
|
|
1254
|
+
if (!state.didFirePressStart) {
|
|
1255
|
+
return false;
|
|
1256
|
+
}
|
|
1257
|
+
state.ignoreClickAfterPress = true;
|
|
1258
|
+
state.didFirePressStart = false;
|
|
1259
|
+
state.isTriggeringEvent = true;
|
|
1260
|
+
let shouldStopPropagation = true;
|
|
1261
|
+
if (onPressEnd) {
|
|
1262
|
+
let event = new PressEvent('pressend', pointerType, originalEvent);
|
|
1263
|
+
onPressEnd(event);
|
|
1264
|
+
shouldStopPropagation = event.shouldStopPropagation;
|
|
1265
|
+
}
|
|
1266
|
+
if (onPressChange) {
|
|
1267
|
+
onPressChange(false);
|
|
1268
|
+
}
|
|
1269
|
+
setPressed(false);
|
|
1270
|
+
if (onPress && wasPressed && !isDisabled) {
|
|
1271
|
+
let event = new PressEvent('press', pointerType, originalEvent);
|
|
1272
|
+
onPress(event);
|
|
1273
|
+
shouldStopPropagation &&= event.shouldStopPropagation;
|
|
1274
|
+
}
|
|
1275
|
+
state.isTriggeringEvent = false;
|
|
1276
|
+
return shouldStopPropagation;
|
|
1277
|
+
});
|
|
1278
|
+
let triggerPressUp = useEffectEvent((originalEvent, pointerType) => {
|
|
1279
|
+
let state = ref.current;
|
|
1280
|
+
if (isDisabled) {
|
|
1281
|
+
return false;
|
|
1282
|
+
}
|
|
1283
|
+
if (onPressUp) {
|
|
1284
|
+
state.isTriggeringEvent = true;
|
|
1285
|
+
let event = new PressEvent('pressup', pointerType, originalEvent);
|
|
1286
|
+
onPressUp(event);
|
|
1287
|
+
state.isTriggeringEvent = false;
|
|
1288
|
+
return event.shouldStopPropagation;
|
|
1289
|
+
}
|
|
1290
|
+
return true;
|
|
1291
|
+
});
|
|
1292
|
+
let cancel = useEffectEvent(e => {
|
|
1293
|
+
let state = ref.current;
|
|
1294
|
+
if (state.isPressed && state.target) {
|
|
1295
|
+
if (state.isOverTarget && state.pointerType != null) {
|
|
1296
|
+
triggerPressEnd(createEvent(state.target, e), state.pointerType, false);
|
|
1297
|
+
}
|
|
1298
|
+
state.isPressed = false;
|
|
1299
|
+
state.isOverTarget = false;
|
|
1300
|
+
state.activePointerId = null;
|
|
1301
|
+
state.pointerType = null;
|
|
1302
|
+
removeAllGlobalListeners();
|
|
1303
|
+
if (!allowTextSelectionOnPress) {
|
|
1304
|
+
restoreTextSelection(state.target);
|
|
1305
|
+
}
|
|
1306
|
+
}
|
|
1307
|
+
});
|
|
1308
|
+
let cancelOnPointerExit = useEffectEvent(e => {
|
|
1309
|
+
if (shouldCancelOnPointerExit) {
|
|
1310
|
+
cancel(e);
|
|
1311
|
+
}
|
|
1312
|
+
});
|
|
1313
|
+
let pressProps = useMemo(() => {
|
|
1314
|
+
let state = ref.current;
|
|
1315
|
+
let pressProps = {
|
|
1316
|
+
onKeyDown(e) {
|
|
1317
|
+
if (isValidKeyboardEvent(e.nativeEvent, e.currentTarget) && e.currentTarget.contains(e.target)) {
|
|
1318
|
+
if (shouldPreventDefaultKeyboard(e.target, e.key)) {
|
|
1319
|
+
e.preventDefault();
|
|
1320
|
+
}
|
|
1321
|
+
|
|
1322
|
+
// If the event is repeating, it may have started on a different element
|
|
1323
|
+
// after which focus moved to the current element. Ignore these events and
|
|
1324
|
+
// only handle the first key down event.
|
|
1325
|
+
let shouldStopPropagation = true;
|
|
1326
|
+
if (!state.isPressed && !e.repeat) {
|
|
1327
|
+
state.target = e.currentTarget;
|
|
1328
|
+
state.isPressed = true;
|
|
1329
|
+
shouldStopPropagation = triggerPressStart(e, 'keyboard');
|
|
1330
|
+
|
|
1331
|
+
// Focus may move before the key up event, so register the event on the document
|
|
1332
|
+
// instead of the same element where the key down event occurred. Make it capturing so that it will trigger
|
|
1333
|
+
// before stopPropagation from useKeyboard on a child element may happen and thus we can still call triggerPress for the parent element.
|
|
1334
|
+
let originalTarget = e.currentTarget;
|
|
1335
|
+
let pressUp = e => {
|
|
1336
|
+
if (isValidKeyboardEvent(e, originalTarget) && !e.repeat && originalTarget.contains(e.target) && state.target) {
|
|
1337
|
+
triggerPressUp(createEvent(state.target, e), 'keyboard');
|
|
1338
|
+
}
|
|
1339
|
+
};
|
|
1340
|
+
addGlobalListener(getOwnerDocument(e.currentTarget), 'keyup', chain(pressUp, onKeyUp), true);
|
|
1341
|
+
}
|
|
1342
|
+
if (shouldStopPropagation) {
|
|
1343
|
+
e.stopPropagation();
|
|
1344
|
+
}
|
|
1345
|
+
|
|
1346
|
+
// Keep track of the keydown events that occur while the Meta (e.g. Command) key is held.
|
|
1347
|
+
// macOS has a bug where keyup events are not fired while the Meta key is down.
|
|
1348
|
+
// When the Meta key itself is released we will get an event for that, and we'll act as if
|
|
1349
|
+
// all of these other keys were released as well.
|
|
1350
|
+
// https://bugs.chromium.org/p/chromium/issues/detail?id=1393524
|
|
1351
|
+
// https://bugs.webkit.org/show_bug.cgi?id=55291
|
|
1352
|
+
// https://bugzilla.mozilla.org/show_bug.cgi?id=1299553
|
|
1353
|
+
if (e.metaKey && isMac()) {
|
|
1354
|
+
var _state$metaKeyEvents;
|
|
1355
|
+
(_state$metaKeyEvents = state.metaKeyEvents) === null || _state$metaKeyEvents === void 0 ? void 0 : _state$metaKeyEvents.set(e.key, e.nativeEvent);
|
|
1356
|
+
}
|
|
1357
|
+
} else if (e.key === 'Meta') {
|
|
1358
|
+
state.metaKeyEvents = new Map();
|
|
1359
|
+
}
|
|
1360
|
+
},
|
|
1361
|
+
onClick(e) {
|
|
1362
|
+
if (e && !e.currentTarget.contains(e.target)) {
|
|
1363
|
+
return;
|
|
1364
|
+
}
|
|
1365
|
+
if (e && e.button === 0 && !state.isTriggeringEvent && !openLink.isOpening) {
|
|
1366
|
+
let shouldStopPropagation = true;
|
|
1367
|
+
if (isDisabled) {
|
|
1368
|
+
e.preventDefault();
|
|
1369
|
+
}
|
|
1370
|
+
|
|
1371
|
+
// If triggered from a screen reader or by using element.click(),
|
|
1372
|
+
// trigger as if it were a keyboard click.
|
|
1373
|
+
if (!state.ignoreClickAfterPress && !state.ignoreEmulatedMouseEvents && !state.isPressed && (state.pointerType === 'virtual' || isVirtualClick(e.nativeEvent))) {
|
|
1374
|
+
// Ensure the element receives focus (VoiceOver on iOS does not do this)
|
|
1375
|
+
if (!isDisabled && !preventFocusOnPress) {
|
|
1376
|
+
focusWithoutScrolling(e.currentTarget);
|
|
1377
|
+
}
|
|
1378
|
+
let stopPressStart = triggerPressStart(e, 'virtual');
|
|
1379
|
+
let stopPressUp = triggerPressUp(e, 'virtual');
|
|
1380
|
+
let stopPressEnd = triggerPressEnd(e, 'virtual');
|
|
1381
|
+
shouldStopPropagation = stopPressStart && stopPressUp && stopPressEnd;
|
|
1382
|
+
}
|
|
1383
|
+
state.ignoreEmulatedMouseEvents = false;
|
|
1384
|
+
state.ignoreClickAfterPress = false;
|
|
1385
|
+
if (shouldStopPropagation) {
|
|
1386
|
+
e.stopPropagation();
|
|
1387
|
+
}
|
|
1388
|
+
}
|
|
1389
|
+
}
|
|
1390
|
+
};
|
|
1391
|
+
let onKeyUp = e => {
|
|
1392
|
+
var _state$metaKeyEvents3;
|
|
1393
|
+
if (state.isPressed && state.target && isValidKeyboardEvent(e, state.target)) {
|
|
1394
|
+
var _state$metaKeyEvents2;
|
|
1395
|
+
if (shouldPreventDefaultKeyboard(e.target, e.key)) {
|
|
1396
|
+
e.preventDefault();
|
|
1397
|
+
}
|
|
1398
|
+
let target = e.target;
|
|
1399
|
+
triggerPressEnd(createEvent(state.target, e), 'keyboard', state.target.contains(target));
|
|
1400
|
+
removeAllGlobalListeners();
|
|
1401
|
+
|
|
1402
|
+
// If a link was triggered with a key other than Enter, open the URL ourselves.
|
|
1403
|
+
// This means the link has a role override, and the default browser behavior
|
|
1404
|
+
// only applies when using the Enter key.
|
|
1405
|
+
if (e.key !== 'Enter' && isHTMLAnchorLink(state.target) && state.target.contains(target) && !e[LINK_CLICKED]) {
|
|
1406
|
+
// Store a hidden property on the event so we only trigger link click once,
|
|
1407
|
+
// even if there are multiple usePress instances attached to the element.
|
|
1408
|
+
e[LINK_CLICKED] = true;
|
|
1409
|
+
openLink(state.target, e, false);
|
|
1410
|
+
}
|
|
1411
|
+
state.isPressed = false;
|
|
1412
|
+
(_state$metaKeyEvents2 = state.metaKeyEvents) === null || _state$metaKeyEvents2 === void 0 ? void 0 : _state$metaKeyEvents2.delete(e.key);
|
|
1413
|
+
} else if (e.key === 'Meta' && (_state$metaKeyEvents3 = state.metaKeyEvents) !== null && _state$metaKeyEvents3 !== void 0 && _state$metaKeyEvents3.size) {
|
|
1414
|
+
// If we recorded keydown events that occurred while the Meta key was pressed,
|
|
1415
|
+
// and those haven't received keyup events already, fire keyup events ourselves.
|
|
1416
|
+
// See comment above for more info about the macOS bug causing this.
|
|
1417
|
+
let events = state.metaKeyEvents;
|
|
1418
|
+
state.metaKeyEvents = undefined;
|
|
1419
|
+
for (let event of events.values()) {
|
|
1420
|
+
var _state$target2;
|
|
1421
|
+
(_state$target2 = state.target) === null || _state$target2 === void 0 ? void 0 : _state$target2.dispatchEvent(new KeyboardEvent('keyup', event));
|
|
1422
|
+
}
|
|
1423
|
+
}
|
|
1424
|
+
};
|
|
1425
|
+
if (typeof PointerEvent !== 'undefined') {
|
|
1426
|
+
pressProps.onPointerDown = e => {
|
|
1427
|
+
// Only handle left clicks, and ignore events that bubbled through portals.
|
|
1428
|
+
if (e.button !== 0 || !e.currentTarget.contains(e.target)) {
|
|
1429
|
+
return;
|
|
1430
|
+
}
|
|
1431
|
+
|
|
1432
|
+
// iOS safari fires pointer events from VoiceOver with incorrect coordinates/target.
|
|
1433
|
+
// Ignore and let the onClick handler take care of it instead.
|
|
1434
|
+
// https://bugs.webkit.org/show_bug.cgi?id=222627
|
|
1435
|
+
// https://bugs.webkit.org/show_bug.cgi?id=223202
|
|
1436
|
+
if (isVirtualPointerEvent(e.nativeEvent)) {
|
|
1437
|
+
state.pointerType = 'virtual';
|
|
1438
|
+
return;
|
|
1439
|
+
}
|
|
1440
|
+
|
|
1441
|
+
// Due to browser inconsistencies, especially on mobile browsers, we prevent
|
|
1442
|
+
// default on pointer down and handle focusing the pressable element ourselves.
|
|
1443
|
+
if (shouldPreventDefault(e.currentTarget)) {
|
|
1444
|
+
e.preventDefault();
|
|
1445
|
+
}
|
|
1446
|
+
state.pointerType = e.pointerType;
|
|
1447
|
+
let shouldStopPropagation = true;
|
|
1448
|
+
if (!state.isPressed) {
|
|
1449
|
+
state.isPressed = true;
|
|
1450
|
+
state.isOverTarget = true;
|
|
1451
|
+
state.activePointerId = e.pointerId;
|
|
1452
|
+
state.target = e.currentTarget;
|
|
1453
|
+
if (!isDisabled && !preventFocusOnPress) {
|
|
1454
|
+
focusWithoutScrolling(e.currentTarget);
|
|
1455
|
+
}
|
|
1456
|
+
if (!allowTextSelectionOnPress) {
|
|
1457
|
+
disableTextSelection(state.target);
|
|
1458
|
+
}
|
|
1459
|
+
shouldStopPropagation = triggerPressStart(e, state.pointerType);
|
|
1460
|
+
addGlobalListener(getOwnerDocument(e.currentTarget), 'pointermove', onPointerMove, false);
|
|
1461
|
+
addGlobalListener(getOwnerDocument(e.currentTarget), 'pointerup', onPointerUp, false);
|
|
1462
|
+
addGlobalListener(getOwnerDocument(e.currentTarget), 'pointercancel', onPointerCancel, false);
|
|
1463
|
+
}
|
|
1464
|
+
if (shouldStopPropagation) {
|
|
1465
|
+
e.stopPropagation();
|
|
1466
|
+
}
|
|
1467
|
+
};
|
|
1468
|
+
pressProps.onMouseDown = e => {
|
|
1469
|
+
if (!e.currentTarget.contains(e.target)) {
|
|
1470
|
+
return;
|
|
1471
|
+
}
|
|
1472
|
+
if (e.button === 0) {
|
|
1473
|
+
// Chrome and Firefox on touch Windows devices require mouse down events
|
|
1474
|
+
// to be canceled in addition to pointer events, or an extra asynchronous
|
|
1475
|
+
// focus event will be fired.
|
|
1476
|
+
if (shouldPreventDefault(e.currentTarget)) {
|
|
1477
|
+
e.preventDefault();
|
|
1478
|
+
}
|
|
1479
|
+
e.stopPropagation();
|
|
1480
|
+
}
|
|
1481
|
+
};
|
|
1482
|
+
pressProps.onPointerUp = e => {
|
|
1483
|
+
// iOS fires pointerup with zero width and height, so check the pointerType recorded during pointerdown.
|
|
1484
|
+
if (!e.currentTarget.contains(e.target) || state.pointerType === 'virtual') {
|
|
1485
|
+
return;
|
|
1486
|
+
}
|
|
1487
|
+
|
|
1488
|
+
// Only handle left clicks
|
|
1489
|
+
// Safari on iOS sometimes fires pointerup events, even
|
|
1490
|
+
// when the touch isn't over the target, so double check.
|
|
1491
|
+
if (e.button === 0 && isOverTarget(e, e.currentTarget)) {
|
|
1492
|
+
triggerPressUp(e, state.pointerType || e.pointerType);
|
|
1493
|
+
}
|
|
1494
|
+
};
|
|
1495
|
+
|
|
1496
|
+
// Safari on iOS < 13.2 does not implement pointerenter/pointerleave events correctly.
|
|
1497
|
+
// Use pointer move events instead to implement our own hit testing.
|
|
1498
|
+
// See https://bugs.webkit.org/show_bug.cgi?id=199803
|
|
1499
|
+
let onPointerMove = e => {
|
|
1500
|
+
if (e.pointerId !== state.activePointerId) {
|
|
1501
|
+
return;
|
|
1502
|
+
}
|
|
1503
|
+
if (state.target && isOverTarget(e, state.target)) {
|
|
1504
|
+
if (!state.isOverTarget && state.pointerType != null) {
|
|
1505
|
+
state.isOverTarget = true;
|
|
1506
|
+
triggerPressStart(createEvent(state.target, e), state.pointerType);
|
|
1507
|
+
}
|
|
1508
|
+
} else if (state.target && state.isOverTarget && state.pointerType != null) {
|
|
1509
|
+
state.isOverTarget = false;
|
|
1510
|
+
triggerPressEnd(createEvent(state.target, e), state.pointerType, false);
|
|
1511
|
+
cancelOnPointerExit(e);
|
|
1512
|
+
}
|
|
1513
|
+
};
|
|
1514
|
+
let onPointerUp = e => {
|
|
1515
|
+
if (e.pointerId === state.activePointerId && state.isPressed && e.button === 0 && state.target) {
|
|
1516
|
+
if (isOverTarget(e, state.target) && state.pointerType != null) {
|
|
1517
|
+
triggerPressEnd(createEvent(state.target, e), state.pointerType);
|
|
1518
|
+
} else if (state.isOverTarget && state.pointerType != null) {
|
|
1519
|
+
triggerPressEnd(createEvent(state.target, e), state.pointerType, false);
|
|
1520
|
+
}
|
|
1521
|
+
state.isPressed = false;
|
|
1522
|
+
state.isOverTarget = false;
|
|
1523
|
+
state.activePointerId = null;
|
|
1524
|
+
state.pointerType = null;
|
|
1525
|
+
removeAllGlobalListeners();
|
|
1526
|
+
if (!allowTextSelectionOnPress) {
|
|
1527
|
+
restoreTextSelection(state.target);
|
|
1528
|
+
}
|
|
1529
|
+
}
|
|
1530
|
+
};
|
|
1531
|
+
let onPointerCancel = e => {
|
|
1532
|
+
cancel(e);
|
|
1533
|
+
};
|
|
1534
|
+
pressProps.onDragStart = e => {
|
|
1535
|
+
if (!e.currentTarget.contains(e.target)) {
|
|
1536
|
+
return;
|
|
1537
|
+
}
|
|
1538
|
+
|
|
1539
|
+
// Safari does not call onPointerCancel when a drag starts, whereas Chrome and Firefox do.
|
|
1540
|
+
cancel(e);
|
|
1541
|
+
};
|
|
1542
|
+
} else {
|
|
1543
|
+
pressProps.onMouseDown = e => {
|
|
1544
|
+
// Only handle left clicks
|
|
1545
|
+
if (e.button !== 0 || !e.currentTarget.contains(e.target)) {
|
|
1546
|
+
return;
|
|
1547
|
+
}
|
|
1548
|
+
|
|
1549
|
+
// Due to browser inconsistencies, especially on mobile browsers, we prevent
|
|
1550
|
+
// default on mouse down and handle focusing the pressable element ourselves.
|
|
1551
|
+
if (shouldPreventDefault(e.currentTarget)) {
|
|
1552
|
+
e.preventDefault();
|
|
1553
|
+
}
|
|
1554
|
+
if (state.ignoreEmulatedMouseEvents) {
|
|
1555
|
+
e.stopPropagation();
|
|
1556
|
+
return;
|
|
1557
|
+
}
|
|
1558
|
+
state.isPressed = true;
|
|
1559
|
+
state.isOverTarget = true;
|
|
1560
|
+
state.target = e.currentTarget;
|
|
1561
|
+
state.pointerType = isVirtualClick(e.nativeEvent) ? 'virtual' : 'mouse';
|
|
1562
|
+
if (!isDisabled && !preventFocusOnPress) {
|
|
1563
|
+
focusWithoutScrolling(e.currentTarget);
|
|
1564
|
+
}
|
|
1565
|
+
let shouldStopPropagation = triggerPressStart(e, state.pointerType);
|
|
1566
|
+
if (shouldStopPropagation) {
|
|
1567
|
+
e.stopPropagation();
|
|
1568
|
+
}
|
|
1569
|
+
addGlobalListener(getOwnerDocument(e.currentTarget), 'mouseup', onMouseUp, false);
|
|
1570
|
+
};
|
|
1571
|
+
pressProps.onMouseEnter = e => {
|
|
1572
|
+
if (!e.currentTarget.contains(e.target)) {
|
|
1573
|
+
return;
|
|
1574
|
+
}
|
|
1575
|
+
let shouldStopPropagation = true;
|
|
1576
|
+
if (state.isPressed && !state.ignoreEmulatedMouseEvents && state.pointerType != null) {
|
|
1577
|
+
state.isOverTarget = true;
|
|
1578
|
+
shouldStopPropagation = triggerPressStart(e, state.pointerType);
|
|
1579
|
+
}
|
|
1580
|
+
if (shouldStopPropagation) {
|
|
1581
|
+
e.stopPropagation();
|
|
1582
|
+
}
|
|
1583
|
+
};
|
|
1584
|
+
pressProps.onMouseLeave = e => {
|
|
1585
|
+
if (!e.currentTarget.contains(e.target)) {
|
|
1586
|
+
return;
|
|
1587
|
+
}
|
|
1588
|
+
let shouldStopPropagation = true;
|
|
1589
|
+
if (state.isPressed && !state.ignoreEmulatedMouseEvents && state.pointerType != null) {
|
|
1590
|
+
state.isOverTarget = false;
|
|
1591
|
+
shouldStopPropagation = triggerPressEnd(e, state.pointerType, false);
|
|
1592
|
+
cancelOnPointerExit(e);
|
|
1593
|
+
}
|
|
1594
|
+
if (shouldStopPropagation) {
|
|
1595
|
+
e.stopPropagation();
|
|
1596
|
+
}
|
|
1597
|
+
};
|
|
1598
|
+
pressProps.onMouseUp = e => {
|
|
1599
|
+
if (!e.currentTarget.contains(e.target)) {
|
|
1600
|
+
return;
|
|
1601
|
+
}
|
|
1602
|
+
if (!state.ignoreEmulatedMouseEvents && e.button === 0) {
|
|
1603
|
+
triggerPressUp(e, state.pointerType || 'mouse');
|
|
1604
|
+
}
|
|
1605
|
+
};
|
|
1606
|
+
let onMouseUp = e => {
|
|
1607
|
+
// Only handle left clicks
|
|
1608
|
+
if (e.button !== 0) {
|
|
1609
|
+
return;
|
|
1610
|
+
}
|
|
1611
|
+
state.isPressed = false;
|
|
1612
|
+
removeAllGlobalListeners();
|
|
1613
|
+
if (state.ignoreEmulatedMouseEvents) {
|
|
1614
|
+
state.ignoreEmulatedMouseEvents = false;
|
|
1615
|
+
return;
|
|
1616
|
+
}
|
|
1617
|
+
if (state.target && isOverTarget(e, state.target) && state.pointerType != null) {
|
|
1618
|
+
triggerPressEnd(createEvent(state.target, e), state.pointerType);
|
|
1619
|
+
} else if (state.target && state.isOverTarget && state.pointerType != null) {
|
|
1620
|
+
triggerPressEnd(createEvent(state.target, e), state.pointerType, false);
|
|
1621
|
+
}
|
|
1622
|
+
state.isOverTarget = false;
|
|
1623
|
+
};
|
|
1624
|
+
pressProps.onTouchStart = e => {
|
|
1625
|
+
if (!e.currentTarget.contains(e.target)) {
|
|
1626
|
+
return;
|
|
1627
|
+
}
|
|
1628
|
+
let touch = getTouchFromEvent(e.nativeEvent);
|
|
1629
|
+
if (!touch) {
|
|
1630
|
+
return;
|
|
1631
|
+
}
|
|
1632
|
+
state.activePointerId = touch.identifier;
|
|
1633
|
+
state.ignoreEmulatedMouseEvents = true;
|
|
1634
|
+
state.isOverTarget = true;
|
|
1635
|
+
state.isPressed = true;
|
|
1636
|
+
state.target = e.currentTarget;
|
|
1637
|
+
state.pointerType = 'touch';
|
|
1638
|
+
|
|
1639
|
+
// Due to browser inconsistencies, especially on mobile browsers, we prevent default
|
|
1640
|
+
// on the emulated mouse event and handle focusing the pressable element ourselves.
|
|
1641
|
+
if (!isDisabled && !preventFocusOnPress) {
|
|
1642
|
+
focusWithoutScrolling(e.currentTarget);
|
|
1643
|
+
}
|
|
1644
|
+
if (!allowTextSelectionOnPress) {
|
|
1645
|
+
disableTextSelection(state.target);
|
|
1646
|
+
}
|
|
1647
|
+
let shouldStopPropagation = triggerPressStart(createTouchEvent(state.target, e), state.pointerType);
|
|
1648
|
+
if (shouldStopPropagation) {
|
|
1649
|
+
e.stopPropagation();
|
|
1650
|
+
}
|
|
1651
|
+
addGlobalListener(getOwnerWindow(e.currentTarget), 'scroll', onScroll, true);
|
|
1652
|
+
};
|
|
1653
|
+
pressProps.onTouchMove = e => {
|
|
1654
|
+
if (!e.currentTarget.contains(e.target)) {
|
|
1655
|
+
return;
|
|
1656
|
+
}
|
|
1657
|
+
if (!state.isPressed) {
|
|
1658
|
+
e.stopPropagation();
|
|
1659
|
+
return;
|
|
1660
|
+
}
|
|
1661
|
+
let touch = getTouchById(e.nativeEvent, state.activePointerId);
|
|
1662
|
+
let shouldStopPropagation = true;
|
|
1663
|
+
if (touch && isOverTarget(touch, e.currentTarget)) {
|
|
1664
|
+
if (!state.isOverTarget && state.pointerType != null) {
|
|
1665
|
+
state.isOverTarget = true;
|
|
1666
|
+
shouldStopPropagation = triggerPressStart(createTouchEvent(state.target, e), state.pointerType);
|
|
1667
|
+
}
|
|
1668
|
+
} else if (state.isOverTarget && state.pointerType != null) {
|
|
1669
|
+
state.isOverTarget = false;
|
|
1670
|
+
shouldStopPropagation = triggerPressEnd(createTouchEvent(state.target, e), state.pointerType, false);
|
|
1671
|
+
cancelOnPointerExit(createTouchEvent(state.target, e));
|
|
1672
|
+
}
|
|
1673
|
+
if (shouldStopPropagation) {
|
|
1674
|
+
e.stopPropagation();
|
|
1675
|
+
}
|
|
1676
|
+
};
|
|
1677
|
+
pressProps.onTouchEnd = e => {
|
|
1678
|
+
if (!e.currentTarget.contains(e.target)) {
|
|
1679
|
+
return;
|
|
1680
|
+
}
|
|
1681
|
+
if (!state.isPressed) {
|
|
1682
|
+
e.stopPropagation();
|
|
1683
|
+
return;
|
|
1684
|
+
}
|
|
1685
|
+
let touch = getTouchById(e.nativeEvent, state.activePointerId);
|
|
1686
|
+
let shouldStopPropagation = true;
|
|
1687
|
+
if (touch && isOverTarget(touch, e.currentTarget) && state.pointerType != null) {
|
|
1688
|
+
triggerPressUp(createTouchEvent(state.target, e), state.pointerType);
|
|
1689
|
+
shouldStopPropagation = triggerPressEnd(createTouchEvent(state.target, e), state.pointerType);
|
|
1690
|
+
} else if (state.isOverTarget && state.pointerType != null) {
|
|
1691
|
+
shouldStopPropagation = triggerPressEnd(createTouchEvent(state.target, e), state.pointerType, false);
|
|
1692
|
+
}
|
|
1693
|
+
if (shouldStopPropagation) {
|
|
1694
|
+
e.stopPropagation();
|
|
1695
|
+
}
|
|
1696
|
+
state.isPressed = false;
|
|
1697
|
+
state.activePointerId = null;
|
|
1698
|
+
state.isOverTarget = false;
|
|
1699
|
+
state.ignoreEmulatedMouseEvents = true;
|
|
1700
|
+
if (state.target && !allowTextSelectionOnPress) {
|
|
1701
|
+
restoreTextSelection(state.target);
|
|
1702
|
+
}
|
|
1703
|
+
removeAllGlobalListeners();
|
|
1704
|
+
};
|
|
1705
|
+
pressProps.onTouchCancel = e => {
|
|
1706
|
+
if (!e.currentTarget.contains(e.target)) {
|
|
1707
|
+
return;
|
|
1708
|
+
}
|
|
1709
|
+
e.stopPropagation();
|
|
1710
|
+
if (state.isPressed) {
|
|
1711
|
+
cancel(createTouchEvent(state.target, e));
|
|
1712
|
+
}
|
|
1713
|
+
};
|
|
1714
|
+
let onScroll = e => {
|
|
1715
|
+
if (state.isPressed && e.target.contains(state.target)) {
|
|
1716
|
+
cancel({
|
|
1717
|
+
currentTarget: state.target,
|
|
1718
|
+
shiftKey: false,
|
|
1719
|
+
ctrlKey: false,
|
|
1720
|
+
metaKey: false,
|
|
1721
|
+
altKey: false
|
|
1722
|
+
});
|
|
1723
|
+
}
|
|
1724
|
+
};
|
|
1725
|
+
pressProps.onDragStart = e => {
|
|
1726
|
+
if (!e.currentTarget.contains(e.target)) {
|
|
1727
|
+
return;
|
|
1728
|
+
}
|
|
1729
|
+
cancel(e);
|
|
1730
|
+
};
|
|
1731
|
+
}
|
|
1732
|
+
return pressProps;
|
|
1733
|
+
}, [addGlobalListener, isDisabled, preventFocusOnPress, removeAllGlobalListeners, allowTextSelectionOnPress, cancel, cancelOnPointerExit, triggerPressEnd, triggerPressStart, triggerPressUp]);
|
|
1734
|
+
|
|
1735
|
+
// Remove user-select: none in case component unmounts immediately after pressStart
|
|
1736
|
+
// eslint-disable-next-line arrow-body-style
|
|
1737
|
+
useEffect(() => {
|
|
1738
|
+
return () => {
|
|
1739
|
+
if (!allowTextSelectionOnPress) {
|
|
1740
|
+
var _ref$current$target;
|
|
1741
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
1742
|
+
restoreTextSelection((_ref$current$target = ref.current.target) !== null && _ref$current$target !== void 0 ? _ref$current$target : undefined);
|
|
1743
|
+
}
|
|
1744
|
+
};
|
|
1745
|
+
}, [allowTextSelectionOnPress]);
|
|
1746
|
+
return {
|
|
1747
|
+
isPressed: isPressedProp || isPressed,
|
|
1748
|
+
pressProps: mergeProps(domProps, pressProps)
|
|
1749
|
+
};
|
|
1750
|
+
}
|
|
1751
|
+
function isHTMLAnchorLink(target) {
|
|
1752
|
+
return target.tagName === 'A' && target.hasAttribute('href');
|
|
1753
|
+
}
|
|
1754
|
+
function isValidKeyboardEvent(event, currentTarget) {
|
|
1755
|
+
const {
|
|
1756
|
+
key,
|
|
1757
|
+
code
|
|
1758
|
+
} = event;
|
|
1759
|
+
const element = currentTarget;
|
|
1760
|
+
const role = element.getAttribute('role');
|
|
1761
|
+
// Accessibility for keyboards. Space and Enter only.
|
|
1762
|
+
// "Spacebar" is for IE 11
|
|
1763
|
+
return (key === 'Enter' || key === ' ' || key === 'Spacebar' || code === 'Space') && !(element instanceof getOwnerWindow(element).HTMLInputElement && !isValidInputKey(element, key) || element instanceof getOwnerWindow(element).HTMLTextAreaElement || element.isContentEditable) &&
|
|
1764
|
+
// Links should only trigger with Enter key
|
|
1765
|
+
!((role === 'link' || !role && isHTMLAnchorLink(element)) && key !== 'Enter');
|
|
1766
|
+
}
|
|
1767
|
+
function getTouchFromEvent(event) {
|
|
1768
|
+
const {
|
|
1769
|
+
targetTouches
|
|
1770
|
+
} = event;
|
|
1771
|
+
if (targetTouches.length > 0) {
|
|
1772
|
+
return targetTouches[0];
|
|
1773
|
+
}
|
|
1774
|
+
return null;
|
|
1775
|
+
}
|
|
1776
|
+
function getTouchById(event, pointerId) {
|
|
1777
|
+
const changedTouches = event.changedTouches;
|
|
1778
|
+
for (let i = 0; i < changedTouches.length; i++) {
|
|
1779
|
+
const touch = changedTouches[i];
|
|
1780
|
+
if (touch.identifier === pointerId) {
|
|
1781
|
+
return touch;
|
|
1782
|
+
}
|
|
1783
|
+
}
|
|
1784
|
+
return null;
|
|
1785
|
+
}
|
|
1786
|
+
function createTouchEvent(target, e) {
|
|
1787
|
+
let clientX = 0;
|
|
1788
|
+
let clientY = 0;
|
|
1789
|
+
if (e.targetTouches && e.targetTouches.length === 1) {
|
|
1790
|
+
clientX = e.targetTouches[0].clientX;
|
|
1791
|
+
clientY = e.targetTouches[0].clientY;
|
|
1792
|
+
}
|
|
1793
|
+
return {
|
|
1794
|
+
currentTarget: target,
|
|
1795
|
+
shiftKey: e.shiftKey,
|
|
1796
|
+
ctrlKey: e.ctrlKey,
|
|
1797
|
+
metaKey: e.metaKey,
|
|
1798
|
+
altKey: e.altKey,
|
|
1799
|
+
clientX,
|
|
1800
|
+
clientY
|
|
1801
|
+
};
|
|
1802
|
+
}
|
|
1803
|
+
function createEvent(target, e) {
|
|
1804
|
+
let clientX = e.clientX;
|
|
1805
|
+
let clientY = e.clientY;
|
|
1806
|
+
return {
|
|
1807
|
+
currentTarget: target,
|
|
1808
|
+
shiftKey: e.shiftKey,
|
|
1809
|
+
ctrlKey: e.ctrlKey,
|
|
1810
|
+
metaKey: e.metaKey,
|
|
1811
|
+
altKey: e.altKey,
|
|
1812
|
+
clientX,
|
|
1813
|
+
clientY
|
|
1814
|
+
};
|
|
1815
|
+
}
|
|
1816
|
+
function getPointClientRect(point) {
|
|
1817
|
+
let offsetX = 0;
|
|
1818
|
+
let offsetY = 0;
|
|
1819
|
+
if (point.width !== undefined) {
|
|
1820
|
+
offsetX = point.width / 2;
|
|
1821
|
+
} else if (point.radiusX !== undefined) {
|
|
1822
|
+
offsetX = point.radiusX;
|
|
1823
|
+
}
|
|
1824
|
+
if (point.height !== undefined) {
|
|
1825
|
+
offsetY = point.height / 2;
|
|
1826
|
+
} else if (point.radiusY !== undefined) {
|
|
1827
|
+
offsetY = point.radiusY;
|
|
1828
|
+
}
|
|
1829
|
+
return {
|
|
1830
|
+
top: point.clientY - offsetY,
|
|
1831
|
+
right: point.clientX + offsetX,
|
|
1832
|
+
bottom: point.clientY + offsetY,
|
|
1833
|
+
left: point.clientX - offsetX
|
|
1834
|
+
};
|
|
1835
|
+
}
|
|
1836
|
+
function areRectanglesOverlapping(a, b) {
|
|
1837
|
+
// check if they cannot overlap on x axis
|
|
1838
|
+
if (a.left > b.right || b.left > a.right) {
|
|
1839
|
+
return false;
|
|
1840
|
+
}
|
|
1841
|
+
// check if they cannot overlap on y axis
|
|
1842
|
+
if (a.top > b.bottom || b.top > a.bottom) {
|
|
1843
|
+
return false;
|
|
1844
|
+
}
|
|
1845
|
+
return true;
|
|
1846
|
+
}
|
|
1847
|
+
function isOverTarget(point, target) {
|
|
1848
|
+
let rect = target.getBoundingClientRect();
|
|
1849
|
+
let pointRect = getPointClientRect(point);
|
|
1850
|
+
return areRectanglesOverlapping(rect, pointRect);
|
|
1851
|
+
}
|
|
1852
|
+
function shouldPreventDefault(target) {
|
|
1853
|
+
// We cannot prevent default if the target is a draggable element.
|
|
1854
|
+
return !(target instanceof HTMLElement) || !target.hasAttribute('draggable');
|
|
1855
|
+
}
|
|
1856
|
+
function shouldPreventDefaultKeyboard(target, key) {
|
|
1857
|
+
if (target instanceof HTMLInputElement) {
|
|
1858
|
+
return !isValidInputKey(target, key);
|
|
1859
|
+
}
|
|
1860
|
+
if (target instanceof HTMLButtonElement) {
|
|
1861
|
+
return target.type !== 'submit' && target.type !== 'reset';
|
|
1862
|
+
}
|
|
1863
|
+
if (isHTMLAnchorLink(target)) {
|
|
1864
|
+
return false;
|
|
1865
|
+
}
|
|
1866
|
+
return true;
|
|
1867
|
+
}
|
|
1868
|
+
const nonTextInputTypes = new Set(['checkbox', 'radio', 'range', 'color', 'file', 'image', 'button', 'submit', 'reset']);
|
|
1869
|
+
function isValidInputKey(target, key) {
|
|
1870
|
+
// Only space should toggle checkboxes and radios, not enter.
|
|
1871
|
+
return target.type === 'checkbox' || target.type === 'radio' ? key === ' ' : nonTextInputTypes.has(target.type);
|
|
1872
|
+
}
|
|
1873
|
+
|
|
1874
|
+
/* eslint-disable prefer-const */
|
|
1875
|
+
// @ts-nocheck
|
|
1876
|
+
/*
|
|
1877
|
+
* Copyright 2020 Adobe. All rights reserved.
|
|
1878
|
+
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
|
|
1879
|
+
* you may not use this file except in compliance with the License. You may obtain a copy
|
|
1880
|
+
* of the License at http://www.apache.org/licenses/LICENSE-2.0
|
|
1881
|
+
*
|
|
1882
|
+
* Unless required by applicable law or agreed to in writing, software distributed under
|
|
1883
|
+
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
|
|
1884
|
+
* OF ANY KIND, either express or implied. See the License for the specific language
|
|
1885
|
+
* governing permissions and limitations under the License.
|
|
1886
|
+
*/
|
|
1887
|
+
|
|
1888
|
+
/**
|
|
1889
|
+
* This function wraps a React event handler to make stopPropagation the default, and support continuePropagation instead.
|
|
1890
|
+
*/
|
|
1891
|
+
function createEventHandler(handler) {
|
|
1892
|
+
if (!handler) {
|
|
1893
|
+
return undefined;
|
|
1894
|
+
}
|
|
1895
|
+
let shouldStopPropagation = true;
|
|
1896
|
+
return e => {
|
|
1897
|
+
let event = _objectSpread2(_objectSpread2({}, e), {}, {
|
|
1898
|
+
preventDefault() {
|
|
1899
|
+
e.preventDefault();
|
|
1900
|
+
},
|
|
1901
|
+
isDefaultPrevented() {
|
|
1902
|
+
return e.isDefaultPrevented();
|
|
1903
|
+
},
|
|
1904
|
+
stopPropagation() {
|
|
1905
|
+
console.error('stopPropagation is now the default behavior for events in React Spectrum. You can use continuePropagation() to revert this behavior.');
|
|
1906
|
+
},
|
|
1907
|
+
continuePropagation() {
|
|
1908
|
+
shouldStopPropagation = false;
|
|
1909
|
+
}
|
|
1910
|
+
});
|
|
1911
|
+
handler(event);
|
|
1912
|
+
if (shouldStopPropagation) {
|
|
1913
|
+
e.stopPropagation();
|
|
1914
|
+
}
|
|
1915
|
+
};
|
|
1916
|
+
}
|
|
1917
|
+
|
|
1918
|
+
/*
|
|
1919
|
+
* Copyright 2020 Adobe. All rights reserved.
|
|
1920
|
+
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
|
|
1921
|
+
* you may not use this file except in compliance with the License. You may obtain a copy
|
|
1922
|
+
* of the License at http://www.apache.org/licenses/LICENSE-2.0
|
|
1923
|
+
*
|
|
1924
|
+
* Unless required by applicable law or agreed to in writing, software distributed under
|
|
1925
|
+
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
|
|
1926
|
+
* OF ANY KIND, either express or implied. See the License for the specific language
|
|
1927
|
+
* governing permissions and limitations under the License.
|
|
1928
|
+
*/
|
|
1929
|
+
/**
|
|
1930
|
+
* Handles keyboard interactions for a focusable element.
|
|
1931
|
+
*/
|
|
1932
|
+
function useKeyboard(props) {
|
|
1933
|
+
return {
|
|
1934
|
+
keyboardProps: props.isDisabled ? {} : {
|
|
1935
|
+
onKeyDown: createEventHandler(props.onKeyDown),
|
|
1936
|
+
onKeyUp: createEventHandler(props.onKeyUp)
|
|
1937
|
+
}
|
|
1938
|
+
};
|
|
1939
|
+
}
|
|
1940
|
+
|
|
1941
|
+
/* eslint-disable prefer-const */
|
|
1942
|
+
// scroll wheel needs to be added not passively so it's cancelable, small helper hook to remember that
|
|
1943
|
+
function useScrollWheel(props, ref) {
|
|
1944
|
+
let {
|
|
1945
|
+
onScroll,
|
|
1946
|
+
isDisabled
|
|
1947
|
+
} = props;
|
|
1948
|
+
let onScrollHandler = useCallback(e => {
|
|
1949
|
+
// If the ctrlKey is pressed, this is a zoom event, do nothing.
|
|
1950
|
+
if (e.ctrlKey) {
|
|
1951
|
+
return;
|
|
1952
|
+
}
|
|
1953
|
+
|
|
1954
|
+
// stop scrolling the page
|
|
1955
|
+
e.preventDefault();
|
|
1956
|
+
e.stopPropagation();
|
|
1957
|
+
if (onScroll) {
|
|
1958
|
+
onScroll({
|
|
1959
|
+
deltaX: e.deltaX,
|
|
1960
|
+
deltaY: e.deltaY
|
|
1961
|
+
});
|
|
1962
|
+
}
|
|
1963
|
+
}, [onScroll]);
|
|
1964
|
+
useEvent(ref, 'wheel', isDisabled ? undefined : onScrollHandler);
|
|
1965
|
+
}
|
|
1966
|
+
|
|
1967
|
+
/* eslint-disable prefer-const */
|
|
1968
|
+
|
|
1969
|
+
/**
|
|
1970
|
+
* A utility function that focuses an element while avoiding undesired side effects such
|
|
1971
|
+
* as page scrolling and screen reader issues with CSS transitions.
|
|
1972
|
+
*/
|
|
1973
|
+
function focusSafely(element) {
|
|
1974
|
+
// If the user is interacting with a virtual cursor, e.g. screen reader, then
|
|
1975
|
+
// wait until after any animated transitions that are currently occurring on
|
|
1976
|
+
// the page before shifting focus. This avoids issues with VoiceOver on iOS
|
|
1977
|
+
// causing the page to scroll when moving focus if the element is transitioning
|
|
1978
|
+
// from off the screen.
|
|
1979
|
+
const ownerDocument = getOwnerDocument(element);
|
|
1980
|
+
if (getInteractionModality() === 'virtual') {
|
|
1981
|
+
let lastFocusedElement = ownerDocument.activeElement;
|
|
1982
|
+
runAfterTransition(() => {
|
|
1983
|
+
// If focus did not move and the element is still in the document, focus it.
|
|
1984
|
+
if (ownerDocument.activeElement === lastFocusedElement && element.isConnected) {
|
|
1985
|
+
focusWithoutScrolling(element);
|
|
1986
|
+
}
|
|
1987
|
+
});
|
|
1988
|
+
} else {
|
|
1989
|
+
focusWithoutScrolling(element);
|
|
1990
|
+
}
|
|
1991
|
+
}
|
|
1992
|
+
|
|
1993
|
+
const _excluded$3 = ["ref"];
|
|
1994
|
+
let FocusableContext = /*#__PURE__*/React__default.createContext(null);
|
|
1995
|
+
function useFocusableContext(ref) {
|
|
1996
|
+
let context = useContext(FocusableContext) || {};
|
|
1997
|
+
useSyncRef(context, ref);
|
|
1998
|
+
|
|
1999
|
+
// eslint-disable-next-line
|
|
2000
|
+
let otherProps = _objectWithoutProperties(context, _excluded$3);
|
|
2001
|
+
return otherProps;
|
|
2002
|
+
}
|
|
2003
|
+
/**
|
|
2004
|
+
* Used to make an element focusable and capable of auto focus.
|
|
2005
|
+
*/
|
|
2006
|
+
function useFocusable(props, domRef) {
|
|
2007
|
+
let {
|
|
2008
|
+
focusProps
|
|
2009
|
+
} = useFocus(props);
|
|
2010
|
+
let {
|
|
2011
|
+
keyboardProps
|
|
2012
|
+
} = useKeyboard(props);
|
|
2013
|
+
let interactions = mergeProps(focusProps, keyboardProps);
|
|
2014
|
+
let domProps = useFocusableContext(domRef);
|
|
2015
|
+
let interactionProps = props.isDisabled ? {} : domProps;
|
|
2016
|
+
let autoFocusRef = useRef(props.autoFocus);
|
|
2017
|
+
useEffect(() => {
|
|
2018
|
+
if (autoFocusRef.current && domRef.current) {
|
|
2019
|
+
focusSafely(domRef.current);
|
|
2020
|
+
}
|
|
2021
|
+
autoFocusRef.current = false;
|
|
2022
|
+
}, [domRef]);
|
|
2023
|
+
return {
|
|
2024
|
+
focusableProps: mergeProps(_objectSpread2(_objectSpread2({}, interactions), {}, {
|
|
2025
|
+
tabIndex: props.excludeFromTabOrder && !props.isDisabled ? -1 : undefined
|
|
2026
|
+
}), interactionProps)
|
|
2027
|
+
};
|
|
2028
|
+
}
|
|
4
2029
|
|
|
5
2030
|
/**
|
|
6
2031
|
* Component variant.
|
|
7
2032
|
*/
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
2033
|
+
const NumberFieldVariant = {
|
|
2034
|
+
default: 'default',
|
|
2035
|
+
underline: 'underline'
|
|
11
2036
|
};
|
|
12
|
-
|
|
2037
|
+
|
|
13
2038
|
/**
|
|
14
2039
|
* Component props.
|
|
15
2040
|
*/
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
value?: number;
|
|
39
|
-
/** Placeholder. */
|
|
40
|
-
placeholder?: number;
|
|
41
|
-
/** A custom aria-label for the decrement button. If not provided, the localized string "Decrement" is used. */
|
|
42
|
-
decrementAriaLabel?: string;
|
|
43
|
-
/** A custom aria-label for the increment button. If not provided, the localized string "Increment" is used. */
|
|
44
|
-
incrementAriaLabel?: string;
|
|
45
|
-
/**
|
|
46
|
-
* Enables or disables changing the value with scroll.
|
|
47
|
-
*/
|
|
48
|
-
isWheelDisabled?: boolean;
|
|
49
|
-
/**
|
|
50
|
-
* Formatting options for the value displayed in the number field.
|
|
51
|
-
* This also affects what characters are allowed to be typed by the user.
|
|
52
|
-
*/
|
|
53
|
-
formatOptions?: Intl.NumberFormatOptions;
|
|
54
|
-
/** The smallest value allowed for the input. */
|
|
55
|
-
minValue?: number;
|
|
56
|
-
/** The largest value allowed for the input. */
|
|
57
|
-
maxValue?: number;
|
|
58
|
-
/** The amount that the input value changes with each increment or decrement "tick". */
|
|
59
|
-
step?: number;
|
|
60
|
-
}
|
|
61
|
-
type StyledNumberFieldProps = Omit<NumberFieldProps, 'color' | 'isColored' | 'isDisabled' | 'isInvalid' | 'isRequired' | 'onChange' | 'onBlur' | 'onFocus'> & {
|
|
62
|
-
$hasLeftIcon: boolean;
|
|
63
|
-
$hasContent: boolean;
|
|
64
|
-
$isColored: NumberFieldProps['isColored'];
|
|
65
|
-
$isDisabled: NumberFieldProps['isDisabled'];
|
|
66
|
-
$isInvalid: NumberFieldProps['isInvalid'];
|
|
67
|
-
$isFocused: boolean;
|
|
68
|
-
$isFocusVisible: boolean;
|
|
69
|
-
$isRequired: NumberFieldProps['isRequired'];
|
|
70
|
-
$theme: NumberFieldProps['theme'];
|
|
71
|
-
$variant: NumberFieldProps['variant'];
|
|
2041
|
+
|
|
2042
|
+
const VALID_VALIDITY_STATE = {
|
|
2043
|
+
badInput: false,
|
|
2044
|
+
customError: false,
|
|
2045
|
+
patternMismatch: false,
|
|
2046
|
+
rangeOverflow: false,
|
|
2047
|
+
rangeUnderflow: false,
|
|
2048
|
+
stepMismatch: false,
|
|
2049
|
+
tooLong: false,
|
|
2050
|
+
tooShort: false,
|
|
2051
|
+
typeMismatch: false,
|
|
2052
|
+
valueMissing: false,
|
|
2053
|
+
valid: true
|
|
2054
|
+
};
|
|
2055
|
+
const CUSTOM_VALIDITY_STATE = _objectSpread2(_objectSpread2({}, VALID_VALIDITY_STATE), {}, {
|
|
2056
|
+
customError: true,
|
|
2057
|
+
valid: false
|
|
2058
|
+
});
|
|
2059
|
+
const DEFAULT_VALIDATION_RESULT = {
|
|
2060
|
+
isInvalid: false,
|
|
2061
|
+
validationDetails: VALID_VALIDITY_STATE,
|
|
2062
|
+
validationErrors: []
|
|
72
2063
|
};
|
|
2064
|
+
const FormValidationContext = /*#__PURE__*/createContext({});
|
|
2065
|
+
const privateValidationStateProp = '__formValidationState' + Date.now();
|
|
2066
|
+
function useFormValidationState(props) {
|
|
2067
|
+
// Private prop for parent components to pass state to children.
|
|
2068
|
+
if (props[privateValidationStateProp]) {
|
|
2069
|
+
let {
|
|
2070
|
+
realtimeValidation,
|
|
2071
|
+
displayValidation,
|
|
2072
|
+
updateValidation,
|
|
2073
|
+
resetValidation,
|
|
2074
|
+
commitValidation
|
|
2075
|
+
} = props[privateValidationStateProp];
|
|
2076
|
+
return {
|
|
2077
|
+
realtimeValidation,
|
|
2078
|
+
displayValidation,
|
|
2079
|
+
updateValidation,
|
|
2080
|
+
resetValidation,
|
|
2081
|
+
commitValidation
|
|
2082
|
+
};
|
|
2083
|
+
}
|
|
2084
|
+
|
|
2085
|
+
// eslint-disable-next-line react-hooks/rules-of-hooks
|
|
2086
|
+
return useFormValidationStateImpl(props);
|
|
2087
|
+
}
|
|
2088
|
+
function useFormValidationStateImpl(props) {
|
|
2089
|
+
var _builtinValidation;
|
|
2090
|
+
let {
|
|
2091
|
+
isInvalid,
|
|
2092
|
+
validationState,
|
|
2093
|
+
name,
|
|
2094
|
+
value,
|
|
2095
|
+
builtinValidation,
|
|
2096
|
+
validate,
|
|
2097
|
+
validationBehavior = 'aria'
|
|
2098
|
+
} = props;
|
|
2099
|
+
|
|
2100
|
+
// backward compatibility.
|
|
2101
|
+
if (validationState) {
|
|
2102
|
+
isInvalid ||= validationState === 'invalid';
|
|
2103
|
+
}
|
|
2104
|
+
|
|
2105
|
+
// If the isInvalid prop is controlled, update validation result in realtime.
|
|
2106
|
+
let controlledError = isInvalid !== undefined ? {
|
|
2107
|
+
isInvalid,
|
|
2108
|
+
validationErrors: [],
|
|
2109
|
+
validationDetails: CUSTOM_VALIDITY_STATE
|
|
2110
|
+
} : null;
|
|
2111
|
+
|
|
2112
|
+
// Perform custom client side validation.
|
|
2113
|
+
let clientError = useMemo(() => getValidationResult(runValidate(validate, value)), [validate, value]);
|
|
2114
|
+
if ((_builtinValidation = builtinValidation) !== null && _builtinValidation !== void 0 && _builtinValidation.validationDetails.valid) {
|
|
2115
|
+
builtinValidation = null;
|
|
2116
|
+
}
|
|
2117
|
+
|
|
2118
|
+
// Get relevant server errors from the form.
|
|
2119
|
+
let serverErrors = useContext(FormValidationContext);
|
|
2120
|
+
let serverErrorMessages = useMemo(() => {
|
|
2121
|
+
if (name) {
|
|
2122
|
+
return Array.isArray(name) ? name.flatMap(name => asArray(serverErrors[name])) : asArray(serverErrors[name]);
|
|
2123
|
+
}
|
|
2124
|
+
return [];
|
|
2125
|
+
}, [serverErrors, name]);
|
|
2126
|
+
|
|
2127
|
+
// Show server errors when the form gets a new value, and clear when the user changes the value.
|
|
2128
|
+
let [lastServerErrors, setLastServerErrors] = useState(serverErrors);
|
|
2129
|
+
let [isServerErrorCleared, setServerErrorCleared] = useState(false);
|
|
2130
|
+
if (serverErrors !== lastServerErrors) {
|
|
2131
|
+
setLastServerErrors(serverErrors);
|
|
2132
|
+
setServerErrorCleared(false);
|
|
2133
|
+
}
|
|
2134
|
+
let serverError = useMemo(() => getValidationResult(isServerErrorCleared ? [] : serverErrorMessages), [isServerErrorCleared, serverErrorMessages]);
|
|
2135
|
+
|
|
2136
|
+
// Track the next validation state in a ref until commitValidation is called.
|
|
2137
|
+
let nextValidation = useRef(DEFAULT_VALIDATION_RESULT);
|
|
2138
|
+
let [currentValidity, setCurrentValidity] = useState(DEFAULT_VALIDATION_RESULT);
|
|
2139
|
+
let lastError = useRef(DEFAULT_VALIDATION_RESULT);
|
|
2140
|
+
let commitValidation = () => {
|
|
2141
|
+
if (!commitQueued) {
|
|
2142
|
+
return;
|
|
2143
|
+
}
|
|
2144
|
+
setCommitQueued(false);
|
|
2145
|
+
let error = clientError || builtinValidation || nextValidation.current;
|
|
2146
|
+
if (!isEqualValidation(error, lastError.current)) {
|
|
2147
|
+
lastError.current = error;
|
|
2148
|
+
setCurrentValidity(error);
|
|
2149
|
+
}
|
|
2150
|
+
};
|
|
2151
|
+
let [commitQueued, setCommitQueued] = useState(false);
|
|
2152
|
+
useEffect(commitValidation);
|
|
2153
|
+
|
|
2154
|
+
// realtimeValidation is used to update the native input element's state based on custom validation logic.
|
|
2155
|
+
// displayValidation is the currently displayed validation state that the user sees (e.g. on input change/form submit).
|
|
2156
|
+
// With validationBehavior="aria", all errors are displayed in realtime rather than on submit.
|
|
2157
|
+
let realtimeValidation = controlledError || serverError || clientError || builtinValidation || DEFAULT_VALIDATION_RESULT;
|
|
2158
|
+
let displayValidation = validationBehavior === 'native' ? controlledError || serverError || currentValidity : controlledError || serverError || clientError || builtinValidation || currentValidity;
|
|
2159
|
+
return {
|
|
2160
|
+
realtimeValidation,
|
|
2161
|
+
displayValidation,
|
|
2162
|
+
updateValidation(value) {
|
|
2163
|
+
// If validationBehavior is 'aria', update in realtime. Otherwise, store in a ref until commit.
|
|
2164
|
+
if (validationBehavior === 'aria' && !isEqualValidation(currentValidity, value)) {
|
|
2165
|
+
setCurrentValidity(value);
|
|
2166
|
+
} else {
|
|
2167
|
+
nextValidation.current = value;
|
|
2168
|
+
}
|
|
2169
|
+
},
|
|
2170
|
+
resetValidation() {
|
|
2171
|
+
// Update the currently displayed validation state to valid on form reset,
|
|
2172
|
+
// even if the native validity says it isn't. It'll show again on the next form submit.
|
|
2173
|
+
let error = DEFAULT_VALIDATION_RESULT;
|
|
2174
|
+
if (!isEqualValidation(error, lastError.current)) {
|
|
2175
|
+
lastError.current = error;
|
|
2176
|
+
setCurrentValidity(error);
|
|
2177
|
+
}
|
|
2178
|
+
|
|
2179
|
+
// Do not commit validation after the next render. This avoids a condition where
|
|
2180
|
+
// useSelect calls commitValidation inside an onReset handler.
|
|
2181
|
+
if (validationBehavior === 'native') {
|
|
2182
|
+
setCommitQueued(false);
|
|
2183
|
+
}
|
|
2184
|
+
setServerErrorCleared(true);
|
|
2185
|
+
},
|
|
2186
|
+
commitValidation() {
|
|
2187
|
+
// Commit validation state so the user sees it on blur/change/submit. Also clear any server errors.
|
|
2188
|
+
// Wait until after the next render to commit so that the latest value has been validated.
|
|
2189
|
+
if (validationBehavior === 'native') {
|
|
2190
|
+
setCommitQueued(true);
|
|
2191
|
+
}
|
|
2192
|
+
setServerErrorCleared(true);
|
|
2193
|
+
}
|
|
2194
|
+
};
|
|
2195
|
+
}
|
|
2196
|
+
function asArray(v) {
|
|
2197
|
+
if (!v) {
|
|
2198
|
+
return [];
|
|
2199
|
+
}
|
|
2200
|
+
return Array.isArray(v) ? v : [v];
|
|
2201
|
+
}
|
|
2202
|
+
function runValidate(validate, value) {
|
|
2203
|
+
if (typeof validate === 'function') {
|
|
2204
|
+
let e = validate(value);
|
|
2205
|
+
if (e && typeof e !== 'boolean') {
|
|
2206
|
+
return asArray(e);
|
|
2207
|
+
}
|
|
2208
|
+
}
|
|
2209
|
+
return [];
|
|
2210
|
+
}
|
|
2211
|
+
function getValidationResult(errors) {
|
|
2212
|
+
return errors.length ? {
|
|
2213
|
+
isInvalid: true,
|
|
2214
|
+
validationErrors: errors,
|
|
2215
|
+
validationDetails: CUSTOM_VALIDITY_STATE
|
|
2216
|
+
} : null;
|
|
2217
|
+
}
|
|
2218
|
+
function isEqualValidation(a, b) {
|
|
2219
|
+
if (a === b) {
|
|
2220
|
+
return true;
|
|
2221
|
+
}
|
|
2222
|
+
return a && b && a.isInvalid === b.isInvalid && a.validationErrors.length === b.validationErrors.length && a.validationErrors.every((a, i) => a === b.validationErrors[i]) && Object.entries(a.validationDetails).every(_ref => {
|
|
2223
|
+
let [k, v] = _ref;
|
|
2224
|
+
return b.validationDetails[k] === v;
|
|
2225
|
+
});
|
|
2226
|
+
}
|
|
2227
|
+
|
|
2228
|
+
/**
|
|
2229
|
+
* Provides state management for a number field component. Number fields allow users to enter a number,
|
|
2230
|
+
* and increment or decrement the value using stepper buttons.
|
|
2231
|
+
*/
|
|
2232
|
+
function useNumberFieldState(props) {
|
|
2233
|
+
let {
|
|
2234
|
+
minValue,
|
|
2235
|
+
maxValue,
|
|
2236
|
+
step,
|
|
2237
|
+
formatOptions,
|
|
2238
|
+
value,
|
|
2239
|
+
defaultValue = NaN,
|
|
2240
|
+
onChange,
|
|
2241
|
+
locale,
|
|
2242
|
+
isDisabled,
|
|
2243
|
+
isReadOnly
|
|
2244
|
+
} = props;
|
|
2245
|
+
if (value === null) {
|
|
2246
|
+
value = NaN;
|
|
2247
|
+
}
|
|
2248
|
+
if (value !== undefined && !isNaN(value)) {
|
|
2249
|
+
if (step !== undefined && !isNaN(step)) {
|
|
2250
|
+
value = snapValueToStep(value, minValue, maxValue, step);
|
|
2251
|
+
} else {
|
|
2252
|
+
value = clamp(value, minValue, maxValue);
|
|
2253
|
+
}
|
|
2254
|
+
}
|
|
2255
|
+
if (!isNaN(defaultValue)) {
|
|
2256
|
+
if (step !== undefined && !isNaN(step)) {
|
|
2257
|
+
defaultValue = snapValueToStep(defaultValue, minValue, maxValue, step);
|
|
2258
|
+
} else {
|
|
2259
|
+
defaultValue = clamp(defaultValue, minValue, maxValue);
|
|
2260
|
+
}
|
|
2261
|
+
}
|
|
2262
|
+
let [numberValue, setNumberValue] = useControlledState(value, isNaN(defaultValue) ? NaN : defaultValue, onChange);
|
|
2263
|
+
let [inputValue, setInputValue] = useState(() => isNaN(numberValue) ? '' : new NumberFormatter(locale, formatOptions).format(numberValue));
|
|
2264
|
+
let numberParser = useMemo(() => new NumberParser(locale, formatOptions), [locale, formatOptions]);
|
|
2265
|
+
let numberingSystem = useMemo(() => numberParser.getNumberingSystem(inputValue), [numberParser, inputValue]);
|
|
2266
|
+
let formatter = useMemo(() => new NumberFormatter(locale, _objectSpread2(_objectSpread2({}, formatOptions), {}, {
|
|
2267
|
+
numberingSystem
|
|
2268
|
+
})), [locale, formatOptions, numberingSystem]);
|
|
2269
|
+
let intlOptions = useMemo(() => formatter.resolvedOptions(), [formatter]);
|
|
2270
|
+
let format = useCallback(value => isNaN(value) || value === null ? '' : formatter.format(value), [formatter]);
|
|
2271
|
+
let validation = useFormValidationState(_objectSpread2(_objectSpread2({}, props), {}, {
|
|
2272
|
+
value: numberValue
|
|
2273
|
+
}));
|
|
2274
|
+
let clampStep = step !== undefined && !isNaN(step) ? step : 1;
|
|
2275
|
+
if (intlOptions.style === 'percent' && (step === undefined || isNaN(step))) {
|
|
2276
|
+
clampStep = 0.01;
|
|
2277
|
+
}
|
|
2278
|
+
|
|
2279
|
+
// Update the input value when the number value or format options change. This is done
|
|
2280
|
+
// in a useEffect so that the controlled behavior is correct and we only update the
|
|
2281
|
+
// textfield after prop changes.
|
|
2282
|
+
let [prevValue, setPrevValue] = useState(numberValue);
|
|
2283
|
+
let [prevLocale, setPrevLocale] = useState(locale);
|
|
2284
|
+
let [prevFormatOptions, setPrevFormatOptions] = useState(formatOptions);
|
|
2285
|
+
if (!Object.is(numberValue, prevValue) || locale !== prevLocale || formatOptions !== prevFormatOptions) {
|
|
2286
|
+
setInputValue(format(numberValue));
|
|
2287
|
+
setPrevValue(numberValue);
|
|
2288
|
+
setPrevLocale(locale);
|
|
2289
|
+
setPrevFormatOptions(formatOptions);
|
|
2290
|
+
}
|
|
2291
|
+
let parsedValue = useMemo(() => numberParser.parse(inputValue), [numberParser, inputValue]);
|
|
2292
|
+
let commit = () => {
|
|
2293
|
+
// Set to empty state if input value is empty
|
|
2294
|
+
if (!inputValue.length) {
|
|
2295
|
+
setNumberValue(NaN);
|
|
2296
|
+
setInputValue(value === undefined ? '' : format(numberValue));
|
|
2297
|
+
return;
|
|
2298
|
+
}
|
|
2299
|
+
|
|
2300
|
+
// if it failed to parse, then reset input to formatted version of current number
|
|
2301
|
+
if (isNaN(parsedValue)) {
|
|
2302
|
+
setInputValue(format(numberValue));
|
|
2303
|
+
return;
|
|
2304
|
+
}
|
|
2305
|
+
|
|
2306
|
+
// Clamp to min and max, round to the nearest step, and round to specified number of digits
|
|
2307
|
+
let clampedValue;
|
|
2308
|
+
if (step === undefined || isNaN(step)) {
|
|
2309
|
+
clampedValue = clamp(parsedValue, minValue, maxValue);
|
|
2310
|
+
} else {
|
|
2311
|
+
clampedValue = snapValueToStep(parsedValue, minValue, maxValue, step);
|
|
2312
|
+
}
|
|
2313
|
+
clampedValue = numberParser.parse(format(clampedValue));
|
|
2314
|
+
setNumberValue(clampedValue);
|
|
2315
|
+
|
|
2316
|
+
// in a controlled state, the numberValue won't change, so we won't go back to our old input without help
|
|
2317
|
+
setInputValue(format(value === undefined ? clampedValue : numberValue));
|
|
2318
|
+
};
|
|
2319
|
+
let safeNextStep = function (operation) {
|
|
2320
|
+
let minMax = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 0;
|
|
2321
|
+
let prev = parsedValue;
|
|
2322
|
+
if (isNaN(prev)) {
|
|
2323
|
+
// if the input is empty, start from the min/max value when incrementing/decrementing,
|
|
2324
|
+
// or zero if there is no min/max value defined.
|
|
2325
|
+
let newValue = isNaN(minMax) ? 0 : minMax;
|
|
2326
|
+
return snapValueToStep(newValue, minValue, maxValue, clampStep);
|
|
2327
|
+
} else {
|
|
2328
|
+
// otherwise, first snap the current value to the nearest step. if it moves in the direction
|
|
2329
|
+
// we're going, use that value, otherwise add the step and snap that value.
|
|
2330
|
+
let newValue = snapValueToStep(prev, minValue, maxValue, clampStep);
|
|
2331
|
+
if (operation === '+' && newValue > prev || operation === '-' && newValue < prev) {
|
|
2332
|
+
return newValue;
|
|
2333
|
+
}
|
|
2334
|
+
return snapValueToStep(handleDecimalOperation(operation, prev, clampStep), minValue, maxValue, clampStep);
|
|
2335
|
+
}
|
|
2336
|
+
};
|
|
2337
|
+
let increment = () => {
|
|
2338
|
+
let newValue = safeNextStep('+', minValue);
|
|
2339
|
+
|
|
2340
|
+
// if we've arrived at the same value that was previously in the state, the
|
|
2341
|
+
// input value should be updated to match
|
|
2342
|
+
// ex type 4, press increment, highlight the number in the input, type 4 again, press increment
|
|
2343
|
+
// you'd be at 5, then incrementing to 5 again, so no re-render would happen and 4 would be left in the input
|
|
2344
|
+
if (newValue === numberValue) {
|
|
2345
|
+
setInputValue(format(newValue));
|
|
2346
|
+
}
|
|
2347
|
+
setNumberValue(newValue);
|
|
2348
|
+
validation.commitValidation();
|
|
2349
|
+
};
|
|
2350
|
+
let decrement = () => {
|
|
2351
|
+
let newValue = safeNextStep('-', maxValue);
|
|
2352
|
+
if (newValue === numberValue) {
|
|
2353
|
+
setInputValue(format(newValue));
|
|
2354
|
+
}
|
|
2355
|
+
setNumberValue(newValue);
|
|
2356
|
+
validation.commitValidation();
|
|
2357
|
+
};
|
|
2358
|
+
let incrementToMax = () => {
|
|
2359
|
+
if (maxValue != null) {
|
|
2360
|
+
setNumberValue(snapValueToStep(maxValue, minValue, maxValue, clampStep));
|
|
2361
|
+
validation.commitValidation();
|
|
2362
|
+
}
|
|
2363
|
+
};
|
|
2364
|
+
let decrementToMin = () => {
|
|
2365
|
+
if (minValue != null) {
|
|
2366
|
+
setNumberValue(minValue);
|
|
2367
|
+
validation.commitValidation();
|
|
2368
|
+
}
|
|
2369
|
+
};
|
|
2370
|
+
let canIncrement = useMemo(() => !isDisabled && !isReadOnly && (isNaN(parsedValue) || maxValue === undefined || isNaN(maxValue) || snapValueToStep(parsedValue, minValue, maxValue, clampStep) > parsedValue || handleDecimalOperation('+', parsedValue, clampStep) <= maxValue), [isDisabled, isReadOnly, minValue, maxValue, clampStep, parsedValue]);
|
|
2371
|
+
let canDecrement = useMemo(() => !isDisabled && !isReadOnly && (isNaN(parsedValue) || minValue === undefined || isNaN(minValue) || snapValueToStep(parsedValue, minValue, maxValue, clampStep) < parsedValue || handleDecimalOperation('-', parsedValue, clampStep) >= minValue), [isDisabled, isReadOnly, minValue, maxValue, clampStep, parsedValue]);
|
|
2372
|
+
let validate = value => numberParser.isValidPartialNumber(value, minValue, maxValue);
|
|
2373
|
+
return _objectSpread2(_objectSpread2({}, validation), {}, {
|
|
2374
|
+
validate,
|
|
2375
|
+
increment,
|
|
2376
|
+
incrementToMax,
|
|
2377
|
+
decrement,
|
|
2378
|
+
decrementToMin,
|
|
2379
|
+
canIncrement,
|
|
2380
|
+
canDecrement,
|
|
2381
|
+
minValue,
|
|
2382
|
+
maxValue,
|
|
2383
|
+
numberValue: parsedValue,
|
|
2384
|
+
setNumberValue,
|
|
2385
|
+
setInputValue,
|
|
2386
|
+
inputValue,
|
|
2387
|
+
commit
|
|
2388
|
+
});
|
|
2389
|
+
}
|
|
2390
|
+
function handleDecimalOperation(operator, value1, value2) {
|
|
2391
|
+
let result = operator === '+' ? value1 + value2 : value1 - value2;
|
|
2392
|
+
|
|
2393
|
+
// Check if we have decimals
|
|
2394
|
+
if (value1 % 1 !== 0 || value2 % 1 !== 0) {
|
|
2395
|
+
const value1Decimal = value1.toString().split('.');
|
|
2396
|
+
const value2Decimal = value2.toString().split('.');
|
|
2397
|
+
const value1DecimalLength = value1Decimal[1] && value1Decimal[1].length || 0;
|
|
2398
|
+
const value2DecimalLength = value2Decimal[1] && value2Decimal[1].length || 0;
|
|
2399
|
+
const multiplier = Math.pow(10, Math.max(value1DecimalLength, value2DecimalLength));
|
|
2400
|
+
|
|
2401
|
+
// Transform the decimals to integers based on the precision
|
|
2402
|
+
value1 = Math.round(value1 * multiplier);
|
|
2403
|
+
value2 = Math.round(value2 * multiplier);
|
|
2404
|
+
|
|
2405
|
+
// Perform the operation on integers values to make sure we don't get a fancy decimal value
|
|
2406
|
+
result = operator === '+' ? value1 + value2 : value1 - value2;
|
|
2407
|
+
|
|
2408
|
+
// Transform the integer result back to decimal
|
|
2409
|
+
result /= multiplier;
|
|
2410
|
+
}
|
|
2411
|
+
return result;
|
|
2412
|
+
}
|
|
2413
|
+
|
|
2414
|
+
var decrease$1 = "Decrease {fieldLabel}";
|
|
2415
|
+
var increase$1 = "Increase {fieldLabel}";
|
|
2416
|
+
var numberField$1 = "Number field";
|
|
2417
|
+
var enUS$1 = {
|
|
2418
|
+
decrease: decrease$1,
|
|
2419
|
+
increase: increase$1,
|
|
2420
|
+
numberField: numberField$1
|
|
2421
|
+
};
|
|
2422
|
+
|
|
2423
|
+
var decrease = "Diminuer {fieldLabel}";
|
|
2424
|
+
var increase = "Augmenter {fieldLabel}";
|
|
2425
|
+
var numberField = "Champ de nombre";
|
|
2426
|
+
var frFR$1 = {
|
|
2427
|
+
decrease: decrease,
|
|
2428
|
+
increase: increase,
|
|
2429
|
+
numberField: numberField
|
|
2430
|
+
};
|
|
2431
|
+
|
|
2432
|
+
var intlMessages$1 = {
|
|
2433
|
+
'en-US': enUS$1,
|
|
2434
|
+
'fr-FR': frFR$1
|
|
2435
|
+
};
|
|
2436
|
+
|
|
2437
|
+
/* eslint-disable prefer-const */
|
|
2438
|
+
/**
|
|
2439
|
+
* Provides the accessibility implementation for labels and their associated elements.
|
|
2440
|
+
* Labels provide context for user inputs.
|
|
2441
|
+
* @param props - The props for labels and fields.
|
|
2442
|
+
*/
|
|
2443
|
+
function useLabel(props) {
|
|
2444
|
+
let {
|
|
2445
|
+
id,
|
|
2446
|
+
label,
|
|
2447
|
+
'aria-labelledby': ariaLabelledby,
|
|
2448
|
+
'aria-label': ariaLabel,
|
|
2449
|
+
labelElementType = 'label'
|
|
2450
|
+
} = props;
|
|
2451
|
+
id = useId(id);
|
|
2452
|
+
let labelId = useId();
|
|
2453
|
+
let labelProps = {};
|
|
2454
|
+
if (label) {
|
|
2455
|
+
ariaLabelledby = ariaLabelledby ? `${labelId} ${ariaLabelledby}` : labelId;
|
|
2456
|
+
labelProps = {
|
|
2457
|
+
id: labelId,
|
|
2458
|
+
htmlFor: labelElementType === 'label' ? id : undefined
|
|
2459
|
+
};
|
|
2460
|
+
} else if (!ariaLabelledby && !ariaLabel) {
|
|
2461
|
+
console.warn('If you do not provide a visible label, you must specify an aria-label or aria-labelledby attribute for accessibility');
|
|
2462
|
+
}
|
|
2463
|
+
let fieldProps = useLabels({
|
|
2464
|
+
id,
|
|
2465
|
+
'aria-label': ariaLabel,
|
|
2466
|
+
'aria-labelledby': ariaLabelledby
|
|
2467
|
+
});
|
|
2468
|
+
return {
|
|
2469
|
+
labelProps,
|
|
2470
|
+
fieldProps
|
|
2471
|
+
};
|
|
2472
|
+
}
|
|
2473
|
+
|
|
2474
|
+
/* eslint-disable prefer-const */
|
|
2475
|
+
/**
|
|
2476
|
+
* Provides the accessibility implementation for input fields.
|
|
2477
|
+
* Fields accept user input, gain context from their label, and may display a description or error message.
|
|
2478
|
+
* @param props - Props for the Field.
|
|
2479
|
+
*/
|
|
2480
|
+
function useField(props) {
|
|
2481
|
+
let {
|
|
2482
|
+
description,
|
|
2483
|
+
errorMessage,
|
|
2484
|
+
isInvalid,
|
|
2485
|
+
validationState
|
|
2486
|
+
} = props;
|
|
2487
|
+
let {
|
|
2488
|
+
labelProps,
|
|
2489
|
+
fieldProps
|
|
2490
|
+
} = useLabel(props);
|
|
2491
|
+
let descriptionId = useSlotId([Boolean(description), Boolean(errorMessage), isInvalid, validationState]);
|
|
2492
|
+
let errorMessageId = useSlotId([Boolean(description), Boolean(errorMessage), isInvalid, validationState]);
|
|
2493
|
+
fieldProps = mergeProps(fieldProps, {
|
|
2494
|
+
'aria-describedby': [descriptionId,
|
|
2495
|
+
// Use aria-describedby for error message because aria-errormessage is unsupported using VoiceOver or NVDA. See https://github.com/adobe/react-spectrum/issues/1346#issuecomment-740136268
|
|
2496
|
+
errorMessageId, props['aria-describedby']].filter(Boolean).join(' ') || undefined
|
|
2497
|
+
});
|
|
2498
|
+
return {
|
|
2499
|
+
labelProps,
|
|
2500
|
+
fieldProps,
|
|
2501
|
+
descriptionProps: {
|
|
2502
|
+
id: descriptionId
|
|
2503
|
+
},
|
|
2504
|
+
errorMessageProps: {
|
|
2505
|
+
id: errorMessageId
|
|
2506
|
+
}
|
|
2507
|
+
};
|
|
2508
|
+
}
|
|
2509
|
+
|
|
2510
|
+
/* eslint-disable prefer-const */
|
|
2511
|
+
function useFormValidation(props, state, ref) {
|
|
2512
|
+
let {
|
|
2513
|
+
validationBehavior,
|
|
2514
|
+
focus
|
|
2515
|
+
} = props;
|
|
2516
|
+
|
|
2517
|
+
// This is a useLayoutEffect so that it runs before the useEffect in useFormValidationState, which commits the validation change.
|
|
2518
|
+
useLayoutEffect(() => {
|
|
2519
|
+
if (validationBehavior === 'native' && ref !== null && ref !== void 0 && ref.current) {
|
|
2520
|
+
let errorMessage = state.realtimeValidation.isInvalid ? state.realtimeValidation.validationErrors.join(' ') || 'Invalid value.' : '';
|
|
2521
|
+
ref.current.setCustomValidity(errorMessage);
|
|
2522
|
+
|
|
2523
|
+
// Prevent default tooltip for validation message.
|
|
2524
|
+
// https://bugzilla.mozilla.org/show_bug.cgi?id=605277
|
|
2525
|
+
if (!ref.current.hasAttribute('title')) {
|
|
2526
|
+
ref.current.title = '';
|
|
2527
|
+
}
|
|
2528
|
+
if (!state.realtimeValidation.isInvalid) {
|
|
2529
|
+
state.updateValidation(getNativeValidity(ref.current));
|
|
2530
|
+
}
|
|
2531
|
+
}
|
|
2532
|
+
});
|
|
2533
|
+
let onReset = useEffectEvent(() => {
|
|
2534
|
+
state.resetValidation();
|
|
2535
|
+
});
|
|
2536
|
+
let onInvalid = useEffectEvent(e => {
|
|
2537
|
+
var _ref$current;
|
|
2538
|
+
// Only commit validation if we are not already displaying one.
|
|
2539
|
+
// This avoids clearing server errors that the user didn't actually fix.
|
|
2540
|
+
if (!state.displayValidation.isInvalid) {
|
|
2541
|
+
state.commitValidation();
|
|
2542
|
+
}
|
|
2543
|
+
|
|
2544
|
+
// Auto focus the first invalid input in a form, unless the error already had its default prevented.
|
|
2545
|
+
let form = ref === null || ref === void 0 ? void 0 : (_ref$current = ref.current) === null || _ref$current === void 0 ? void 0 : _ref$current.form;
|
|
2546
|
+
if (!e.defaultPrevented && ref && form && getFirstInvalidInput(form) === ref.current) {
|
|
2547
|
+
if (focus) {
|
|
2548
|
+
focus();
|
|
2549
|
+
} else {
|
|
2550
|
+
var _ref$current2;
|
|
2551
|
+
(_ref$current2 = ref.current) === null || _ref$current2 === void 0 ? void 0 : _ref$current2.focus();
|
|
2552
|
+
}
|
|
2553
|
+
|
|
2554
|
+
// Always show focus ring.
|
|
2555
|
+
setInteractionModality('keyboard');
|
|
2556
|
+
}
|
|
2557
|
+
|
|
2558
|
+
// Prevent default browser error UI from appearing.
|
|
2559
|
+
e.preventDefault();
|
|
2560
|
+
});
|
|
2561
|
+
let onChange = useEffectEvent(() => {
|
|
2562
|
+
state.commitValidation();
|
|
2563
|
+
});
|
|
2564
|
+
useEffect(() => {
|
|
2565
|
+
let input = ref === null || ref === void 0 ? void 0 : ref.current;
|
|
2566
|
+
if (!input) {
|
|
2567
|
+
return;
|
|
2568
|
+
}
|
|
2569
|
+
let form = input.form;
|
|
2570
|
+
input.addEventListener('invalid', onInvalid);
|
|
2571
|
+
input.addEventListener('change', onChange);
|
|
2572
|
+
form === null || form === void 0 ? void 0 : form.addEventListener('reset', onReset);
|
|
2573
|
+
return () => {
|
|
2574
|
+
input.removeEventListener('invalid', onInvalid);
|
|
2575
|
+
input.removeEventListener('change', onChange);
|
|
2576
|
+
form === null || form === void 0 ? void 0 : form.removeEventListener('reset', onReset);
|
|
2577
|
+
};
|
|
2578
|
+
}, [ref, onInvalid, onChange, onReset, validationBehavior]);
|
|
2579
|
+
}
|
|
2580
|
+
function getValidity(input) {
|
|
2581
|
+
// The native ValidityState object is live, meaning each property is a getter that returns the current state.
|
|
2582
|
+
// We need to create a snapshot of the validity state at the time this function is called to avoid unpredictable React renders.
|
|
2583
|
+
let validity = input.validity;
|
|
2584
|
+
return {
|
|
2585
|
+
badInput: validity.badInput,
|
|
2586
|
+
customError: validity.customError,
|
|
2587
|
+
patternMismatch: validity.patternMismatch,
|
|
2588
|
+
rangeOverflow: validity.rangeOverflow,
|
|
2589
|
+
rangeUnderflow: validity.rangeUnderflow,
|
|
2590
|
+
stepMismatch: validity.stepMismatch,
|
|
2591
|
+
tooLong: validity.tooLong,
|
|
2592
|
+
tooShort: validity.tooShort,
|
|
2593
|
+
typeMismatch: validity.typeMismatch,
|
|
2594
|
+
valueMissing: validity.valueMissing,
|
|
2595
|
+
valid: validity.valid
|
|
2596
|
+
};
|
|
2597
|
+
}
|
|
2598
|
+
function getNativeValidity(input) {
|
|
2599
|
+
return {
|
|
2600
|
+
isInvalid: !input.validity.valid,
|
|
2601
|
+
validationDetails: getValidity(input),
|
|
2602
|
+
validationErrors: input.validationMessage ? [input.validationMessage] : []
|
|
2603
|
+
};
|
|
2604
|
+
}
|
|
2605
|
+
function getFirstInvalidInput(form) {
|
|
2606
|
+
for (let i = 0; i < form.elements.length; i++) {
|
|
2607
|
+
let element = form.elements[i];
|
|
2608
|
+
if (!element.validity.valid) {
|
|
2609
|
+
return element;
|
|
2610
|
+
}
|
|
2611
|
+
}
|
|
2612
|
+
return null;
|
|
2613
|
+
}
|
|
2614
|
+
|
|
2615
|
+
/**
|
|
2616
|
+
* A map of HTML element names and their interface types.
|
|
2617
|
+
* For example `'a'` -> `HTMLAnchorElement`.
|
|
2618
|
+
*/
|
|
2619
|
+
|
|
2620
|
+
/**
|
|
2621
|
+
* A map of HTML element names and their attribute interface types.
|
|
2622
|
+
* For example `'a'` -> `AnchorHTMLAttributes<HTMLAnchorElement>`.
|
|
2623
|
+
*/
|
|
2624
|
+
|
|
2625
|
+
/**
|
|
2626
|
+
* The intrinsic HTML element names that `useTextField` supports; e.g. `input`,
|
|
2627
|
+
* `textarea`.
|
|
2628
|
+
*/
|
|
2629
|
+
|
|
2630
|
+
/**
|
|
2631
|
+
* The HTML element interfaces that `useTextField` supports based on what is
|
|
2632
|
+
* defined for `TextFieldIntrinsicElements`; e.g. `HTMLInputElement`,
|
|
2633
|
+
* `HTMLTextAreaElement`.
|
|
2634
|
+
*/
|
|
2635
|
+
|
|
2636
|
+
/**
|
|
2637
|
+
* The HTML attributes interfaces that `useTextField` supports based on what
|
|
2638
|
+
* is defined for `TextFieldIntrinsicElements`; e.g. `InputHTMLAttributes`,
|
|
2639
|
+
* `TextareaHTMLAttributes`.
|
|
2640
|
+
*/
|
|
2641
|
+
|
|
2642
|
+
/**
|
|
2643
|
+
* The type of `inputProps` returned by `useTextField`; e.g. `InputHTMLAttributes`,
|
|
2644
|
+
* `TextareaHTMLAttributes`.
|
|
2645
|
+
*/
|
|
2646
|
+
|
|
2647
|
+
/**
|
|
2648
|
+
* The type of `ref` object that can be passed to `useTextField` based on the given
|
|
2649
|
+
* intrinsic HTML element name; e.g.`RefObject<HTMLInputElement>`,
|
|
2650
|
+
* `RefObject<HTMLTextAreaElement>`.
|
|
2651
|
+
*/
|
|
2652
|
+
|
|
2653
|
+
/**
|
|
2654
|
+
* Provides the behavior and accessibility implementation for a text field.
|
|
2655
|
+
* @param props - Props for the text field.
|
|
2656
|
+
* @param ref - Ref to the HTML input or textarea element.
|
|
2657
|
+
*/
|
|
2658
|
+
function useTextField(props, ref) {
|
|
2659
|
+
let {
|
|
2660
|
+
inputElementType = 'input',
|
|
2661
|
+
isDisabled = false,
|
|
2662
|
+
isRequired = false,
|
|
2663
|
+
isReadOnly = false,
|
|
2664
|
+
type = 'text',
|
|
2665
|
+
validationBehavior = 'aria'
|
|
2666
|
+
} = props;
|
|
2667
|
+
let [value, setValue] = useControlledState(props.value, props.defaultValue || '', props.onChange);
|
|
2668
|
+
let {
|
|
2669
|
+
focusableProps
|
|
2670
|
+
} = useFocusable(props, ref);
|
|
2671
|
+
let validationState = useFormValidationState(_objectSpread2(_objectSpread2({}, props), {}, {
|
|
2672
|
+
value
|
|
2673
|
+
}));
|
|
2674
|
+
let {
|
|
2675
|
+
isInvalid,
|
|
2676
|
+
validationErrors,
|
|
2677
|
+
validationDetails
|
|
2678
|
+
} = validationState.displayValidation;
|
|
2679
|
+
let {
|
|
2680
|
+
labelProps,
|
|
2681
|
+
fieldProps,
|
|
2682
|
+
descriptionProps,
|
|
2683
|
+
errorMessageProps
|
|
2684
|
+
} = useField(_objectSpread2(_objectSpread2({}, props), {}, {
|
|
2685
|
+
isInvalid,
|
|
2686
|
+
errorMessage: props.errorMessage || validationErrors
|
|
2687
|
+
}));
|
|
2688
|
+
let domProps = filterDOMProps(props, {
|
|
2689
|
+
labelable: true
|
|
2690
|
+
});
|
|
2691
|
+
const inputOnlyProps = {
|
|
2692
|
+
type,
|
|
2693
|
+
pattern: props.pattern
|
|
2694
|
+
};
|
|
2695
|
+
useFormReset(ref, value, setValue);
|
|
2696
|
+
useFormValidation(props, validationState, ref);
|
|
2697
|
+
useEffect(() => {
|
|
2698
|
+
// This works around a React/Chrome bug that prevents textarea elements from validating when controlled.
|
|
2699
|
+
// We prevent React from updating defaultValue (i.e. children) of textarea when `value` changes,
|
|
2700
|
+
// which causes Chrome to skip validation. Only updating `value` is ok in our case since our
|
|
2701
|
+
// textareas are always controlled. React is planning on removing this synchronization in a
|
|
2702
|
+
// future major version.
|
|
2703
|
+
// https://github.com/facebook/react/issues/19474
|
|
2704
|
+
// https://github.com/facebook/react/issues/11896
|
|
2705
|
+
if (ref.current instanceof getOwnerWindow(ref.current).HTMLTextAreaElement) {
|
|
2706
|
+
let input = ref.current;
|
|
2707
|
+
Object.defineProperty(input, 'defaultValue', {
|
|
2708
|
+
get: () => input.value,
|
|
2709
|
+
set: () => {},
|
|
2710
|
+
configurable: true
|
|
2711
|
+
});
|
|
2712
|
+
}
|
|
2713
|
+
}, [ref]);
|
|
2714
|
+
return {
|
|
2715
|
+
labelProps,
|
|
2716
|
+
inputProps: mergeProps(domProps, inputElementType === 'input' ? inputOnlyProps : undefined, _objectSpread2(_objectSpread2({
|
|
2717
|
+
disabled: isDisabled,
|
|
2718
|
+
readOnly: isReadOnly,
|
|
2719
|
+
required: isRequired && validationBehavior === 'native',
|
|
2720
|
+
'aria-required': isRequired && validationBehavior === 'aria' || undefined,
|
|
2721
|
+
'aria-invalid': isInvalid || undefined,
|
|
2722
|
+
'aria-errormessage': props['aria-errormessage'],
|
|
2723
|
+
'aria-activedescendant': props['aria-activedescendant'],
|
|
2724
|
+
'aria-autocomplete': props['aria-autocomplete'],
|
|
2725
|
+
'aria-haspopup': props['aria-haspopup'],
|
|
2726
|
+
value,
|
|
2727
|
+
onChange: e => setValue(e.target.value),
|
|
2728
|
+
autoComplete: props.autoComplete,
|
|
2729
|
+
autoCapitalize: props.autoCapitalize,
|
|
2730
|
+
maxLength: props.maxLength,
|
|
2731
|
+
minLength: props.minLength,
|
|
2732
|
+
name: props.name,
|
|
2733
|
+
placeholder: props.placeholder,
|
|
2734
|
+
inputMode: props.inputMode,
|
|
2735
|
+
// Clipboard events
|
|
2736
|
+
onCopy: props.onCopy,
|
|
2737
|
+
onCut: props.onCut,
|
|
2738
|
+
onPaste: props.onPaste,
|
|
2739
|
+
// Composition events
|
|
2740
|
+
onCompositionEnd: props.onCompositionEnd,
|
|
2741
|
+
onCompositionStart: props.onCompositionStart,
|
|
2742
|
+
onCompositionUpdate: props.onCompositionUpdate,
|
|
2743
|
+
// Selection events
|
|
2744
|
+
onSelect: props.onSelect,
|
|
2745
|
+
// Input events
|
|
2746
|
+
onBeforeInput: props.onBeforeInput,
|
|
2747
|
+
onInput: props.onInput
|
|
2748
|
+
}, focusableProps), fieldProps)),
|
|
2749
|
+
descriptionProps,
|
|
2750
|
+
errorMessageProps,
|
|
2751
|
+
isInvalid,
|
|
2752
|
+
validationErrors,
|
|
2753
|
+
validationDetails
|
|
2754
|
+
};
|
|
2755
|
+
}
|
|
2756
|
+
|
|
2757
|
+
const _excluded$2 = ["labelProps", "inputProps", "descriptionProps", "errorMessageProps"];
|
|
2758
|
+
function supportsNativeBeforeInputEvent() {
|
|
2759
|
+
return typeof window !== 'undefined' && window.InputEvent &&
|
|
2760
|
+
// @ts-ignore
|
|
2761
|
+
typeof InputEvent.prototype.getTargetRanges === 'function';
|
|
2762
|
+
}
|
|
2763
|
+
function useFormattedTextField(props, state, inputRef) {
|
|
2764
|
+
// All browsers implement the 'beforeinput' event natively except Firefox
|
|
2765
|
+
// (currently behind a flag as of Firefox 84). React's polyfill does not
|
|
2766
|
+
// run in all cases that the native event fires, e.g. when deleting text.
|
|
2767
|
+
// Use the native event if available so that we can prevent invalid deletions.
|
|
2768
|
+
// We do not attempt to polyfill this in Firefox since it would be very complicated,
|
|
2769
|
+
// the benefit of doing so is fairly minor, and it's going to be natively supported soon.
|
|
2770
|
+
let onBeforeInputFallback = useEffectEvent(e => {
|
|
2771
|
+
let input = inputRef.current;
|
|
2772
|
+
|
|
2773
|
+
// Compute the next value of the input if the event is allowed to proceed.
|
|
2774
|
+
// See https://www.w3.org/TR/input-events-2/#interface-InputEvent-Attributes for a full list of input types.
|
|
2775
|
+
let nextValue;
|
|
2776
|
+
switch (e.inputType) {
|
|
2777
|
+
case 'historyUndo':
|
|
2778
|
+
case 'historyRedo':
|
|
2779
|
+
// Explicitly allow undo/redo. e.data is null in this case, but there's no need to validate,
|
|
2780
|
+
// because presumably the input would have already been validated previously.
|
|
2781
|
+
return;
|
|
2782
|
+
case 'insertLineBreak':
|
|
2783
|
+
// Explicitly allow "insertLineBreak" event, to allow onSubmit for "enter" key. e.data is null in this case.
|
|
2784
|
+
return;
|
|
2785
|
+
case 'deleteContent':
|
|
2786
|
+
case 'deleteByCut':
|
|
2787
|
+
case 'deleteByDrag':
|
|
2788
|
+
nextValue = input.value.slice(0, input.selectionStart) + input.value.slice(input.selectionEnd);
|
|
2789
|
+
break;
|
|
2790
|
+
case 'deleteContentForward':
|
|
2791
|
+
// This is potentially incorrect, since the browser may actually delete more than a single UTF-16
|
|
2792
|
+
// character. In reality, a full Unicode grapheme cluster consisting of multiple UTF-16 characters
|
|
2793
|
+
// or code points may be deleted. However, in our currently supported locales, there are no such cases.
|
|
2794
|
+
// If we support additional locales in the future, this may need to change.
|
|
2795
|
+
nextValue = input.selectionEnd === input.selectionStart ? input.value.slice(0, input.selectionStart) + input.value.slice(input.selectionEnd + 1) : input.value.slice(0, input.selectionStart) + input.value.slice(input.selectionEnd);
|
|
2796
|
+
break;
|
|
2797
|
+
case 'deleteContentBackward':
|
|
2798
|
+
nextValue = input.selectionEnd === input.selectionStart ? input.value.slice(0, input.selectionStart - 1) + input.value.slice(input.selectionStart) : input.value.slice(0, input.selectionStart) + input.value.slice(input.selectionEnd);
|
|
2799
|
+
break;
|
|
2800
|
+
case 'deleteSoftLineBackward':
|
|
2801
|
+
case 'deleteHardLineBackward':
|
|
2802
|
+
nextValue = input.value.slice(input.selectionStart);
|
|
2803
|
+
break;
|
|
2804
|
+
default:
|
|
2805
|
+
if (e.data != null) {
|
|
2806
|
+
nextValue = input.value.slice(0, input.selectionStart) + e.data + input.value.slice(input.selectionEnd);
|
|
2807
|
+
}
|
|
2808
|
+
break;
|
|
2809
|
+
}
|
|
2810
|
+
|
|
2811
|
+
// If we did not compute a value, or the new value is invalid, prevent the event
|
|
2812
|
+
// so that the browser does not update the input text, move the selection, or add to
|
|
2813
|
+
// the undo/redo stack.
|
|
2814
|
+
if (nextValue == null || !state.validate(nextValue)) {
|
|
2815
|
+
e.preventDefault();
|
|
2816
|
+
}
|
|
2817
|
+
});
|
|
2818
|
+
useEffect(() => {
|
|
2819
|
+
if (!supportsNativeBeforeInputEvent()) {
|
|
2820
|
+
return;
|
|
2821
|
+
}
|
|
2822
|
+
let input = inputRef.current;
|
|
2823
|
+
input.addEventListener('beforeinput', onBeforeInputFallback, false);
|
|
2824
|
+
return () => {
|
|
2825
|
+
input.removeEventListener('beforeinput', onBeforeInputFallback, false);
|
|
2826
|
+
};
|
|
2827
|
+
}, [inputRef, onBeforeInputFallback]);
|
|
2828
|
+
let onBeforeInput = !supportsNativeBeforeInputEvent() ? e => {
|
|
2829
|
+
let nextValue = e.target.value.slice(0, e.target.selectionStart) + e.data + e.target.value.slice(e.target.selectionEnd);
|
|
2830
|
+
if (!state.validate(nextValue)) {
|
|
2831
|
+
e.preventDefault();
|
|
2832
|
+
}
|
|
2833
|
+
} : null;
|
|
2834
|
+
let _useTextField = useTextField(props, inputRef),
|
|
2835
|
+
{
|
|
2836
|
+
labelProps,
|
|
2837
|
+
inputProps: textFieldProps,
|
|
2838
|
+
descriptionProps,
|
|
2839
|
+
errorMessageProps
|
|
2840
|
+
} = _useTextField,
|
|
2841
|
+
validation = _objectWithoutProperties(_useTextField, _excluded$2);
|
|
2842
|
+
let compositionStartState = useRef(null);
|
|
2843
|
+
return _objectSpread2({
|
|
2844
|
+
inputProps: mergeProps(textFieldProps, {
|
|
2845
|
+
onBeforeInput,
|
|
2846
|
+
onCompositionStart() {
|
|
2847
|
+
// Chrome does not implement Input Events Level 2, which specifies the insertFromComposition
|
|
2848
|
+
// and deleteByComposition inputType values for the beforeinput event. These are meant to occur
|
|
2849
|
+
// at the end of a composition (e.g. Pinyin IME, Android auto correct, etc.), and crucially, are
|
|
2850
|
+
// cancelable. The insertCompositionText and deleteCompositionText input types are not cancelable,
|
|
2851
|
+
// nor would we want to cancel them because the input from the user is incomplete at that point.
|
|
2852
|
+
// In Safari, insertFromComposition/deleteFromComposition will fire, however, allowing us to cancel
|
|
2853
|
+
// the final composition result if it is invalid. As a fallback for Chrome and Firefox, which either
|
|
2854
|
+
// don't support Input Events Level 2, or beforeinput at all, we store the state of the input when
|
|
2855
|
+
// the compositionstart event fires, and undo the changes in compositionend (below) if it is invalid.
|
|
2856
|
+
// Unfortunately, this messes up the undo/redo stack, but until insertFromComposition/deleteByComposition
|
|
2857
|
+
// are implemented, there is no other way to prevent composed input.
|
|
2858
|
+
// See https://bugs.chromium.org/p/chromium/issues/detail?id=1022204
|
|
2859
|
+
let {
|
|
2860
|
+
value,
|
|
2861
|
+
selectionStart,
|
|
2862
|
+
selectionEnd
|
|
2863
|
+
} = inputRef.current;
|
|
2864
|
+
compositionStartState.current = {
|
|
2865
|
+
value,
|
|
2866
|
+
selectionStart,
|
|
2867
|
+
selectionEnd
|
|
2868
|
+
};
|
|
2869
|
+
},
|
|
2870
|
+
onCompositionEnd() {
|
|
2871
|
+
if (!state.validate(inputRef.current.value)) {
|
|
2872
|
+
// Restore the input value in the DOM immediately so we can synchronously update the selection position.
|
|
2873
|
+
// But also update the value in React state as well so it is correct for future updates.
|
|
2874
|
+
let {
|
|
2875
|
+
value,
|
|
2876
|
+
selectionStart,
|
|
2877
|
+
selectionEnd
|
|
2878
|
+
} = compositionStartState.current;
|
|
2879
|
+
inputRef.current.value = value;
|
|
2880
|
+
inputRef.current.setSelectionRange(selectionStart, selectionEnd);
|
|
2881
|
+
state.setInputValue(value);
|
|
2882
|
+
}
|
|
2883
|
+
}
|
|
2884
|
+
}),
|
|
2885
|
+
labelProps,
|
|
2886
|
+
descriptionProps,
|
|
2887
|
+
errorMessageProps
|
|
2888
|
+
}, validation);
|
|
2889
|
+
}
|
|
2890
|
+
|
|
2891
|
+
/* eslint-disable prefer-const */
|
|
2892
|
+
// @ts-nocheck
|
|
2893
|
+
/*
|
|
2894
|
+
* Copyright 2020 Adobe. All rights reserved.
|
|
2895
|
+
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
|
|
2896
|
+
* you may not use this file except in compliance with the License. You may obtain a copy
|
|
2897
|
+
* of the License at http://www.apache.org/licenses/LICENSE-2.0
|
|
2898
|
+
*
|
|
2899
|
+
* Unless required by applicable law or agreed to in writing, software distributed under
|
|
2900
|
+
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
|
|
2901
|
+
* OF ANY KIND, either express or implied. See the License for the specific language
|
|
2902
|
+
* governing permissions and limitations under the License.
|
|
2903
|
+
*/
|
|
2904
|
+
|
|
2905
|
+
/* Inspired by https://github.com/AlmeroSteyn/react-aria-live */
|
|
2906
|
+
const LIVEREGION_TIMEOUT_DELAY = 7000;
|
|
2907
|
+
let liveAnnouncer = null;
|
|
2908
|
+
|
|
2909
|
+
/**
|
|
2910
|
+
* Announces the message using screen reader technology.
|
|
2911
|
+
*/
|
|
2912
|
+
function announce(message) {
|
|
2913
|
+
let assertiveness = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 'assertive';
|
|
2914
|
+
let timeout = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : LIVEREGION_TIMEOUT_DELAY;
|
|
2915
|
+
if (!liveAnnouncer) {
|
|
2916
|
+
liveAnnouncer = new LiveAnnouncer();
|
|
2917
|
+
}
|
|
2918
|
+
liveAnnouncer.announce(message, assertiveness, timeout);
|
|
2919
|
+
}
|
|
2920
|
+
|
|
2921
|
+
/**
|
|
2922
|
+
* Stops all queued announcements.
|
|
2923
|
+
*/
|
|
2924
|
+
function clearAnnouncer(assertiveness) {
|
|
2925
|
+
if (liveAnnouncer) {
|
|
2926
|
+
liveAnnouncer.clear(assertiveness);
|
|
2927
|
+
}
|
|
2928
|
+
}
|
|
2929
|
+
|
|
2930
|
+
// LiveAnnouncer is implemented using vanilla DOM, not React. That's because as of React 18
|
|
2931
|
+
// ReactDOM.render is deprecated, and the replacement, ReactDOM.createRoot is moved into a
|
|
2932
|
+
// subpath import `react-dom/client`. That makes it hard for us to support multiple React versions.
|
|
2933
|
+
// As a global API, we can't use portals without introducing a breaking API change. LiveAnnouncer
|
|
2934
|
+
// is simple enough to implement without React, so that's what we do here.
|
|
2935
|
+
// See this discussion for more details: https://github.com/reactwg/react-18/discussions/125#discussioncomment-2382638
|
|
2936
|
+
class LiveAnnouncer {
|
|
2937
|
+
constructor() {
|
|
2938
|
+
_defineProperty(this, "node", void 0);
|
|
2939
|
+
_defineProperty(this, "assertiveLog", void 0);
|
|
2940
|
+
_defineProperty(this, "politeLog", void 0);
|
|
2941
|
+
this.node = document.createElement('div');
|
|
2942
|
+
this.node.dataset.liveAnnouncer = 'true';
|
|
2943
|
+
// copied from VisuallyHidden
|
|
2944
|
+
Object.assign(this.node.style, {
|
|
2945
|
+
border: 0,
|
|
2946
|
+
clip: 'rect(0 0 0 0)',
|
|
2947
|
+
clipPath: 'inset(50%)',
|
|
2948
|
+
height: '1px',
|
|
2949
|
+
margin: '-1px',
|
|
2950
|
+
overflow: 'hidden',
|
|
2951
|
+
padding: 0,
|
|
2952
|
+
position: 'absolute',
|
|
2953
|
+
width: '1px',
|
|
2954
|
+
whiteSpace: 'nowrap'
|
|
2955
|
+
});
|
|
2956
|
+
this.assertiveLog = this.createLog('assertive');
|
|
2957
|
+
this.node.appendChild(this.assertiveLog);
|
|
2958
|
+
this.politeLog = this.createLog('polite');
|
|
2959
|
+
this.node.appendChild(this.politeLog);
|
|
2960
|
+
document.body.prepend(this.node);
|
|
2961
|
+
}
|
|
2962
|
+
createLog(ariaLive) {
|
|
2963
|
+
let node = document.createElement('div');
|
|
2964
|
+
node.setAttribute('role', 'log');
|
|
2965
|
+
node.setAttribute('aria-live', ariaLive);
|
|
2966
|
+
node.setAttribute('aria-relevant', 'additions');
|
|
2967
|
+
return node;
|
|
2968
|
+
}
|
|
2969
|
+
destroy() {
|
|
2970
|
+
if (!this.node) {
|
|
2971
|
+
return;
|
|
2972
|
+
}
|
|
2973
|
+
document.body.removeChild(this.node);
|
|
2974
|
+
this.node = null;
|
|
2975
|
+
}
|
|
2976
|
+
announce(message) {
|
|
2977
|
+
let assertiveness = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 'assertive';
|
|
2978
|
+
let timeout = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : LIVEREGION_TIMEOUT_DELAY;
|
|
2979
|
+
if (!this.node) {
|
|
2980
|
+
return;
|
|
2981
|
+
}
|
|
2982
|
+
let node = document.createElement('div');
|
|
2983
|
+
node.textContent = message;
|
|
2984
|
+
if (assertiveness === 'assertive') {
|
|
2985
|
+
this.assertiveLog.appendChild(node);
|
|
2986
|
+
} else {
|
|
2987
|
+
this.politeLog.appendChild(node);
|
|
2988
|
+
}
|
|
2989
|
+
if (message !== '') {
|
|
2990
|
+
setTimeout(() => {
|
|
2991
|
+
node.remove();
|
|
2992
|
+
}, timeout);
|
|
2993
|
+
}
|
|
2994
|
+
}
|
|
2995
|
+
clear(assertiveness) {
|
|
2996
|
+
if (!this.node) {
|
|
2997
|
+
return;
|
|
2998
|
+
}
|
|
2999
|
+
if (!assertiveness || assertiveness === 'assertive') {
|
|
3000
|
+
this.assertiveLog.innerHTML = '';
|
|
3001
|
+
}
|
|
3002
|
+
if (!assertiveness || assertiveness === 'polite') {
|
|
3003
|
+
this.politeLog.innerHTML = '';
|
|
3004
|
+
}
|
|
3005
|
+
}
|
|
3006
|
+
}
|
|
3007
|
+
|
|
3008
|
+
var Empty$1 = "Empty";
|
|
3009
|
+
var enUS = {
|
|
3010
|
+
Empty: Empty$1
|
|
3011
|
+
};
|
|
3012
|
+
|
|
3013
|
+
var Empty = "Vide";
|
|
3014
|
+
var frFR = {
|
|
3015
|
+
Empty: Empty
|
|
3016
|
+
};
|
|
3017
|
+
|
|
3018
|
+
var intlMessages = {
|
|
3019
|
+
'en-US': enUS,
|
|
3020
|
+
'fr-FR': frFR
|
|
3021
|
+
};
|
|
3022
|
+
|
|
3023
|
+
/* eslint-disable prefer-const */
|
|
3024
|
+
function useSpinButton(props) {
|
|
3025
|
+
const _async = useRef();
|
|
3026
|
+
let {
|
|
3027
|
+
value,
|
|
3028
|
+
textValue,
|
|
3029
|
+
minValue,
|
|
3030
|
+
maxValue,
|
|
3031
|
+
isDisabled,
|
|
3032
|
+
isReadOnly,
|
|
3033
|
+
isRequired,
|
|
3034
|
+
onIncrement,
|
|
3035
|
+
onIncrementPage,
|
|
3036
|
+
onDecrement,
|
|
3037
|
+
onDecrementPage,
|
|
3038
|
+
onDecrementToMin,
|
|
3039
|
+
onIncrementToMax
|
|
3040
|
+
} = props;
|
|
3041
|
+
const format = useMessageFormatter(intlMessages);
|
|
3042
|
+
const clearAsync = () => clearTimeout(_async.current);
|
|
3043
|
+
|
|
3044
|
+
// eslint-disable-next-line arrow-body-style
|
|
3045
|
+
useEffect(() => {
|
|
3046
|
+
return () => clearAsync();
|
|
3047
|
+
}, []);
|
|
3048
|
+
let onKeyDown = e => {
|
|
3049
|
+
if (e.ctrlKey || e.metaKey || e.shiftKey || e.altKey || isReadOnly) {
|
|
3050
|
+
return;
|
|
3051
|
+
}
|
|
3052
|
+
switch (e.key) {
|
|
3053
|
+
case 'PageUp':
|
|
3054
|
+
if (onIncrementPage) {
|
|
3055
|
+
e.preventDefault();
|
|
3056
|
+
onIncrementPage === null || onIncrementPage === void 0 ? void 0 : onIncrementPage();
|
|
3057
|
+
break;
|
|
3058
|
+
}
|
|
3059
|
+
// fallthrough!
|
|
3060
|
+
case 'ArrowUp':
|
|
3061
|
+
case 'Up':
|
|
3062
|
+
if (onIncrement) {
|
|
3063
|
+
e.preventDefault();
|
|
3064
|
+
onIncrement === null || onIncrement === void 0 ? void 0 : onIncrement();
|
|
3065
|
+
}
|
|
3066
|
+
break;
|
|
3067
|
+
case 'PageDown':
|
|
3068
|
+
if (onDecrementPage) {
|
|
3069
|
+
e.preventDefault();
|
|
3070
|
+
onDecrementPage === null || onDecrementPage === void 0 ? void 0 : onDecrementPage();
|
|
3071
|
+
break;
|
|
3072
|
+
}
|
|
3073
|
+
// fallthrough
|
|
3074
|
+
case 'ArrowDown':
|
|
3075
|
+
case 'Down':
|
|
3076
|
+
if (onDecrement) {
|
|
3077
|
+
e.preventDefault();
|
|
3078
|
+
onDecrement === null || onDecrement === void 0 ? void 0 : onDecrement();
|
|
3079
|
+
}
|
|
3080
|
+
break;
|
|
3081
|
+
case 'Home':
|
|
3082
|
+
if (onDecrementToMin) {
|
|
3083
|
+
e.preventDefault();
|
|
3084
|
+
onDecrementToMin === null || onDecrementToMin === void 0 ? void 0 : onDecrementToMin();
|
|
3085
|
+
}
|
|
3086
|
+
break;
|
|
3087
|
+
case 'End':
|
|
3088
|
+
if (onIncrementToMax) {
|
|
3089
|
+
e.preventDefault();
|
|
3090
|
+
onIncrementToMax === null || onIncrementToMax === void 0 ? void 0 : onIncrementToMax();
|
|
3091
|
+
}
|
|
3092
|
+
break;
|
|
3093
|
+
}
|
|
3094
|
+
};
|
|
3095
|
+
let isFocused = useRef(false);
|
|
3096
|
+
let onFocus = () => {
|
|
3097
|
+
isFocused.current = true;
|
|
3098
|
+
};
|
|
3099
|
+
let onBlur = () => {
|
|
3100
|
+
isFocused.current = false;
|
|
3101
|
+
};
|
|
3102
|
+
|
|
3103
|
+
// Replace Unicode hyphen-minus (U+002D) with minus sign (U+2212).
|
|
3104
|
+
// This ensures that macOS VoiceOver announces it as "minus" even with other characters between the minus sign
|
|
3105
|
+
// and the number (e.g. currency symbol). Otherwise it announces nothing because it assumes the character is a hyphen.
|
|
3106
|
+
// In addition, replace the empty string with the word "Empty" so that iOS VoiceOver does not read "50%" for an empty field.
|
|
3107
|
+
let ariaTextValue = textValue === '' ? format('Empty') : (textValue || `${value}`).replace('-', '\u2212');
|
|
3108
|
+
useEffect(() => {
|
|
3109
|
+
if (isFocused.current) {
|
|
3110
|
+
clearAnnouncer('assertive');
|
|
3111
|
+
announce(ariaTextValue, 'assertive');
|
|
3112
|
+
}
|
|
3113
|
+
}, [ariaTextValue]);
|
|
3114
|
+
const onIncrementPressStart = useEffectEvent(initialStepDelay => {
|
|
3115
|
+
clearAsync();
|
|
3116
|
+
onIncrement === null || onIncrement === void 0 ? void 0 : onIncrement();
|
|
3117
|
+
// Start spinning after initial delay
|
|
3118
|
+
_async.current = window.setTimeout(() => {
|
|
3119
|
+
if (maxValue === undefined || isNaN(maxValue) || value === undefined || isNaN(value) || value < maxValue) {
|
|
3120
|
+
onIncrementPressStart(60);
|
|
3121
|
+
}
|
|
3122
|
+
}, initialStepDelay);
|
|
3123
|
+
});
|
|
3124
|
+
const onDecrementPressStart = useEffectEvent(initialStepDelay => {
|
|
3125
|
+
clearAsync();
|
|
3126
|
+
onDecrement === null || onDecrement === void 0 ? void 0 : onDecrement();
|
|
3127
|
+
// Start spinning after initial delay
|
|
3128
|
+
_async.current = window.setTimeout(() => {
|
|
3129
|
+
if (minValue === undefined || isNaN(minValue) || value === undefined || isNaN(value) || value > minValue) {
|
|
3130
|
+
onDecrementPressStart(60);
|
|
3131
|
+
}
|
|
3132
|
+
}, initialStepDelay);
|
|
3133
|
+
});
|
|
3134
|
+
let cancelContextMenu = e => {
|
|
3135
|
+
e.preventDefault();
|
|
3136
|
+
};
|
|
3137
|
+
let {
|
|
3138
|
+
addGlobalListener,
|
|
3139
|
+
removeAllGlobalListeners
|
|
3140
|
+
} = useGlobalListeners();
|
|
3141
|
+
return {
|
|
3142
|
+
spinButtonProps: {
|
|
3143
|
+
role: 'spinbutton',
|
|
3144
|
+
'aria-valuenow': value !== undefined && !isNaN(value) ? value : undefined,
|
|
3145
|
+
'aria-valuetext': ariaTextValue,
|
|
3146
|
+
'aria-valuemin': minValue,
|
|
3147
|
+
'aria-valuemax': maxValue,
|
|
3148
|
+
'aria-disabled': isDisabled || undefined,
|
|
3149
|
+
'aria-readonly': isReadOnly || undefined,
|
|
3150
|
+
'aria-required': isRequired || undefined,
|
|
3151
|
+
onKeyDown,
|
|
3152
|
+
onFocus,
|
|
3153
|
+
onBlur
|
|
3154
|
+
},
|
|
3155
|
+
incrementButtonProps: {
|
|
3156
|
+
onPressStart: () => {
|
|
3157
|
+
onIncrementPressStart(400);
|
|
3158
|
+
addGlobalListener(window, 'contextmenu', cancelContextMenu);
|
|
3159
|
+
},
|
|
3160
|
+
onPressEnd: () => {
|
|
3161
|
+
clearAsync();
|
|
3162
|
+
removeAllGlobalListeners();
|
|
3163
|
+
},
|
|
3164
|
+
onFocus,
|
|
3165
|
+
onBlur
|
|
3166
|
+
},
|
|
3167
|
+
decrementButtonProps: {
|
|
3168
|
+
onPressStart: () => {
|
|
3169
|
+
onDecrementPressStart(400);
|
|
3170
|
+
addGlobalListener(window, 'contextmenu', cancelContextMenu);
|
|
3171
|
+
},
|
|
3172
|
+
onPressEnd: () => {
|
|
3173
|
+
clearAsync();
|
|
3174
|
+
removeAllGlobalListeners();
|
|
3175
|
+
},
|
|
3176
|
+
onFocus,
|
|
3177
|
+
onBlur
|
|
3178
|
+
}
|
|
3179
|
+
};
|
|
3180
|
+
}
|
|
3181
|
+
|
|
3182
|
+
const _excluded$1 = ["id", "decrementAriaLabel", "incrementAriaLabel", "isDisabled", "isReadOnly", "isRequired", "minValue", "maxValue", "autoFocus", "label", "formatOptions", "onBlur", "onFocus", "onFocusChange", "onKeyDown", "onKeyUp", "description", "errorMessage", "isWheelDisabled"];
|
|
3183
|
+
/**
|
|
3184
|
+
* Provides the behavior and accessibility implementation for a number field component.
|
|
3185
|
+
* Number fields allow users to enter a number, and increment or decrement the value using stepper buttons.
|
|
3186
|
+
*/
|
|
3187
|
+
function useNumberField(props, state, inputRef) {
|
|
3188
|
+
let {
|
|
3189
|
+
id,
|
|
3190
|
+
decrementAriaLabel,
|
|
3191
|
+
incrementAriaLabel,
|
|
3192
|
+
isDisabled,
|
|
3193
|
+
isReadOnly,
|
|
3194
|
+
isRequired,
|
|
3195
|
+
minValue,
|
|
3196
|
+
maxValue,
|
|
3197
|
+
autoFocus,
|
|
3198
|
+
label,
|
|
3199
|
+
formatOptions,
|
|
3200
|
+
onBlur = () => {},
|
|
3201
|
+
onFocus,
|
|
3202
|
+
onFocusChange,
|
|
3203
|
+
onKeyDown,
|
|
3204
|
+
onKeyUp,
|
|
3205
|
+
description,
|
|
3206
|
+
errorMessage,
|
|
3207
|
+
isWheelDisabled
|
|
3208
|
+
} = props,
|
|
3209
|
+
otherProps = _objectWithoutProperties(props, _excluded$1);
|
|
3210
|
+
let {
|
|
3211
|
+
increment,
|
|
3212
|
+
incrementToMax,
|
|
3213
|
+
decrement,
|
|
3214
|
+
decrementToMin,
|
|
3215
|
+
numberValue,
|
|
3216
|
+
inputValue,
|
|
3217
|
+
commit,
|
|
3218
|
+
commitValidation
|
|
3219
|
+
} = state;
|
|
3220
|
+
const format = useMessageFormatter(intlMessages$1);
|
|
3221
|
+
let inputId = useId(id);
|
|
3222
|
+
let {
|
|
3223
|
+
focusProps
|
|
3224
|
+
} = useFocus({
|
|
3225
|
+
onBlur() {
|
|
3226
|
+
// Set input value to normalized valid value
|
|
3227
|
+
commit();
|
|
3228
|
+
}
|
|
3229
|
+
});
|
|
3230
|
+
let numberFormatter = useNumberFormatter(formatOptions);
|
|
3231
|
+
let intlOptions = useMemo(() => numberFormatter.resolvedOptions(), [numberFormatter]);
|
|
3232
|
+
|
|
3233
|
+
// Replace negative textValue formatted using currencySign: 'accounting'
|
|
3234
|
+
// with a textValue that can be announced using a minus sign.
|
|
3235
|
+
let textValueFormatter = useNumberFormatter(_objectSpread2(_objectSpread2({}, formatOptions), {}, {
|
|
3236
|
+
currencySign: undefined
|
|
3237
|
+
}));
|
|
3238
|
+
let textValue = useMemo(() => isNaN(numberValue) ? '' : textValueFormatter.format(numberValue), [textValueFormatter, numberValue]);
|
|
3239
|
+
let {
|
|
3240
|
+
spinButtonProps,
|
|
3241
|
+
incrementButtonProps: incButtonProps,
|
|
3242
|
+
decrementButtonProps: decButtonProps
|
|
3243
|
+
} = useSpinButton({
|
|
3244
|
+
isDisabled,
|
|
3245
|
+
isReadOnly,
|
|
3246
|
+
isRequired,
|
|
3247
|
+
maxValue,
|
|
3248
|
+
minValue,
|
|
3249
|
+
onIncrement: increment,
|
|
3250
|
+
onIncrementToMax: incrementToMax,
|
|
3251
|
+
onDecrement: decrement,
|
|
3252
|
+
onDecrementToMin: decrementToMin,
|
|
3253
|
+
value: numberValue,
|
|
3254
|
+
textValue
|
|
3255
|
+
});
|
|
3256
|
+
let [focusWithin, setFocusWithin] = useState(false);
|
|
3257
|
+
let {
|
|
3258
|
+
focusWithinProps
|
|
3259
|
+
} = useFocusWithin({
|
|
3260
|
+
isDisabled,
|
|
3261
|
+
onFocusWithinChange: setFocusWithin
|
|
3262
|
+
});
|
|
3263
|
+
let onWheel = useCallback(e => {
|
|
3264
|
+
// if on a trackpad, users can scroll in both X and Y at once, check the magnitude of the change
|
|
3265
|
+
// if it's mostly in the X direction, then just return, the user probably doesn't mean to inc/dec
|
|
3266
|
+
// this isn't perfect, events come in fast with small deltas and a part of the scroll may give a false indication
|
|
3267
|
+
// especially if the user is scrolling near 45deg
|
|
3268
|
+
if (Math.abs(e.deltaY) <= Math.abs(e.deltaX)) {
|
|
3269
|
+
return;
|
|
3270
|
+
}
|
|
3271
|
+
if (e.deltaY > 0) {
|
|
3272
|
+
increment();
|
|
3273
|
+
} else if (e.deltaY < 0) {
|
|
3274
|
+
decrement();
|
|
3275
|
+
}
|
|
3276
|
+
}, [decrement, increment]);
|
|
3277
|
+
// If the input isn't supposed to receive input, disable scrolling.
|
|
3278
|
+
let scrollingDisabled = isWheelDisabled || isDisabled || isReadOnly || !focusWithin;
|
|
3279
|
+
useScrollWheel({
|
|
3280
|
+
onScroll: onWheel,
|
|
3281
|
+
isDisabled: scrollingDisabled
|
|
3282
|
+
}, inputRef);
|
|
3283
|
+
|
|
3284
|
+
// The inputMode attribute influences the software keyboard that is shown on touch devices.
|
|
3285
|
+
// Browsers and operating systems are quite inconsistent about what keys are available, however.
|
|
3286
|
+
// We choose between numeric and decimal based on whether we allow negative and fractional numbers,
|
|
3287
|
+
// and based on testing on various devices to determine what keys are available in each inputMode.
|
|
3288
|
+
let hasDecimals = intlOptions.maximumFractionDigits > 0;
|
|
3289
|
+
let hasNegative = state.minValue === undefined || isNaN(state.minValue) || state.minValue < 0;
|
|
3290
|
+
let inputMode = 'numeric';
|
|
3291
|
+
if (isIPhone()) {
|
|
3292
|
+
// iPhone doesn't have a minus sign in either numeric or decimal.
|
|
3293
|
+
// Note this is only for iPhone, not iPad, which always has both
|
|
3294
|
+
// minus and decimal in numeric.
|
|
3295
|
+
if (hasNegative) {
|
|
3296
|
+
inputMode = 'text';
|
|
3297
|
+
} else if (hasDecimals) {
|
|
3298
|
+
inputMode = 'decimal';
|
|
3299
|
+
}
|
|
3300
|
+
} else if (isAndroid()) {
|
|
3301
|
+
// Android numeric has both a decimal point and minus key.
|
|
3302
|
+
// decimal does not have a minus key.
|
|
3303
|
+
if (hasNegative) {
|
|
3304
|
+
inputMode = 'numeric';
|
|
3305
|
+
} else if (hasDecimals) {
|
|
3306
|
+
inputMode = 'decimal';
|
|
3307
|
+
}
|
|
3308
|
+
}
|
|
3309
|
+
let onChange = value => {
|
|
3310
|
+
if (state.validate(value)) {
|
|
3311
|
+
state.setInputValue(value);
|
|
3312
|
+
}
|
|
3313
|
+
};
|
|
3314
|
+
let domProps = filterDOMProps(props);
|
|
3315
|
+
let onKeyDownEnter = useCallback(e => {
|
|
3316
|
+
if (e.key === 'Enter') {
|
|
3317
|
+
commit();
|
|
3318
|
+
commitValidation();
|
|
3319
|
+
} else {
|
|
3320
|
+
e.continuePropagation();
|
|
3321
|
+
}
|
|
3322
|
+
}, [commit, commitValidation]);
|
|
3323
|
+
let {
|
|
3324
|
+
isInvalid,
|
|
3325
|
+
validationErrors,
|
|
3326
|
+
validationDetails
|
|
3327
|
+
} = state.displayValidation;
|
|
3328
|
+
let {
|
|
3329
|
+
labelProps,
|
|
3330
|
+
inputProps: textFieldProps,
|
|
3331
|
+
descriptionProps,
|
|
3332
|
+
errorMessageProps
|
|
3333
|
+
} = useFormattedTextField(_objectSpread2(_objectSpread2(_objectSpread2({}, otherProps), domProps), {}, {
|
|
3334
|
+
name: undefined,
|
|
3335
|
+
label,
|
|
3336
|
+
autoFocus,
|
|
3337
|
+
isDisabled,
|
|
3338
|
+
isReadOnly,
|
|
3339
|
+
isRequired,
|
|
3340
|
+
validate: undefined,
|
|
3341
|
+
[privateValidationStateProp]: state,
|
|
3342
|
+
value: inputValue,
|
|
3343
|
+
defaultValue: undefined,
|
|
3344
|
+
// defaultValue already used to populate state.inputValue, unneeded here
|
|
3345
|
+
autoComplete: 'off',
|
|
3346
|
+
'aria-label': props['aria-label'] || undefined,
|
|
3347
|
+
'aria-labelledby': props['aria-labelledby'] || undefined,
|
|
3348
|
+
id: inputId,
|
|
3349
|
+
type: 'text',
|
|
3350
|
+
// Can't use type="number" because then we can't have things like $ in the field.
|
|
3351
|
+
inputMode,
|
|
3352
|
+
onChange,
|
|
3353
|
+
onBlur,
|
|
3354
|
+
onFocus,
|
|
3355
|
+
onFocusChange,
|
|
3356
|
+
onKeyDown: useMemo(() => chain(onKeyDownEnter, onKeyDown), [onKeyDownEnter, onKeyDown]),
|
|
3357
|
+
onKeyUp,
|
|
3358
|
+
description,
|
|
3359
|
+
errorMessage
|
|
3360
|
+
}), state, inputRef);
|
|
3361
|
+
useFormReset(inputRef, state.numberValue, state.setNumberValue);
|
|
3362
|
+
let inputProps = mergeProps(spinButtonProps, focusProps, textFieldProps, {
|
|
3363
|
+
// override the spinbutton role, we can't focus a spin button with VO
|
|
3364
|
+
role: null,
|
|
3365
|
+
// ignore aria-roledescription on iOS so that required state will announce when it is present
|
|
3366
|
+
'aria-roledescription': !isIOS() ? format('numberField') : null,
|
|
3367
|
+
'aria-valuemax': null,
|
|
3368
|
+
'aria-valuemin': null,
|
|
3369
|
+
'aria-valuenow': null,
|
|
3370
|
+
'aria-valuetext': null,
|
|
3371
|
+
autoCorrect: 'off',
|
|
3372
|
+
spellCheck: 'false'
|
|
3373
|
+
});
|
|
3374
|
+
if (props.validationBehavior === 'native') {
|
|
3375
|
+
inputProps['aria-required'] = undefined;
|
|
3376
|
+
}
|
|
3377
|
+
let onButtonPressStart = e => {
|
|
3378
|
+
// If focus is already on the input, keep it there so we don't hide the
|
|
3379
|
+
// software keyboard when tapping the increment/decrement buttons.
|
|
3380
|
+
if (document.activeElement === inputRef.current) {
|
|
3381
|
+
return;
|
|
3382
|
+
}
|
|
3383
|
+
|
|
3384
|
+
// Otherwise, when using a mouse, move focus to the input.
|
|
3385
|
+
// On touch, or with a screen reader, focus the button so that the software
|
|
3386
|
+
// keyboard does not appear and the screen reader cursor is not moved off the button.
|
|
3387
|
+
if (e.pointerType === 'mouse') {
|
|
3388
|
+
var _inputRef$current;
|
|
3389
|
+
(_inputRef$current = inputRef.current) === null || _inputRef$current === void 0 ? void 0 : _inputRef$current.focus();
|
|
3390
|
+
} else {
|
|
3391
|
+
e.target.focus();
|
|
3392
|
+
}
|
|
3393
|
+
};
|
|
3394
|
+
|
|
3395
|
+
// Determine the label for the increment and decrement buttons. There are 4 cases:
|
|
3396
|
+
//
|
|
3397
|
+
// 1. With a visible label that is a string: aria-label: `Increase ${props.label}`
|
|
3398
|
+
// 2. With a visible label that is JSX: aria-label: 'Increase', aria-labelledby: '${incrementId} ${labelId}'
|
|
3399
|
+
// 3. With an aria-label: aria-label: `Increase ${props['aria-label']}`
|
|
3400
|
+
// 4. With an aria-labelledby: aria-label: 'Increase', aria-labelledby: `${incrementId} ${props['aria-labelledby']}`
|
|
3401
|
+
//
|
|
3402
|
+
// (1) and (2) could possibly be combined and both use aria-labelledby. However, placing the label in
|
|
3403
|
+
// the aria-label string rather than using aria-labelledby gives more flexibility to translators to change
|
|
3404
|
+
// the order or add additional words around the label if needed.
|
|
3405
|
+
let fieldLabel = props['aria-label'] || (typeof props.label === 'string' ? props.label : '');
|
|
3406
|
+
let ariaLabelledby;
|
|
3407
|
+
if (!fieldLabel) {
|
|
3408
|
+
ariaLabelledby = props.label != null ? labelProps.id : props['aria-labelledby'];
|
|
3409
|
+
}
|
|
3410
|
+
let incrementId = useId();
|
|
3411
|
+
let decrementId = useId();
|
|
3412
|
+
let incrementButtonProps = mergeProps(incButtonProps, {
|
|
3413
|
+
'aria-label': incrementAriaLabel || format('increase', {
|
|
3414
|
+
fieldLabel
|
|
3415
|
+
}).trim(),
|
|
3416
|
+
id: ariaLabelledby && !incrementAriaLabel ? incrementId : null,
|
|
3417
|
+
'aria-labelledby': ariaLabelledby && !incrementAriaLabel ? `${incrementId} ${ariaLabelledby}` : null,
|
|
3418
|
+
'aria-controls': inputId,
|
|
3419
|
+
excludeFromTabOrder: true,
|
|
3420
|
+
preventFocusOnPress: true,
|
|
3421
|
+
allowFocusWhenDisabled: true,
|
|
3422
|
+
isDisabled: !state.canIncrement,
|
|
3423
|
+
onPressStart: onButtonPressStart
|
|
3424
|
+
});
|
|
3425
|
+
let decrementButtonProps = mergeProps(decButtonProps, {
|
|
3426
|
+
'aria-label': decrementAriaLabel || format('decrease', {
|
|
3427
|
+
fieldLabel
|
|
3428
|
+
}).trim(),
|
|
3429
|
+
id: ariaLabelledby && !decrementAriaLabel ? decrementId : null,
|
|
3430
|
+
'aria-labelledby': ariaLabelledby && !decrementAriaLabel ? `${decrementId} ${ariaLabelledby}` : null,
|
|
3431
|
+
'aria-controls': inputId,
|
|
3432
|
+
excludeFromTabOrder: true,
|
|
3433
|
+
preventFocusOnPress: true,
|
|
3434
|
+
allowFocusWhenDisabled: true,
|
|
3435
|
+
isDisabled: !state.canDecrement,
|
|
3436
|
+
onPressStart: onButtonPressStart
|
|
3437
|
+
});
|
|
3438
|
+
return {
|
|
3439
|
+
groupProps: _objectSpread2(_objectSpread2({}, focusWithinProps), {}, {
|
|
3440
|
+
role: 'group',
|
|
3441
|
+
'aria-disabled': isDisabled,
|
|
3442
|
+
'aria-invalid': isInvalid ? 'true' : undefined
|
|
3443
|
+
}),
|
|
3444
|
+
labelProps,
|
|
3445
|
+
inputProps,
|
|
3446
|
+
incrementButtonProps,
|
|
3447
|
+
decrementButtonProps,
|
|
3448
|
+
errorMessageProps,
|
|
3449
|
+
descriptionProps,
|
|
3450
|
+
isInvalid,
|
|
3451
|
+
validationErrors,
|
|
3452
|
+
validationDetails
|
|
3453
|
+
};
|
|
3454
|
+
}
|
|
3455
|
+
|
|
3456
|
+
/* eslint-disable prefer-const */
|
|
3457
|
+
|
|
3458
|
+
// Order with overrides is important: 'button' should be default
|
|
3459
|
+
|
|
3460
|
+
/**
|
|
3461
|
+
* Provides the behavior and accessibility implementation for a button component. Handles mouse, keyboard, and touch interactions,
|
|
3462
|
+
* focus behavior, and ARIA props for both native button elements and custom element types.
|
|
3463
|
+
* @param props - Props to be applied to the button.
|
|
3464
|
+
* @param ref - A ref to a DOM element for the button.
|
|
3465
|
+
*/
|
|
3466
|
+
function useButton(props, ref) {
|
|
3467
|
+
let {
|
|
3468
|
+
elementType = 'button',
|
|
3469
|
+
isDisabled,
|
|
3470
|
+
onPress,
|
|
3471
|
+
onPressStart,
|
|
3472
|
+
onPressEnd,
|
|
3473
|
+
onPressUp,
|
|
3474
|
+
onPressChange,
|
|
3475
|
+
// @ts-ignore - undocumented
|
|
3476
|
+
preventFocusOnPress,
|
|
3477
|
+
// @ts-ignore - undocumented
|
|
3478
|
+
allowFocusWhenDisabled,
|
|
3479
|
+
// @ts-ignore
|
|
3480
|
+
onClick: deprecatedOnClick,
|
|
3481
|
+
href,
|
|
3482
|
+
target,
|
|
3483
|
+
rel,
|
|
3484
|
+
type = 'button'
|
|
3485
|
+
} = props;
|
|
3486
|
+
let additionalProps;
|
|
3487
|
+
if (elementType === 'button') {
|
|
3488
|
+
additionalProps = {
|
|
3489
|
+
type,
|
|
3490
|
+
disabled: isDisabled
|
|
3491
|
+
};
|
|
3492
|
+
} else {
|
|
3493
|
+
additionalProps = {
|
|
3494
|
+
role: 'button',
|
|
3495
|
+
tabIndex: isDisabled ? undefined : 0,
|
|
3496
|
+
href: elementType === 'a' && isDisabled ? undefined : href,
|
|
3497
|
+
target: elementType === 'a' ? target : undefined,
|
|
3498
|
+
type: elementType === 'input' ? type : undefined,
|
|
3499
|
+
disabled: elementType === 'input' ? isDisabled : undefined,
|
|
3500
|
+
'aria-disabled': !isDisabled || elementType === 'input' ? undefined : isDisabled,
|
|
3501
|
+
rel: elementType === 'a' ? rel : undefined
|
|
3502
|
+
};
|
|
3503
|
+
}
|
|
3504
|
+
let {
|
|
3505
|
+
pressProps,
|
|
3506
|
+
isPressed
|
|
3507
|
+
} = usePress({
|
|
3508
|
+
onPressStart,
|
|
3509
|
+
onPressEnd,
|
|
3510
|
+
onPressChange,
|
|
3511
|
+
onPress,
|
|
3512
|
+
onPressUp,
|
|
3513
|
+
isDisabled,
|
|
3514
|
+
preventFocusOnPress,
|
|
3515
|
+
ref
|
|
3516
|
+
});
|
|
3517
|
+
let {
|
|
3518
|
+
focusableProps
|
|
3519
|
+
} = useFocusable(props, ref);
|
|
3520
|
+
if (allowFocusWhenDisabled) {
|
|
3521
|
+
focusableProps.tabIndex = isDisabled ? -1 : focusableProps.tabIndex;
|
|
3522
|
+
}
|
|
3523
|
+
let buttonProps = mergeProps(focusableProps, pressProps, filterDOMProps(props, {
|
|
3524
|
+
labelable: true
|
|
3525
|
+
}));
|
|
3526
|
+
return {
|
|
3527
|
+
isPressed,
|
|
3528
|
+
// Used to indicate press state for visual
|
|
3529
|
+
buttonProps: mergeProps(additionalProps, buttonProps, {
|
|
3530
|
+
'aria-haspopup': props['aria-haspopup'],
|
|
3531
|
+
'aria-expanded': props['aria-expanded'],
|
|
3532
|
+
'aria-controls': props['aria-controls'],
|
|
3533
|
+
'aria-pressed': props['aria-pressed'],
|
|
3534
|
+
onClick: e => {
|
|
3535
|
+
if (deprecatedOnClick) {
|
|
3536
|
+
deprecatedOnClick(e);
|
|
3537
|
+
console.warn('onClick is deprecated, please use onPress');
|
|
3538
|
+
}
|
|
3539
|
+
}
|
|
3540
|
+
})
|
|
3541
|
+
};
|
|
3542
|
+
}
|
|
3543
|
+
|
|
3544
|
+
/**
|
|
3545
|
+
* Component style.
|
|
3546
|
+
*/
|
|
3547
|
+
const StyledNumberField = styled.div`
|
|
3548
|
+
height: fit-content;
|
|
3549
|
+
position: relative;
|
|
3550
|
+
width: fit-content;
|
|
3551
|
+
margin: 0;
|
|
3552
|
+
min-width: 0;
|
|
3553
|
+
padding: 0;
|
|
3554
|
+
${baseStyling}
|
|
3555
|
+
|
|
3556
|
+
border: 0;
|
|
3557
|
+
display: inline-flex;
|
|
3558
|
+
flex-direction: column;
|
|
3559
|
+
vertical-align: top;
|
|
3560
|
+
|
|
3561
|
+
${_ref => {
|
|
3562
|
+
let {
|
|
3563
|
+
$hasLeftIcon,
|
|
3564
|
+
$hasContent,
|
|
3565
|
+
$isFocused,
|
|
3566
|
+
$isDisabled,
|
|
3567
|
+
$isInvalid,
|
|
3568
|
+
$isColored,
|
|
3569
|
+
$theme,
|
|
3570
|
+
$variant
|
|
3571
|
+
} = _ref;
|
|
3572
|
+
return css`
|
|
3573
|
+
${$isDisabled ? css`
|
|
3574
|
+
pointer-events: none;
|
|
3575
|
+
` : ''}
|
|
3576
|
+
|
|
3577
|
+
.redsift-number-field__label {
|
|
3578
|
+
font-family: var(--redsift-typography-input-text-font-family);
|
|
3579
|
+
font-size: var(--redsift-typography-input-text-font-size);
|
|
3580
|
+
font-weight: var(--redsift-typography-input-text-font-weight);
|
|
3581
|
+
line-height: var(--redsift-typography-input-text-line-height);
|
|
3582
|
+
left: ${$hasLeftIcon && !$hasContent ? '32px' : '0'};
|
|
3583
|
+
overflow: hidden;
|
|
3584
|
+
pointer-events: none;
|
|
3585
|
+
position: absolute;
|
|
3586
|
+
text-overflow: ellipsis;
|
|
3587
|
+
top: ${$hasContent ? '-8px' : '-5px'};
|
|
3588
|
+
transform-origin: top left;
|
|
3589
|
+
transition: color 200ms cubic-bezier(0, 0, 0.2, 1) 0ms, transform 200ms cubic-bezier(0, 0, 0.2, 1) 0ms,
|
|
3590
|
+
max-width 200ms cubic-bezier(0, 0, 0.2, 1) 0ms;
|
|
3591
|
+
white-space: nowrap;
|
|
3592
|
+
z-index: 1;
|
|
3593
|
+
|
|
3594
|
+
${!$hasContent ? css`
|
|
3595
|
+
max-width: calc(100% - 24px - 42px - ${$variant === NumberFieldVariant.underline ? '0px' : '14px'});
|
|
3596
|
+
transform: translate(${$variant === NumberFieldVariant.underline ? '0px' : '14px'}, 16px) scale(1);
|
|
3597
|
+
` : css`
|
|
3598
|
+
max-width: calc(133% - 32px - 42px - ${$variant === NumberFieldVariant.underline ? '0px' : '14px'});
|
|
3599
|
+
transform: translate(${$variant === NumberFieldVariant.underline ? '0px' : '14px'}, 1px) scale(0.733);
|
|
3600
|
+
`}
|
|
3601
|
+
|
|
3602
|
+
${$isDisabled ? css`
|
|
3603
|
+
color: var(--redsift-color-neutral-light-grey);
|
|
3604
|
+
` : $isInvalid ? css`
|
|
3605
|
+
color: var(--redsift-color-notifications-error-primary);
|
|
3606
|
+
` : $isFocused ? css`
|
|
3607
|
+
color: ${$isColored ? 'var(--redsift-color-primary-n)' : 'var(--redsift-color-notifications-question-primary)'};
|
|
3608
|
+
` : css`
|
|
3609
|
+
color: var(--redsift-color-neutral-${$theme === Theme.light ? 'x-dark-grey' : 'light-grey'});
|
|
3610
|
+
`}
|
|
3611
|
+
}
|
|
3612
|
+
|
|
3613
|
+
.redsift-number-field__input-wrapper {
|
|
3614
|
+
align-items: flex-start;
|
|
3615
|
+
box-sizing: border-box;
|
|
3616
|
+
cursor: text;
|
|
3617
|
+
display: inline-flex;
|
|
3618
|
+
min-height: 42px;
|
|
3619
|
+
position: relative;
|
|
3620
|
+
width: 100%;
|
|
3621
|
+
}
|
|
3622
|
+
|
|
3623
|
+
.redsift-number-field-input-wrapper__input {
|
|
3624
|
+
background: none;
|
|
3625
|
+
border: 0;
|
|
3626
|
+
box-sizing: content-box;
|
|
3627
|
+
display: flex;
|
|
3628
|
+
flex: 1 1 auto;
|
|
3629
|
+
font-family: var(--redsift-typography-input-text-font-family);
|
|
3630
|
+
font-size: var(--redsift-typography-input-text-font-size);
|
|
3631
|
+
font-weight: var(--redsift-typography-input-text-font-weight);
|
|
3632
|
+
line-height: var(--redsift-typography-input-text-line-height);
|
|
3633
|
+
min-width: 0;
|
|
3634
|
+
min-width: 100px;
|
|
3635
|
+
padding: 2px 0;
|
|
3636
|
+
width: auto;
|
|
3637
|
+
padding-bottom: 6px;
|
|
3638
|
+
${$variant !== NumberFieldVariant.underline && !$hasLeftIcon ? css`
|
|
3639
|
+
padding-left: 16px;
|
|
3640
|
+
` : ''}
|
|
3641
|
+
padding-top: 10px;
|
|
3642
|
+
|
|
3643
|
+
${$isDisabled ? css`
|
|
3644
|
+
color: var(--redsift-color-neutral-light-grey);
|
|
3645
|
+
` : css`
|
|
3646
|
+
color: var(--redsift-color-neutral-${$theme === Theme.dark ? 'white' : 'x-dark-grey'});
|
|
3647
|
+
`}
|
|
3648
|
+
}
|
|
3649
|
+
|
|
3650
|
+
.redsift-number-field-input-wrapper__input::placeholder {
|
|
3651
|
+
color: var(--redsift-color-neutral-mid-grey);
|
|
3652
|
+
}
|
|
3653
|
+
|
|
3654
|
+
.redsift-number-field-input-wrapper__input:focus {
|
|
3655
|
+
outline: 0;
|
|
3656
|
+
}
|
|
3657
|
+
|
|
3658
|
+
.redsift-number-field-input-wrapper__fieldset {
|
|
3659
|
+
border-style: solid;
|
|
3660
|
+
bottom: 0;
|
|
3661
|
+
left: 1px;
|
|
3662
|
+
margin: 0;
|
|
3663
|
+
min-width: 0%;
|
|
3664
|
+
overflow: hidden;
|
|
3665
|
+
padding: 0 8px;
|
|
3666
|
+
pointer-events: none;
|
|
3667
|
+
position: absolute;
|
|
3668
|
+
right: 0;
|
|
3669
|
+
text-align: left;
|
|
3670
|
+
top: -5px;
|
|
3671
|
+
|
|
3672
|
+
${$variant === NumberFieldVariant.underline ? css`
|
|
3673
|
+
border-bottom-width: 2px;
|
|
3674
|
+
` : css`
|
|
3675
|
+
border-radius: 4px;
|
|
3676
|
+
border-width: 2px;
|
|
3677
|
+
`}
|
|
3678
|
+
|
|
3679
|
+
${$isDisabled ? css`
|
|
3680
|
+
border-color: var(--redsift-color-neutral-light-grey);
|
|
3681
|
+
` : $isInvalid ? css`
|
|
3682
|
+
border-color: var(--redsift-color-notifications-error-primary);
|
|
3683
|
+
` : $isFocused ? css`
|
|
3684
|
+
border-color: ${$isColored ? 'var(--redsift-color-primary-n)' : 'var(--redsift-color-notifications-question-primary)'};
|
|
3685
|
+
` : css`
|
|
3686
|
+
border-color: var(--redsift-color-neutral-mid-grey);
|
|
3687
|
+
`}
|
|
3688
|
+
}
|
|
3689
|
+
|
|
3690
|
+
.redsift-number-field-input-wrapper-fieldset__legend {
|
|
3691
|
+
display: block;
|
|
3692
|
+
float: unset;
|
|
3693
|
+
font-size: 11px;
|
|
3694
|
+
height: 11px;
|
|
3695
|
+
overflow: hidden;
|
|
3696
|
+
padding: 0;
|
|
3697
|
+
visibility: hidden;
|
|
3698
|
+
white-space: nowrap;
|
|
3699
|
+
width: auto;
|
|
3700
|
+
|
|
3701
|
+
${!$hasContent ? css`
|
|
3702
|
+
max-width: 0.01px;
|
|
3703
|
+
transition: max-width 50ms cubic-bezier(0, 0, 0.2, 1) 0ms;
|
|
3704
|
+
` : css`
|
|
3705
|
+
max-width: calc(100% - 42px);
|
|
3706
|
+
transition: max-width 100ms cubic-bezier(0, 0, 0.2, 1) 50ms;
|
|
3707
|
+
`}
|
|
3708
|
+
}
|
|
3709
|
+
|
|
3710
|
+
.redsift-number-field-input-wrapper-fieldset__legend > span {
|
|
3711
|
+
display: inline-block;
|
|
3712
|
+
opacity: 0;
|
|
3713
|
+
padding-left: 5px;
|
|
3714
|
+
padding-right: 5px;
|
|
3715
|
+
visibility: visible;
|
|
3716
|
+
}
|
|
3717
|
+
|
|
3718
|
+
.redsift-icon-button {
|
|
3719
|
+
padding: 2px;
|
|
3720
|
+
}
|
|
3721
|
+
|
|
3722
|
+
.redsift-number-field-input-wrapper__toolbar {
|
|
3723
|
+
> button {
|
|
3724
|
+
height: 21px;
|
|
3725
|
+
width: 42px;
|
|
3726
|
+
${$isDisabled ? css`
|
|
3727
|
+
border-color: var(--redsift-color-neutral-light-grey);
|
|
3728
|
+
` : $isInvalid ? css`
|
|
3729
|
+
border-color: var(--redsift-color-notifications-error-primary);
|
|
3730
|
+
` : $isFocused ? css`
|
|
3731
|
+
border-color: ${$isColored ? 'var(--redsift-color-primary-n)' : 'var(--redsift-color-notifications-question-primary)'};
|
|
3732
|
+
` : css`
|
|
3733
|
+
border-color: var(--redsift-color-neutral-mid-grey);
|
|
3734
|
+
`}
|
|
3735
|
+
}
|
|
3736
|
+
|
|
3737
|
+
.redsift-number-field-input-wrapper-toolbar__increment-button {
|
|
3738
|
+
border-top-width: 0;
|
|
3739
|
+
border-right-width: 0;
|
|
3740
|
+
border-bottom-width: 1px;
|
|
3741
|
+
border-left-width: 2px;
|
|
3742
|
+
border-top-left-radius: 0;
|
|
3743
|
+
border-top-right-radius: 4px;
|
|
3744
|
+
border-bottom-right-radius: 0;
|
|
3745
|
+
border-bottom-left-radius: 0;
|
|
3746
|
+
}
|
|
3747
|
+
|
|
3748
|
+
.redsift-number-field-input-wrapper-toolbar__decrement-button {
|
|
3749
|
+
border-top-width: 1px;
|
|
3750
|
+
border-right-width: 0;
|
|
3751
|
+
border-bottom-width: 0;
|
|
3752
|
+
border-left-width: 2px;
|
|
3753
|
+
border-top-left-radius: 0;
|
|
3754
|
+
border-top-right-radius: 0;
|
|
3755
|
+
border-bottom-right-radius: 4px;
|
|
3756
|
+
border-bottom-left-radius: 0;
|
|
3757
|
+
}
|
|
3758
|
+
}
|
|
3759
|
+
|
|
3760
|
+
${$variant !== NumberFieldVariant.underline ? css`
|
|
3761
|
+
.redsift-icon.left {
|
|
3762
|
+
padding-left: 12px;
|
|
3763
|
+
padding-right: 8px;
|
|
3764
|
+
line-height: 28px;
|
|
3765
|
+
top: 10px;
|
|
3766
|
+
}
|
|
3767
|
+
` : css`
|
|
3768
|
+
.redsift-icon.left {
|
|
3769
|
+
padding-right: 8px;
|
|
3770
|
+
top: 10px;
|
|
3771
|
+
}
|
|
3772
|
+
`}
|
|
3773
|
+
`;
|
|
3774
|
+
}}
|
|
3775
|
+
`;
|
|
3776
|
+
|
|
3777
|
+
const _excluded = ["autoFocus", "className", "decrementAriaLabel", "defaultValue", "formatOptions", "id", "incrementAriaLabel", "inputProps", "inputRef", "isColored", "isDisabled", "isInvalid", "isReadOnly", "isRequired", "isWheelDisabled", "label", "leftIcon", "maxValue", "minValue", "onBlur", "onChange", "onFocus", "onFocusChange", "onKeyDown", "onKeyUp", "placeholder", "step", "theme", "value", "variant"];
|
|
3778
|
+
const COMPONENT_NAME = 'NumberField';
|
|
3779
|
+
const CLASSNAME = 'redsift-number-field';
|
|
73
3780
|
|
|
74
3781
|
/**
|
|
75
3782
|
* The NumberField component.
|
|
76
3783
|
* Can be used as controlled or uncontrolled.
|
|
77
3784
|
*/
|
|
78
|
-
|
|
3785
|
+
const NumberField = /*#__PURE__*/forwardRef((props, ref) => {
|
|
3786
|
+
const {
|
|
3787
|
+
autoFocus,
|
|
3788
|
+
className,
|
|
3789
|
+
decrementAriaLabel,
|
|
3790
|
+
defaultValue,
|
|
3791
|
+
formatOptions,
|
|
3792
|
+
id,
|
|
3793
|
+
incrementAriaLabel,
|
|
3794
|
+
inputProps: propsInputProps,
|
|
3795
|
+
inputRef: propsInputRef,
|
|
3796
|
+
isColored = true,
|
|
3797
|
+
isDisabled,
|
|
3798
|
+
isInvalid,
|
|
3799
|
+
isReadOnly,
|
|
3800
|
+
isRequired,
|
|
3801
|
+
isWheelDisabled,
|
|
3802
|
+
label,
|
|
3803
|
+
leftIcon,
|
|
3804
|
+
maxValue,
|
|
3805
|
+
minValue,
|
|
3806
|
+
onBlur: onBlurProps,
|
|
3807
|
+
onChange: propsOnChange,
|
|
3808
|
+
onFocus: onFocusProps,
|
|
3809
|
+
onFocusChange,
|
|
3810
|
+
onKeyDown,
|
|
3811
|
+
onKeyUp,
|
|
3812
|
+
placeholder,
|
|
3813
|
+
step,
|
|
3814
|
+
theme: propsTheme,
|
|
3815
|
+
value,
|
|
3816
|
+
variant = NumberFieldVariant.default
|
|
3817
|
+
} = props,
|
|
3818
|
+
forwardedProps = _objectWithoutProperties(props, _excluded);
|
|
3819
|
+
const numberFieldProps = {
|
|
3820
|
+
id,
|
|
3821
|
+
isDisabled,
|
|
3822
|
+
isReadOnly,
|
|
3823
|
+
isRequired,
|
|
3824
|
+
minValue,
|
|
3825
|
+
maxValue,
|
|
3826
|
+
step,
|
|
3827
|
+
autoFocus,
|
|
3828
|
+
label,
|
|
3829
|
+
formatOptions,
|
|
3830
|
+
onBlur: onBlurProps,
|
|
3831
|
+
onFocus: onFocusProps,
|
|
3832
|
+
onFocusChange,
|
|
3833
|
+
onKeyDown,
|
|
3834
|
+
onKeyUp,
|
|
3835
|
+
value,
|
|
3836
|
+
defaultValue,
|
|
3837
|
+
onChange: propsOnChange
|
|
3838
|
+
};
|
|
3839
|
+
const theme = useTheme(propsTheme);
|
|
3840
|
+
const {
|
|
3841
|
+
locale
|
|
3842
|
+
} = useLocale();
|
|
3843
|
+
const state = useNumberFieldState(_objectSpread2({
|
|
3844
|
+
locale
|
|
3845
|
+
}, numberFieldProps));
|
|
3846
|
+
const _inputRef = React__default.useRef(null);
|
|
3847
|
+
const inputRef = propsInputRef !== null && propsInputRef !== void 0 ? propsInputRef : _inputRef;
|
|
3848
|
+
const {
|
|
3849
|
+
labelProps,
|
|
3850
|
+
groupProps,
|
|
3851
|
+
inputProps,
|
|
3852
|
+
incrementButtonProps,
|
|
3853
|
+
decrementButtonProps
|
|
3854
|
+
} = useNumberField(_objectSpread2({
|
|
3855
|
+
decrementAriaLabel,
|
|
3856
|
+
incrementAriaLabel,
|
|
3857
|
+
isWheelDisabled
|
|
3858
|
+
}, numberFieldProps), state, inputRef);
|
|
3859
|
+
const incrementButtonRef = useRef(null);
|
|
3860
|
+
const {
|
|
3861
|
+
buttonProps: incrementButtonForwardedProps
|
|
3862
|
+
} = useButton(incrementButtonProps, incrementButtonRef);
|
|
3863
|
+
const decrementButtonRef = useRef(null);
|
|
3864
|
+
const {
|
|
3865
|
+
buttonProps: decrementButtonForwardedProps
|
|
3866
|
+
} = useButton(decrementButtonProps, decrementButtonRef);
|
|
3867
|
+
const {
|
|
3868
|
+
isFocusVisible,
|
|
3869
|
+
isFocused,
|
|
3870
|
+
focusProps: {
|
|
3871
|
+
onFocus,
|
|
3872
|
+
onBlur
|
|
3873
|
+
}
|
|
3874
|
+
} = useFocusRing({
|
|
3875
|
+
autoFocus
|
|
3876
|
+
});
|
|
3877
|
+
const [isFocusWithin, setFocusWithin] = useState(Boolean(autoFocus));
|
|
3878
|
+
const {
|
|
3879
|
+
focusWithinProps
|
|
3880
|
+
} = useFocusWithin({
|
|
3881
|
+
onFocusWithinChange: isFocusWithin => setFocusWithin(isFocusWithin)
|
|
3882
|
+
});
|
|
3883
|
+
warnIfNoAccessibleLabelFound(props, [label], 'NumberField');
|
|
3884
|
+
return /*#__PURE__*/React__default.createElement(StyledNumberField, _extends({}, forwardedProps, focusWithinProps, {
|
|
3885
|
+
$hasContent: isFocusWithin || Boolean(inputProps.value !== undefined && inputProps.value !== '') || Boolean(placeholder),
|
|
3886
|
+
$hasLeftIcon: Boolean(leftIcon),
|
|
3887
|
+
$isColored: isColored,
|
|
3888
|
+
$isDisabled: isDisabled,
|
|
3889
|
+
$isFocusVisible: isFocusVisible,
|
|
3890
|
+
$isInvalid: isInvalid || isRequired && !(inputProps.value !== undefined),
|
|
3891
|
+
$isRequired: isRequired,
|
|
3892
|
+
$isFocused: isFocused,
|
|
3893
|
+
$theme: theme,
|
|
3894
|
+
$variant: variant,
|
|
3895
|
+
className: classNames(NumberField.className, className),
|
|
3896
|
+
ref: ref
|
|
3897
|
+
}), label ? /*#__PURE__*/React__default.createElement("label", _extends({}, labelProps, {
|
|
3898
|
+
className: `${NumberField.className}__label`
|
|
3899
|
+
}), /*#__PURE__*/React__default.createElement("span", null, label)) : null, /*#__PURE__*/React__default.createElement("div", {
|
|
3900
|
+
className: `${NumberField.className}__input-wrapper`
|
|
3901
|
+
}, leftIcon ? /*#__PURE__*/React__default.createElement(Icon, _extends({
|
|
3902
|
+
color: isDisabled ? 'question' : 'black'
|
|
3903
|
+
}, leftIcon, {
|
|
3904
|
+
"aria-hidden": "true",
|
|
3905
|
+
className: "left"
|
|
3906
|
+
})) : null, /*#__PURE__*/React__default.createElement(Flexbox, _extends({}, groupProps, {
|
|
3907
|
+
flexDirection: "row",
|
|
3908
|
+
gap: "0",
|
|
3909
|
+
width: "100%"
|
|
3910
|
+
}), /*#__PURE__*/React__default.createElement("input", _extends({}, propsInputProps, inputProps, {
|
|
3911
|
+
onChange: event => {
|
|
3912
|
+
if (propsOnChange) {
|
|
3913
|
+
propsOnChange(Number(event.target.value));
|
|
3914
|
+
} else {
|
|
3915
|
+
var _inputProps$onChange;
|
|
3916
|
+
(_inputProps$onChange = inputProps.onChange) === null || _inputProps$onChange === void 0 ? void 0 : _inputProps$onChange.call(inputProps, event);
|
|
3917
|
+
}
|
|
3918
|
+
},
|
|
3919
|
+
onBlur: event => {
|
|
3920
|
+
onBlur === null || onBlur === void 0 ? void 0 : onBlur(event);
|
|
3921
|
+
onBlurProps === null || onBlurProps === void 0 ? void 0 : onBlurProps(event);
|
|
3922
|
+
},
|
|
3923
|
+
onFocus: event => {
|
|
3924
|
+
onFocus === null || onFocus === void 0 ? void 0 : onFocus(event);
|
|
3925
|
+
onFocusProps === null || onFocusProps === void 0 ? void 0 : onFocusProps(event);
|
|
3926
|
+
},
|
|
3927
|
+
placeholder: placeholder ? `${placeholder}` : undefined,
|
|
3928
|
+
className: `${NumberField.className}-input-wrapper__input`,
|
|
3929
|
+
ref: inputRef,
|
|
3930
|
+
width: "100%"
|
|
3931
|
+
})), /*#__PURE__*/React__default.createElement(Flexbox, {
|
|
3932
|
+
className: `${NumberField.className}-input-wrapper__toolbar`,
|
|
3933
|
+
flexDirection: "column",
|
|
3934
|
+
gap: "0"
|
|
3935
|
+
}, /*#__PURE__*/React__default.createElement(StyledIconButton, _extends({
|
|
3936
|
+
$color: "grey",
|
|
3937
|
+
$isActive: false,
|
|
3938
|
+
$isDisabled: isDisabled,
|
|
3939
|
+
$isGradient: false,
|
|
3940
|
+
$isHovered: false,
|
|
3941
|
+
$isLoading: false,
|
|
3942
|
+
$variant: variant === NumberFieldVariant.underline ? 'unstyled' : 'secondary',
|
|
3943
|
+
$theme: theme
|
|
3944
|
+
}, incrementButtonForwardedProps, {
|
|
3945
|
+
className: `${NumberField.className}-input-wrapper-toolbar__increment-button`
|
|
3946
|
+
}), /*#__PURE__*/React__default.createElement(Icon, {
|
|
3947
|
+
icon: mdiMenuUp,
|
|
3948
|
+
color: isDisabled ? undefined : 'grey'
|
|
3949
|
+
})), /*#__PURE__*/React__default.createElement(StyledIconButton, _extends({
|
|
3950
|
+
$color: "grey",
|
|
3951
|
+
$isActive: false,
|
|
3952
|
+
$isDisabled: isDisabled,
|
|
3953
|
+
$isGradient: false,
|
|
3954
|
+
$isHovered: false,
|
|
3955
|
+
$isLoading: false,
|
|
3956
|
+
$variant: variant === NumberFieldVariant.underline ? 'unstyled' : 'secondary',
|
|
3957
|
+
$theme: theme
|
|
3958
|
+
}, decrementButtonForwardedProps, {
|
|
3959
|
+
className: `${NumberField.className}-input-wrapper-toolbar__decrement-button`
|
|
3960
|
+
}), /*#__PURE__*/React__default.createElement(Icon, {
|
|
3961
|
+
icon: mdiMenuDown,
|
|
3962
|
+
color: isDisabled ? undefined : 'grey'
|
|
3963
|
+
})))), /*#__PURE__*/React__default.createElement("fieldset", {
|
|
3964
|
+
"aria-hidden": "true",
|
|
3965
|
+
className: `${NumberField.className}-input-wrapper__fieldset`
|
|
3966
|
+
}, /*#__PURE__*/React__default.createElement("legend", {
|
|
3967
|
+
className: `${NumberField.className}-input-wrapper-fieldset__legend`
|
|
3968
|
+
}, label ? /*#__PURE__*/React__default.createElement("span", null, label) : null))));
|
|
3969
|
+
});
|
|
3970
|
+
NumberField.className = CLASSNAME;
|
|
3971
|
+
NumberField.displayName = COMPONENT_NAME;
|
|
79
3972
|
|
|
80
|
-
export { NumberFieldVariant as N,
|
|
3973
|
+
export { NumberFieldVariant as N, NumberField as a };
|
|
3974
|
+
//# sourceMappingURL=NumberField.js.map
|