@jobber/components 8.15.0 → 8.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/dist/Card/index.cjs +1 -1
  2. package/dist/Card/index.mjs +1 -1
  3. package/dist/DataDump/index.cjs +1 -1
  4. package/dist/DataDump/index.mjs +1 -1
  5. package/dist/InputNumberExperimental-cjs.js +27 -23
  6. package/dist/InputNumberExperimental-es.js +12 -8
  7. package/dist/Menu/index.cjs +1 -1
  8. package/dist/Menu/index.mjs +1 -1
  9. package/dist/MenuSubmenuTrigger-cjs.js +1060 -1060
  10. package/dist/MenuSubmenuTrigger-es.js +928 -928
  11. package/dist/NumberFieldInput-cjs.js +53 -53
  12. package/dist/NumberFieldInput-es.js +1 -1
  13. package/dist/Page/index.cjs +1 -1
  14. package/dist/Page/index.mjs +1 -1
  15. package/dist/ScrollAreaViewport-cjs.js +4436 -4436
  16. package/dist/ScrollAreaViewport-es.js +4372 -4372
  17. package/dist/{useValueChanged-cjs.js → clamp-cjs.js} +594 -592
  18. package/dist/{useValueChanged-es.js → clamp-es.js} +595 -594
  19. package/dist/index.cjs +1 -1
  20. package/dist/index.mjs +1 -1
  21. package/dist/primitives/BottomSheet/index.cjs +2 -2
  22. package/dist/primitives/BottomSheet/index.mjs +2 -2
  23. package/dist/primitives/InputNumberExperimental/index.cjs +2 -2
  24. package/dist/primitives/InputNumberExperimental/index.mjs +2 -2
  25. package/dist/primitives/InputNumberExperimental/types.d.ts +5 -0
  26. package/dist/primitives/index.cjs +2 -2
  27. package/dist/primitives/index.mjs +2 -2
  28. package/dist/styles.css +1 -2
  29. package/dist/unstyledPrimitives/index.cjs +6271 -6134
  30. package/dist/unstyledPrimitives/index.d.ts +1 -0
  31. package/dist/unstyledPrimitives/index.mjs +6318 -6182
  32. package/dist/useRenderElement-cjs.js +30 -30
  33. package/dist/useRenderElement-es.js +30 -30
  34. package/package.json +2 -2
@@ -1,36 +1,7 @@
1
1
  import * as React from 'react';
2
- import { a as isHTMLElement } from './floating-ui.utils.dom-es.js';
3
- import { a as useRefWithInit, f as formatErrorMessage, m as mergeProps, c as makeEventPreventable, E as EMPTY_OBJECT } from './useRenderElement-es.js';
2
+ import { a as useRefWithInit, E as EMPTY_OBJECT, f as formatErrorMessage, m as mergeProps, c as makeEventPreventable } from './useRenderElement-es.js';
4
3
  import * as ReactDOM from 'react-dom';
5
-
6
- let TransitionStatusDataAttributes = /*#__PURE__*/function (TransitionStatusDataAttributes) {
7
- /**
8
- * Present when the component is animating in.
9
- */
10
- TransitionStatusDataAttributes["startingStyle"] = "data-starting-style";
11
- /**
12
- * Present when the component is animating out.
13
- */
14
- TransitionStatusDataAttributes["endingStyle"] = "data-ending-style";
15
- return TransitionStatusDataAttributes;
16
- }({});
17
- const STARTING_HOOK = {
18
- [TransitionStatusDataAttributes.startingStyle]: ''
19
- };
20
- const ENDING_HOOK = {
21
- [TransitionStatusDataAttributes.endingStyle]: ''
22
- };
23
- const transitionStatusMapping = {
24
- transitionStatus(value) {
25
- if (value === 'starting') {
26
- return STARTING_HOOK;
27
- }
28
- if (value === 'ending') {
29
- return ENDING_HOOK;
30
- }
31
- return null;
32
- }
33
- };
4
+ import { a as isHTMLElement } from './floating-ui.utils.dom-es.js';
34
5
 
35
6
  // https://github.com/mui/material-ui/issues/41190#issuecomment-2040873379
36
7
  const useInsertionEffect = React[`useInsertionEffect${Math.random().toFixed(1)}`.slice(0, -3)];
@@ -72,392 +43,187 @@ function assertNotCalled() {
72
43
  }
73
44
  }
74
45
 
