@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.
- package/package.json +7 -5
- package/src/autocomplete/useAutocomplete.ts +656 -0
- package/src/collections/BaseCollection.ts +374 -0
- package/src/collections/CollectionBuilder.ts +343 -0
- package/src/collections/Document.ts +451 -0
- package/src/collections/Hidden.ts +84 -0
- package/src/collections/useCachedChildren.ts +103 -0
- package/src/components/Autocomplete.ts +124 -0
- package/src/components/Breadcrumbs.ts +170 -0
- package/src/components/Button.ts +209 -0
- package/src/components/Checkbox.ts +593 -0
- package/src/components/Collection.ts +301 -0
- package/src/components/ComboBox.ts +477 -0
- package/src/components/Dialog.ts +195 -0
- package/src/components/Disclosure.ts +322 -0
- package/src/components/DragAndDrop.ts +177 -0
- package/src/components/FieldError.ts +79 -0
- package/src/components/Form.ts +65 -0
- package/src/components/GridList.ts +1099 -0
- package/src/components/Group.ts +146 -0
- package/src/components/Header.ts +32 -0
- package/src/components/Heading.ts +45 -0
- package/src/components/Input.ts +139 -0
- package/src/components/Keyboard.ts +23 -0
- package/src/components/Label.ts +30 -0
- package/src/components/Link.ts +137 -0
- package/src/components/ListBox.ts +886 -0
- package/src/components/Menu.ts +685 -0
- package/src/components/Meter.ts +100 -0
- package/src/components/Modal.ts +352 -0
- package/src/components/NumberField.ts +221 -0
- package/src/components/OverlayArrow.ts +117 -0
- package/src/components/Popover.ts +385 -0
- package/src/components/ProgressBar.ts +112 -0
- package/src/components/RadioGroup.ts +542 -0
- package/src/components/SearchField.ts +208 -0
- package/src/components/Select.ts +505 -0
- package/src/components/SelectionIndicator.ts +52 -0
- package/src/components/Separator.ts +97 -0
- package/src/components/SharedElementTransition.ts +209 -0
- package/src/components/Slider.ts +472 -0
- package/src/components/Switch.ts +449 -0
- package/src/components/Tabs.ts +675 -0
- package/src/components/TagGroup.ts +443 -0
- package/src/components/Text.ts +27 -0
- package/src/components/TextArea.ts +92 -0
- package/src/components/TextField.ts +200 -0
- package/src/components/ToggleButton.ts +147 -0
- package/src/components/ToggleButtonGroup.ts +106 -0
- package/src/components/Toolbar.ts +74 -0
- package/src/components/Tooltip.ts +249 -0
- package/src/components/index.ts +302 -0
- package/src/components/useDragAndDrop.ts +164 -0
- package/src/components/utils.ts +521 -0
- package/src/intl/autocomplete/ar-AE.json +3 -0
- package/src/intl/autocomplete/bg-BG.json +3 -0
- package/src/intl/autocomplete/cs-CZ.json +3 -0
- package/src/intl/autocomplete/da-DK.json +3 -0
- package/src/intl/autocomplete/de-DE.json +3 -0
- package/src/intl/autocomplete/el-GR.json +3 -0
- package/src/intl/autocomplete/en-US.json +3 -0
- package/src/intl/autocomplete/es-ES.json +3 -0
- package/src/intl/autocomplete/et-EE.json +3 -0
- package/src/intl/autocomplete/fi-FI.json +3 -0
- package/src/intl/autocomplete/fr-FR.json +3 -0
- package/src/intl/autocomplete/he-IL.json +3 -0
- package/src/intl/autocomplete/hr-HR.json +3 -0
- package/src/intl/autocomplete/hu-HU.json +3 -0
- package/src/intl/autocomplete/index.ts +75 -0
- package/src/intl/autocomplete/it-IT.json +3 -0
- package/src/intl/autocomplete/ja-JP.json +3 -0
- package/src/intl/autocomplete/ko-KR.json +3 -0
- package/src/intl/autocomplete/lt-LT.json +3 -0
- package/src/intl/autocomplete/lv-LV.json +3 -0
- package/src/intl/autocomplete/nb-NO.json +3 -0
- package/src/intl/autocomplete/nl-NL.json +3 -0
- package/src/intl/autocomplete/pl-PL.json +3 -0
- package/src/intl/autocomplete/pt-BR.json +3 -0
- package/src/intl/autocomplete/pt-PT.json +3 -0
- package/src/intl/autocomplete/ro-RO.json +3 -0
- package/src/intl/autocomplete/ru-RU.json +3 -0
- package/src/intl/autocomplete/sk-SK.json +3 -0
- package/src/intl/autocomplete/sl-SI.json +3 -0
- package/src/intl/autocomplete/sr-SP.json +3 -0
- package/src/intl/autocomplete/sv-SE.json +3 -0
- package/src/intl/autocomplete/tr-TR.json +3 -0
- package/src/intl/autocomplete/uk-UA.json +3 -0
- package/src/intl/autocomplete/zh-CN.json +3 -0
- package/src/intl/autocomplete/zh-TW.json +3 -0
- package/src/stately/autocomplete/useAutocompleteState.ts +85 -0
- package/src/utils/animation.ts +149 -0
- package/src/utils/useLoadMoreSentinel.ts +78 -0
- package/src/utils/useViewportSize.ts +123 -0
|
@@ -0,0 +1,477 @@
|
|
|
1
|
+
// Ported from react-aria-components (source: .react-spectrum/packages/react-aria-components/src/ComboBox.tsx).
|
|
2
|
+
// octane adaptations: `.tsx` → `.ts`, JSX → `createElement`; NO forwardRef —
|
|
3
|
+
// `createHideableComponent` forwards `props.ref` positionally exactly like TextField; the
|
|
4
|
+
// plain-`.ts` components use the S()/subSlot component-slot convention. The collection
|
|
5
|
+
// composes the Phase-4 engine (`CollectionBuilder`); the open listbox reuses ./ListBox by
|
|
6
|
+
// providing a ListState via `ListStateContext` (ListBox short-circuits into ListBoxInner
|
|
7
|
+
// without rebuilding state). react-aria/react-stately private imports come from the
|
|
8
|
+
// binding's ported modules (`../combobox/useComboBox`, `../stately/combobox/
|
|
9
|
+
// useComboBoxState`). NATIVE EVENTS: the per-keystroke wiring rides octane's native
|
|
10
|
+
// `onInput` (produced inside the ported useComboBox → useTextField); no synthetic
|
|
11
|
+
// `onChange` is added here. Hooks are hoisted out of argument object literals per the
|
|
12
|
+
// binding convention; explicit dependency arrays are preserved verbatim.
|
|
13
|
+
import type { Collection as ICollection, Key, Node } from '@react-types/shared';
|
|
14
|
+
import {
|
|
15
|
+
createContext,
|
|
16
|
+
createElement,
|
|
17
|
+
useCallback,
|
|
18
|
+
useContext,
|
|
19
|
+
useMemo,
|
|
20
|
+
useRef,
|
|
21
|
+
useState,
|
|
22
|
+
} from 'octane';
|
|
23
|
+
|
|
24
|
+
import { CollectionBuilder } from '../collections/CollectionBuilder';
|
|
25
|
+
import { createHideableComponent } from '../collections/Hidden';
|
|
26
|
+
import { type AriaComboBoxProps, useComboBox } from '../combobox/useComboBox';
|
|
27
|
+
import { useFilter } from '../i18n/useFilter';
|
|
28
|
+
import { useListFormatter } from '../i18n/useListFormatter';
|
|
29
|
+
import { S, subSlot } from '../internal';
|
|
30
|
+
import { type ComboBoxState, useComboBoxState } from '../stately/combobox/useComboBoxState';
|
|
31
|
+
import { filterDOMProps } from '../utils/filterDOMProps';
|
|
32
|
+
import { useResizeObserver } from '../utils/useResizeObserver';
|
|
33
|
+
import { ButtonContext } from './Button';
|
|
34
|
+
import { OverlayTriggerStateContext } from './Dialog';
|
|
35
|
+
import { FieldErrorContext } from './FieldError';
|
|
36
|
+
import { FormContext } from './Form';
|
|
37
|
+
import { GroupContext } from './Group';
|
|
38
|
+
import { InputContext } from './Input';
|
|
39
|
+
import { LabelContext } from './Label';
|
|
40
|
+
import { ListBoxContext, ListStateContext } from './ListBox';
|
|
41
|
+
import { PopoverContext } from './Popover';
|
|
42
|
+
import { TextContext } from './Text';
|
|
43
|
+
import {
|
|
44
|
+
type ClassNameOrFunction,
|
|
45
|
+
type ContextValue,
|
|
46
|
+
dom,
|
|
47
|
+
Provider,
|
|
48
|
+
type RACValidation,
|
|
49
|
+
removeDataAttributes,
|
|
50
|
+
type RenderProps,
|
|
51
|
+
type SlotProps,
|
|
52
|
+
useContextProps,
|
|
53
|
+
useRenderProps,
|
|
54
|
+
useSlot,
|
|
55
|
+
useSlottedContext,
|
|
56
|
+
} from './utils';
|
|
57
|
+
|
|
58
|
+
// octane adaptations: structural aliases for the React types upstream drags along.
|
|
59
|
+
type ReactNode = any;
|
|
60
|
+
type ReactElement = any;
|
|
61
|
+
type RefObject<T> = { current: T };
|
|
62
|
+
type GlobalDOMAttributes = Record<string, any>;
|
|
63
|
+
// octane adaptation: structural prop bag (upstream extends React's HTMLAttributes).
|
|
64
|
+
type HTMLAttributes = Record<string, any>;
|
|
65
|
+
|
|
66
|
+
type SelectionMode = 'single' | 'multiple';
|
|
67
|
+
|
|
68
|
+
export interface ComboBoxRenderProps {
|
|
69
|
+
/**
|
|
70
|
+
* Whether the combobox is currently open.
|
|
71
|
+
*
|
|
72
|
+
* @selector [data-open]
|
|
73
|
+
*/
|
|
74
|
+
isOpen: boolean;
|
|
75
|
+
/**
|
|
76
|
+
* Whether the combobox is disabled.
|
|
77
|
+
*
|
|
78
|
+
* @selector [data-disabled]
|
|
79
|
+
*/
|
|
80
|
+
isDisabled: boolean;
|
|
81
|
+
/**
|
|
82
|
+
* Whether the combobox is invalid.
|
|
83
|
+
*
|
|
84
|
+
* @selector [data-invalid]
|
|
85
|
+
*/
|
|
86
|
+
isInvalid: boolean;
|
|
87
|
+
/**
|
|
88
|
+
* Whether the combobox is required.
|
|
89
|
+
*
|
|
90
|
+
* @selector [data-required]
|
|
91
|
+
*/
|
|
92
|
+
isRequired: boolean;
|
|
93
|
+
/**
|
|
94
|
+
* Whether the combobox is read only.
|
|
95
|
+
*
|
|
96
|
+
* @selector [data-readonly]
|
|
97
|
+
*/
|
|
98
|
+
isReadOnly: boolean;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export interface ComboBoxProps<T, M extends SelectionMode = 'single'>
|
|
102
|
+
extends
|
|
103
|
+
Omit<
|
|
104
|
+
AriaComboBoxProps<T, M>,
|
|
105
|
+
| 'children'
|
|
106
|
+
| 'placeholder'
|
|
107
|
+
| 'label'
|
|
108
|
+
| 'description'
|
|
109
|
+
| 'errorMessage'
|
|
110
|
+
| 'validationState'
|
|
111
|
+
| 'validationBehavior'
|
|
112
|
+
>,
|
|
113
|
+
RACValidation,
|
|
114
|
+
RenderProps<ComboBoxRenderProps>,
|
|
115
|
+
SlotProps,
|
|
116
|
+
GlobalDOMAttributes {
|
|
117
|
+
/**
|
|
118
|
+
* The CSS [className](https://developer.mozilla.org/en-US/docs/Web/API/Element/className) for the
|
|
119
|
+
* element. A function may be provided to compute the class based on component state.
|
|
120
|
+
*
|
|
121
|
+
* @default 'react-aria-ComboBox'
|
|
122
|
+
*/
|
|
123
|
+
className?: ClassNameOrFunction<ComboBoxRenderProps>;
|
|
124
|
+
/** The filter function used to determine if a option should be included in the combo box list. */
|
|
125
|
+
defaultFilter?: (textValue: string, inputValue: string) => boolean;
|
|
126
|
+
/**
|
|
127
|
+
* Whether the text or key of the selected item is submitted as part of an HTML form. When
|
|
128
|
+
* `allowsCustomValue` is `true`, this option does not apply and the text is always submitted.
|
|
129
|
+
*
|
|
130
|
+
* @default 'key'
|
|
131
|
+
*/
|
|
132
|
+
formValue?: 'text' | 'key';
|
|
133
|
+
/** Whether the combo box allows the menu to be open when the collection is empty. */
|
|
134
|
+
allowsEmptyCollection?: boolean;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export const ComboBoxContext =
|
|
138
|
+
createContext<ContextValue<ComboBoxProps<any, SelectionMode>, HTMLDivElement>>(null);
|
|
139
|
+
export const ComboBoxStateContext = createContext<ComboBoxState<any, SelectionMode> | null>(null);
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* A combo box combines a text input with a listbox, allowing users to filter a list of options to
|
|
143
|
+
* items matching a query.
|
|
144
|
+
*/
|
|
145
|
+
export const ComboBox: <T, M extends SelectionMode = 'single'>(
|
|
146
|
+
props: ComboBoxProps<T, M> & { ref?: any },
|
|
147
|
+
) => any = /*#__PURE__*/ createHideableComponent(function ComboBox<
|
|
148
|
+
T,
|
|
149
|
+
M extends SelectionMode = 'single',
|
|
150
|
+
>(props: ComboBoxProps<T, M>, ref: any): any {
|
|
151
|
+
const slot = S('ComboBox');
|
|
152
|
+
[props, ref] = useContextProps(props as any, ref, ComboBoxContext, subSlot(slot, 'ctx')) as any;
|
|
153
|
+
let {
|
|
154
|
+
children,
|
|
155
|
+
isDisabled = false,
|
|
156
|
+
isInvalid = false,
|
|
157
|
+
isRequired = false,
|
|
158
|
+
isReadOnly = false,
|
|
159
|
+
} = props;
|
|
160
|
+
let content = useMemo(
|
|
161
|
+
() =>
|
|
162
|
+
createElement(ListBoxContext.Provider, {
|
|
163
|
+
value: { items: props.items ?? props.defaultItems },
|
|
164
|
+
children:
|
|
165
|
+
typeof children === 'function'
|
|
166
|
+
? children({
|
|
167
|
+
isOpen: false,
|
|
168
|
+
isDisabled,
|
|
169
|
+
isInvalid,
|
|
170
|
+
isRequired,
|
|
171
|
+
defaultChildren: null,
|
|
172
|
+
isReadOnly,
|
|
173
|
+
})
|
|
174
|
+
: children,
|
|
175
|
+
}),
|
|
176
|
+
[children, isDisabled, isInvalid, isRequired, isReadOnly, props.items, props.defaultItems],
|
|
177
|
+
subSlot(slot, 'content'),
|
|
178
|
+
);
|
|
179
|
+
|
|
180
|
+
return createElement(CollectionBuilder, {
|
|
181
|
+
content,
|
|
182
|
+
children: (collection: ICollection<Node<any>>) =>
|
|
183
|
+
createElement(ComboBoxInner as any, { props, collection, comboBoxRef: ref }),
|
|
184
|
+
});
|
|
185
|
+
}) as any;
|
|
186
|
+
|
|
187
|
+
// Contexts to clear inside the popover.
|
|
188
|
+
const CLEAR_CONTEXTS = [LabelContext, ButtonContext, InputContext, GroupContext, TextContext];
|
|
189
|
+
|
|
190
|
+
interface ComboBoxInnerProps<T> {
|
|
191
|
+
props: ComboBoxProps<T, SelectionMode>;
|
|
192
|
+
collection: ICollection<Node<T>>;
|
|
193
|
+
comboBoxRef: RefObject<HTMLDivElement | null>;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function ComboBoxInner<T>({ props, collection, comboBoxRef: ref }: ComboBoxInnerProps<T>): any {
|
|
197
|
+
const slot = S('ComboBoxInner');
|
|
198
|
+
let { name, formValue = 'key', allowsCustomValue } = props;
|
|
199
|
+
if (allowsCustomValue) {
|
|
200
|
+
formValue = 'text';
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
let { validationBehavior: formValidationBehavior } = useSlottedContext(FormContext) || {};
|
|
204
|
+
let validationBehavior = props.validationBehavior ?? formValidationBehavior ?? 'native';
|
|
205
|
+
let { contains } = useFilter({ sensitivity: 'base' }, subSlot(slot, 'filter'));
|
|
206
|
+
let state = useComboBoxState(
|
|
207
|
+
{
|
|
208
|
+
...props,
|
|
209
|
+
defaultFilter: props.defaultFilter || contains,
|
|
210
|
+
// If props.items isn't provided, rely on collection filtering (aka listbox.items is provided or defaultItems provided to Combobox)
|
|
211
|
+
items: props.items,
|
|
212
|
+
children: undefined,
|
|
213
|
+
collection,
|
|
214
|
+
validationBehavior,
|
|
215
|
+
} as any,
|
|
216
|
+
subSlot(slot, 'state'),
|
|
217
|
+
);
|
|
218
|
+
|
|
219
|
+
let buttonRef = useRef<HTMLButtonElement | null>(null, subSlot(slot, 'buttonRef'));
|
|
220
|
+
let inputRef = useRef<HTMLInputElement | null>(null, subSlot(slot, 'inputRef'));
|
|
221
|
+
let groupRef = useRef<HTMLDivElement | null>(null, subSlot(slot, 'groupRef'));
|
|
222
|
+
let listBoxRef = useRef<HTMLDivElement | null>(null, subSlot(slot, 'listBoxRef'));
|
|
223
|
+
let popoverRef = useRef<HTMLDivElement | null>(null, subSlot(slot, 'popoverRef'));
|
|
224
|
+
let [labelRef, label] = useSlot(
|
|
225
|
+
!props['aria-label'] && !props['aria-labelledby'],
|
|
226
|
+
subSlot(slot, 'labelSlot'),
|
|
227
|
+
);
|
|
228
|
+
let {
|
|
229
|
+
buttonProps,
|
|
230
|
+
inputProps,
|
|
231
|
+
listBoxProps,
|
|
232
|
+
labelProps,
|
|
233
|
+
descriptionProps,
|
|
234
|
+
errorMessageProps,
|
|
235
|
+
valueProps,
|
|
236
|
+
...validation
|
|
237
|
+
} = useComboBox(
|
|
238
|
+
{
|
|
239
|
+
...removeDataAttributes(props),
|
|
240
|
+
label,
|
|
241
|
+
inputRef,
|
|
242
|
+
buttonRef,
|
|
243
|
+
listBoxRef,
|
|
244
|
+
popoverRef,
|
|
245
|
+
name: formValue === 'text' ? name : undefined,
|
|
246
|
+
validationBehavior,
|
|
247
|
+
} as any,
|
|
248
|
+
state,
|
|
249
|
+
subSlot(slot, 'combobox'),
|
|
250
|
+
);
|
|
251
|
+
|
|
252
|
+
// Make menu width match input + button
|
|
253
|
+
// Left for backward compatibility in case a <Group> is not rendered.
|
|
254
|
+
let [menuWidth, setMenuWidth] = useState<string | null>(null, subSlot(slot, 'menuWidth'));
|
|
255
|
+
let onResize = useCallback(
|
|
256
|
+
() => {
|
|
257
|
+
if (inputRef.current && !groupRef.current) {
|
|
258
|
+
let buttonRect = buttonRef.current?.getBoundingClientRect();
|
|
259
|
+
let inputRect = inputRef.current.getBoundingClientRect();
|
|
260
|
+
let minX = buttonRect ? Math.min(buttonRect.left, inputRect.left) : inputRect.left;
|
|
261
|
+
let maxX = buttonRect ? Math.max(buttonRect.right, inputRect.right) : inputRect.right;
|
|
262
|
+
setMenuWidth(maxX - minX + 'px');
|
|
263
|
+
}
|
|
264
|
+
},
|
|
265
|
+
[buttonRef, inputRef, setMenuWidth],
|
|
266
|
+
subSlot(slot, 'onResize'),
|
|
267
|
+
);
|
|
268
|
+
|
|
269
|
+
useResizeObserver(
|
|
270
|
+
{
|
|
271
|
+
ref: inputRef,
|
|
272
|
+
onResize: onResize,
|
|
273
|
+
},
|
|
274
|
+
subSlot(slot, 'resizeObserver'),
|
|
275
|
+
);
|
|
276
|
+
|
|
277
|
+
// Position popover relative to group if available, otherwise input.
|
|
278
|
+
let triggerRef = useMemo(
|
|
279
|
+
() => ({
|
|
280
|
+
get current() {
|
|
281
|
+
return groupRef.current || inputRef.current;
|
|
282
|
+
},
|
|
283
|
+
}),
|
|
284
|
+
[groupRef, inputRef],
|
|
285
|
+
subSlot(slot, 'triggerRef'),
|
|
286
|
+
);
|
|
287
|
+
|
|
288
|
+
// Only expose a subset of state to renderProps function to avoid infinite render loop
|
|
289
|
+
let renderPropsState = useMemo(
|
|
290
|
+
() => ({
|
|
291
|
+
isOpen: state.isOpen,
|
|
292
|
+
isDisabled: props.isDisabled || false,
|
|
293
|
+
isInvalid: validation.isInvalid || false,
|
|
294
|
+
isRequired: props.isRequired || false,
|
|
295
|
+
isReadOnly: props.isReadOnly || false,
|
|
296
|
+
}),
|
|
297
|
+
[state.isOpen, props.isDisabled, validation.isInvalid, props.isRequired, props.isReadOnly],
|
|
298
|
+
subSlot(slot, 'renderPropsState'),
|
|
299
|
+
);
|
|
300
|
+
|
|
301
|
+
let renderProps = useRenderProps(
|
|
302
|
+
{
|
|
303
|
+
...props,
|
|
304
|
+
values: renderPropsState,
|
|
305
|
+
defaultClassName: 'react-aria-ComboBox',
|
|
306
|
+
} as any,
|
|
307
|
+
subSlot(slot, 'render'),
|
|
308
|
+
);
|
|
309
|
+
|
|
310
|
+
let DOMProps = filterDOMProps(props, { global: true });
|
|
311
|
+
delete DOMProps.id;
|
|
312
|
+
|
|
313
|
+
let inputs: ReactElement[] = [];
|
|
314
|
+
if (name && formValue === 'key') {
|
|
315
|
+
let values: (Key | null)[] = Array.isArray(state.value)
|
|
316
|
+
? (state.value as Key[])
|
|
317
|
+
: [state.value as Key | null];
|
|
318
|
+
if (values.length === 0) {
|
|
319
|
+
values = [null];
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
inputs = values.map((value, i) =>
|
|
323
|
+
createElement('input', {
|
|
324
|
+
key: i,
|
|
325
|
+
type: 'hidden',
|
|
326
|
+
name,
|
|
327
|
+
form: props.form,
|
|
328
|
+
value: value ?? '',
|
|
329
|
+
}),
|
|
330
|
+
);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
return createElement(Provider, {
|
|
334
|
+
values: [
|
|
335
|
+
[ComboBoxStateContext, state],
|
|
336
|
+
[LabelContext, { ...labelProps, ref: labelRef }],
|
|
337
|
+
[ButtonContext, { ...buttonProps, ref: buttonRef, isPressed: state.isOpen }],
|
|
338
|
+
[InputContext, { ...inputProps, ref: inputRef }],
|
|
339
|
+
[OverlayTriggerStateContext, state],
|
|
340
|
+
[
|
|
341
|
+
PopoverContext,
|
|
342
|
+
{
|
|
343
|
+
ref: popoverRef,
|
|
344
|
+
triggerRef,
|
|
345
|
+
scrollRef: listBoxRef,
|
|
346
|
+
placement: 'bottom start',
|
|
347
|
+
isNonModal: true,
|
|
348
|
+
trigger: 'ComboBox',
|
|
349
|
+
style: { '--trigger-width': menuWidth },
|
|
350
|
+
clearContexts: CLEAR_CONTEXTS,
|
|
351
|
+
},
|
|
352
|
+
],
|
|
353
|
+
[ListBoxContext, { ...listBoxProps, ref: listBoxRef }],
|
|
354
|
+
[ListStateContext, state],
|
|
355
|
+
[
|
|
356
|
+
TextContext,
|
|
357
|
+
{
|
|
358
|
+
slots: {
|
|
359
|
+
description: descriptionProps,
|
|
360
|
+
errorMessage: errorMessageProps,
|
|
361
|
+
},
|
|
362
|
+
},
|
|
363
|
+
],
|
|
364
|
+
[
|
|
365
|
+
GroupContext,
|
|
366
|
+
{ ref: groupRef, isInvalid: validation.isInvalid, isDisabled: props.isDisabled || false },
|
|
367
|
+
],
|
|
368
|
+
[FieldErrorContext, validation],
|
|
369
|
+
[ComboBoxValueContext, valueProps],
|
|
370
|
+
] as any,
|
|
371
|
+
children: createElement(
|
|
372
|
+
dom.div,
|
|
373
|
+
{
|
|
374
|
+
...DOMProps,
|
|
375
|
+
...renderProps,
|
|
376
|
+
ref,
|
|
377
|
+
slot: props.slot || undefined,
|
|
378
|
+
'data-focused': state.isFocused || undefined,
|
|
379
|
+
'data-open': state.isOpen || undefined,
|
|
380
|
+
'data-disabled': props.isDisabled || undefined,
|
|
381
|
+
'data-readonly': props.isReadOnly || undefined,
|
|
382
|
+
'data-invalid': validation.isInvalid || undefined,
|
|
383
|
+
'data-required': props.isRequired || undefined,
|
|
384
|
+
},
|
|
385
|
+
renderProps.children,
|
|
386
|
+
inputs,
|
|
387
|
+
),
|
|
388
|
+
});
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
export interface ComboBoxValueRenderProps<T> {
|
|
392
|
+
/**
|
|
393
|
+
* Whether the value is a placeholder.
|
|
394
|
+
*
|
|
395
|
+
* @selector [data-placeholder]
|
|
396
|
+
*/
|
|
397
|
+
isPlaceholder: boolean;
|
|
398
|
+
/** The object values of the currently selected items. */
|
|
399
|
+
selectedItems: (T | null)[];
|
|
400
|
+
/** The textValue of the currently selected items. */
|
|
401
|
+
selectedText: string;
|
|
402
|
+
/** The state of the ComboBox. */
|
|
403
|
+
state: ComboBoxState<T, 'single' | 'multiple'>;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
export interface ComboBoxValueProps<T>
|
|
407
|
+
extends
|
|
408
|
+
Omit<HTMLAttributes, keyof RenderProps<unknown>>,
|
|
409
|
+
RenderProps<ComboBoxValueRenderProps<T>, 'div'> {
|
|
410
|
+
/**
|
|
411
|
+
* The CSS [className](https://developer.mozilla.org/en-US/docs/Web/API/Element/className) for the
|
|
412
|
+
* element. A function may be provided to compute the class based on component state.
|
|
413
|
+
*
|
|
414
|
+
* @default 'react-aria-ComboBoxValue'
|
|
415
|
+
*/
|
|
416
|
+
className?: ClassNameOrFunction<ComboBoxValueRenderProps<T>>;
|
|
417
|
+
/** A value to display when no items are selected. */
|
|
418
|
+
placeholder?: ReactNode;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
export const ComboBoxValueContext =
|
|
422
|
+
createContext<ContextValue<ComboBoxValueProps<any>, HTMLDivElement>>(null);
|
|
423
|
+
|
|
424
|
+
/**
|
|
425
|
+
* ComboBoxValue renders the selected values of a ComboBox, or a placeholder if no value is
|
|
426
|
+
* selected. By default, the items are rendered as a comma separated list. Use the render function
|
|
427
|
+
* to customize this.
|
|
428
|
+
*/
|
|
429
|
+
export const ComboBoxValue: <T>(props: ComboBoxValueProps<T> & { ref?: any }) => any =
|
|
430
|
+
/*#__PURE__*/ createHideableComponent(function ComboBoxValue<T>(
|
|
431
|
+
props: ComboBoxValueProps<T>,
|
|
432
|
+
ref: any,
|
|
433
|
+
): any {
|
|
434
|
+
const slot = S('ComboBoxValue');
|
|
435
|
+
[props, ref] = useContextProps(props, ref, ComboBoxValueContext, subSlot(slot, 'ctx'));
|
|
436
|
+
let state = useContext(ComboBoxStateContext)!;
|
|
437
|
+
let formatter = useListFormatter(undefined, subSlot(slot, 'formatter'));
|
|
438
|
+
let selectedText = useMemo(
|
|
439
|
+
() =>
|
|
440
|
+
formatter.format(
|
|
441
|
+
state.selectedItems.map((item) => item?.textValue || '').filter((v) => v !== ''),
|
|
442
|
+
),
|
|
443
|
+
[formatter, state.selectedItems],
|
|
444
|
+
subSlot(slot, 'selectedText'),
|
|
445
|
+
);
|
|
446
|
+
|
|
447
|
+
// octane adaptation: hook hoisted out of the useRenderProps argument object below.
|
|
448
|
+
let selectedItems = useMemo(
|
|
449
|
+
() => state.selectedItems.map((item) => (item.value as T) ?? null),
|
|
450
|
+
[state.selectedItems],
|
|
451
|
+
subSlot(slot, 'selectedItems'),
|
|
452
|
+
);
|
|
453
|
+
|
|
454
|
+
let renderProps = useRenderProps(
|
|
455
|
+
{
|
|
456
|
+
...props,
|
|
457
|
+
defaultChildren: selectedText || props.placeholder,
|
|
458
|
+
defaultClassName: 'react-aria-ComboBoxValue',
|
|
459
|
+
values: {
|
|
460
|
+
selectedItems,
|
|
461
|
+
selectedText,
|
|
462
|
+
isPlaceholder: state.selectedItems.length === 0,
|
|
463
|
+
state,
|
|
464
|
+
},
|
|
465
|
+
} as any,
|
|
466
|
+
subSlot(slot, 'render'),
|
|
467
|
+
);
|
|
468
|
+
|
|
469
|
+
let DOMProps = filterDOMProps(props, { global: true });
|
|
470
|
+
|
|
471
|
+
return createElement(dom.div, {
|
|
472
|
+
ref,
|
|
473
|
+
...DOMProps,
|
|
474
|
+
...renderProps,
|
|
475
|
+
'data-placeholder': state.selectedItems.length === 0 || undefined,
|
|
476
|
+
});
|
|
477
|
+
}) as any;
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
// Ported from react-aria-components (source: .react-spectrum/packages/react-aria-components/src/Dialog.tsx).
|
|
2
|
+
// octane adaptations: `.tsx` → `.ts`, JSX → `createElement`; NO forwardRef — the forwarded ref
|
|
3
|
+
// is `props.ref`, passed into `useContextProps` explicitly; the plain-`.ts` components use the
|
|
4
|
+
// S()/subSlot component-slot convention; `RootMenuTriggerStateContext` comes from the Phase-5
|
|
5
|
+
// `./Menu` stub module; React's ReactNode/JSX types → `any`; the dev-only accessibility
|
|
6
|
+
// console.warn keeps its upstream NODE_ENV guard (same as `./utils`' DOMElement warnings).
|
|
7
|
+
import { createContext, createElement, useContext, useRef } from 'octane';
|
|
8
|
+
|
|
9
|
+
import { type AriaDialogProps, useDialog } from '../dialog/useDialog';
|
|
10
|
+
import { ButtonContext } from './Button';
|
|
11
|
+
import { S, subSlot } from '../internal';
|
|
12
|
+
import { filterDOMProps } from '../utils/filterDOMProps';
|
|
13
|
+
import { HeadingContext } from './Heading';
|
|
14
|
+
import { mergeProps } from '../utils/mergeProps';
|
|
15
|
+
import type {
|
|
16
|
+
OverlayTriggerProps,
|
|
17
|
+
OverlayTriggerState,
|
|
18
|
+
} from '../stately/overlays/useOverlayTriggerState';
|
|
19
|
+
import { PopoverContext } from './Popover';
|
|
20
|
+
import { PressResponder } from '../interactions/PressResponder';
|
|
21
|
+
import { RootMenuTriggerStateContext } from './Menu';
|
|
22
|
+
import { useId } from '../utils/useId';
|
|
23
|
+
import { useMenuTriggerState } from '../stately/menu/useMenuTriggerState';
|
|
24
|
+
import { useOverlayTrigger } from '../overlays/useOverlayTrigger';
|
|
25
|
+
import {
|
|
26
|
+
type ContextValue,
|
|
27
|
+
DEFAULT_SLOT,
|
|
28
|
+
dom,
|
|
29
|
+
type DOMRenderProps,
|
|
30
|
+
Provider,
|
|
31
|
+
type SlotProps,
|
|
32
|
+
type StyleProps,
|
|
33
|
+
useContextProps,
|
|
34
|
+
useRenderProps,
|
|
35
|
+
} from './utils';
|
|
36
|
+
|
|
37
|
+
// octane adaptations: structural aliases for the React types upstream drags along.
|
|
38
|
+
type ReactNode = any;
|
|
39
|
+
type GlobalDOMAttributes = Record<string, any>;
|
|
40
|
+
|
|
41
|
+
export interface DialogTriggerProps extends OverlayTriggerProps {
|
|
42
|
+
children: ReactNode;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface DialogRenderProps {
|
|
46
|
+
close: () => void;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface DialogProps
|
|
50
|
+
extends
|
|
51
|
+
AriaDialogProps,
|
|
52
|
+
StyleProps,
|
|
53
|
+
SlotProps,
|
|
54
|
+
DOMRenderProps<'section', undefined>,
|
|
55
|
+
GlobalDOMAttributes {
|
|
56
|
+
/**
|
|
57
|
+
* The CSS [className](https://developer.mozilla.org/en-US/docs/Web/API/Element/className) for the
|
|
58
|
+
* element.
|
|
59
|
+
*
|
|
60
|
+
* @default 'react-aria-Dialog'
|
|
61
|
+
*/
|
|
62
|
+
className?: string;
|
|
63
|
+
/** Children of the dialog. A function may be provided to access a function to close the dialog. */
|
|
64
|
+
children?: ReactNode | ((opts: DialogRenderProps) => ReactNode);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export const DialogContext = createContext<ContextValue<DialogProps, HTMLElement>>(null);
|
|
68
|
+
export const OverlayTriggerStateContext = createContext<OverlayTriggerState | null>(null);
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* A DialogTrigger opens a dialog when a trigger element is pressed.
|
|
72
|
+
*/
|
|
73
|
+
export function DialogTrigger(props: DialogTriggerProps): any {
|
|
74
|
+
const slot = S('DialogTrigger');
|
|
75
|
+
// Use useMenuTriggerState instead of useOverlayTriggerState in case a menu is embedded in the dialog.
|
|
76
|
+
// This is needed to handle submenus.
|
|
77
|
+
let state = useMenuTriggerState(props, subSlot(slot, 'state'));
|
|
78
|
+
|
|
79
|
+
let buttonRef = useRef<HTMLButtonElement | null>(null, subSlot(slot, 'buttonRef'));
|
|
80
|
+
let { triggerProps, overlayProps } = useOverlayTrigger(
|
|
81
|
+
{ type: 'dialog' },
|
|
82
|
+
state,
|
|
83
|
+
buttonRef,
|
|
84
|
+
subSlot(slot, 'trigger'),
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
// Label dialog by the trigger as a fallback if there is no title slot.
|
|
88
|
+
// This is done in RAC instead of hooks because otherwise we cannot distinguish
|
|
89
|
+
// between context and props. Normally aria-labelledby overrides the title
|
|
90
|
+
// but when sent by context we want the title to win.
|
|
91
|
+
triggerProps.id = useId(undefined, subSlot(slot, 'triggerId'));
|
|
92
|
+
(overlayProps as any)['aria-labelledby'] = triggerProps.id;
|
|
93
|
+
|
|
94
|
+
return createElement(Provider, {
|
|
95
|
+
values: [
|
|
96
|
+
[OverlayTriggerStateContext, state],
|
|
97
|
+
[RootMenuTriggerStateContext, state],
|
|
98
|
+
[DialogContext, overlayProps],
|
|
99
|
+
[
|
|
100
|
+
PopoverContext,
|
|
101
|
+
{
|
|
102
|
+
trigger: 'DialogTrigger',
|
|
103
|
+
triggerRef: buttonRef,
|
|
104
|
+
'aria-labelledby': (overlayProps as any)['aria-labelledby'],
|
|
105
|
+
},
|
|
106
|
+
],
|
|
107
|
+
] as any,
|
|
108
|
+
children: createElement(PressResponder, {
|
|
109
|
+
...triggerProps,
|
|
110
|
+
ref: buttonRef,
|
|
111
|
+
isPressed: state.isOpen,
|
|
112
|
+
children: props.children,
|
|
113
|
+
}),
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* A dialog is an overlay shown above other content in an application.
|
|
119
|
+
*/
|
|
120
|
+
export function Dialog(props: DialogProps): any {
|
|
121
|
+
const slot = S('Dialog');
|
|
122
|
+
let originalAriaLabelledby = props['aria-labelledby'];
|
|
123
|
+
let ref: any;
|
|
124
|
+
[props, ref] = useContextProps(props, (props as any).ref, DialogContext, subSlot(slot, 'ctx'));
|
|
125
|
+
let { dialogProps, titleProps } = useDialog(
|
|
126
|
+
{
|
|
127
|
+
...props,
|
|
128
|
+
// Only pass aria-labelledby from props, not context.
|
|
129
|
+
// Context is used as a fallback below.
|
|
130
|
+
'aria-labelledby': originalAriaLabelledby,
|
|
131
|
+
},
|
|
132
|
+
ref,
|
|
133
|
+
subSlot(slot, 'dialog'),
|
|
134
|
+
);
|
|
135
|
+
let state = useContext(OverlayTriggerStateContext);
|
|
136
|
+
|
|
137
|
+
if (!dialogProps['aria-label'] && !dialogProps['aria-labelledby']) {
|
|
138
|
+
// If aria-labelledby exists on props, we know it came from context.
|
|
139
|
+
// Use that as a fallback in case there is no title slot.
|
|
140
|
+
if (props['aria-labelledby']) {
|
|
141
|
+
dialogProps['aria-labelledby'] = props['aria-labelledby'];
|
|
142
|
+
} else if (process.env.NODE_ENV !== 'production') {
|
|
143
|
+
console.warn(
|
|
144
|
+
'If a Dialog does not contain a <Heading slot="title">, it must have an aria-label or aria-labelledby attribute for accessibility.',
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
let renderProps = useRenderProps(
|
|
150
|
+
{
|
|
151
|
+
defaultClassName: 'react-aria-Dialog',
|
|
152
|
+
className: props.className,
|
|
153
|
+
style: props.style,
|
|
154
|
+
children: props.children,
|
|
155
|
+
values: {
|
|
156
|
+
close: state?.close || (() => {}),
|
|
157
|
+
},
|
|
158
|
+
},
|
|
159
|
+
subSlot(slot, 'render'),
|
|
160
|
+
);
|
|
161
|
+
|
|
162
|
+
let DOMProps = filterDOMProps(props, { global: true });
|
|
163
|
+
|
|
164
|
+
return createElement(dom.section, {
|
|
165
|
+
...mergeProps(DOMProps, renderProps, dialogProps),
|
|
166
|
+
render: props.render,
|
|
167
|
+
ref,
|
|
168
|
+
slot: props.slot || undefined,
|
|
169
|
+
children: createElement(Provider, {
|
|
170
|
+
values: [
|
|
171
|
+
[
|
|
172
|
+
HeadingContext,
|
|
173
|
+
{
|
|
174
|
+
slots: {
|
|
175
|
+
[DEFAULT_SLOT]: {},
|
|
176
|
+
title: { ...titleProps, level: 2 },
|
|
177
|
+
},
|
|
178
|
+
},
|
|
179
|
+
],
|
|
180
|
+
[
|
|
181
|
+
ButtonContext,
|
|
182
|
+
{
|
|
183
|
+
slots: {
|
|
184
|
+
[DEFAULT_SLOT]: {},
|
|
185
|
+
close: {
|
|
186
|
+
onPress: () => state?.close(),
|
|
187
|
+
},
|
|
188
|
+
},
|
|
189
|
+
},
|
|
190
|
+
],
|
|
191
|
+
] as any,
|
|
192
|
+
children: renderProps.children,
|
|
193
|
+
}),
|
|
194
|
+
});
|
|
195
|
+
}
|