@octanejs/aria 0.0.4 → 0.0.5

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 (93) hide show
  1. package/package.json +7 -5
  2. package/src/autocomplete/useAutocomplete.ts +656 -0
  3. package/src/collections/BaseCollection.ts +374 -0
  4. package/src/collections/CollectionBuilder.ts +343 -0
  5. package/src/collections/Document.ts +451 -0
  6. package/src/collections/Hidden.ts +84 -0
  7. package/src/collections/useCachedChildren.ts +103 -0
  8. package/src/components/Autocomplete.ts +124 -0
  9. package/src/components/Breadcrumbs.ts +170 -0
  10. package/src/components/Button.ts +209 -0
  11. package/src/components/Checkbox.ts +593 -0
  12. package/src/components/Collection.ts +301 -0
  13. package/src/components/ComboBox.ts +477 -0
  14. package/src/components/Dialog.ts +195 -0
  15. package/src/components/Disclosure.ts +322 -0
  16. package/src/components/DragAndDrop.ts +177 -0
  17. package/src/components/FieldError.ts +79 -0
  18. package/src/components/Form.ts +65 -0
  19. package/src/components/GridList.ts +1099 -0
  20. package/src/components/Group.ts +146 -0
  21. package/src/components/Header.ts +32 -0
  22. package/src/components/Heading.ts +45 -0
  23. package/src/components/Input.ts +139 -0
  24. package/src/components/Keyboard.ts +23 -0
  25. package/src/components/Label.ts +30 -0
  26. package/src/components/Link.ts +137 -0
  27. package/src/components/ListBox.ts +886 -0
  28. package/src/components/Menu.ts +685 -0
  29. package/src/components/Meter.ts +100 -0
  30. package/src/components/Modal.ts +352 -0
  31. package/src/components/NumberField.ts +221 -0
  32. package/src/components/OverlayArrow.ts +117 -0
  33. package/src/components/Popover.ts +385 -0
  34. package/src/components/ProgressBar.ts +112 -0
  35. package/src/components/RadioGroup.ts +542 -0
  36. package/src/components/SearchField.ts +208 -0
  37. package/src/components/Select.ts +505 -0
  38. package/src/components/SelectionIndicator.ts +52 -0
  39. package/src/components/Separator.ts +97 -0
  40. package/src/components/SharedElementTransition.ts +209 -0
  41. package/src/components/Slider.ts +472 -0
  42. package/src/components/Switch.ts +449 -0
  43. package/src/components/Tabs.ts +675 -0
  44. package/src/components/TagGroup.ts +443 -0
  45. package/src/components/Text.ts +27 -0
  46. package/src/components/TextArea.ts +92 -0
  47. package/src/components/TextField.ts +200 -0
  48. package/src/components/ToggleButton.ts +147 -0
  49. package/src/components/ToggleButtonGroup.ts +106 -0
  50. package/src/components/Toolbar.ts +74 -0
  51. package/src/components/Tooltip.ts +249 -0
  52. package/src/components/index.ts +302 -0
  53. package/src/components/useDragAndDrop.ts +164 -0
  54. package/src/components/utils.ts +521 -0
  55. package/src/intl/autocomplete/ar-AE.json +3 -0
  56. package/src/intl/autocomplete/bg-BG.json +3 -0
  57. package/src/intl/autocomplete/cs-CZ.json +3 -0
  58. package/src/intl/autocomplete/da-DK.json +3 -0
  59. package/src/intl/autocomplete/de-DE.json +3 -0
  60. package/src/intl/autocomplete/el-GR.json +3 -0
  61. package/src/intl/autocomplete/en-US.json +3 -0
  62. package/src/intl/autocomplete/es-ES.json +3 -0
  63. package/src/intl/autocomplete/et-EE.json +3 -0
  64. package/src/intl/autocomplete/fi-FI.json +3 -0
  65. package/src/intl/autocomplete/fr-FR.json +3 -0
  66. package/src/intl/autocomplete/he-IL.json +3 -0
  67. package/src/intl/autocomplete/hr-HR.json +3 -0
  68. package/src/intl/autocomplete/hu-HU.json +3 -0
  69. package/src/intl/autocomplete/index.ts +75 -0
  70. package/src/intl/autocomplete/it-IT.json +3 -0
  71. package/src/intl/autocomplete/ja-JP.json +3 -0
  72. package/src/intl/autocomplete/ko-KR.json +3 -0
  73. package/src/intl/autocomplete/lt-LT.json +3 -0
  74. package/src/intl/autocomplete/lv-LV.json +3 -0
  75. package/src/intl/autocomplete/nb-NO.json +3 -0
  76. package/src/intl/autocomplete/nl-NL.json +3 -0
  77. package/src/intl/autocomplete/pl-PL.json +3 -0
  78. package/src/intl/autocomplete/pt-BR.json +3 -0
  79. package/src/intl/autocomplete/pt-PT.json +3 -0
  80. package/src/intl/autocomplete/ro-RO.json +3 -0
  81. package/src/intl/autocomplete/ru-RU.json +3 -0
  82. package/src/intl/autocomplete/sk-SK.json +3 -0
  83. package/src/intl/autocomplete/sl-SI.json +3 -0
  84. package/src/intl/autocomplete/sr-SP.json +3 -0
  85. package/src/intl/autocomplete/sv-SE.json +3 -0
  86. package/src/intl/autocomplete/tr-TR.json +3 -0
  87. package/src/intl/autocomplete/uk-UA.json +3 -0
  88. package/src/intl/autocomplete/zh-CN.json +3 -0
  89. package/src/intl/autocomplete/zh-TW.json +3 -0
  90. package/src/stately/autocomplete/useAutocompleteState.ts +85 -0
  91. package/src/utils/animation.ts +149 -0
  92. package/src/utils/useLoadMoreSentinel.ts +78 -0
  93. package/src/utils/useViewportSize.ts +123 -0
