@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,208 @@
|
|
|
1
|
+
// Ported from react-aria-components (source: .react-spectrum/packages/react-aria-components/src/SearchField.tsx).
|
|
2
|
+
// octane adaptations: `.tsx` → `.ts`, JSX → `createElement`; NO forwardRef — the forwarded ref
|
|
3
|
+
// arrives positionally from `createHideableComponent` (which forwards `props.ref`); the
|
|
4
|
+
// plain-`.ts` component uses the S()/subSlot component-slot convention. NATIVE EVENTS: the
|
|
5
|
+
// per-keystroke wiring rides octane's native `onInput` (produced inside useSearchField), and
|
|
6
|
+
// the clear button clears through the native path (useSearchField's clearButtonProps → the
|
|
7
|
+
// ButtonContext); no synthetic `onChange` is added. Upstream's `GlobalDOMAttributes` → a
|
|
8
|
+
// structural record.
|
|
9
|
+
import { createContext, createElement, useRef } from 'octane';
|
|
10
|
+
|
|
11
|
+
import { createHideableComponent } from '../collections/Hidden';
|
|
12
|
+
import { S, subSlot } from '../internal';
|
|
13
|
+
import { type AriaSearchFieldProps, useSearchField } from '../searchfield/useSearchField';
|
|
14
|
+
import {
|
|
15
|
+
type SearchFieldState,
|
|
16
|
+
useSearchFieldState,
|
|
17
|
+
} from '../stately/searchfield/useSearchFieldState';
|
|
18
|
+
import { filterDOMProps } from '../utils/filterDOMProps';
|
|
19
|
+
import { FieldInputContext } from './Autocomplete';
|
|
20
|
+
import { ButtonContext } from './Button';
|
|
21
|
+
import { FieldErrorContext } from './FieldError';
|
|
22
|
+
import { FormContext } from './Form';
|
|
23
|
+
import { GroupContext } from './Group';
|
|
24
|
+
import { InputContext } from './Input';
|
|
25
|
+
import { LabelContext } from './Label';
|
|
26
|
+
import { TextContext } from './Text';
|
|
27
|
+
import {
|
|
28
|
+
type ClassNameOrFunction,
|
|
29
|
+
type ContextValue,
|
|
30
|
+
dom,
|
|
31
|
+
Provider,
|
|
32
|
+
type RACValidation,
|
|
33
|
+
removeDataAttributes,
|
|
34
|
+
type RenderProps,
|
|
35
|
+
type SlotProps,
|
|
36
|
+
useContextProps,
|
|
37
|
+
useRenderProps,
|
|
38
|
+
useSlot,
|
|
39
|
+
useSlottedContext,
|
|
40
|
+
} from './utils';
|
|
41
|
+
|
|
42
|
+
// octane adaptation: structural bag (upstream's `GlobalDOMAttributes` drags React handler types).
|
|
43
|
+
type GlobalDOMAttributes = Record<string, any>;
|
|
44
|
+
|
|
45
|
+
export interface SearchFieldRenderProps {
|
|
46
|
+
/**
|
|
47
|
+
* Whether the search field is empty.
|
|
48
|
+
*
|
|
49
|
+
* @selector [data-empty]
|
|
50
|
+
*/
|
|
51
|
+
isEmpty: boolean;
|
|
52
|
+
/**
|
|
53
|
+
* Whether the search field is disabled.
|
|
54
|
+
*
|
|
55
|
+
* @selector [data-disabled]
|
|
56
|
+
*/
|
|
57
|
+
isDisabled: boolean;
|
|
58
|
+
/**
|
|
59
|
+
* Whether the search field is invalid.
|
|
60
|
+
*
|
|
61
|
+
* @selector [data-invalid]
|
|
62
|
+
*/
|
|
63
|
+
isInvalid: boolean;
|
|
64
|
+
/**
|
|
65
|
+
* Whether the search field is read only.
|
|
66
|
+
*
|
|
67
|
+
* @selector [data-readonly]
|
|
68
|
+
*/
|
|
69
|
+
isReadOnly: boolean;
|
|
70
|
+
/**
|
|
71
|
+
* Whether the search field is required.
|
|
72
|
+
*
|
|
73
|
+
* @selector [data-required]
|
|
74
|
+
*/
|
|
75
|
+
isRequired: boolean;
|
|
76
|
+
/**
|
|
77
|
+
* State of the search field.
|
|
78
|
+
*/
|
|
79
|
+
state: SearchFieldState;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export interface SearchFieldProps
|
|
83
|
+
extends
|
|
84
|
+
Omit<
|
|
85
|
+
AriaSearchFieldProps,
|
|
86
|
+
| 'label'
|
|
87
|
+
| 'placeholder'
|
|
88
|
+
| 'description'
|
|
89
|
+
| 'errorMessage'
|
|
90
|
+
| 'validationState'
|
|
91
|
+
| 'validationBehavior'
|
|
92
|
+
>,
|
|
93
|
+
RACValidation,
|
|
94
|
+
RenderProps<SearchFieldRenderProps>,
|
|
95
|
+
SlotProps,
|
|
96
|
+
GlobalDOMAttributes {
|
|
97
|
+
/**
|
|
98
|
+
* The CSS [className](https://developer.mozilla.org/en-US/docs/Web/API/Element/className) for the
|
|
99
|
+
* element. A function may be provided to compute the class based on component state.
|
|
100
|
+
*
|
|
101
|
+
* @default 'react-aria-SearchField'
|
|
102
|
+
*/
|
|
103
|
+
className?: ClassNameOrFunction<SearchFieldRenderProps>;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export const SearchFieldContext =
|
|
107
|
+
createContext<ContextValue<SearchFieldProps, HTMLDivElement>>(null);
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* A search field allows a user to enter and clear a search query.
|
|
111
|
+
*/
|
|
112
|
+
export const SearchField = /*#__PURE__*/ createHideableComponent(function SearchField(
|
|
113
|
+
props: SearchFieldProps,
|
|
114
|
+
ref: any,
|
|
115
|
+
) {
|
|
116
|
+
const slot = S('SearchField');
|
|
117
|
+
[props, ref] = useContextProps(props, ref, SearchFieldContext, subSlot(slot, 'ctx'));
|
|
118
|
+
let { validationBehavior: formValidationBehavior } = useSlottedContext(FormContext) || {};
|
|
119
|
+
let validationBehavior = props.validationBehavior ?? formValidationBehavior ?? 'native';
|
|
120
|
+
let inputRef: any = useRef<HTMLInputElement | null>(null, subSlot(slot, 'input'));
|
|
121
|
+
// octane adaptation: destructuring assignment instead of upstream's `inputRef as unknown`
|
|
122
|
+
// lvalue cast — the merged object ref replaces the local ref exactly like upstream.
|
|
123
|
+
[props, inputRef] = useContextProps(
|
|
124
|
+
props,
|
|
125
|
+
inputRef,
|
|
126
|
+
FieldInputContext as any,
|
|
127
|
+
subSlot(slot, 'fieldInput'),
|
|
128
|
+
);
|
|
129
|
+
let [labelRef, label] = useSlot(
|
|
130
|
+
!props['aria-label'] && !props['aria-labelledby'],
|
|
131
|
+
subSlot(slot, 'labelSlot'),
|
|
132
|
+
);
|
|
133
|
+
let state = useSearchFieldState(
|
|
134
|
+
{
|
|
135
|
+
...props,
|
|
136
|
+
validationBehavior,
|
|
137
|
+
},
|
|
138
|
+
subSlot(slot, 'state'),
|
|
139
|
+
);
|
|
140
|
+
|
|
141
|
+
let {
|
|
142
|
+
labelProps,
|
|
143
|
+
inputProps,
|
|
144
|
+
clearButtonProps,
|
|
145
|
+
descriptionProps,
|
|
146
|
+
errorMessageProps,
|
|
147
|
+
...validation
|
|
148
|
+
} = useSearchField(
|
|
149
|
+
{
|
|
150
|
+
...removeDataAttributes(props),
|
|
151
|
+
label,
|
|
152
|
+
validationBehavior,
|
|
153
|
+
},
|
|
154
|
+
state,
|
|
155
|
+
inputRef,
|
|
156
|
+
subSlot(slot, 'searchField'),
|
|
157
|
+
);
|
|
158
|
+
|
|
159
|
+
let renderProps = useRenderProps(
|
|
160
|
+
{
|
|
161
|
+
...props,
|
|
162
|
+
values: {
|
|
163
|
+
isEmpty: state.value === '',
|
|
164
|
+
isDisabled: props.isDisabled || false,
|
|
165
|
+
isInvalid: validation.isInvalid || false,
|
|
166
|
+
isReadOnly: props.isReadOnly || false,
|
|
167
|
+
isRequired: props.isRequired || false,
|
|
168
|
+
state,
|
|
169
|
+
},
|
|
170
|
+
defaultClassName: 'react-aria-SearchField',
|
|
171
|
+
},
|
|
172
|
+
subSlot(slot, 'render'),
|
|
173
|
+
);
|
|
174
|
+
|
|
175
|
+
let DOMProps = filterDOMProps(props, { global: true });
|
|
176
|
+
delete DOMProps.id;
|
|
177
|
+
|
|
178
|
+
return createElement(dom.div, {
|
|
179
|
+
...DOMProps,
|
|
180
|
+
...renderProps,
|
|
181
|
+
ref,
|
|
182
|
+
slot: props.slot || undefined,
|
|
183
|
+
'data-empty': state.value === '' || undefined,
|
|
184
|
+
'data-disabled': props.isDisabled || undefined,
|
|
185
|
+
'data-invalid': validation.isInvalid || undefined,
|
|
186
|
+
'data-readonly': props.isReadOnly || undefined,
|
|
187
|
+
'data-required': props.isRequired || undefined,
|
|
188
|
+
children: createElement(Provider, {
|
|
189
|
+
values: [
|
|
190
|
+
[LabelContext, { ...labelProps, ref: labelRef }],
|
|
191
|
+
[InputContext, { ...inputProps, ref: inputRef }],
|
|
192
|
+
[ButtonContext, clearButtonProps],
|
|
193
|
+
[
|
|
194
|
+
TextContext,
|
|
195
|
+
{
|
|
196
|
+
slots: {
|
|
197
|
+
description: descriptionProps,
|
|
198
|
+
errorMessage: errorMessageProps,
|
|
199
|
+
},
|
|
200
|
+
},
|
|
201
|
+
],
|
|
202
|
+
[GroupContext, { isInvalid: validation.isInvalid, isDisabled: props.isDisabled || false }],
|
|
203
|
+
[FieldErrorContext, validation],
|
|
204
|
+
] as any,
|
|
205
|
+
children: renderProps.children,
|
|
206
|
+
}),
|
|
207
|
+
});
|
|
208
|
+
});
|
|
@@ -0,0 +1,505 @@
|
|
|
1
|
+
// Ported from react-aria-components (source: .react-spectrum/packages/react-aria-components/src/Select.tsx).
|
|
2
|
+
// octane adaptations: `.tsx` → `.ts`, JSX → `createElement` (the trigger wrapper's two
|
|
3
|
+
// children arrive positionally); NO forwardRef — `createHideableComponent` forwards
|
|
4
|
+
// `props.ref` positionally exactly like TextField; the plain-`.ts` components use the
|
|
5
|
+
// S()/subSlot component-slot convention. The collection composes the Phase-4 engine
|
|
6
|
+
// (`CollectionBuilder`); the open listbox reuses ./ListBox by providing a ListState via
|
|
7
|
+
// `ListStateContext` (ListBox short-circuits into ListBoxInner without rebuilding state).
|
|
8
|
+
// react-aria/react-stately private imports come from the binding's ported modules
|
|
9
|
+
// (`../select/useSelect` + `../select/HiddenSelect`, `../stately/select/useSelectState`).
|
|
10
|
+
// Upstream's Parcel glob intl import (`../intl/*.json`, the react-aria-components package
|
|
11
|
+
// dictionary) is ported module-locally below with just the `selectPlaceholder` key this
|
|
12
|
+
// module reads (first consumer; hoist to a generated ../intl/components index when another
|
|
13
|
+
// RAC component needs the package dictionary). Hooks are hoisted out of argument object
|
|
14
|
+
// literals per the binding convention; explicit dependency arrays are preserved verbatim.
|
|
15
|
+
import type { Collection as ICollection, Node } from '@react-types/shared';
|
|
16
|
+
import { Fragment, createContext, createElement, useContext, useMemo, useRef } from 'octane';
|
|
17
|
+
|
|
18
|
+
import { CollectionBuilder } from '../collections/CollectionBuilder';
|
|
19
|
+
import { createHideableComponent } from '../collections/Hidden';
|
|
20
|
+
import { useFocusRing } from '../focus/useFocusRing';
|
|
21
|
+
import { useListFormatter } from '../i18n/useListFormatter';
|
|
22
|
+
import { useLocalizedStringFormatter } from '../i18n/useLocalizedStringFormatter';
|
|
23
|
+
import { S, subSlot } from '../internal';
|
|
24
|
+
import { HiddenSelect } from '../select/HiddenSelect';
|
|
25
|
+
import { type AriaSelectProps, useSelect } from '../select/useSelect';
|
|
26
|
+
import { type SelectState, useSelectState } from '../stately/select/useSelectState';
|
|
27
|
+
import { filterDOMProps } from '../utils/filterDOMProps';
|
|
28
|
+
import { mergeProps } from '../utils/mergeProps';
|
|
29
|
+
import { ButtonContext } from './Button';
|
|
30
|
+
import type { ItemRenderProps } from './Collection';
|
|
31
|
+
import { OverlayTriggerStateContext } from './Dialog';
|
|
32
|
+
import { FieldErrorContext } from './FieldError';
|
|
33
|
+
import { FormContext } from './Form';
|
|
34
|
+
import { LabelContext } from './Label';
|
|
35
|
+
import { ListBoxContext, ListStateContext } from './ListBox';
|
|
36
|
+
import { PopoverContext } from './Popover';
|
|
37
|
+
import { TextContext } from './Text';
|
|
38
|
+
import {
|
|
39
|
+
type ClassNameOrFunction,
|
|
40
|
+
type ContextValue,
|
|
41
|
+
dom,
|
|
42
|
+
Provider,
|
|
43
|
+
type RACValidation,
|
|
44
|
+
removeDataAttributes,
|
|
45
|
+
type RenderProps,
|
|
46
|
+
type SlotProps,
|
|
47
|
+
useContextProps,
|
|
48
|
+
useRenderProps,
|
|
49
|
+
useSlot,
|
|
50
|
+
useSlottedContext,
|
|
51
|
+
} from './utils';
|
|
52
|
+
|
|
53
|
+
// octane adaptations: structural aliases for the React types upstream drags along.
|
|
54
|
+
type ReactNode = any;
|
|
55
|
+
type RefObject<T> = { current: T };
|
|
56
|
+
type GlobalDOMAttributes = Record<string, any>;
|
|
57
|
+
// octane adaptation: structural prop bag (upstream extends React's HTMLAttributes).
|
|
58
|
+
type HTMLAttributes = Record<string, any>;
|
|
59
|
+
|
|
60
|
+
// The react-aria-components package intl dictionary, reduced to the key this module
|
|
61
|
+
// reads. Strings are copied VERBATIM from .react-spectrum/packages/react-aria-components/
|
|
62
|
+
// intl/*.json; refresh from the pinned checkout on version bumps, never hand-edit.
|
|
63
|
+
const intlMessages = {
|
|
64
|
+
'ar-AE': { selectPlaceholder: 'حدد عنصرًا' },
|
|
65
|
+
'bg-BG': { selectPlaceholder: 'Изберете предмет' },
|
|
66
|
+
'cs-CZ': { selectPlaceholder: 'Vyberte položku' },
|
|
67
|
+
'da-DK': { selectPlaceholder: 'Vælg et element' },
|
|
68
|
+
'de-DE': { selectPlaceholder: 'Element wählen' },
|
|
69
|
+
'el-GR': { selectPlaceholder: 'Επιλέξτε ένα αντικείμενο' },
|
|
70
|
+
'en-US': { selectPlaceholder: 'Select an item' },
|
|
71
|
+
'es-ES': { selectPlaceholder: 'Seleccionar un artículo' },
|
|
72
|
+
'et-EE': { selectPlaceholder: 'Valige üksus' },
|
|
73
|
+
'fi-FI': { selectPlaceholder: 'Valitse kohde' },
|
|
74
|
+
'fr-FR': { selectPlaceholder: 'Sélectionner un élément' },
|
|
75
|
+
'he-IL': { selectPlaceholder: 'בחר פריט' },
|
|
76
|
+
'hr-HR': { selectPlaceholder: 'Odaberite stavku' },
|
|
77
|
+
'hu-HU': { selectPlaceholder: 'Válasszon ki egy elemet' },
|
|
78
|
+
'it-IT': { selectPlaceholder: 'Seleziona un elemento' },
|
|
79
|
+
'ja-JP': { selectPlaceholder: '項目を選択' },
|
|
80
|
+
'ko-KR': { selectPlaceholder: '항목 선택' },
|
|
81
|
+
'lt-LT': { selectPlaceholder: 'Pasirinkite elementą' },
|
|
82
|
+
'lv-LV': { selectPlaceholder: 'Izvēlēties vienumu' },
|
|
83
|
+
'nb-NO': { selectPlaceholder: 'Velg et element' },
|
|
84
|
+
'nl-NL': { selectPlaceholder: 'Selecteer een item' },
|
|
85
|
+
'pl-PL': { selectPlaceholder: 'Wybierz element' },
|
|
86
|
+
'pt-BR': { selectPlaceholder: 'Selecione um item' },
|
|
87
|
+
'pt-PT': { selectPlaceholder: 'Selecione um item' },
|
|
88
|
+
'ro-RO': { selectPlaceholder: 'Selectați un element' },
|
|
89
|
+
'ru-RU': { selectPlaceholder: 'Выберите элемент' },
|
|
90
|
+
'sk-SK': { selectPlaceholder: 'Vyberte položku' },
|
|
91
|
+
'sl-SI': { selectPlaceholder: 'Izberite element' },
|
|
92
|
+
'sr-SP': { selectPlaceholder: 'Izaberite stavku' },
|
|
93
|
+
'sv-SE': { selectPlaceholder: 'Välj en artikel' },
|
|
94
|
+
'tr-TR': { selectPlaceholder: 'Bir öğe seçin' },
|
|
95
|
+
'uk-UA': { selectPlaceholder: 'Виберіть елемент' },
|
|
96
|
+
'zh-CN': { selectPlaceholder: '选择一个项目' },
|
|
97
|
+
'zh-TW': { selectPlaceholder: '選取項目' },
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
type SelectionMode = 'single' | 'multiple';
|
|
101
|
+
|
|
102
|
+
export interface SelectRenderProps {
|
|
103
|
+
/**
|
|
104
|
+
* Whether the select is focused, either via a mouse or keyboard.
|
|
105
|
+
*
|
|
106
|
+
* @selector [data-focused]
|
|
107
|
+
*/
|
|
108
|
+
isFocused: boolean;
|
|
109
|
+
/**
|
|
110
|
+
* Whether the select is keyboard focused.
|
|
111
|
+
*
|
|
112
|
+
* @selector [data-focus-visible]
|
|
113
|
+
*/
|
|
114
|
+
isFocusVisible: boolean;
|
|
115
|
+
/**
|
|
116
|
+
* Whether the select is disabled.
|
|
117
|
+
*
|
|
118
|
+
* @selector [data-disabled]
|
|
119
|
+
*/
|
|
120
|
+
isDisabled: boolean;
|
|
121
|
+
/**
|
|
122
|
+
* Whether the select is currently open.
|
|
123
|
+
*
|
|
124
|
+
* @selector [data-open]
|
|
125
|
+
*/
|
|
126
|
+
isOpen: boolean;
|
|
127
|
+
/**
|
|
128
|
+
* Whether the select is invalid.
|
|
129
|
+
*
|
|
130
|
+
* @selector [data-invalid]
|
|
131
|
+
*/
|
|
132
|
+
isInvalid: boolean;
|
|
133
|
+
/**
|
|
134
|
+
* Whether the select is required.
|
|
135
|
+
*
|
|
136
|
+
* @selector [data-required]
|
|
137
|
+
*/
|
|
138
|
+
isRequired: boolean;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export interface SelectProps<T, M extends SelectionMode = 'single'>
|
|
142
|
+
extends
|
|
143
|
+
Omit<
|
|
144
|
+
AriaSelectProps<T, M>,
|
|
145
|
+
| 'children'
|
|
146
|
+
| 'label'
|
|
147
|
+
| 'description'
|
|
148
|
+
| 'errorMessage'
|
|
149
|
+
| 'validationState'
|
|
150
|
+
| 'validationBehavior'
|
|
151
|
+
| 'items'
|
|
152
|
+
>,
|
|
153
|
+
RACValidation,
|
|
154
|
+
RenderProps<SelectRenderProps>,
|
|
155
|
+
SlotProps,
|
|
156
|
+
GlobalDOMAttributes {
|
|
157
|
+
/**
|
|
158
|
+
* The CSS [className](https://developer.mozilla.org/en-US/docs/Web/API/Element/className) for the
|
|
159
|
+
* element. A function may be provided to compute the class based on component state.
|
|
160
|
+
*
|
|
161
|
+
* @default 'react-aria-Select'
|
|
162
|
+
*/
|
|
163
|
+
className?: ClassNameOrFunction<SelectRenderProps>;
|
|
164
|
+
/**
|
|
165
|
+
* Temporary text that occupies the select when it is empty.
|
|
166
|
+
*
|
|
167
|
+
* @default 'Select an item' (localized)
|
|
168
|
+
*/
|
|
169
|
+
placeholder?: string;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export const SelectContext =
|
|
173
|
+
createContext<ContextValue<SelectProps<any, SelectionMode>, HTMLDivElement>>(null);
|
|
174
|
+
export const SelectStateContext = createContext<SelectState<unknown, SelectionMode> | null>(null);
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* A select displays a collapsible list of options and allows a user to select one of them.
|
|
178
|
+
*/
|
|
179
|
+
export const Select: <T, M extends SelectionMode = 'single'>(
|
|
180
|
+
props: SelectProps<T, M> & { ref?: any },
|
|
181
|
+
) => any = /*#__PURE__*/ createHideableComponent(function Select<
|
|
182
|
+
T,
|
|
183
|
+
M extends SelectionMode = 'single',
|
|
184
|
+
>(props: SelectProps<T, M>, ref: any): any {
|
|
185
|
+
const slot = S('Select');
|
|
186
|
+
[props, ref] = useContextProps(props as any, ref, SelectContext, subSlot(slot, 'ctx')) as any;
|
|
187
|
+
let { children, isDisabled = false, isInvalid = false, isRequired = false } = props;
|
|
188
|
+
let content = useMemo(
|
|
189
|
+
() =>
|
|
190
|
+
typeof children === 'function'
|
|
191
|
+
? children({
|
|
192
|
+
isOpen: false,
|
|
193
|
+
isDisabled,
|
|
194
|
+
isInvalid,
|
|
195
|
+
isRequired,
|
|
196
|
+
isFocused: false,
|
|
197
|
+
isFocusVisible: false,
|
|
198
|
+
defaultChildren: null,
|
|
199
|
+
})
|
|
200
|
+
: children,
|
|
201
|
+
[children, isDisabled, isInvalid, isRequired],
|
|
202
|
+
subSlot(slot, 'content'),
|
|
203
|
+
);
|
|
204
|
+
|
|
205
|
+
return createElement(CollectionBuilder, {
|
|
206
|
+
content,
|
|
207
|
+
children: (collection: ICollection<Node<any>>) =>
|
|
208
|
+
createElement(SelectInner as any, { props, collection, selectRef: ref }),
|
|
209
|
+
});
|
|
210
|
+
}) as any;
|
|
211
|
+
|
|
212
|
+
// Contexts to clear inside the popover.
|
|
213
|
+
const CLEAR_CONTEXTS = [LabelContext, ButtonContext, TextContext];
|
|
214
|
+
|
|
215
|
+
interface SelectInnerProps<T> {
|
|
216
|
+
props: SelectProps<T, SelectionMode>;
|
|
217
|
+
selectRef: RefObject<HTMLDivElement | null>;
|
|
218
|
+
collection: ICollection<Node<T>>;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function SelectInner<T>({ props, selectRef: ref, collection }: SelectInnerProps<T>): any {
|
|
222
|
+
const slot = S('SelectInner');
|
|
223
|
+
let { validationBehavior: formValidationBehavior } = useSlottedContext(FormContext) || {};
|
|
224
|
+
let validationBehavior = props.validationBehavior ?? formValidationBehavior ?? 'native';
|
|
225
|
+
let state = useSelectState(
|
|
226
|
+
{
|
|
227
|
+
...props,
|
|
228
|
+
collection,
|
|
229
|
+
children: undefined,
|
|
230
|
+
validationBehavior,
|
|
231
|
+
} as any,
|
|
232
|
+
subSlot(slot, 'state'),
|
|
233
|
+
);
|
|
234
|
+
|
|
235
|
+
let { isFocusVisible, focusProps } = useFocusRing({ within: true }, subSlot(slot, 'focusRing'));
|
|
236
|
+
|
|
237
|
+
// Get props for child elements from useSelect
|
|
238
|
+
let buttonRef = useRef<HTMLButtonElement | null>(null, subSlot(slot, 'buttonRef'));
|
|
239
|
+
let [labelRef, label] = useSlot(
|
|
240
|
+
!props['aria-label'] && !props['aria-labelledby'],
|
|
241
|
+
subSlot(slot, 'labelSlot'),
|
|
242
|
+
);
|
|
243
|
+
let {
|
|
244
|
+
labelProps,
|
|
245
|
+
triggerProps,
|
|
246
|
+
valueProps,
|
|
247
|
+
menuProps,
|
|
248
|
+
descriptionProps,
|
|
249
|
+
errorMessageProps,
|
|
250
|
+
hiddenSelectProps,
|
|
251
|
+
...validation
|
|
252
|
+
} = useSelect(
|
|
253
|
+
{
|
|
254
|
+
...removeDataAttributes(props),
|
|
255
|
+
label,
|
|
256
|
+
validationBehavior,
|
|
257
|
+
} as any,
|
|
258
|
+
state,
|
|
259
|
+
buttonRef,
|
|
260
|
+
subSlot(slot, 'select'),
|
|
261
|
+
);
|
|
262
|
+
|
|
263
|
+
// Only expose a subset of state to renderProps function to avoid infinite render loop
|
|
264
|
+
let renderPropsState = useMemo(
|
|
265
|
+
() => ({
|
|
266
|
+
isOpen: state.isOpen,
|
|
267
|
+
isFocused: state.isFocused,
|
|
268
|
+
isFocusVisible,
|
|
269
|
+
isDisabled: props.isDisabled || false,
|
|
270
|
+
isInvalid: validation.isInvalid || false,
|
|
271
|
+
isRequired: props.isRequired || false,
|
|
272
|
+
}),
|
|
273
|
+
[
|
|
274
|
+
state.isOpen,
|
|
275
|
+
state.isFocused,
|
|
276
|
+
isFocusVisible,
|
|
277
|
+
props.isDisabled,
|
|
278
|
+
validation.isInvalid,
|
|
279
|
+
props.isRequired,
|
|
280
|
+
],
|
|
281
|
+
subSlot(slot, 'renderPropsState'),
|
|
282
|
+
);
|
|
283
|
+
|
|
284
|
+
let renderProps = useRenderProps(
|
|
285
|
+
{
|
|
286
|
+
...props,
|
|
287
|
+
values: renderPropsState,
|
|
288
|
+
defaultClassName: 'react-aria-Select',
|
|
289
|
+
} as any,
|
|
290
|
+
subSlot(slot, 'render'),
|
|
291
|
+
);
|
|
292
|
+
|
|
293
|
+
let DOMProps = filterDOMProps(props, { global: true });
|
|
294
|
+
delete DOMProps.id;
|
|
295
|
+
|
|
296
|
+
let scrollRef = useRef(null, subSlot(slot, 'scrollRef'));
|
|
297
|
+
|
|
298
|
+
return createElement(Provider, {
|
|
299
|
+
values: [
|
|
300
|
+
[SelectContext, props],
|
|
301
|
+
[SelectStateContext, state],
|
|
302
|
+
[SelectValueContext, valueProps],
|
|
303
|
+
[LabelContext, { ...labelProps, ref: labelRef, elementType: 'span' }],
|
|
304
|
+
[
|
|
305
|
+
ButtonContext,
|
|
306
|
+
{ ...triggerProps, ref: buttonRef, isPressed: state.isOpen, autoFocus: props.autoFocus },
|
|
307
|
+
],
|
|
308
|
+
[OverlayTriggerStateContext, state],
|
|
309
|
+
[
|
|
310
|
+
PopoverContext,
|
|
311
|
+
{
|
|
312
|
+
trigger: 'Select',
|
|
313
|
+
triggerRef: buttonRef,
|
|
314
|
+
scrollRef,
|
|
315
|
+
placement: 'bottom start',
|
|
316
|
+
'aria-labelledby': (menuProps as any)['aria-labelledby'],
|
|
317
|
+
clearContexts: CLEAR_CONTEXTS,
|
|
318
|
+
},
|
|
319
|
+
],
|
|
320
|
+
[ListBoxContext, { ...menuProps, ref: scrollRef }],
|
|
321
|
+
[ListStateContext, state],
|
|
322
|
+
[
|
|
323
|
+
TextContext,
|
|
324
|
+
{
|
|
325
|
+
slots: {
|
|
326
|
+
description: descriptionProps,
|
|
327
|
+
errorMessage: errorMessageProps,
|
|
328
|
+
},
|
|
329
|
+
},
|
|
330
|
+
],
|
|
331
|
+
[FieldErrorContext, validation],
|
|
332
|
+
] as any,
|
|
333
|
+
children: createElement(
|
|
334
|
+
dom.div,
|
|
335
|
+
{
|
|
336
|
+
...mergeProps(DOMProps, renderProps, focusProps),
|
|
337
|
+
ref,
|
|
338
|
+
slot: props.slot || undefined,
|
|
339
|
+
'data-focused': state.isFocused || undefined,
|
|
340
|
+
'data-focus-visible': isFocusVisible || undefined,
|
|
341
|
+
'data-open': state.isOpen || undefined,
|
|
342
|
+
'data-disabled': props.isDisabled || undefined,
|
|
343
|
+
'data-invalid': validation.isInvalid || undefined,
|
|
344
|
+
'data-required': props.isRequired || undefined,
|
|
345
|
+
},
|
|
346
|
+
renderProps.children,
|
|
347
|
+
createElement(HiddenSelect as any, {
|
|
348
|
+
...hiddenSelectProps,
|
|
349
|
+
autoComplete: props.autoComplete,
|
|
350
|
+
}),
|
|
351
|
+
),
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
export interface SelectValueRenderProps<T> {
|
|
356
|
+
/**
|
|
357
|
+
* Whether the value is a placeholder.
|
|
358
|
+
*
|
|
359
|
+
* @selector [data-placeholder]
|
|
360
|
+
*/
|
|
361
|
+
isPlaceholder: boolean;
|
|
362
|
+
/**
|
|
363
|
+
* The object value of the first selected item.
|
|
364
|
+
*
|
|
365
|
+
* @deprecated
|
|
366
|
+
*/
|
|
367
|
+
selectedItem: T | null;
|
|
368
|
+
/** The object values of the currently selected items. */
|
|
369
|
+
selectedItems: (T | null)[];
|
|
370
|
+
/** The textValue of the currently selected items. */
|
|
371
|
+
selectedText: string;
|
|
372
|
+
/** The state of the select. */
|
|
373
|
+
state: SelectState<T, 'single' | 'multiple'>;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
export interface SelectValueProps<T>
|
|
377
|
+
extends
|
|
378
|
+
Omit<HTMLAttributes, keyof RenderProps<unknown>>,
|
|
379
|
+
RenderProps<SelectValueRenderProps<T>, 'span'> {
|
|
380
|
+
/**
|
|
381
|
+
* The CSS [className](https://developer.mozilla.org/en-US/docs/Web/API/Element/className) for the
|
|
382
|
+
* element. A function may be provided to compute the class based on component state.
|
|
383
|
+
*
|
|
384
|
+
* @default 'react-aria-SelectValue'
|
|
385
|
+
*/
|
|
386
|
+
className?: ClassNameOrFunction<SelectValueRenderProps<T>>;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
export const SelectValueContext =
|
|
390
|
+
createContext<ContextValue<SelectValueProps<any>, HTMLSpanElement>>(null);
|
|
391
|
+
|
|
392
|
+
/**
|
|
393
|
+
* SelectValue renders the current value of a Select, or a placeholder if no value is selected.
|
|
394
|
+
* It is usually placed within the button element.
|
|
395
|
+
*/
|
|
396
|
+
export const SelectValue: <T>(props: SelectValueProps<T> & { ref?: any }) => any =
|
|
397
|
+
/*#__PURE__*/ createHideableComponent(function SelectValue<T>(
|
|
398
|
+
props: SelectValueProps<T>,
|
|
399
|
+
ref: any,
|
|
400
|
+
): any {
|
|
401
|
+
const slot = S('SelectValue');
|
|
402
|
+
[props, ref] = useContextProps(props, ref, SelectValueContext, subSlot(slot, 'ctx'));
|
|
403
|
+
let state = useContext(SelectStateContext)! as SelectState<T, 'single' | 'multiple'>;
|
|
404
|
+
let { placeholder } = useSlottedContext(SelectContext)!;
|
|
405
|
+
let rendered = state.selectedItems.map((item) => {
|
|
406
|
+
let rendered = item.props?.children;
|
|
407
|
+
// If the selected item has a function as a child, we need to call it to render an element.
|
|
408
|
+
if (typeof rendered === 'function') {
|
|
409
|
+
let fn = rendered as (s: ItemRenderProps) => ReactNode;
|
|
410
|
+
rendered = fn({
|
|
411
|
+
isHovered: false,
|
|
412
|
+
isPressed: false,
|
|
413
|
+
isSelected: false,
|
|
414
|
+
isFocused: false,
|
|
415
|
+
isFocusVisible: false,
|
|
416
|
+
isDisabled: false,
|
|
417
|
+
selectionMode: 'single',
|
|
418
|
+
selectionBehavior: 'toggle',
|
|
419
|
+
});
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
return rendered;
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
let formatter = useListFormatter(undefined, subSlot(slot, 'formatter'));
|
|
426
|
+
let textValue = useMemo(
|
|
427
|
+
() => state.selectedItems.map((item) => item?.textValue),
|
|
428
|
+
[state.selectedItems],
|
|
429
|
+
subSlot(slot, 'textValue'),
|
|
430
|
+
);
|
|
431
|
+
let selectionMode = state.selectionManager.selectionMode;
|
|
432
|
+
let selectedText = useMemo(
|
|
433
|
+
() => (selectionMode === 'single' ? (textValue[0] ?? '') : formatter.format(textValue)),
|
|
434
|
+
[selectionMode, formatter, textValue],
|
|
435
|
+
subSlot(slot, 'selectedText'),
|
|
436
|
+
);
|
|
437
|
+
|
|
438
|
+
let defaultChildren = useMemo(
|
|
439
|
+
() => {
|
|
440
|
+
if (selectionMode === 'single') {
|
|
441
|
+
return rendered[0];
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
let parts = formatter.formatToParts(textValue);
|
|
445
|
+
if (parts.length === 0) {
|
|
446
|
+
return null;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
let index = 0;
|
|
450
|
+
return parts.map((part) => {
|
|
451
|
+
if (part.type === 'element') {
|
|
452
|
+
return createElement(Fragment, { key: index }, rendered[index++]);
|
|
453
|
+
} else {
|
|
454
|
+
return part.value;
|
|
455
|
+
}
|
|
456
|
+
});
|
|
457
|
+
},
|
|
458
|
+
[selectionMode, formatter, textValue, rendered],
|
|
459
|
+
subSlot(slot, 'defaultChildren'),
|
|
460
|
+
);
|
|
461
|
+
|
|
462
|
+
let stringFormatter = useLocalizedStringFormatter(
|
|
463
|
+
intlMessages,
|
|
464
|
+
'react-aria-components',
|
|
465
|
+
subSlot(slot, 'stringFormatter'),
|
|
466
|
+
);
|
|
467
|
+
|
|
468
|
+
// octane adaptation: hook hoisted out of the useRenderProps argument object below.
|
|
469
|
+
let selectedItems = useMemo(
|
|
470
|
+
() => state.selectedItems.map((item) => (item.value as T) ?? null),
|
|
471
|
+
[state.selectedItems],
|
|
472
|
+
subSlot(slot, 'selectedItems'),
|
|
473
|
+
);
|
|
474
|
+
|
|
475
|
+
let renderProps = useRenderProps(
|
|
476
|
+
{
|
|
477
|
+
...props,
|
|
478
|
+
defaultChildren:
|
|
479
|
+
defaultChildren ?? placeholder ?? stringFormatter.format('selectPlaceholder'),
|
|
480
|
+
defaultClassName: 'react-aria-SelectValue',
|
|
481
|
+
values: {
|
|
482
|
+
selectedItem: (state.selectedItems[0]?.value as T) ?? null,
|
|
483
|
+
selectedItems,
|
|
484
|
+
selectedText,
|
|
485
|
+
isPlaceholder: state.selectedItems.length === 0,
|
|
486
|
+
state,
|
|
487
|
+
},
|
|
488
|
+
} as any,
|
|
489
|
+
subSlot(slot, 'render'),
|
|
490
|
+
);
|
|
491
|
+
|
|
492
|
+
let DOMProps = filterDOMProps(props, { global: true });
|
|
493
|
+
|
|
494
|
+
return createElement(dom.span, {
|
|
495
|
+
ref,
|
|
496
|
+
...DOMProps,
|
|
497
|
+
...renderProps,
|
|
498
|
+
'data-placeholder': state.selectedItems.length === 0 || undefined,
|
|
499
|
+
// clear description and error message slots
|
|
500
|
+
children: createElement(TextContext.Provider, {
|
|
501
|
+
value: undefined,
|
|
502
|
+
children: renderProps.children,
|
|
503
|
+
}),
|
|
504
|
+
});
|
|
505
|
+
}) as any;
|