75
- let set;
76
- if (process.env.NODE_ENV !== 'production') {
77
- set = new Set();
78
- }
79
- function error(...messages) {
46
+ function useControlled({
47
+ controlled,
48
+ default: defaultProp,
49
+ name,
50
+ state = 'value'
51
+ }) {
52
+ // isControlled is ignored in the hook dependency lists as it should never change.
53
+ const {
54
+ current: isControlled
55
+ } = React.useRef(controlled !== undefined);
56
+ const [valueState, setValue] = React.useState(defaultProp);
57
+ const value = isControlled ? controlled : valueState;
80
58
  if (process.env.NODE_ENV !== 'production') {
81
- const messageKey = messages.join(' ');
82
- if (!set.has(messageKey)) {
83
- set.add(messageKey);
84
- console.error(`Base UI: ${messageKey}`);
85
- }
59
+ React.useEffect(() => {
60
+ if (isControlled !== (controlled !== undefined)) {
61
+ console.error([`Base UI: A component is changing the ${isControlled ? '' : 'un'}controlled ${state} state of ${name} to be ${isControlled ? 'un' : ''}controlled.`, 'Elements should not switch from uncontrolled to controlled (or vice versa).', `Decide between using a controlled or uncontrolled ${name} ` + 'element for the lifetime of the component.', "The nature of the state is determined during the first render. It's considered controlled if the value is not `undefined`.", 'More info: https://fb.me/react-controlled-components'].join('\n'));
62
+ }
63
+ }, [state, name, controlled]);
64
+ const {
65
+ current: defaultValue
66
+ } = React.useRef(defaultProp);
67
+ React.useEffect(() => {
68
+ // See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/is for more details.
69
+ if (!isControlled && JSON.stringify(defaultValue) !== JSON.stringify(defaultProp)) {
70
+ console.error([`Base UI: A component is changing the default ${state} state of an uncontrolled ${name} after being initialized. ` + `To suppress this warning opt to use a controlled ${name}.`].join('\n'));
71
+ }
72
+ }, [JSON.stringify(defaultProp)]);
86
73
  }
74
+ const setValueIfUncontrolled = React.useCallback(newValue => {
75
+ if (!isControlled) {
76
+ setValue(newValue);
77
+ }
78
+ }, []);
79
+ return [value, setValueIfUncontrolled];
87
80
  }
88
81
 
89
- // https://github.com/mui/material-ui/issues/41190#issuecomment-2040873379
90
- const SafeReact = {
91
- ...React
92
- };
93
-
94
82
  const noop = () => {};
95
83
  const useIsoLayoutEffect = typeof document !== 'undefined' ? React.useLayoutEffect : noop;
96
84
 
97
- const CompositeRootContext = /*#__PURE__*/React.createContext(undefined);
98
- if (process.env.NODE_ENV !== "production") CompositeRootContext.displayName = "CompositeRootContext";
99
- function useCompositeRootContext(optional = false) {
100
- const context = React.useContext(CompositeRootContext);
101
- if (context === undefined && !optional) {
102
- throw new Error(process.env.NODE_ENV !== "production" ? 'Base UI: CompositeRootContext is missing. Composite parts must be placed within <Composite.Root>.' : formatErrorMessage(16));
103
- }
104
- return context;
85
+ const visuallyHiddenBase = {
86
+ clipPath: 'inset(50%)',
87
+ overflow: 'hidden',
88
+ whiteSpace: 'nowrap',
89
+ border: 0,
90
+ padding: 0,
91
+ width: 1,
92
+ height: 1,
93
+ margin: -1
94
+ };
95
+ const visuallyHidden = {
96
+ ...visuallyHiddenBase,
97
+ position: 'fixed',
98
+ top: 0,
99
+ left: 0
100
+ };
101
+ const visuallyHiddenInput = {
102
+ ...visuallyHiddenBase,
103
+ position: 'absolute'
104
+ };
105
+
106
+ const EMPTY$2 = [];
107
+
108
+ /**
109
+ * A React.useEffect equivalent that runs once, when the component is mounted.
110
+ */
111
+ function useOnMount(fn) {
112
+ // TODO: uncomment once we enable eslint-plugin-react-compiler // eslint-disable-next-line react-compiler/react-compiler -- no need to put `fn` in the dependency array
113
+ /* eslint-disable react-hooks/exhaustive-deps */
114
+ React.useEffect(fn, EMPTY$2);
115
+ /* eslint-enable react-hooks/exhaustive-deps */
105
116
  }
106
117
 
107
- function useFocusableWhenDisabled(parameters) {
108
- const {
109
- focusableWhenDisabled,
110
- disabled,
111
- composite = false,
112
- tabIndex: tabIndexProp = 0,
113
- isNativeButton
114
- } = parameters;
115
- const isFocusableComposite = composite && focusableWhenDisabled !== false;
116
- const isNonFocusableComposite = composite && focusableWhenDisabled === false;
118
+ /** Unlike `setTimeout`, rAF doesn't guarantee a positive integer return value, so we can't have
119
+ * a monomorphic `uint` type with `0` meaning empty.
120
+ * See warning note at:
121
+ * https://developer.mozilla.org/en-US/docs/Web/API/Window/requestAnimationFrame#return_value */
122
+ const EMPTY$1 = null;
123
+ let LAST_RAF = globalThis.requestAnimationFrame;
124
+ class Scheduler {
125
+ /* This implementation uses an array as a backing data-structure for frame callbacks.
126
+ * It allows `O(1)` callback cancelling by inserting a `null` in the array, though it
127
+ * never calls the native `cancelAnimationFrame` if there are no frames left. This can
128
+ * be much more efficient if there is a call pattern that alterns as
129
+ * "request-cancel-request-cancel-…".
130
+ * But in the case of "request-request-…-cancel-cancel-…", it leaves the final animation
131
+ * frame to run anyway. We turn that frame into a `O(1)` no-op via `callbacksCount`. */
117
132
 
118
- // we can't explicitly assign `undefined` to any of these props because it
119
- // would otherwise prevent subsequently merged props from setting them
120
- const props = React.useMemo(() => {
121
- const additionalProps = {
122
- // allow Tabbing away from focusableWhenDisabled elements
123
- onKeyDown(event) {
124
- if (disabled && focusableWhenDisabled && event.key !== 'Tab') {
125
- event.preventDefault();
126
- }
127
- }
128
- };
129
- if (!composite) {
130
- additionalProps.tabIndex = tabIndexProp;
131
- if (!isNativeButton && disabled) {
132
- additionalProps.tabIndex = focusableWhenDisabled ? tabIndexProp : -1;
133
+ callbacks = [];
134
+ callbacksCount = 0;
135
+ nextId = 1;
136
+ startId = 1;
137
+ isScheduled = false;
138
+ tick = timestamp => {
139
+ this.isScheduled = false;
140
+ const currentCallbacks = this.callbacks;
141
+ const currentCallbacksCount = this.callbacksCount;
142
+
143
+ // Update these before iterating, callbacks could call `requestAnimationFrame` again.
144
+ this.callbacks = [];
145
+ this.callbacksCount = 0;
146
+ this.startId = this.nextId;
147
+ if (currentCallbacksCount > 0) {
148
+ for (let i = 0; i < currentCallbacks.length; i += 1) {
149
+ currentCallbacks[i]?.(timestamp);
133
150
  }
134
151
  }
135
- if (isNativeButton && (focusableWhenDisabled || isFocusableComposite) || !isNativeButton && disabled) {
136
- additionalProps['aria-disabled'] = disabled;
137
- }
138
- if (isNativeButton && (!focusableWhenDisabled || isNonFocusableComposite)) {
139
- additionalProps.disabled = disabled;
140
- }
141
- return additionalProps;
142
- }, [composite, disabled, focusableWhenDisabled, isFocusableComposite, isNonFocusableComposite, isNativeButton, tabIndexProp]);
143
- return {
144
- props
145
152
  };
146
- }
153
+ request(fn) {
154
+ const id = this.nextId;
155
+ this.nextId += 1;
156
+ this.callbacks.push(fn);
157
+ this.callbacksCount += 1;
147
158
 
148
- function useButton(parameters = {}) {
149
- const {
150
- disabled = false,
151
- focusableWhenDisabled,
152
- tabIndex = 0,
153
- native: isNativeButton = true,
154
- composite: compositeProp
155
- } = parameters;
156
- const elementRef = React.useRef(null);
157
- const compositeRootContext = useCompositeRootContext(true);
158
- const isCompositeItem = compositeProp ?? compositeRootContext !== undefined;
159
- const {
160
- props: focusableWhenDisabledProps
161
- } = useFocusableWhenDisabled({
162
- focusableWhenDisabled,
163
- disabled,
164
- composite: isCompositeItem,
165
- tabIndex,
166
- isNativeButton
167
- });
168
- if (process.env.NODE_ENV !== 'production') {
169
- // eslint-disable-next-line react-hooks/rules-of-hooks
170
- React.useEffect(() => {
171
- if (!elementRef.current) {
172
- return;
173
- }
174
- const isButtonTag = isButtonElement(elementRef.current);
175
- if (isNativeButton) {
176
- if (!isButtonTag) {
177
- const ownerStackMessage = SafeReact.captureOwnerStack?.() || '';
178
- const message = 'A component that acts as a button expected a native <button> because the ' + '`nativeButton` prop is true. Rendering a non-<button> removes native button ' + 'semantics, which can impact forms and accessibility. Use a real <button> in the ' + '`render` prop, or set `nativeButton` to `false`.';
179
- error(`${message}${ownerStackMessage}`);
180
- }
181
- } else if (isButtonTag) {
182
- const ownerStackMessage = SafeReact.captureOwnerStack?.() || '';
183
- const message = 'A component that acts as a button expected a non-<button> because the `nativeButton` ' + 'prop is false. Rendering a <button> keeps native behavior while Base UI applies ' + 'non-native attributes and handlers, which can add unintended extra attributes (such ' + 'as `role` or `aria-disabled`). Use a non-<button> in the `render` prop, or set ' + '`nativeButton` to `true`.';
184
- error(`${message}${ownerStackMessage}`);
185
- }
186
- }, [isNativeButton]);
159
+ /* In a test environment with fake timers, a fake `requestAnimationFrame` can be called
160
+ * but there's no guarantee that the animation frame will actually run before the fake
161
+ * timers are teared, which leaves `isScheduled` set, but won't run our `tick()`. */
162
+ const didRAFChange = process.env.NODE_ENV !== 'production' && LAST_RAF !== requestAnimationFrame && (LAST_RAF = requestAnimationFrame, true);
163
+ if (!this.isScheduled || didRAFChange) {
164
+ requestAnimationFrame(this.tick);
165
+ this.isScheduled = true;
166
+ }
167
+ return id;
187
168
  }
188
-
189
- // handles a disabled composite button rendering another button, e.g.
190
- // <Toolbar.Button disabled render={<Menu.Trigger />} />
191
- // the `disabled` prop needs to pass through 2 `useButton`s then finally
192
- // delete the `disabled` attribute from DOM
193
- const updateDisabled = React.useCallback(() => {
194
- const element = elementRef.current;
195
- if (!isButtonElement(element)) {
169
+ cancel(id) {
170
+ const index = id - this.startId;
171
+ if (index < 0 || index >= this.callbacks.length) {
196
172
  return;
197
173
  }
198
- if (isCompositeItem && disabled && focusableWhenDisabledProps.disabled === undefined && element.disabled) {
199
- element.disabled = false;
200
- }
201
- }, [disabled, focusableWhenDisabledProps.disabled, isCompositeItem]);
202
- useIsoLayoutEffect(updateDisabled, [updateDisabled]);
203
- const getButtonProps = React.useCallback((externalProps = {}) => {
204
- const {
205
- onClick: externalOnClick,
206
- onMouseDown: externalOnMouseDown,
207
- onKeyUp: externalOnKeyUp,
208
- onKeyDown: externalOnKeyDown,
209
- onPointerDown: externalOnPointerDown,
210
- ...otherExternalProps
211
- } = externalProps;
212
- const type = isNativeButton ? 'button' : undefined;
213
- return mergeProps({
214
- type,
215
- onClick(event) {
216
- if (disabled) {
217
- event.preventDefault();
218
- return;
219
- }
220
- externalOnClick?.(event);
221
- },
222
- onMouseDown(event) {
223
- if (!disabled) {
224
- externalOnMouseDown?.(event);
225
- }
226
- },
227
- onKeyDown(event) {
228
- if (disabled) {
229
- return;
230
- }
231
- makeEventPreventable(event);
232
- externalOnKeyDown?.(event);
233
- if (event.baseUIHandlerPrevented) {
234
- return;
235
- }
236
- const isCurrentTarget = event.target === event.currentTarget;
237
- const currentTarget = event.currentTarget;
238
- const isButton = isButtonElement(currentTarget);
239
- const isLink = !isNativeButton && isValidLinkElement(currentTarget);
240
- const shouldClick = isCurrentTarget && (isNativeButton ? isButton : !isLink);
241
- const isEnterKey = event.key === 'Enter';
242
- const isSpaceKey = event.key === ' ';
243
- const role = currentTarget.getAttribute('role');
244
- const isTextNavigationRole = role?.startsWith('menuitem') || role === 'option' || role === 'gridcell';
245
- if (isCurrentTarget && isCompositeItem && isSpaceKey) {
246
- if (event.defaultPrevented && isTextNavigationRole) {
247
- return;
248
- }
249
- event.preventDefault();
250
- if (isLink || isNativeButton && isButton) {
251
- currentTarget.click();
252
- event.preventBaseUIHandler();
253
- } else if (shouldClick) {
254
- externalOnClick?.(event);
255
- event.preventBaseUIHandler();
256
- }
257
- return;
258
- }
259
-
260
- // Keyboard accessibility for native and non-native elements.
261
- if (shouldClick) {
262
- if (!isNativeButton && (isSpaceKey || isEnterKey)) {
263
- event.preventDefault();
264
- }
265
- if (!isNativeButton && isEnterKey) {
266
- externalOnClick?.(event);
267
- }
268
- }
269
- },
270
- onKeyUp(event) {
271
- if (disabled) {
272
- return;
273
- }
274
-
275
- // calling preventDefault in keyUp on a <button> will not dispatch a click event if Space is pressed
276
- // https://codesandbox.io/p/sandbox/button-keyup-preventdefault-dn7f0
277
- makeEventPreventable(event);
278
- externalOnKeyUp?.(event);
279
- if (event.target === event.currentTarget && isNativeButton && isCompositeItem && isButtonElement(event.currentTarget) && event.key === ' ') {
280
- event.preventDefault();
281
- return;
282
- }
283
- if (event.baseUIHandlerPrevented) {
284
- return;
285
- }
286
-
287
- // Keyboard accessibility for non interactive elements
288
- if (event.target === event.currentTarget && !isNativeButton && !isCompositeItem && event.key === ' ') {
289
- externalOnClick?.(event);
290
- }
291
- },
292
- onPointerDown(event) {
293
- if (disabled) {
294
- event.preventDefault();
295
- return;
296
- }
297
- externalOnPointerDown?.(event);
298
- }
299
- }, !isNativeButton ? {
300
- role: 'button'
301
- } : undefined, focusableWhenDisabledProps, otherExternalProps);
302
- }, [disabled, focusableWhenDisabledProps, isCompositeItem, isNativeButton]);
303
- const buttonRef = useStableCallback(element => {
304
- elementRef.current = element;
305
- updateDisabled();
306
- });
307
- return {
308
- getButtonProps,
309
- buttonRef
310
- };
311
- }
312
- function isButtonElement(elem) {
313
- return isHTMLElement(elem) && elem.tagName === 'BUTTON';
314
- }
315
- function isValidLinkElement(elem) {
316
- return Boolean(elem?.tagName === 'A' && elem?.href);
174
+ this.callbacks[index] = null;
175
+ this.callbacksCount -= 1;
176
+ }
317
177
  }
178
+ const scheduler = new Scheduler();
179
+ class AnimationFrame {
180
+ static create() {
181
+ return new AnimationFrame();
182
+ }
183
+ static request(fn) {
184
+ return scheduler.request(fn);
185
+ }
186
+ static cancel(id) {
187
+ return scheduler.cancel(id);
188
+ }
189
+ currentId = EMPTY$1;
318
190
 
319
- const none = 'none';
320
- const triggerPress = 'trigger-press';
321
- const triggerHover = 'trigger-hover';
322
- const triggerFocus = 'trigger-focus';
323
- const outsidePress = 'outside-press';
324
- const itemPress = 'item-press';
325
- const closePress = 'close-press';
326
- const clearPress = 'clear-press';
327
- const chipRemovePress = 'chip-remove-press';
328
- const incrementPress = 'increment-press';
329
- const decrementPress = 'decrement-press';
330
- const inputChange = 'input-change';
331
- const inputClear = 'input-clear';
332
- const inputBlur = 'input-blur';
333
- const inputPaste = 'input-paste';
334
- const inputPress = 'input-press';
335
- const focusOut = 'focus-out';
336
- const escapeKey = 'escape-key';
337
- const closeWatcher = 'close-watcher';
338
- const listNavigation = 'list-navigation';
339
- const keyboard = 'keyboard';
340
- const scrub = 'scrub';
341
- const cancelOpen = 'cancel-open';
342
- const siblingOpen = 'sibling-open';
343
- const imperativeAction = 'imperative-action';
344
- const swipe = 'swipe';
345
-
346
- /**
347
- * Maps a change `reason` string to the corresponding native event type.
348
- */
349
-
350
- /**
351
- * Details of custom change events emitted by Base UI components.
352
- */
353
-
354
- /**
355
- * Details of custom generic events emitted by Base UI components.
356
- */
357
-
358
- /**
359
- * Creates a Base UI event details object with the given reason and utilities
360
- * for preventing Base UI's internal event handling.
361
- */
362
- function createChangeEventDetails(reason, event, trigger, customProperties) {
363
- let canceled = false;
364
- let allowPropagation = false;
365
- const custom = customProperties ?? EMPTY_OBJECT;
366
- const details = {
367
- reason,
368
- event: event ?? new Event('base-ui'),
369
- cancel() {
370
- canceled = true;
371
- },
372
- allowPropagation() {
373
- allowPropagation = true;
374
- },
375
- get isCanceled() {
376
- return canceled;
377
- },
378
- get isPropagationAllowed() {
379
- return allowPropagation;
380
- },
381
- trigger,
382
- ...custom
191
+ /**
192
+ * Executes `fn` after `delay`, clearing any previously scheduled call.
193
+ */
194
+ request(fn) {
195
+ this.cancel();
196
+ this.currentId = scheduler.request(() => {
197
+ this.currentId = EMPTY$1;
198
+ fn();
199
+ });
200
+ }
201
+ cancel = () => {
202
+ if (this.currentId !== EMPTY$1) {
203
+ scheduler.cancel(this.currentId);
204
+ this.currentId = EMPTY$1;
205
+ }
383
206
  };
384
- return details;
385
- }
386
- function createGenericEventDetails(reason, event, customProperties) {
387
- const custom = customProperties ?? EMPTY_OBJECT;
388
- const details = {
389
- reason,
390
- event: event ?? new Event('base-ui'),
391
- ...custom
207
+ disposeEffect = () => {
208
+ return this.cancel;
392
209
  };
393
- return details;
394
- }
395
-
396
- let globalId = 0;
397
-
398
- // TODO React 17: Remove `useGlobalId` once React 17 support is removed
399
- function useGlobalId(idOverride, prefix = 'mui') {
400
- const [defaultId, setDefaultId] = React.useState(idOverride);
401
- const id = idOverride || defaultId;
402
- React.useEffect(() => {
403
- if (defaultId == null) {
404
- // Fallback to this default id when possible.
405
- // Use the incrementing value for client-side rendering only.
406
- // We can't use it server-side.
407
- // If you want to use random values please consider the Birthday Problem: https://en.wikipedia.org/wiki/Birthday_problem
408
- globalId += 1;
409
- setDefaultId(`${prefix}-${globalId}`);
410
- }
411
- }, [defaultId, prefix]);
412
- return id;
413
- }
414
- const maybeReactUseId = SafeReact.useId;
415
-
416
- /**
417
- *
418
- * @example <div id={useId()} />
419
- * @param idOverride
420
- * @returns {string}
421
- */
422
- function useId(idOverride, prefix) {
423
- // React.useId() is only available from React 17.0.0.
424
- if (maybeReactUseId !== undefined) {
425
- const reactId = maybeReactUseId();
426
- return idOverride ?? (prefix ? `${prefix}-${reactId}` : reactId);
427
- }
428
-
429
- // TODO: uncomment once we enable eslint-plugin-react-compiler // eslint-disable-next-line react-compiler/react-compiler
430
- // eslint-disable-next-line react-hooks/rules-of-hooks -- `React.useId` is invariant at runtime.
431
- return useGlobalId(idOverride, prefix);
432
210
  }
433
211
 
434
212
  /**
435
- * Wraps `useId` and prefixes generated `id`s with `base-ui-`
436
- * @param {string | undefined} idOverride overrides the generated id when provided
437
- * @returns {string | undefined}
438
- */
439
- function useBaseUiId(idOverride) {
440
- return useId(idOverride, 'base-ui');
441
- }
442
-
443
- const EMPTY$2 = [];
444
-
445
- /**
446
- * A React.useEffect equivalent that runs once, when the component is mounted.
213
+ * A `requestAnimationFrame` with automatic cleanup and guard.
447
214
  */
448
- function useOnMount(fn) {
449
- // TODO: uncomment once we enable eslint-plugin-react-compiler // eslint-disable-next-line react-compiler/react-compiler -- no need to put `fn` in the dependency array
450
- /* eslint-disable react-hooks/exhaustive-deps */
451
- React.useEffect(fn, EMPTY$2);
452
- /* eslint-enable react-hooks/exhaustive-deps */
215
+ function useAnimationFrame() {
216
+ const timeout = useRefWithInit(AnimationFrame.create).current;
217
+ useOnMount(timeout.disposeEffect);
218
+ return timeout;
453
219
  }
454
220
 
455
- const EMPTY$1 = 0;
221
+ const EMPTY = 0;
456
222
  class Timeout {
457
223
  static create() {
458
224
  return new Timeout();
459
225
  }
460
- currentId = EMPTY$1;
226
+ currentId = EMPTY;
461
227
 
462
228
  /**
463
229
  * Executes `fn` after `delay`, clearing any previously scheduled call.
@@ -465,17 +231,17 @@ class Timeout {
465
231
  start(delay, fn) {
466
232
  this.clear();
467
233
  this.currentId = setTimeout(() => {
468
- this.currentId = EMPTY$1;
234
+ this.currentId = EMPTY;
469
235
  fn();
470
236
  }, delay); /* Node.js types are enabled in development */
471
237
  }
472
238
  isStarted() {
473
- return this.currentId !== EMPTY$1;
239
+ return this.currentId !== EMPTY;
474
240
  }
475
241
  clear = () => {
476
- if (this.currentId !== EMPTY$1) {
242
+ if (this.currentId !== EMPTY) {
477
243
  clearTimeout(this.currentId);
478
- this.currentId = EMPTY$1;
244
+ this.currentId = EMPTY;
479
245
  }
480
246
  };
481
247
  disposeEffect = () => {
@@ -492,6 +258,30 @@ function useTimeout() {
492
258
  return timeout;
493
259
  }
494
260
 
261
+ /**
262
+ * Untracks the provided value by turning it into a ref to remove its reactivity.
263
+ *
264
+ * Used to access the passed value inside `React.useEffect` without causing the effect to re-run when the value changes.
265
+ */
266
+ function useValueAsRef(value) {
267
+ const latest = useRefWithInit(createLatestRef, value).current;
268
+ latest.next = value;
269
+
270
+ // eslint-disable-next-line react-hooks/exhaustive-deps
271
+ useIsoLayoutEffect(latest.effect);
272
+ return latest;
273
+ }
274
+ function createLatestRef(value) {
275
+ const latest = {
276
+ current: value,
277
+ next: value,
278
+ effect: () => {
279
+ latest.current = latest.next;
280
+ }
281
+ };
282
+ return latest;
283
+ }
284
+
495
285
  const hasNavigator = typeof navigator !== 'undefined';
496
286
  const nav = getNavigatorData();
497
287
  const platform = getPlatform();
@@ -594,153 +384,126 @@ function ownerDocument(node) {
594
384
  return node?.ownerDocument || document;
595
385
  }
596
386
 
387
+ const none = 'none';
388
+ const triggerPress = 'trigger-press';
389
+ const triggerHover = 'trigger-hover';
390
+ const triggerFocus = 'trigger-focus';
391
+ const outsidePress = 'outside-press';
392
+ const itemPress = 'item-press';
393
+ const closePress = 'close-press';
394
+ const clearPress = 'clear-press';
395
+ const chipRemovePress = 'chip-remove-press';
396
+ const incrementPress = 'increment-press';
397
+ const decrementPress = 'decrement-press';
398
+ const inputChange = 'input-change';
399
+ const inputClear = 'input-clear';
400
+ const inputBlur = 'input-blur';
401
+ const inputPaste = 'input-paste';
402
+ const inputPress = 'input-press';
403
+ const focusOut = 'focus-out';
404
+ const escapeKey = 'escape-key';
405
+ const closeWatcher = 'close-watcher';
406
+ const listNavigation = 'list-navigation';
407
+ const keyboard = 'keyboard';
408
+ const pointer = 'pointer';
409
+ const scrub = 'scrub';
410
+ const cancelOpen = 'cancel-open';
411
+ const siblingOpen = 'sibling-open';
412
+ const imperativeAction = 'imperative-action';
413
+ const swipe = 'swipe';
414
+
597
415
  /**
598
- * Untracks the provided value by turning it into a ref to remove its reactivity.
599
- *
600
- * Used to access the passed value inside `React.useEffect` without causing the effect to re-run when the value changes.
416
+ * Maps a change `reason` string to the corresponding native event type.
601
417
  */
602
- function useValueAsRef(value) {
603
- const latest = useRefWithInit(createLatestRef, value).current;
604
- latest.next = value;
605
418
 
606
- // eslint-disable-next-line react-hooks/exhaustive-deps
607
- useIsoLayoutEffect(latest.effect);
608
- return latest;
609
- }
610
- function createLatestRef(value) {
611
- const latest = {
612
- current: value,
613
- next: value,
614
- effect: () => {
615
- latest.current = latest.next;
616
- }
617
- };
618
- return latest;
619
- }
620
-
621
- /** Unlike `setTimeout`, rAF doesn't guarantee a positive integer return value, so we can't have
622
- * a monomorphic `uint` type with `0` meaning empty.
623
- * See warning note at:
624
- * https://developer.mozilla.org/en-US/docs/Web/API/Window/requestAnimationFrame#return_value */
625
- const EMPTY = null;
626
- let LAST_RAF = globalThis.requestAnimationFrame;
627
- class Scheduler {
628
- /* This implementation uses an array as a backing data-structure for frame callbacks.
629
- * It allows `O(1)` callback cancelling by inserting a `null` in the array, though it
630
- * never calls the native `cancelAnimationFrame` if there are no frames left. This can
631
- * be much more efficient if there is a call pattern that alterns as
632
- * "request-cancel-request-cancel-…".
633
- * But in the case of "request-request-…-cancel-cancel-…", it leaves the final animation
634
- * frame to run anyway. We turn that frame into a `O(1)` no-op via `callbacksCount`. */
419
+ /**
420
+ * Details of custom change events emitted by Base UI components.
421
+ */
635
422
 
636
- callbacks = [];
637
- callbacksCount = 0;
638
- nextId = 1;
639
- startId = 1;
640
- isScheduled = false;
641
- tick = timestamp => {
642
- this.isScheduled = false;
643
- const currentCallbacks = this.callbacks;
644
- const currentCallbacksCount = this.callbacksCount;
423
+ /**
424
+ * Details of custom generic events emitted by Base UI components.
425
+ */
645
426
 
646
- // Update these before iterating, callbacks could call `requestAnimationFrame` again.
647
- this.callbacks = [];
648
- this.callbacksCount = 0;
649
- this.startId = this.nextId;
650
- if (currentCallbacksCount > 0) {
651
- for (let i = 0; i < currentCallbacks.length; i += 1) {
652
- currentCallbacks[i]?.(timestamp);
653
- }
654
- }
427
+ /**
428
+ * Creates a Base UI event details object with the given reason and utilities
429
+ * for preventing Base UI's internal event handling.
430
+ */
431
+ function createChangeEventDetails(reason, event, trigger, customProperties) {
432
+ let canceled = false;
433
+ let allowPropagation = false;
434
+ const custom = customProperties ?? EMPTY_OBJECT;
435
+ const details = {
436
+ reason,
437
+ event: event ?? new Event('base-ui'),
438
+ cancel() {
439
+ canceled = true;
440
+ },
441
+ allowPropagation() {
442
+ allowPropagation = true;
443
+ },
444
+ get isCanceled() {
445
+ return canceled;
446
+ },
447
+ get isPropagationAllowed() {
448
+ return allowPropagation;
449
+ },
450
+ trigger,
451
+ ...custom
655
452
  };
656
- request(fn) {
657
- const id = this.nextId;
658
- this.nextId += 1;
659
- this.callbacks.push(fn);
660
- this.callbacksCount += 1;
661
-
662
- /* In a test environment with fake timers, a fake `requestAnimationFrame` can be called
663
- * but there's no guarantee that the animation frame will actually run before the fake
664
- * timers are teared, which leaves `isScheduled` set, but won't run our `tick()`. */
665
- const didRAFChange = process.env.NODE_ENV !== 'production' && LAST_RAF !== requestAnimationFrame && (LAST_RAF = requestAnimationFrame, true);
666
- if (!this.isScheduled || didRAFChange) {
667
- requestAnimationFrame(this.tick);
668
- this.isScheduled = true;
669
- }
670
- return id;
671
- }
672
- cancel(id) {
673
- const index = id - this.startId;
674
- if (index < 0 || index >= this.callbacks.length) {
675
- return;
676
- }
677
- this.callbacks[index] = null;
678
- this.callbacksCount -= 1;
679
- }
453
+ return details;
454
+ }
455
+ function createGenericEventDetails(reason, event, customProperties) {
456
+ const custom = customProperties ?? EMPTY_OBJECT;
457
+ const details = {
458
+ reason,
459
+ event: event ?? new Event('base-ui'),
460
+ ...custom
461
+ };
462
+ return details;
680
463
  }
681
- const scheduler = new Scheduler();
682
- class AnimationFrame {
683
- static create() {
684
- return new AnimationFrame();
685
- }
686
- static request(fn) {
687
- return scheduler.request(fn);
688
- }
689
- static cancel(id) {
690
- return scheduler.cancel(id);
691
- }
692
- currentId = EMPTY;
693
464
 
694
- /**
695
- * Executes `fn` after `delay`, clearing any previously scheduled call.
696
- */
697
- request(fn) {
698
- this.cancel();
699
- this.currentId = scheduler.request(() => {
700
- this.currentId = EMPTY;
701
- fn();
702
- });
703
- }
704
- cancel = () => {
705
- if (this.currentId !== EMPTY) {
706
- scheduler.cancel(this.currentId);
707
- this.currentId = EMPTY;
465
+ // https://github.com/mui/material-ui/issues/41190#issuecomment-2040873379
466
+ const SafeReact = {
467
+ ...React
468
+ };
469
+
470
+ let globalId = 0;
471
+
472
+ // TODO React 17: Remove `useGlobalId` once React 17 support is removed
473
+ function useGlobalId(idOverride, prefix = 'mui') {
474
+ const [defaultId, setDefaultId] = React.useState(idOverride);
475
+ const id = idOverride || defaultId;
476
+ React.useEffect(() => {
477
+ if (defaultId == null) {
478
+ // Fallback to this default id when possible.
479
+ // Use the incrementing value for client-side rendering only.
480
+ // We can't use it server-side.
481
+ // If you want to use random values please consider the Birthday Problem: https://en.wikipedia.org/wiki/Birthday_problem
482
+ globalId += 1;
483
+ setDefaultId(`${prefix}-${globalId}`);
708
484
  }
709
- };
710
- disposeEffect = () => {
711
- return this.cancel;
712
- };
485
+ }, [defaultId, prefix]);
486
+ return id;
713
487
  }
488
+ const maybeReactUseId = SafeReact.useId;
714
489
 
715
490
  /**
716
- * A `requestAnimationFrame` with automatic cleanup and guard.
491
+ *
492
+ * @example <div id={useId()} />
493
+ * @param idOverride
494
+ * @returns {string}
717
495
  */
718
- function useAnimationFrame() {
719
- const timeout = useRefWithInit(AnimationFrame.create).current;
720
- useOnMount(timeout.disposeEffect);
721
- return timeout;
722
- }
496
+ function useId(idOverride, prefix) {
497
+ // React.useId() is only available from React 17.0.0.
498
+ if (maybeReactUseId !== undefined) {
499
+ const reactId = maybeReactUseId();
500
+ return idOverride ?? (prefix ? `${prefix}-${reactId}` : reactId);
501
+ }
723
502
 
724
- const visuallyHiddenBase = {
725
- clipPath: 'inset(50%)',
726
- overflow: 'hidden',
727
- whiteSpace: 'nowrap',
728
- border: 0,
729
- padding: 0,
730
- width: 1,
731
- height: 1,
732
- margin: -1
733
- };
734
- const visuallyHidden = {
735
- ...visuallyHiddenBase,
736
- position: 'fixed',
737
- top: 0,
738
- left: 0
739
- };
740
- const visuallyHiddenInput = {
741
- ...visuallyHiddenBase,
742
- position: 'absolute'
743
- };
503
+ // TODO: uncomment once we enable eslint-plugin-react-compiler // eslint-disable-next-line react-compiler/react-compiler
504
+ // eslint-disable-next-line react-hooks/rules-of-hooks -- `React.useId` is invariant at runtime.
505
+ return useGlobalId(idOverride, prefix);
506
+ }
744
507
 
745
508
  /**
746
509
  * If the provided argument is a ref object, returns its `current` value.
@@ -816,6 +579,35 @@ function useTransitionStatus(open, enableIdleState = false, deferEndingState = f
816
579
  }), [mounted, transitionStatus]);
817
580
  }
818
581
 
582
+ let TransitionStatusDataAttributes = /*#__PURE__*/function (TransitionStatusDataAttributes) {
583
+ /**
584
+ * Present when the component is animating in.
585
+ */
586
+ TransitionStatusDataAttributes["startingStyle"] = "data-starting-style";
587
+ /**
588
+ * Present when the component is animating out.
589
+ */
590
+ TransitionStatusDataAttributes["endingStyle"] = "data-ending-style";
591
+ return TransitionStatusDataAttributes;
592
+ }({});
593
+ const STARTING_HOOK = {
594
+ [TransitionStatusDataAttributes.startingStyle]: ''
595
+ };
596
+ const ENDING_HOOK = {
597
+ [TransitionStatusDataAttributes.endingStyle]: ''
598
+ };
599
+ const transitionStatusMapping = {
600
+ transitionStatus(value) {
601
+ if (value === 'starting') {
602
+ return STARTING_HOOK;
603
+ }
604
+ if (value === 'ending') {
605
+ return ENDING_HOOK;
606
+ }
607
+ return null;
608
+ }
609
+ };
610
+
819
611
  /**
820
612
  * Executes a function once all animations have finished on the provided element.
821
613
  * @param elementOrRef - The element to watch for animations.
@@ -892,91 +684,300 @@ function useAnimationsFinished(elementOrRef, waitForStartingStyleRemoved = false
892
684
  }
893
685
  });
894
686
  }
895
- if (waitForStartingStyleRemoved) {
896
- execWaitForStartingStyleRemoved();
897
- return;
687
+ if (waitForStartingStyleRemoved) {
688
+ execWaitForStartingStyleRemoved();
689
+ return;
690
+ }
691
+ frame.request(exec);
692
+ }
693
+ });
694
+ }
695
+
696
+ /**
697
+ * Calls the provided function when the CSS open/close animation or transition completes.
698
+ */
699
+ function useOpenChangeComplete(parameters) {
700
+ const {
701
+ enabled = true,
702
+ open,
703
+ ref,
704
+ onComplete: onCompleteParam
705
+ } = parameters;
706
+ const onComplete = useStableCallback(onCompleteParam);
707
+ const runOnceAnimationsFinish = useAnimationsFinished(ref, open, false);
708
+ React.useEffect(() => {
709
+ if (!enabled) {
710
+ return undefined;
711
+ }
712
+ const abortController = new AbortController();
713
+ runOnceAnimationsFinish(onComplete, abortController.signal);
714
+ return () => {
715
+ abortController.abort();
716
+ };
717
+ }, [enabled, open, onComplete, runOnceAnimationsFinish]);
718
+ }
719
+
720
+ /**
721
+ * Wraps `useId` and prefixes generated `id`s with `base-ui-`
722
+ * @param {string | undefined} idOverride overrides the generated id when provided
723
+ * @returns {string | undefined}
724
+ */
725
+ function useBaseUiId(idOverride) {
726
+ return useId(idOverride, 'base-ui');
727
+ }
728
+
729
+ function useValueChanged(value, onChange) {
730
+ const valueRef = React.useRef(value);
731
+ const onChangeCallback = useStableCallback(onChange);
732
+ useIsoLayoutEffect(() => {
733
+ if (valueRef.current === value) {
734
+ return;
735
+ }
736
+ onChangeCallback(valueRef.current);
737
+ }, [value, onChangeCallback]);
738
+ useIsoLayoutEffect(() => {
739
+ valueRef.current = value;
740
+ }, [value]);
741
+ }
742
+
743
+ let set;
744
+ if (process.env.NODE_ENV !== 'production') {
745
+ set = new Set();
746
+ }
747
+ function error(...messages) {
748
+ if (process.env.NODE_ENV !== 'production') {
749
+ const messageKey = messages.join(' ');
750
+ if (!set.has(messageKey)) {
751
+ set.add(messageKey);
752
+ console.error(`Base UI: ${messageKey}`);
753
+ }
754
+ }
755
+ }
756
+
757
+ const CompositeRootContext = /*#__PURE__*/React.createContext(undefined);
758
+ if (process.env.NODE_ENV !== "production") CompositeRootContext.displayName = "CompositeRootContext";
759
+ function useCompositeRootContext(optional = false) {
760
+ const context = React.useContext(CompositeRootContext);
761
+ if (context === undefined && !optional) {
762
+ throw new Error(process.env.NODE_ENV !== "production" ? 'Base UI: CompositeRootContext is missing. Composite parts must be placed within <Composite.Root>.' : formatErrorMessage(16));
763
+ }
764
+ return context;
765
+ }
766
+
767
+ function useFocusableWhenDisabled(parameters) {
768
+ const {
769
+ focusableWhenDisabled,
770
+ disabled,
771
+ composite = false,
772
+ tabIndex: tabIndexProp = 0,
773
+ isNativeButton
774
+ } = parameters;
775
+ const isFocusableComposite = composite && focusableWhenDisabled !== false;
776
+ const isNonFocusableComposite = composite && focusableWhenDisabled === false;
777
+
778
+ // we can't explicitly assign `undefined` to any of these props because it
779
+ // would otherwise prevent subsequently merged props from setting them
780
+ const props = React.useMemo(() => {
781
+ const additionalProps = {
782
+ // allow Tabbing away from focusableWhenDisabled elements
783
+ onKeyDown(event) {
784
+ if (disabled && focusableWhenDisabled && event.key !== 'Tab') {
785
+ event.preventDefault();
786
+ }
787
+ }
788
+ };
789
+ if (!composite) {
790
+ additionalProps.tabIndex = tabIndexProp;
791
+ if (!isNativeButton && disabled) {
792
+ additionalProps.tabIndex = focusableWhenDisabled ? tabIndexProp : -1;
898
793
  }
899
- frame.request(exec);
900
794
  }
901
- });
795
+ if (isNativeButton && (focusableWhenDisabled || isFocusableComposite) || !isNativeButton && disabled) {
796
+ additionalProps['aria-disabled'] = disabled;
797
+ }
798
+ if (isNativeButton && (!focusableWhenDisabled || isNonFocusableComposite)) {
799
+ additionalProps.disabled = disabled;
800
+ }
801
+ return additionalProps;
802
+ }, [composite, disabled, focusableWhenDisabled, isFocusableComposite, isNonFocusableComposite, isNativeButton, tabIndexProp]);
803
+ return {
804
+ props
805
+ };
902
806
  }
903
807
 
904
- /**
905
- * Calls the provided function when the CSS open/close animation or transition completes.
906
- */
907
- function useOpenChangeComplete(parameters) {
808
+ function useButton(parameters = {}) {
908
809
  const {
909
- enabled = true,
910
- open,
911
- ref,
912
- onComplete: onCompleteParam
810
+ disabled = false,
811
+ focusableWhenDisabled,
812
+ tabIndex = 0,
813
+ native: isNativeButton = true,
814
+ composite: compositeProp
913
815
  } = parameters;
914
- const onComplete = useStableCallback(onCompleteParam);
915
- const runOnceAnimationsFinish = useAnimationsFinished(ref, open, false);
916
- React.useEffect(() => {
917
- if (!enabled) {
918
- return undefined;
919
- }
920
- const abortController = new AbortController();
921
- runOnceAnimationsFinish(onComplete, abortController.signal);
922
- return () => {
923
- abortController.abort();
924
- };
925
- }, [enabled, open, onComplete, runOnceAnimationsFinish]);
926
- }
927
-
928
- function clamp(val, min = Number.MIN_SAFE_INTEGER, max = Number.MAX_SAFE_INTEGER) {
929
- return Math.max(min, Math.min(val, max));
930
- }
931
-
932
- function useControlled({
933
- controlled,
934
- default: defaultProp,
935
- name,
936
- state = 'value'
937
- }) {
938
- // isControlled is ignored in the hook dependency lists as it should never change.
816
+ const elementRef = React.useRef(null);
817
+ const compositeRootContext = useCompositeRootContext(true);
818
+ const isCompositeItem = compositeProp ?? compositeRootContext !== undefined;
939
819
  const {
940
- current: isControlled
941
- } = React.useRef(controlled !== undefined);
942
- const [valueState, setValue] = React.useState(defaultProp);
943
- const value = isControlled ? controlled : valueState;
820
+ props: focusableWhenDisabledProps
821
+ } = useFocusableWhenDisabled({
822
+ focusableWhenDisabled,
823
+ disabled,
824
+ composite: isCompositeItem,
825
+ tabIndex,
826
+ isNativeButton
827
+ });
944
828
  if (process.env.NODE_ENV !== 'production') {
829
+ // eslint-disable-next-line react-hooks/rules-of-hooks
945
830
  React.useEffect(() => {
946
- if (isControlled !== (controlled !== undefined)) {
947
- console.error([`Base UI: A component is changing the ${isControlled ? '' : 'un'}controlled ${state} state of ${name} to be ${isControlled ? 'un' : ''}controlled.`, 'Elements should not switch from uncontrolled to controlled (or vice versa).', `Decide between using a controlled or uncontrolled ${name} ` + 'element for the lifetime of the component.', "The nature of the state is determined during the first render. It's considered controlled if the value is not `undefined`.", 'More info: https://fb.me/react-controlled-components'].join('\n'));
831
+ if (!elementRef.current) {
832
+ return;
948
833
  }
949
- }, [state, name, controlled]);
950
- const {
951
- current: defaultValue
952
- } = React.useRef(defaultProp);
953
- React.useEffect(() => {
954
- // See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/is for more details.
955
- if (!isControlled && JSON.stringify(defaultValue) !== JSON.stringify(defaultProp)) {
956
- console.error([`Base UI: A component is changing the default ${state} state of an uncontrolled ${name} after being initialized. ` + `To suppress this warning opt to use a controlled ${name}.`].join('\n'));
834
+ const isButtonTag = isButtonElement(elementRef.current);
835
+ if (isNativeButton) {
836
+ if (!isButtonTag) {
837
+ const ownerStackMessage = SafeReact.captureOwnerStack?.() || '';
838
+ const message = 'A component that acts as a button expected a native <button> because the ' + '`nativeButton` prop is true. Rendering a non-<button> removes native button ' + 'semantics, which can impact forms and accessibility. Use a real <button> in the ' + '`render` prop, or set `nativeButton` to `false`.';
839
+ error(`${message}${ownerStackMessage}`);
840
+ }
841
+ } else if (isButtonTag) {
842
+ const ownerStackMessage = SafeReact.captureOwnerStack?.() || '';
843
+ const message = 'A component that acts as a button expected a non-<button> because the `nativeButton` ' + 'prop is false. Rendering a <button> keeps native behavior while Base UI applies ' + 'non-native attributes and handlers, which can add unintended extra attributes (such ' + 'as `role` or `aria-disabled`). Use a non-<button> in the `render` prop, or set ' + '`nativeButton` to `true`.';
844
+ error(`${message}${ownerStackMessage}`);
957
845
  }
958
- }, [JSON.stringify(defaultProp)]);
846
+ }, [isNativeButton]);
959
847
  }
960
- const setValueIfUncontrolled = React.useCallback(newValue => {
961
- if (!isControlled) {
962
- setValue(newValue);
963
- }
964
- }, []);
965
- return [value, setValueIfUncontrolled];
966
- }
967
848
 
968
- function useValueChanged(value, onChange) {
969
- const valueRef = React.useRef(value);
970
- const onChangeCallback = useStableCallback(onChange);
971
- useIsoLayoutEffect(() => {
972
- if (valueRef.current === value) {
849
+ // handles a disabled composite button rendering another button, e.g.
850
+ // <Toolbar.Button disabled render={<Menu.Trigger />} />
851
+ // the `disabled` prop needs to pass through 2 `useButton`s then finally
852
+ // delete the `disabled` attribute from DOM
853
+ const updateDisabled = React.useCallback(() => {
854
+ const element = elementRef.current;
855
+ if (!isButtonElement(element)) {
973
856
  return;
974
857
  }
975
- onChangeCallback(valueRef.current);
976
- }, [value, onChangeCallback]);
977
- useIsoLayoutEffect(() => {
978
- valueRef.current = value;
979
- }, [value]);
858
+ if (isCompositeItem && disabled && focusableWhenDisabledProps.disabled === undefined && element.disabled) {
859
+ element.disabled = false;
860
+ }
861
+ }, [disabled, focusableWhenDisabledProps.disabled, isCompositeItem]);
862
+ useIsoLayoutEffect(updateDisabled, [updateDisabled]);
863
+ const getButtonProps = React.useCallback((externalProps = {}) => {
864
+ const {
865
+ onClick: externalOnClick,
866
+ onMouseDown: externalOnMouseDown,
867
+ onKeyUp: externalOnKeyUp,
868
+ onKeyDown: externalOnKeyDown,
869
+ onPointerDown: externalOnPointerDown,
870
+ ...otherExternalProps
871
+ } = externalProps;
872
+ const type = isNativeButton ? 'button' : undefined;
873
+ return mergeProps({
874
+ type,
875
+ onClick(event) {
876
+ if (disabled) {
877
+ event.preventDefault();
878
+ return;
879
+ }
880
+ externalOnClick?.(event);
881
+ },
882
+ onMouseDown(event) {
883
+ if (!disabled) {
884
+ externalOnMouseDown?.(event);
885
+ }
886
+ },
887
+ onKeyDown(event) {
888
+ if (disabled) {
889
+ return;
890
+ }
891
+ makeEventPreventable(event);
892
+ externalOnKeyDown?.(event);
893
+ if (event.baseUIHandlerPrevented) {
894
+ return;
895
+ }
896
+ const isCurrentTarget = event.target === event.currentTarget;
897
+ const currentTarget = event.currentTarget;
898
+ const isButton = isButtonElement(currentTarget);
899
+ const isLink = !isNativeButton && isValidLinkElement(currentTarget);
900
+ const shouldClick = isCurrentTarget && (isNativeButton ? isButton : !isLink);
901
+ const isEnterKey = event.key === 'Enter';
902
+ const isSpaceKey = event.key === ' ';
903
+ const role = currentTarget.getAttribute('role');
904
+ const isTextNavigationRole = role?.startsWith('menuitem') || role === 'option' || role === 'gridcell';
905
+ if (isCurrentTarget && isCompositeItem && isSpaceKey) {
906
+ if (event.defaultPrevented && isTextNavigationRole) {
907
+ return;
908
+ }
909
+ event.preventDefault();
910
+ if (isLink || isNativeButton && isButton) {
911
+ currentTarget.click();
912
+ event.preventBaseUIHandler();
913
+ } else if (shouldClick) {
914
+ externalOnClick?.(event);
915
+ event.preventBaseUIHandler();
916
+ }
917
+ return;
918
+ }
919
+
920
+ // Keyboard accessibility for native and non-native elements.
921
+ if (shouldClick) {
922
+ if (!isNativeButton && (isSpaceKey || isEnterKey)) {
923
+ event.preventDefault();
924
+ }
925
+ if (!isNativeButton && isEnterKey) {
926
+ externalOnClick?.(event);
927
+ }
928
+ }
929
+ },
930
+ onKeyUp(event) {
931
+ if (disabled) {
932
+ return;
933
+ }
934
+
935
+ // calling preventDefault in keyUp on a <button> will not dispatch a click event if Space is pressed
936
+ // https://codesandbox.io/p/sandbox/button-keyup-preventdefault-dn7f0
937
+ makeEventPreventable(event);
938
+ externalOnKeyUp?.(event);
939
+ if (event.target === event.currentTarget && isNativeButton && isCompositeItem && isButtonElement(event.currentTarget) && event.key === ' ') {
940
+ event.preventDefault();
941
+ return;
942
+ }
943
+ if (event.baseUIHandlerPrevented) {
944
+ return;
945
+ }
946
+
947
+ // Keyboard accessibility for non interactive elements
948
+ if (event.target === event.currentTarget && !isNativeButton && !isCompositeItem && event.key === ' ') {
949
+ externalOnClick?.(event);
950
+ }
951
+ },
952
+ onPointerDown(event) {
953
+ if (disabled) {
954
+ event.preventDefault();
955
+ return;
956
+ }
957
+ externalOnPointerDown?.(event);
958
+ }
959
+ }, !isNativeButton ? {
960
+ role: 'button'
961
+ } : undefined, focusableWhenDisabledProps, otherExternalProps);
962
+ }, [disabled, focusableWhenDisabledProps, isCompositeItem, isNativeButton]);
963
+ const buttonRef = useStableCallback(element => {
964
+ elementRef.current = element;
965
+ updateDisabled();
966
+ });
967
+ return {
968
+ getButtonProps,
969
+ buttonRef
970
+ };
971
+ }
972
+ function isButtonElement(elem) {
973
+ return isHTMLElement(elem) && elem.tagName === 'BUTTON';
974
+ }
975
+ function isValidLinkElement(elem) {
976
+ return Boolean(elem?.tagName === 'A' && elem?.href);
977
+ }
978
+
979
+ function clamp(val, min = Number.MIN_SAFE_INTEGER, max = Number.MAX_SAFE_INTEGER) {
980
+ return Math.max(min, Math.min(val, max));
980
981
  }
981
982
 
982
- export { isWebKit as $, stopEvent as A, keyboard as B, closePress as C, isMouseLikePointerType as D, useId as E, triggerFocus as F, isMac as G, isSafari as H, triggerPress as I, escapeKey as J, triggerHover as K, listNavigation as L, focusOut as M, isVirtualClick as N, isVirtualPointerEvent as O, itemPress as P, outsidePress as Q, useAnimationsFinished as R, siblingOpen as S, Timeout as T, imperativeAction as U, useCompositeRootContext as V, cancelOpen as W, TransitionStatusDataAttributes as X, isJSDOM as Y, useAnimationFrame as Z, resolveRef as _, useStableCallback as a, isClickLikeEvent as a0, isReactEvent as a1, AnimationFrame as a2, isAndroid as a3, closeWatcher as a4, swipe as a5, inputPress as a6, isFirefox as a7, clearPress as a8, SafeReact as a9, error as aa, chipRemovePress as ab, scrub as ac, useTimeout as b, useTransitionStatus as c, useIsoLayoutEffect as d, useOpenChangeComplete as e, useOnMount as f, clamp as g, useControlled as h, useValueAsRef as i, isIOS as j, createChangeEventDetails as k, visuallyHidden as l, inputChange as m, none as n, inputClear as o, inputBlur as p, inputPaste as q, createGenericEventDetails as r, ownerDocument as s, transitionStatusMapping as t, useBaseUiId as u, visuallyHiddenInput as v, incrementPress as w, decrementPress as x, useButton as y, useValueChanged as z };
983
+ export { isClickLikeEvent as $, stopEvent as A, keyboard as B, isMouseLikePointerType as C, useId as D, triggerFocus as E, isMac as F, isSafari as G, triggerPress as H, escapeKey as I, triggerHover as J, listNavigation as K, focusOut as L, isVirtualClick as M, isVirtualPointerEvent as N, closePress as O, itemPress as P, outsidePress as Q, useAnimationsFinished as R, siblingOpen as S, Timeout as T, imperativeAction as U, useCompositeRootContext as V, cancelOpen as W, isJSDOM as X, useAnimationFrame as Y, resolveRef as Z, isWebKit as _, useStableCallback as a, isReactEvent as a0, TransitionStatusDataAttributes as a1, AnimationFrame as a2, isAndroid as a3, closeWatcher as a4, inputPress as a5, pointer as a6, isFirefox as a7, clearPress as a8, swipe as a9, SafeReact as aa, error as ab, chipRemovePress as ac, scrub as ad, useTimeout as b, useTransitionStatus as c, useIsoLayoutEffect as d, useOpenChangeComplete as e, useOnMount as f, clamp as g, useControlled as h, useValueAsRef as i, isIOS as j, createChangeEventDetails as k, visuallyHidden as l, inputChange as m, none as n, inputClear as o, inputBlur as p, inputPaste as q, createGenericEventDetails as r, ownerDocument as s, transitionStatusMapping as t, useBaseUiId as u, visuallyHiddenInput as v, incrementPress as w, decrementPress as x, useButton as y, useValueChanged as z };