@@ -0,0 +1,100 @@
1
+ // Ported from react-aria-components (source: .react-spectrum/packages/react-aria-components/src/Meter.tsx).
2
+ // octane adaptations: `.tsx` → `.ts`, JSX → `createElement`; NO forwardRef — the forwarded ref
3
+ // is `props.ref`, passed into `useContextProps` explicitly and re-applied as the merged object
4
+ // ref; the plain-`.ts` component uses the S()/subSlot component-slot convention; `clamp` comes
5
+ // from the binding's stately utils port.
6
+ import { createContext, createElement } from 'octane';
7
+
8
+ import { S, subSlot } from '../internal';
9
+ import { type AriaMeterProps, useMeter } from '../meter/useMeter';
10
+ import { clamp } from '../stately/utils/number';
11
+ import { filterDOMProps } from '../utils/filterDOMProps';
12
+ import { mergeProps } from '../utils/mergeProps';
13
+ import { LabelContext } from './Label';
14
+ import {
15
+ type ClassNameOrFunction,
16
+ type ContextValue,
17
+ dom,
18
+ type RenderProps,
19
+ type SlotProps,
20
+ useContextProps,
21
+ useRenderProps,
22
+ useSlot,
23
+ } from './utils';
24
+
25
+ // octane adaptation: structural bag (upstream's `GlobalDOMAttributes` drags React handler types).
26
+ type GlobalDOMAttributes = Record<string, any>;
27
+
28
+ export interface MeterProps
29
+ extends
30
+ Omit<AriaMeterProps, 'label'>,
31
+ RenderProps<MeterRenderProps>,
32
+ SlotProps,
33
+ GlobalDOMAttributes {
34
+ /**
35
+ * The CSS [className](https://developer.mozilla.org/en-US/docs/Web/API/Element/className) for the
36
+ * element. A function may be provided to compute the class based on component state.
37
+ *
38
+ * @default 'react-aria-Meter'
39
+ */
40
+ className?: ClassNameOrFunction<MeterRenderProps>;
41
+ }
42
+
43
+ export interface MeterRenderProps {
44
+ /**
45
+ * The value as a percentage between the minimum and maximum.
46
+ */
47
+ percentage: number;
48
+ /**
49
+ * A formatted version of the value.
50
+ *
51
+ * @selector [aria-valuetext]
52
+ */
53
+ valueText: string | undefined;
54
+ }
55
+
56
+ export const MeterContext = createContext<ContextValue<MeterProps, HTMLDivElement>>(null);
57
+
58
+ /**
59
+ * A meter represents a quantity within a known range, or a fractional value.
60
+ */
61
+ export function Meter(props: MeterProps): any {
62
+ const slot = S('Meter');
63
+ let ref: any;
64
+ [props, ref] = useContextProps(props, props.ref, MeterContext, subSlot(slot, 'ctx'));
65
+ let { value = 0, minValue = 0, maxValue = 100 } = props;
66
+ value = clamp(value, minValue, maxValue);
67
+
68
+ let [labelRef, label] = useSlot(
69
+ !props['aria-label'] && !props['aria-labelledby'],
70
+ subSlot(slot, 'labelSlot'),
71
+ );
72
+ let { meterProps, labelProps } = useMeter({ ...props, label }, subSlot(slot, 'meter'));
73
+
74
+ // Calculate the width of the progress bar as a percentage
75
+ let percentage = ((value - minValue) / (maxValue - minValue)) * 100;
76
+
77
+ let renderProps = useRenderProps(
78
+ {
79
+ ...props,
80
+ defaultClassName: 'react-aria-Meter',
81
+ values: {
82
+ percentage,
83
+ valueText: meterProps['aria-valuetext'],
84
+ },
85
+ },
86
+ subSlot(slot, 'render'),
87
+ );
88
+
89
+ let DOMProps = filterDOMProps(props, { global: true });
90
+
91
+ return createElement(dom.div, {
92
+ ...mergeProps(DOMProps, renderProps, meterProps),
93
+ ref,
94
+ slot: props.slot || undefined,
95
+ children: createElement(LabelContext.Provider, {
96
+ value: { ...labelProps, ref: labelRef, elementType: 'span' },
97
+ children: renderProps.children,
98
+ }),
99
+ });
100
+ }
@@ -0,0 +1,352 @@
1
+ // Ported from react-aria-components (source: .react-spectrum/packages/react-aria-components/src/Modal.tsx).
2
+ // octane adaptations: `.tsx` → `.ts`, JSX → `createElement` (multi-child elements use the
3
+ // positional-children form); NO forwardRef — the forwarded ref is `props.ref`, passed into
4
+ // `useContextProps` (ModalOverlay) or forwarded as ModalContent's `modalRef` (Modal); the
5
+ // plain-`.ts` components use the S()/subSlot component-slot convention; react-aria private
6
+ // imports (animation, isScrollable, useViewportSize) come from the binding's ported modules;
7
+ // the dev-only misuse console.warns keep their upstream NODE_ENV guards.
8
+ import type { RefObject as SharedRefObject } from '@react-types/shared';
9
+ import { createContext, createElement, useContext, useMemo, useRef } from 'octane';
10
+
11
+ import { type AriaModalOverlayProps, useModalOverlay } from '../overlays/useModalOverlay';
12
+ import { DismissButton } from '../overlays/DismissButton';
13
+ import { S, subSlot } from '../internal';
14
+ import { filterDOMProps } from '../utils/filterDOMProps';
15
+ import { isScrollable } from '../utils/isScrollable';
16
+ import { mergeProps } from '../utils/mergeProps';
17
+ import { mergeRefs } from '../utils/mergeRefs';
18
+ import { Overlay } from '../overlays/Overlay';
19
+ import {
20
+ type OverlayTriggerProps,
21
+ type OverlayTriggerState,
22
+ useOverlayTriggerState,
23
+ } from '../stately/overlays/useOverlayTriggerState';
24
+ import { OverlayTriggerStateContext } from './Dialog';
25
+ import { useEnterAnimation, useExitAnimation } from '../utils/animation';
26
+ import { useIsSSR } from '../ssr/SSRProvider';
27
+ import { useObjectRef } from '../utils/useObjectRef';
28
+ import { useViewportSize } from '../utils/useViewportSize';
29
+ import {
30
+ type ClassNameOrFunction,
31
+ type ContextValue,
32
+ dom,
33
+ Provider,
34
+ type RenderProps,
35
+ type SlotProps,
36
+ useContextProps,
37
+ useRenderProps,
38
+ } from './utils';
39
+
40
+ // octane adaptations: structural aliases for the React types upstream drags along.
41
+ type GlobalDOMAttributes = Record<string, any>;
42
+ type DOMAttributes = Record<string, any>;
43
+ type RefObject<T> = SharedRefObject<T>;
44
+ type ForwardedRef<T> = any;
45
+
46
+ export interface ModalOverlayProps
47
+ extends
48
+ AriaModalOverlayProps,
49
+ OverlayTriggerProps,
50
+ RenderProps<ModalRenderProps>,
51
+ SlotProps,
52
+ GlobalDOMAttributes {
53
+ /**
54
+ * The CSS [className](https://developer.mozilla.org/en-US/docs/Web/API/Element/className) for the
55
+ * element. A function may be provided to compute the class based on component state.
56
+ *
57
+ * @default 'react-aria-ModalOverlay'
58
+ */
59
+ className?: ClassNameOrFunction<ModalRenderProps>;
60
+ /**
61
+ * Whether the modal is currently performing an entry animation.
62
+ */
63
+ isEntering?: boolean;
64
+ /**
65
+ * Whether the modal is currently performing an exit animation.
66
+ */
67
+ isExiting?: boolean;
68
+ /**
69
+ * The container element in which the overlay portal will be placed. This may have unknown
70
+ * behavior depending on where it is portalled to.
71
+ *
72
+ * @deprecated - Use a parent UNSAFE_PortalProvider to set your portal container instead.
73
+ * @default document.body
74
+ */
75
+ UNSTABLE_portalContainer?: Element;
76
+ }
77
+
78
+ interface InternalModalContextValue {
79
+ modalProps: DOMAttributes;
80
+ modalRef: RefObject<HTMLDivElement | null>;
81
+ isExiting: boolean;
82
+ isDismissable?: boolean;
83
+ }
84
+
85
+ export const ModalContext = createContext<ContextValue<ModalOverlayProps, HTMLDivElement>>(null);
86
+ const InternalModalContext = createContext<InternalModalContextValue | null>(null);
87
+
88
+ export interface ModalRenderProps {
89
+ /**
90
+ * Whether the modal is currently entering. Use this to apply animations.
91
+ *
92
+ * @selector [data-entering]
93
+ */
94
+ isEntering: boolean;
95
+ /**
96
+ * Whether the modal is currently exiting. Use this to apply animations.
97
+ *
98
+ * @selector [data-exiting]
99
+ */
100
+ isExiting: boolean;
101
+ /**
102
+ * State of the modal.
103
+ */
104
+ state: OverlayTriggerState;
105
+ }
106
+
107
+ /**
108
+ * A modal is an overlay element which blocks interaction with elements outside it.
109
+ */
110
+ export function Modal(allProps: ModalOverlayProps): any {
111
+ let { ref, ...props } = allProps as ModalOverlayProps & { ref?: ForwardedRef<HTMLDivElement> };
112
+ let ctx = useContext(InternalModalContext);
113
+
114
+ if (ctx) {
115
+ if (
116
+ process.env.NODE_ENV !== 'production' &&
117
+ (props.onOpenChange || props.defaultOpen !== undefined || props.isOpen !== undefined)
118
+ ) {
119
+ // create a list of props that are passed in but not allowed when using an external ModalOverlay
120
+ const invalidSet = new Set([
121
+ 'isDismissable',
122
+ 'isKeyboardDismissDisabled',
123
+ 'isOpen',
124
+ 'defaultOpen',
125
+ 'onOpenChange',
126
+ 'isEntering',
127
+ 'isExiting',
128
+ 'UNSTABLE_portalContainer',
129
+ 'shouldCloseOnInteractOutside',
130
+ ]);
131
+ const invalidProps = Object.keys(props).filter((key) => invalidSet.has(key));
132
+ console.warn(
133
+ `This modal is already wrapped in a ModalOverlay, props [${invalidProps.join(', ')}] should be placed on the ModalOverlay instead.`,
134
+ );
135
+ }
136
+ return createElement(ModalContent, {
137
+ ...props,
138
+ modalRef: ref,
139
+ children: props.children,
140
+ });
141
+ }
142
+
143
+ let {
144
+ isDismissable,
145
+ isKeyboardDismissDisabled,
146
+ isOpen,
147
+ defaultOpen,
148
+ onOpenChange,
149
+ children,
150
+ isEntering,
151
+ isExiting,
152
+ UNSTABLE_portalContainer,
153
+ shouldCloseOnInteractOutside,
154
+ ...otherProps
155
+ } = props;
156
+
157
+ return createElement(ModalOverlay, {
158
+ isDismissable,
159
+ isKeyboardDismissDisabled,
160
+ isOpen,
161
+ defaultOpen,
162
+ onOpenChange,
163
+ isEntering,
164
+ isExiting,
165
+ UNSTABLE_portalContainer,
166
+ shouldCloseOnInteractOutside,
167
+ children: createElement(ModalContent, {
168
+ ...otherProps,
169
+ modalRef: ref,
170
+ children,
171
+ }),
172
+ });
173
+ }
174
+
175
+ interface ModalOverlayInnerProps extends ModalOverlayProps {
176
+ overlayRef: RefObject<HTMLDivElement | null>;
177
+ modalRef: RefObject<HTMLDivElement | null>;
178
+ state: OverlayTriggerState;
179
+ isExiting: boolean;
180
+ }
181
+
182
+ /**
183
+ * A ModalOverlay is a wrapper for a Modal which allows customizing the backdrop element.
184
+ */
185
+ export function ModalOverlay(props: ModalOverlayProps): any {
186
+ const slot = S('ModalOverlay');
187
+ let ref: any;
188
+ [props, ref] = useContextProps(props, (props as any).ref, ModalContext, subSlot(slot, 'ctx'));
189
+ let contextState = useContext(OverlayTriggerStateContext);
190
+ let localState = useOverlayTriggerState(props, subSlot(slot, 'state'));
191
+ let state =
192
+ props.isOpen != null || props.defaultOpen != null || !contextState ? localState : contextState;
193
+ if (state === contextState) {
194
+ if (
195
+ process.env.NODE_ENV !== 'production' &&
196
+ (props.onOpenChange || props.defaultOpen !== undefined || props.isOpen !== undefined)
197
+ ) {
198
+ console.warn(
199
+ 'This modals state is controlled by a trigger, place onOpenChange on the trigger instead.',
200
+ );
201
+ }
202
+ }
203
+
204
+ let objectRef = useObjectRef<HTMLDivElement>(ref, subSlot(slot, 'objectRef'));
205
+ let modalRef = useRef<HTMLDivElement | null>(null, subSlot(slot, 'modalRef'));
206
+ let isOverlayExiting = useExitAnimation(objectRef, state.isOpen, subSlot(slot, 'overlayExit'));
207
+ let isModalExiting = useExitAnimation(modalRef, state.isOpen, subSlot(slot, 'modalExit'));
208
+ let isExiting = isOverlayExiting || isModalExiting || props.isExiting || false;
209
+ let isSSR = useIsSSR(subSlot(slot, 'ssr'));
210
+
211
+ if ((!state.isOpen && !isExiting) || isSSR) {
212
+ return null;
213
+ }
214
+
215
+ return createElement(ModalOverlayInner, {
216
+ ...props,
217
+ state,
218
+ isExiting,
219
+ overlayRef: objectRef,
220
+ modalRef,
221
+ });
222
+ }
223
+
224
+ function ModalOverlayInner(allProps: ModalOverlayInnerProps): any {
225
+ const slot = S('ModalOverlayInner');
226
+ let { UNSTABLE_portalContainer, ...props } = allProps;
227
+ let modalRef = props.modalRef;
228
+ let { state } = props;
229
+ let { modalProps, underlayProps } = useModalOverlay(
230
+ props,
231
+ state,
232
+ modalRef,
233
+ subSlot(slot, 'modalOverlay'),
234
+ );
235
+
236
+ let entering =
237
+ useEnterAnimation(props.overlayRef, undefined, subSlot(slot, 'enter')) ||
238
+ props.isEntering ||
239
+ false;
240
+ let renderProps = useRenderProps(
241
+ {
242
+ ...props,
243
+ defaultClassName: 'react-aria-ModalOverlay',
244
+ values: {
245
+ isEntering: entering,
246
+ isExiting: props.isExiting,
247
+ state,
248
+ },
249
+ },
250
+ subSlot(slot, 'render'),
251
+ );
252
+
253
+ let viewport = useViewportSize(subSlot(slot, 'viewport'));
254
+ let pageWidth: number | undefined = undefined;
255
+ let pageHeight: number | undefined = undefined;
256
+ if (typeof document !== 'undefined') {
257
+ let scrollingElement = isScrollable(document.body)
258
+ ? document.body
259
+ : document.scrollingElement || document.documentElement;
260
+ // Prevent Firefox from adding scrollbars when the page has a fractional width/height.
261
+ let fractionalWidthDifference = scrollingElement.getBoundingClientRect().width % 1;
262
+ let fractionalHeightDifference = scrollingElement.getBoundingClientRect().height % 1;
263
+ pageWidth = scrollingElement.scrollWidth - fractionalWidthDifference;
264
+ pageHeight = scrollingElement.scrollHeight - fractionalHeightDifference;
265
+ }
266
+
267
+ let style = {
268
+ ...renderProps.style,
269
+ '--visual-viewport-width': viewport.width + 'px',
270
+ '--visual-viewport-height': viewport.height + 'px',
271
+ '--page-width': pageWidth !== undefined ? pageWidth + 'px' : undefined,
272
+ '--page-height': pageHeight !== undefined ? pageHeight + 'px' : undefined,
273
+ };
274
+
275
+ return createElement(Overlay, {
276
+ isExiting: props.isExiting,
277
+ portalContainer: UNSTABLE_portalContainer,
278
+ children: createElement(dom.div, {
279
+ ...mergeProps(filterDOMProps(props, { global: true }), underlayProps),
280
+ ...renderProps,
281
+ style,
282
+ ref: props.overlayRef,
283
+ 'data-entering': entering || undefined,
284
+ 'data-exiting': props.isExiting || undefined,
285
+ children: createElement(Provider, {
286
+ values: [
287
+ [
288
+ InternalModalContext,
289
+ {
290
+ modalProps,
291
+ modalRef,
292
+ isExiting: props.isExiting,
293
+ isDismissable: props.isDismissable,
294
+ },
295
+ ],
296
+ [OverlayTriggerStateContext, state],
297
+ ] as any,
298
+ children: renderProps.children,
299
+ }),
300
+ }),
301
+ });
302
+ }
303
+
304
+ interface ModalContentProps extends RenderProps<ModalRenderProps>, GlobalDOMAttributes {
305
+ /**
306
+ * The CSS [className](https://developer.mozilla.org/en-US/docs/Web/API/Element/className) for the
307
+ * element. A function may be provided to compute the class based on component state.
308
+ *
309
+ * @default 'react-aria-ModalContent'
310
+ */
311
+ className?: ClassNameOrFunction<ModalRenderProps>;
312
+ modalRef: ForwardedRef<HTMLDivElement>;
313
+ }
314
+
315
+ function ModalContent(props: ModalContentProps): any {
316
+ const slot = S('ModalContent');
317
+ let { modalProps, modalRef, isExiting, isDismissable } = useContext(InternalModalContext)!;
318
+ let state = useContext(OverlayTriggerStateContext)!;
319
+ let mergedRefs = useMemo(
320
+ () => mergeRefs(props.modalRef, modalRef),
321
+ [props.modalRef, modalRef],
322
+ subSlot(slot, 'mergedRefs'),
323
+ );
324
+
325
+ let ref = useObjectRef(mergedRefs, subSlot(slot, 'ref'));
326
+ let entering = useEnterAnimation(ref, undefined, subSlot(slot, 'enter'));
327
+ let renderProps = useRenderProps(
328
+ {
329
+ ...props,
330
+ defaultClassName: 'react-aria-Modal',
331
+ values: {
332
+ isEntering: entering,
333
+ isExiting,
334
+ state,
335
+ },
336
+ },
337
+ subSlot(slot, 'render'),
338
+ );
339
+
340
+ return createElement(
341
+ dom.div,
342
+ {
343
+ ...mergeProps(filterDOMProps(props, { global: true }), modalProps),
344
+ ...renderProps,
345
+ ref,
346
+ 'data-entering': entering || undefined,
347
+ 'data-exiting': isExiting || undefined,
348
+ },
349
+ isDismissable ? createElement(DismissButton, { onDismiss: state.close }) : null,
350
+ renderProps.children,
351
+ );
352
+ }
@@ -0,0 +1,221 @@
1
+ // Ported from react-aria-components (source: .react-spectrum/packages/react-aria-components/src/NumberField.tsx).
2
+ // octane adaptations: `.tsx` → `.ts`, JSX → `createElement`; NO forwardRef — the forwarded ref
3
+ // is `props.ref`, passed into `useContextProps` explicitly and re-applied as the merged object
4
+ // ref; the plain-`.ts` component uses the S()/subSlot component-slot convention; the
5
+ // Provider's two children (group div + optional hidden form input) keep a stable two-item
6
+ // array shape (null in the hidden-input position when `name` is absent). NATIVE EVENTS: the
7
+ // per-keystroke wiring rides octane's native `onInput` (produced inside useNumberField); the
8
+ // stepper buttons ride incrementButtonProps/decrementButtonProps through ButtonContext slots
9
+ // exactly like upstream. Upstream's `GlobalDOMAttributes` → a structural record.
10
+ import type { InputDOMProps } from '@react-types/shared';
11
+ import { createContext, createElement, useRef } from 'octane';
12
+
13
+ import { useLocale } from '../i18n/I18nProvider';
14
+ import { S, subSlot } from '../internal';
15
+ import { type AriaNumberFieldProps, useNumberField } from '../numberfield/useNumberField';
16
+ import {
17
+ type NumberFieldState,
18
+ useNumberFieldState,
19
+ } from '../stately/numberfield/useNumberFieldState';
20
+ import { filterDOMProps } from '../utils/filterDOMProps';
21
+ import { ButtonContext } from './Button';
22
+ import { FieldErrorContext } from './FieldError';
23
+ import { FormContext } from './Form';
24
+ import { GroupContext } from './Group';
25
+ import { InputContext } from './Input';
26
+ import { LabelContext } from './Label';
27
+ import { TextContext } from './Text';
28
+ import {
29
+ type ClassNameOrFunction,
30
+ type ContextValue,
31
+ dom,
32
+ Provider,
33
+ type RACValidation,
34
+ removeDataAttributes,
35
+ type RenderProps,
36
+ type SlotProps,
37
+ useContextProps,
38
+ useRenderProps,
39
+ useSlot,
40
+ useSlottedContext,
41
+ } from './utils';
42
+
43
+ // octane adaptation: structural bag (upstream's `GlobalDOMAttributes` drags React handler types).
44
+ type GlobalDOMAttributes = Record<string, any>;
45
+
46
+ export interface NumberFieldRenderProps {
47
+ /**
48
+ * Whether the number field is disabled.
49
+ *
50
+ * @selector [data-disabled]
51
+ */
52
+ isDisabled: boolean;
53
+ /**
54
+ * Whether the number field is invalid.
55
+ *
56
+ * @selector [data-invalid]
57
+ */
58
+ isInvalid: boolean;
59
+ /**
60
+ * Whether the number field is read only.
61
+ *
62
+ * @selector [data-readonly]
63
+ */
64
+ isReadOnly: boolean;
65
+ /**
66
+ * Whether the number field is required.
67
+ *
68
+ * @selector [data-required]
69
+ */
70
+ isRequired: boolean;
71
+ /**
72
+ * State of the number field.
73
+ */
74
+ state: NumberFieldState;
75
+ }
76
+
77
+ export interface NumberFieldProps
78
+ extends
79
+ Omit<
80
+ AriaNumberFieldProps,
81
+ | 'label'
82
+ | 'placeholder'
83
+ | 'description'
84
+ | 'errorMessage'
85
+ | 'validationState'
86
+ | 'validationBehavior'
87
+ >,
88
+ RACValidation,
89
+ InputDOMProps,
90
+ RenderProps<NumberFieldRenderProps>,
91
+ SlotProps,
92
+ GlobalDOMAttributes {
93
+ /**
94
+ * The CSS [className](https://developer.mozilla.org/en-US/docs/Web/API/Element/className) for the
95
+ * element. A function may be provided to compute the class based on component state.
96
+ *
97
+ * @default 'react-aria-NumberField'
98
+ */
99
+ className?: ClassNameOrFunction<NumberFieldRenderProps>;
100
+ }
101
+
102
+ export const NumberFieldContext =
103
+ createContext<ContextValue<NumberFieldProps, HTMLDivElement>>(null);
104
+ export const NumberFieldStateContext = createContext<NumberFieldState | null>(null);
105
+
106
+ /**
107
+ * A number field allows a user to enter a number, and increment or decrement the value using
108
+ * stepper buttons.
109
+ */
110
+ export function NumberField(props: NumberFieldProps): any {
111
+ const slot = S('NumberField');
112
+ let ref: any;
113
+ [props, ref] = useContextProps(props, props.ref, NumberFieldContext, subSlot(slot, 'ctx'));
114
+ let { validationBehavior: formValidationBehavior } = useSlottedContext(FormContext) || {};
115
+ let validationBehavior = props.validationBehavior ?? formValidationBehavior ?? 'native';
116
+ let { locale } = useLocale(subSlot(slot, 'locale'));
117
+ let state = useNumberFieldState(
118
+ {
119
+ ...props,
120
+ locale,
121
+ validationBehavior,
122
+ },
123
+ subSlot(slot, 'state'),
124
+ );
125
+
126
+ let inputRef = useRef<HTMLInputElement | null>(null, subSlot(slot, 'input'));
127
+ let [labelRef, label] = useSlot(
128
+ !props['aria-label'] && !props['aria-labelledby'],
129
+ subSlot(slot, 'labelSlot'),
130
+ );
131
+ let {
132
+ labelProps,
133
+ groupProps,
134
+ inputProps,
135
+ incrementButtonProps,
136
+ decrementButtonProps,
137
+ descriptionProps,
138
+ errorMessageProps,
139
+ ...validation
140
+ } = useNumberField(
141
+ {
142
+ ...removeDataAttributes(props),
143
+ label,
144
+ validationBehavior,
145
+ },
146
+ state,
147
+ inputRef,
148
+ subSlot(slot, 'numberField'),
149
+ );
150
+
151
+ let renderProps = useRenderProps(
152
+ {
153
+ ...props,
154
+ values: {
155
+ state,
156
+ isDisabled: props.isDisabled || false,
157
+ isInvalid: validation.isInvalid || false,
158
+ isRequired: props.isRequired || false,
159
+ isReadOnly: props.isReadOnly || false,
160
+ },
161
+ defaultClassName: 'react-aria-NumberField',
162
+ },
163
+ subSlot(slot, 'render'),
164
+ );
165
+
166
+ let DOMProps = filterDOMProps(props, { global: true });
167
+ delete DOMProps.id;
168
+
169
+ return createElement(Provider, {
170
+ values: [
171
+ [NumberFieldStateContext, state],
172
+ [GroupContext, groupProps],
173
+ [InputContext, { ...inputProps, ref: inputRef }],
174
+ [LabelContext, { ...labelProps, ref: labelRef }],
175
+ [
176
+ ButtonContext,
177
+ {
178
+ slots: {
179
+ increment: incrementButtonProps,
180
+ decrement: decrementButtonProps,
181
+ },
182
+ },
183
+ ],
184
+ [
185
+ TextContext,
186
+ {
187
+ slots: {
188
+ description: descriptionProps,
189
+ errorMessage: errorMessageProps,
190
+ },
191
+ },
192
+ ],
193
+ [FieldErrorContext, validation],
194
+ ] as any,
195
+ // octane adaptation: keys keep the two-item child array (group div + optional
196
+ // hidden form input) reconciling stably (upstream renders a JSX fragment).
197
+ children: [
198
+ createElement(dom.div, {
199
+ key: 'numberfield',
200
+ ...DOMProps,
201
+ ...renderProps,
202
+ ref,
203
+ slot: props.slot || undefined,
204
+ 'data-disabled': props.isDisabled || undefined,
205
+ 'data-readonly': props.isReadOnly || undefined,
206
+ 'data-required': props.isRequired || undefined,
207
+ 'data-invalid': validation.isInvalid || undefined,
208
+ }),
209
+ props.name
210
+ ? createElement('input', {
211
+ key: 'hidden-input',
212
+ type: 'hidden',
213
+ name: props.name,
214
+ form: props.form,
215
+ value: isNaN(state.numberValue) ? '' : state.numberValue,
216
+ disabled: props.isDisabled || undefined,
217
+ })
218
+ : null,
219
+ ],
220
+ });
221
+ }