react-aria-components 0.0.4 → 1.0.0-alpha.2

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 (91) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +12 -29
  3. package/dist/import.mjs +5223 -0
  4. package/dist/main.js +5345 -0
  5. package/dist/main.js.map +1 -0
  6. package/dist/module.js +5223 -0
  7. package/dist/module.js.map +1 -0
  8. package/dist/types.d.ts +1481 -0
  9. package/dist/types.d.ts.map +1 -0
  10. package/package.json +36 -44
  11. package/src/Breadcrumbs.tsx +90 -0
  12. package/src/Button.tsx +79 -0
  13. package/src/Calendar.tsx +471 -0
  14. package/src/Checkbox.tsx +240 -0
  15. package/src/Collection.tsx +787 -0
  16. package/src/ComboBox.tsx +129 -0
  17. package/src/DateField.tsx +244 -0
  18. package/src/DatePicker.tsx +178 -0
  19. package/src/Dialog.tsx +95 -0
  20. package/src/GridList.tsx +411 -0
  21. package/src/Group.tsx +69 -0
  22. package/src/Header.tsx +34 -0
  23. package/src/Heading.tsx +35 -0
  24. package/src/Input.tsx +73 -0
  25. package/src/Keyboard.tsx +24 -0
  26. package/src/Label.tsx +30 -0
  27. package/src/Link.tsx +98 -0
  28. package/src/ListBox.tsx +406 -0
  29. package/src/Menu.tsx +227 -0
  30. package/src/Meter.tsx +73 -0
  31. package/src/Modal.tsx +192 -0
  32. package/src/NumberField.tsx +79 -0
  33. package/src/OverlayArrow.tsx +70 -0
  34. package/src/Popover.tsx +125 -0
  35. package/src/ProgressBar.tsx +81 -0
  36. package/src/RadioGroup.tsx +219 -0
  37. package/src/SearchField.tsx +78 -0
  38. package/src/Select.tsx +180 -0
  39. package/src/Separator.tsx +53 -0
  40. package/src/Slider.tsx +217 -0
  41. package/src/Switch.tsx +127 -0
  42. package/src/Table.tsx +917 -0
  43. package/src/Tabs.tsx +282 -0
  44. package/src/Text.tsx +30 -0
  45. package/src/TextField.tsx +62 -0
  46. package/src/ToggleButton.tsx +59 -0
  47. package/src/Tooltip.tsx +135 -0
  48. package/src/index.ts +94 -0
  49. package/src/useDragAndDrop.tsx +172 -0
  50. package/src/utils.tsx +235 -0
  51. package/.babelrc +0 -17
  52. package/.eslintrc +0 -12
  53. package/src/accordion/accordion.css +0 -4
  54. package/src/accordion/accordion.js +0 -20
  55. package/src/accordion/index.js +0 -2
  56. package/src/accordion/section.css +0 -21
  57. package/src/accordion/section.js +0 -85
  58. package/src/examples/accordion-example.js +0 -73
  59. package/src/examples/example.js +0 -21
  60. package/src/examples/grid-cells/fancy-input-grid-cell.js +0 -206
  61. package/src/examples/grid-cells/input-grid-cell.js +0 -44
  62. package/src/examples/grid-example.css +0 -23
  63. package/src/examples/grid-example.js +0 -231
  64. package/src/examples/index.html +0 -10
  65. package/src/examples/index.js +0 -53
  66. package/src/examples/tabs-example.js +0 -52
  67. package/src/grid/column-header.css +0 -4
  68. package/src/grid/column-header.js +0 -29
  69. package/src/grid/grid-cell.css +0 -16
  70. package/src/grid/grid-cell.js +0 -104
  71. package/src/grid/grid-context.js +0 -3
  72. package/src/grid/grid.css +0 -4
  73. package/src/grid/grid.js +0 -110
  74. package/src/grid/index.js +0 -6
  75. package/src/grid/interactive-grid-cell.js +0 -96
  76. package/src/grid/row-headers.css +0 -6
  77. package/src/grid/row-headers.js +0 -22
  78. package/src/grid/row.css +0 -5
  79. package/src/grid/row.js +0 -21
  80. package/src/prop-types/ref.js +0 -3
  81. package/src/tabs/index.js +0 -4
  82. package/src/tabs/tab-list.css +0 -6
  83. package/src/tabs/tab-list.js +0 -103
  84. package/src/tabs/tab-panels.js +0 -35
  85. package/src/tabs/tab.css +0 -10
  86. package/src/tabs/tab.js +0 -45
  87. package/src/tabs/tabs.js +0 -52
  88. package/src/utils/debounce.js +0 -12
  89. package/src/utils/event-handlers-factory.js +0 -42
  90. package/src/utils/unique-id.js +0 -6
  91. package/webpack.config.js +0 -43
@@ -0,0 +1,125 @@
1
+ /*
2
+ * Copyright 2022 Adobe. All rights reserved.
3
+ * This file is licensed to you under the Apache License, Version 2.0 (the "License");
4
+ * you may not use this file except in compliance with the License. You may obtain a copy
5
+ * of the License at http://www.apache.org/licenses/LICENSE-2.0
6
+ *
7
+ * Unless required by applicable law or agreed to in writing, software distributed under
8
+ * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
9
+ * OF ANY KIND, either express or implied. See the License for the specific language
10
+ * governing permissions and limitations under the License.
11
+ */
12
+
13
+ import {AriaPopoverProps, DismissButton, Overlay, PlacementAxis, PositionProps, usePopover} from 'react-aria';
14
+ import {ContextValue, HiddenContext, RenderProps, SlotProps, useContextProps, useEnterAnimation, useExitAnimation, useRenderProps} from './utils';
15
+ import {OverlayArrowContext} from './OverlayArrow';
16
+ import {OverlayTriggerState} from 'react-stately';
17
+ import React, {createContext, ForwardedRef, forwardRef, RefObject} from 'react';
18
+
19
+ export interface PopoverProps extends Omit<PositionProps, 'isOpen'>, Omit<AriaPopoverProps, 'popoverRef' | 'triggerRef'>, RenderProps<PopoverRenderProps>, SlotProps {
20
+ /**
21
+ * The ref for the element which the popover positions itself with respect to.
22
+ *
23
+ * When used within a trigger component such as DialogTrigger, MenuTrigger, Select, etc.,
24
+ * this is set automatically. It is only required when used standalone.
25
+ */
26
+ triggerRef?: RefObject<Element>
27
+ }
28
+
29
+ export interface PopoverRenderProps {
30
+ /**
31
+ * The placement of the tooltip relative to the trigger.
32
+ * @selector [data-placement="left | right | top | bottom"]
33
+ */
34
+ placement: PlacementAxis,
35
+ /**
36
+ * Whether the popover is currently entering. Use this to apply animations.
37
+ * @selector [data-entering]
38
+ */
39
+ isEntering: boolean,
40
+ /**
41
+ * Whether the popover is currently exiting. Use this to apply animations.
42
+ * @selector [data-exiting]
43
+ */
44
+ isExiting: boolean
45
+ }
46
+
47
+ interface PopoverContextValue extends PopoverProps {
48
+ state: OverlayTriggerState,
49
+ preserveChildren?: boolean,
50
+ triggerRef: RefObject<Element>
51
+ }
52
+
53
+ export const PopoverContext = createContext<ContextValue<PopoverContextValue, HTMLElement>>(null);
54
+
55
+ function Popover(props: PopoverProps, ref: ForwardedRef<HTMLElement>) {
56
+ [props, ref] = useContextProps(props, ref, PopoverContext);
57
+ let {preserveChildren, state, triggerRef} = props as PopoverContextValue;
58
+ let isExiting = useExitAnimation(ref, state.isOpen);
59
+
60
+ if (state && !state.isOpen && !isExiting) {
61
+ return preserveChildren ? <HiddenContext.Provider value>{props.children}</HiddenContext.Provider> : null;
62
+ }
63
+
64
+ return (
65
+ <PopoverInner
66
+ {...props}
67
+ triggerRef={triggerRef}
68
+ state={state}
69
+ popoverRef={ref}
70
+ isExiting={isExiting} />
71
+ );
72
+ }
73
+
74
+ /**
75
+ * A popover is an overlay element positioned relative to a trigger.
76
+ */
77
+ const _Popover = forwardRef(Popover);
78
+ export {_Popover as Popover};
79
+
80
+ interface PopoverInnerProps extends AriaPopoverProps, RenderProps<PopoverRenderProps>, SlotProps {
81
+ state: OverlayTriggerState,
82
+ isExiting: boolean
83
+ }
84
+
85
+ function PopoverInner({children, state, isExiting, ...props}: PopoverInnerProps) {
86
+ let {popoverProps, underlayProps, arrowProps, placement} = usePopover({
87
+ ...props,
88
+ offset: props.offset ?? 8
89
+ }, state);
90
+
91
+ let ref = props.popoverRef as RefObject<HTMLDivElement>;
92
+ let isEntering = useEnterAnimation(ref, !!placement);
93
+ let renderProps = useRenderProps({
94
+ ...props,
95
+ defaultClassName: 'react-aria-Popover',
96
+ values: {
97
+ placement,
98
+ isEntering,
99
+ isExiting
100
+ }
101
+ });
102
+
103
+ let style = {...renderProps.style, ...popoverProps.style};
104
+
105
+ return (
106
+ <Overlay>
107
+ {!props.isNonModal && <div {...underlayProps} style={{position: 'fixed', inset: 0}} />}
108
+ <div
109
+ {...popoverProps}
110
+ {...renderProps}
111
+ ref={ref}
112
+ slot={props.slot}
113
+ style={style}
114
+ data-placement={placement}
115
+ data-entering={isEntering || undefined}
116
+ data-exiting={isExiting || undefined}>
117
+ {!props.isNonModal && <DismissButton onDismiss={state.close} />}
118
+ <OverlayArrowContext.Provider value={{arrowProps, placement}}>
119
+ {children}
120
+ </OverlayArrowContext.Provider>
121
+ <DismissButton onDismiss={state.close} />
122
+ </div>
123
+ </Overlay>
124
+ );
125
+ }
@@ -0,0 +1,81 @@
1
+ /*
2
+ * Copyright 2022 Adobe. All rights reserved.
3
+ * This file is licensed to you under the Apache License, Version 2.0 (the "License");
4
+ * you may not use this file except in compliance with the License. You may obtain a copy
5
+ * of the License at http://www.apache.org/licenses/LICENSE-2.0
6
+ *
7
+ * Unless required by applicable law or agreed to in writing, software distributed under
8
+ * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
9
+ * OF ANY KIND, either express or implied. See the License for the specific language
10
+ * governing permissions and limitations under the License.
11
+ */
12
+
13
+ import {AriaProgressBarProps, useProgressBar} from 'react-aria';
14
+ import {ContextValue, RenderProps, SlotProps, useContextProps, useRenderProps, useSlot} from './utils';
15
+ import {LabelContext} from './Label';
16
+ import React, {createContext, ForwardedRef, forwardRef} from 'react';
17
+
18
+ export interface ProgressBarProps extends Omit<AriaProgressBarProps, 'label'>, RenderProps<ProgressBarRenderProps>, SlotProps {}
19
+
20
+ export interface ProgressBarRenderProps {
21
+ /**
22
+ * The value as a percentage between the minimum and maximum.
23
+ */
24
+ percentage?: number,
25
+ /**
26
+ * A formatted version of the value.
27
+ * @selector [aria-valuetext]
28
+ */
29
+ valueText?: string,
30
+ /**
31
+ * Whether the progress bar is indeterminate.
32
+ * @selector :not([aria-valuenow])
33
+ */
34
+ isIndeterminate: boolean
35
+ }
36
+
37
+ export const ProgressBarContext = createContext<ContextValue<ProgressBarProps, HTMLDivElement>>(null);
38
+
39
+ function ProgressBar(props: ProgressBarProps, ref: ForwardedRef<HTMLDivElement>) {
40
+ [props, ref] = useContextProps(props, ref, ProgressBarContext);
41
+ let {
42
+ value = 0,
43
+ minValue = 0,
44
+ maxValue = 100,
45
+ isIndeterminate = false
46
+ } = props;
47
+
48
+ let [labelRef, label] = useSlot();
49
+ let {
50
+ progressBarProps,
51
+ labelProps
52
+ } = useProgressBar({...props, label});
53
+
54
+ // Calculate the width of the progress bar as a percentage
55
+ let percentage = isIndeterminate ? undefined : (value - minValue) / (maxValue - minValue) * 100;
56
+
57
+ let renderProps = useRenderProps({
58
+ ...props,
59
+ defaultClassName: 'react-aria-ProgressBar',
60
+ values: {
61
+ percentage,
62
+ valueText: progressBarProps['aria-valuetext'],
63
+ isIndeterminate
64
+ }
65
+ });
66
+
67
+ return (
68
+ <div {...progressBarProps} {...renderProps} ref={ref} slot={props.slot}>
69
+ <LabelContext.Provider value={{...labelProps, ref: labelRef, elementType: 'span'}}>
70
+ {renderProps.children}
71
+ </LabelContext.Provider>
72
+ </div>
73
+ );
74
+ }
75
+
76
+ /**
77
+ * Progress bars show either determinate or indeterminate progress of an operation
78
+ * over time.
79
+ */
80
+ const _ProgressBar = forwardRef(ProgressBar);
81
+ export {_ProgressBar as ProgressBar};
@@ -0,0 +1,219 @@
1
+ /*
2
+ * Copyright 2022 Adobe. All rights reserved.
3
+ * This file is licensed to you under the Apache License, Version 2.0 (the "License");
4
+ * you may not use this file except in compliance with the License. You may obtain a copy
5
+ * of the License at http://www.apache.org/licenses/LICENSE-2.0
6
+ *
7
+ * Unless required by applicable law or agreed to in writing, software distributed under
8
+ * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
9
+ * OF ANY KIND, either express or implied. See the License for the specific language
10
+ * governing permissions and limitations under the License.
11
+ */
12
+
13
+ import {AriaRadioGroupProps, AriaRadioProps, Orientation, useFocusRing, useHover, usePress, useRadio, useRadioGroup, VisuallyHidden} from 'react-aria';
14
+ import {ContextValue, Provider, RenderProps, SlotProps, useContextProps, useRenderProps, useSlot} from './utils';
15
+ import {LabelContext} from './Label';
16
+ import {mergeProps, useObjectRef} from '@react-aria/utils';
17
+ import {RadioGroupState, useRadioGroupState, ValidationState} from 'react-stately';
18
+ import React, {createContext, ForwardedRef, forwardRef, useState} from 'react';
19
+ import {TextContext} from './Text';
20
+
21
+ export interface RadioGroupProps extends Omit<AriaRadioGroupProps, 'children' | 'label' | 'description' | 'errorMessage'>, RenderProps<RadioGroupRenderProps>, SlotProps {}
22
+ export interface RadioProps extends Omit<AriaRadioProps, 'children'>, RenderProps<RadioRenderProps> {}
23
+
24
+ export interface RadioGroupRenderProps {
25
+ /**
26
+ * The orientation of the radio group.
27
+ * @selector [aria-orientation="horizontal | vertical"]
28
+ */
29
+ orientation: Orientation,
30
+ /**
31
+ * Whether the radio group is disabled.
32
+ * @selector [aria-disabled]
33
+ */
34
+ isDisabled: boolean,
35
+ /**
36
+ * Whether the radio group is read only.
37
+ * @selector [aria-readonly]
38
+ */
39
+ isReadOnly: boolean,
40
+ /**
41
+ * Whether the radio group is required.
42
+ * @selector [aria-required]
43
+ */
44
+ isRequired: boolean,
45
+ /**
46
+ * The validation state of the radio group.
47
+ * @selector [aria-invalid]
48
+ */
49
+ validationState: ValidationState | null
50
+ }
51
+
52
+ export interface RadioRenderProps {
53
+ /**
54
+ * Whether the radio is selected.
55
+ * @selector [data-selected]
56
+ */
57
+ isSelected: boolean,
58
+ /**
59
+ * Whether the radio is currently hovered with a mouse.
60
+ * @selector [data-hovered]
61
+ */
62
+ isHovered: boolean,
63
+ /**
64
+ * Whether the radio is currently in a pressed state.
65
+ * @selector [data-pressed]
66
+ */
67
+ isPressed: boolean,
68
+ /**
69
+ * Whether the radio is focused, either via a mouse or keyboard.
70
+ * @selector [data-focused]
71
+ */
72
+ isFocused: boolean,
73
+ /**
74
+ * Whether the radio is keyboard focused.
75
+ * @selector [data-focus-visible]
76
+ */
77
+ isFocusVisible: boolean,
78
+ /**
79
+ * Whether the radio is disabled.
80
+ * @selector [data-disabled]
81
+ */
82
+ isDisabled: boolean,
83
+ /**
84
+ * Whether the radio is read only.
85
+ * @selector [data-readonly]
86
+ */
87
+ isReadOnly: boolean,
88
+ /**
89
+ * Whether the radio is valid or invalid.
90
+ * @selector [data-validation-state="valid | invalid"]
91
+ */
92
+ validationState: ValidationState | null,
93
+ /**
94
+ * Whether the checkbox is required.
95
+ * @selector [data-required]
96
+ */
97
+ isRequired: boolean
98
+ }
99
+
100
+ export const RadioGroupContext = createContext<ContextValue<RadioGroupProps, HTMLDivElement>>(null);
101
+ let InternalRadioContext = createContext<RadioGroupState | null>(null);
102
+
103
+ function RadioGroup(props: RadioGroupProps, ref: ForwardedRef<HTMLDivElement>) {
104
+ [props, ref] = useContextProps(props, ref, RadioGroupContext);
105
+ let state = useRadioGroupState(props);
106
+ let [labelRef, label] = useSlot();
107
+ let {radioGroupProps, labelProps, descriptionProps, errorMessageProps} = useRadioGroup({
108
+ ...props,
109
+ label
110
+ }, state);
111
+
112
+ let renderProps = useRenderProps({
113
+ ...props,
114
+ values: {
115
+ orientation: props.orientation || 'vertical',
116
+ isDisabled: state.isDisabled,
117
+ isReadOnly: state.isReadOnly,
118
+ isRequired: state.isRequired,
119
+ validationState: state.validationState
120
+ },
121
+ defaultClassName: 'react-aria-RadioGroup'
122
+ });
123
+
124
+ return (
125
+ <div {...radioGroupProps} {...renderProps} ref={ref} slot={props.slot}>
126
+ <Provider
127
+ values={[
128
+ [InternalRadioContext, state],
129
+ [LabelContext, {...labelProps, ref: labelRef}],
130
+ [TextContext, {
131
+ slots: {
132
+ description: descriptionProps,
133
+ errorMessage: errorMessageProps
134
+ }
135
+ }]
136
+ ]}>
137
+ {renderProps.children}
138
+ </Provider>
139
+ </div>
140
+ );
141
+ }
142
+
143
+ function Radio(props: RadioProps, ref: ForwardedRef<HTMLInputElement>) {
144
+ let state = React.useContext(InternalRadioContext)!;
145
+ let domRef = useObjectRef(ref);
146
+ let {inputProps, isSelected, isDisabled, isPressed: isPressedKeyboard} = useRadio(props, state, domRef);
147
+ let {isFocused, isFocusVisible, focusProps} = useFocusRing();
148
+ let interactionDisabled = isDisabled || state.isReadOnly;
149
+
150
+ // Handle press state for full label. Keyboard press state is returned by useCheckbox
151
+ // since it is handled on the <input> element itself.
152
+ let [isPressed, setPressed] = useState(false);
153
+ let {pressProps} = usePress({
154
+ isDisabled: interactionDisabled,
155
+ onPressStart(e) {
156
+ if (e.pointerType !== 'keyboard') {
157
+ setPressed(true);
158
+ }
159
+ },
160
+ onPressEnd(e) {
161
+ if (e.pointerType !== 'keyboard') {
162
+ setPressed(false);
163
+ }
164
+ }
165
+ });
166
+
167
+ let {hoverProps, isHovered} = useHover({
168
+ isDisabled: interactionDisabled
169
+ });
170
+
171
+ let pressed = interactionDisabled ? false : (isPressed || isPressedKeyboard);
172
+
173
+ let renderProps = useRenderProps({
174
+ ...props,
175
+ defaultClassName: 'react-aria-Radio',
176
+ values: {
177
+ isSelected,
178
+ isPressed: pressed,
179
+ isHovered,
180
+ isFocused,
181
+ isFocusVisible,
182
+ isDisabled,
183
+ isReadOnly: state.isReadOnly,
184
+ validationState: state.validationState,
185
+ isRequired: state.isRequired
186
+ }
187
+ });
188
+
189
+ return (
190
+ <label
191
+ {...mergeProps(pressProps, hoverProps, renderProps)}
192
+ data-selected={isSelected || undefined}
193
+ data-pressed={pressed || undefined}
194
+ data-hovered={isHovered || undefined}
195
+ data-focused={isFocused || undefined}
196
+ data-focus-visible={isFocusVisible || undefined}
197
+ data-disabled={isDisabled || undefined}
198
+ data-readonly={state.isReadOnly || undefined}
199
+ data-validation-state={state.validationState || undefined}
200
+ data-required={state.isRequired || undefined}>
201
+ <VisuallyHidden>
202
+ <input {...inputProps} {...focusProps} ref={domRef} />
203
+ </VisuallyHidden>
204
+ {renderProps.children}
205
+ </label>
206
+ );
207
+ }
208
+
209
+ /**
210
+ * A radio group allows a user to select a single item from a list of mutually exclusive options.
211
+ */
212
+ const _RadioGroup = forwardRef(RadioGroup);
213
+
214
+ /**
215
+ * A radio represents an individual option within a radio group.
216
+ */
217
+ const _Radio = forwardRef(Radio);
218
+
219
+ export {_RadioGroup as RadioGroup, _Radio as Radio};
@@ -0,0 +1,78 @@
1
+ /*
2
+ * Copyright 2022 Adobe. All rights reserved.
3
+ * This file is licensed to you under the Apache License, Version 2.0 (the "License");
4
+ * you may not use this file except in compliance with the License. You may obtain a copy
5
+ * of the License at http://www.apache.org/licenses/LICENSE-2.0
6
+ *
7
+ * Unless required by applicable law or agreed to in writing, software distributed under
8
+ * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
9
+ * OF ANY KIND, either express or implied. See the License for the specific language
10
+ * governing permissions and limitations under the License.
11
+ */
12
+
13
+ import {AriaSearchFieldProps, useSearchField} from 'react-aria';
14
+ import {ButtonContext} from './Button';
15
+ import {ContextValue, Provider, RenderProps, SlotProps, useContextProps, useRenderProps, useSlot} from './utils';
16
+ import {InputContext} from './Input';
17
+ import {LabelContext} from './Label';
18
+ import React, {createContext, ForwardedRef, forwardRef, useRef} from 'react';
19
+ import {SearchFieldState, useSearchFieldState} from 'react-stately';
20
+ import {TextContext} from './Text';
21
+
22
+ export interface SearchFieldProps extends Omit<AriaSearchFieldProps, 'label' | 'placeholder' | 'description' | 'errorMessage'>, RenderProps<SearchFieldState>, SlotProps {}
23
+
24
+ export interface SearchFieldRenderProps {
25
+ /**
26
+ * Whether the search field is empty.
27
+ * @selector [data-empty]
28
+ */
29
+ isEmpty: boolean
30
+ }
31
+
32
+ export const SearchFieldContext = createContext<ContextValue<SearchFieldProps, HTMLDivElement>>(null);
33
+
34
+ function SearchField(props: SearchFieldProps, ref: ForwardedRef<HTMLDivElement>) {
35
+ [props, ref] = useContextProps(props, ref, SearchFieldContext);
36
+ let inputRef = useRef<HTMLInputElement>(null);
37
+ let [labelRef, label] = useSlot();
38
+ let state = useSearchFieldState(props);
39
+ let {labelProps, inputProps, clearButtonProps, descriptionProps, errorMessageProps} = useSearchField({
40
+ ...props,
41
+ label
42
+ }, state, inputRef);
43
+
44
+ let renderProps = useRenderProps({
45
+ ...props,
46
+ values: state,
47
+ defaultClassName: 'react-aria-SearchField'
48
+ });
49
+
50
+ return (
51
+ <div
52
+ {...renderProps}
53
+ ref={ref}
54
+ slot={props.slot}
55
+ data-empty={state.value === '' || undefined}>
56
+ <Provider
57
+ values={[
58
+ [LabelContext, {...labelProps, ref: labelRef}],
59
+ [InputContext, {...inputProps, ref: inputRef}],
60
+ [ButtonContext, clearButtonProps],
61
+ [TextContext, {
62
+ slots: {
63
+ description: descriptionProps,
64
+ errorMessage: errorMessageProps
65
+ }
66
+ }]
67
+ ]}>
68
+ {renderProps.children}
69
+ </Provider>
70
+ </div>
71
+ );
72
+ }
73
+
74
+ /**
75
+ * A search field allows a user to enter and clear a search query.
76
+ */
77
+ const _SearchField = forwardRef(SearchField);
78
+ export {_SearchField as SearchField};
package/src/Select.tsx ADDED
@@ -0,0 +1,180 @@
1
+ /*
2
+ * Copyright 2022 Adobe. All rights reserved.
3
+ * This file is licensed to you under the Apache License, Version 2.0 (the "License");
4
+ * you may not use this file except in compliance with the License. You may obtain a copy
5
+ * of the License at http://www.apache.org/licenses/LICENSE-2.0
6
+ *
7
+ * Unless required by applicable law or agreed to in writing, software distributed under
8
+ * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
9
+ * OF ANY KIND, either express or implied. See the License for the specific language
10
+ * governing permissions and limitations under the License.
11
+ */
12
+
13
+ import {AriaSelectProps, HiddenSelect, useSelect} from 'react-aria';
14
+ import {ButtonContext} from './Button';
15
+ import {ContextValue, forwardRefType, Provider, RenderProps, slotCallbackSymbol, SlotProps, useContextProps, useRenderProps, useSlot} from './utils';
16
+ import {createContext, ForwardedRef, HTMLAttributes, ReactNode, useCallback, useContext, useRef, useState} from 'react';
17
+ import {ItemRenderProps, useCollection} from './Collection';
18
+ import {LabelContext} from './Label';
19
+ import {ListBoxContext, ListBoxProps} from './ListBox';
20
+ import {PopoverContext} from './Popover';
21
+ import React, {forwardRef} from 'react';
22
+ import {SelectState, useSelectState} from 'react-stately';
23
+ import {TextContext} from './Text';
24
+ import {useResizeObserver} from '@react-aria/utils';
25
+
26
+ export interface SelectProps<T extends object> extends Omit<AriaSelectProps<T>, 'children' | 'label' | 'description' | 'errorMessage'>, RenderProps<SelectState<T>>, SlotProps {}
27
+
28
+ interface SelectValueContext {
29
+ state: SelectState<unknown>,
30
+ valueProps: HTMLAttributes<HTMLElement>
31
+ }
32
+
33
+ export const SelectContext = createContext<ContextValue<SelectProps<any>, HTMLDivElement>>(null);
34
+ const InternalSelectContext = createContext<SelectValueContext | null>(null);
35
+
36
+ function Select<T extends object>(props: SelectProps<T>, ref: ForwardedRef<HTMLDivElement>) {
37
+ [props, ref] = useContextProps(props, ref, SelectContext);
38
+ let [listBoxProps, setListBoxProps] = useState<ListBoxProps<any>>({children: []});
39
+
40
+ let {portal, collection} = useCollection({
41
+ items: props.items ?? listBoxProps.items,
42
+ children: listBoxProps.children
43
+ });
44
+ let state = useSelectState({
45
+ ...props,
46
+ collection,
47
+ children: undefined
48
+ });
49
+
50
+ // Get props for child elements from useSelect
51
+ let buttonRef = useRef<HTMLButtonElement>(null);
52
+ let [labelRef, label] = useSlot();
53
+ let {
54
+ labelProps,
55
+ triggerProps,
56
+ valueProps,
57
+ menuProps,
58
+ descriptionProps,
59
+ errorMessageProps
60
+ } = useSelect({...props, label}, state, buttonRef);
61
+
62
+ // Make menu width match input + button
63
+ let [buttonWidth, setButtonWidth] = useState<string | null>(null);
64
+ let onResize = useCallback(() => {
65
+ if (buttonRef.current) {
66
+ setButtonWidth(buttonRef.current.offsetWidth + 'px');
67
+ }
68
+ }, [buttonRef]);
69
+
70
+ useResizeObserver({
71
+ ref: buttonRef,
72
+ onResize: onResize
73
+ });
74
+
75
+ let renderProps = useRenderProps({
76
+ ...props,
77
+ values: state,
78
+ defaultClassName: 'react-aria-Select'
79
+ });
80
+
81
+ return (
82
+ <Provider
83
+ values={[
84
+ [InternalSelectContext, {state, valueProps}],
85
+ [LabelContext, {...labelProps, ref: labelRef, elementType: 'span'}],
86
+ [ButtonContext, {...triggerProps, ref: buttonRef, isPressed: state.isOpen}],
87
+ [PopoverContext, {
88
+ state,
89
+ triggerRef: buttonRef,
90
+ preserveChildren: true,
91
+ placement: 'bottom start',
92
+ style: {'--trigger-width': buttonWidth} as React.CSSProperties
93
+ }],
94
+ [ListBoxContext, {state, [slotCallbackSymbol]: setListBoxProps, ...menuProps}],
95
+ [TextContext, {
96
+ slots: {
97
+ description: descriptionProps,
98
+ errorMessage: errorMessageProps
99
+ }
100
+ }]
101
+ ]}>
102
+ <div {...renderProps} ref={ref} slot={props.slot}>
103
+ {props.children}
104
+ </div>
105
+ {portal}
106
+ <HiddenSelect
107
+ state={state}
108
+ triggerRef={buttonRef}
109
+ label={label}
110
+ name={props.name} />
111
+ </Provider>
112
+ );
113
+ }
114
+
115
+ /**
116
+ * A select displays a collapsible list of options and allows a user to select one of them.
117
+ */
118
+ const _Select = (forwardRef as forwardRefType)(Select);
119
+ export {_Select as Select};
120
+
121
+ export interface SelectValueRenderProps<T> {
122
+ /**
123
+ * Whether the value is a placeholder.
124
+ * @selector [data-placeholder]
125
+ */
126
+ isPlaceholder: boolean,
127
+ /** The object value of the currently selected item. */
128
+ selectedItem: T | null,
129
+ /** The textValue of the currently selected item. */
130
+ selectedText: string | null
131
+ }
132
+
133
+ export interface SelectValueProps<T extends object> extends Omit<HTMLAttributes<HTMLElement>, keyof RenderProps<unknown>>, RenderProps<SelectValueRenderProps<T>> {}
134
+
135
+ function SelectValue<T extends object>(props: SelectValueProps<T>, ref: ForwardedRef<HTMLSpanElement>) {
136
+ let {state, valueProps} = useContext(InternalSelectContext)!;
137
+ let rendered = state.selectedItem?.rendered;
138
+ if (typeof rendered === 'function') {
139
+ // If the selected item has a function as a child, we need to call it to render to JSX.
140
+ let fn = rendered as (s: ItemRenderProps) => ReactNode;
141
+ rendered = fn({
142
+ isHovered: false,
143
+ isPressed: false,
144
+ isSelected: false,
145
+ isFocused: false,
146
+ isFocusVisible: false,
147
+ isDisabled: false,
148
+ selectionMode: 'single',
149
+ selectionBehavior: 'toggle'
150
+ });
151
+ }
152
+
153
+ let renderProps = useRenderProps({
154
+ ...props,
155
+ // TODO: localize this.
156
+ defaultChildren: rendered || 'Select an item',
157
+ defaultClassName: 'react-aria-SelectValue',
158
+ values: {
159
+ selectedItem: state.selectedItem?.value as T ?? null,
160
+ selectedText: state.selectedItem?.textValue ?? null,
161
+ isPlaceholder: !state.selectedItem
162
+ }
163
+ });
164
+
165
+ return (
166
+ <span ref={ref} {...valueProps} {...renderProps} data-placeholder={!state.selectedItem || undefined}>
167
+ {/* clear description and error message slots */}
168
+ <TextContext.Provider value={undefined}>
169
+ {renderProps.children}
170
+ </TextContext.Provider>
171
+ </span>
172
+ );
173
+ }
174
+
175
+ /**
176
+ * SelectValue renders the current value of a Select, or a placeholder if no value is selected.
177
+ * It is usually placed within the button element.
178
+ */
179
+ const _SelectValue = (forwardRef as forwardRefType)(SelectValue);
180
+ export {_SelectValue as SelectValue};