ionbase-ui 0.51.0 → 0.59.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/dist/components/Accordion.d.ts +54 -0
- package/dist/components/Accordion.d.ts.map +1 -0
- package/dist/components/Accordion.js +68 -0
- package/dist/components/Accordion.js.map +1 -0
- package/dist/components/Breadcrumb.d.ts +45 -0
- package/dist/components/Breadcrumb.d.ts.map +1 -0
- package/dist/components/Breadcrumb.js +31 -0
- package/dist/components/Breadcrumb.js.map +1 -0
- package/dist/components/Checkbox.d.ts +2 -1
- package/dist/components/Checkbox.d.ts.map +1 -1
- package/dist/components/Checkbox.js +4 -2
- package/dist/components/Checkbox.js.map +1 -1
- package/dist/components/Combobox.d.ts +86 -0
- package/dist/components/Combobox.d.ts.map +1 -0
- package/dist/components/Combobox.js +261 -0
- package/dist/components/Combobox.js.map +1 -0
- package/dist/components/Drawer.d.ts +52 -0
- package/dist/components/Drawer.d.ts.map +1 -0
- package/dist/components/Drawer.js +66 -0
- package/dist/components/Drawer.js.map +1 -0
- package/dist/components/FileUpload.d.ts +79 -0
- package/dist/components/FileUpload.d.ts.map +1 -0
- package/dist/components/FileUpload.js +218 -0
- package/dist/components/FileUpload.js.map +1 -0
- package/dist/components/ProgressBar.d.ts +46 -0
- package/dist/components/ProgressBar.d.ts.map +1 -0
- package/dist/components/ProgressBar.js +52 -0
- package/dist/components/ProgressBar.js.map +1 -0
- package/dist/components/Select.d.ts +9 -2
- package/dist/components/Select.d.ts.map +1 -1
- package/dist/components/Select.js.map +1 -1
- package/dist/components/Skeleton.d.ts +39 -0
- package/dist/components/Skeleton.d.ts.map +1 -0
- package/dist/components/Skeleton.js +50 -0
- package/dist/components/Skeleton.js.map +1 -0
- package/dist/components/Spinner.d.ts +45 -0
- package/dist/components/Spinner.d.ts.map +1 -0
- package/dist/components/Spinner.js +44 -0
- package/dist/components/Spinner.js.map +1 -0
- package/dist/components/Toggle.d.ts +2 -1
- package/dist/components/Toggle.d.ts.map +1 -1
- package/dist/components/Toggle.js +4 -2
- package/dist/components/Toggle.js.map +1 -1
- package/dist/components/index.d.ts +16 -0
- package/dist/components/index.d.ts.map +1 -1
- package/dist/components/index.js +8 -0
- package/dist/components/index.js.map +1 -1
- package/dist/components/resolve-selection.d.ts +36 -0
- package/dist/components/resolve-selection.d.ts.map +1 -0
- package/dist/components/resolve-selection.js +14 -0
- package/dist/components/resolve-selection.js.map +1 -0
- package/dist/figma-descriptions.json +201 -146
- package/dist/figma-map.json +2296 -1684
- package/dist/meta/Accordion.json +123 -0
- package/dist/meta/AccordionItem.json +90 -0
- package/dist/meta/Breadcrumb.json +87 -0
- package/dist/meta/BreadcrumbItem.json +78 -0
- package/dist/meta/Checkbox.json +18 -1
- package/dist/meta/Combobox.json +265 -0
- package/dist/meta/Drawer.json +234 -0
- package/dist/meta/EmptyState.json +1 -1
- package/dist/meta/FileUpload.json +246 -0
- package/dist/meta/PhoneInput.json +39 -39
- package/dist/meta/Popover.json +3 -3
- package/dist/meta/ProgressBar.json +166 -0
- package/dist/meta/Select.json +4 -3
- package/dist/meta/Skeleton.json +120 -0
- package/dist/meta/Spinner.json +123 -0
- package/dist/meta/Toggle.json +18 -1
- package/dist/meta/Tooltip.json +3 -3
- package/dist/meta/components.json +1837 -270
- package/dist/meta/contrast.json +2873 -194
- package/dist/meta/index.json +128 -7
- package/dist/meta/patterns/index.json +1 -1
- package/dist/styles/accordion.css +102 -0
- package/dist/styles/breadcrumb.css +72 -0
- package/dist/styles/combobox.css +154 -0
- package/dist/styles/drawer.css +181 -0
- package/dist/styles/empty-state.css +16 -4
- package/dist/styles/file-upload.css +236 -0
- package/dist/styles/index.css +8 -0
- package/dist/styles/progress-bar.css +107 -0
- package/dist/styles/skeleton.css +76 -0
- package/dist/styles/spinner.css +70 -0
- package/llms.txt +2 -2
- package/package.json +19 -19
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useMemo, useRef, useState } from 'react';
|
|
4
|
+
import { Overlay, usePopover, useComboBox, useListBox, useOption, useButton, useFilter, DismissButton, mergeProps, } from 'react-aria';
|
|
5
|
+
import { useComboBoxState, Item } from 'react-stately';
|
|
6
|
+
const ChevronDown = () => (_jsx("svg", { viewBox: "0 0 24 24", fill: "none", "aria-hidden": "true", focusable: "false", children: _jsx("path", { d: "m6 9 6 6 6-6", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round" }) }));
|
|
7
|
+
/* ------------------------------------------------------------------ option */
|
|
8
|
+
function ComboboxOptionRow({ item, state, }) {
|
|
9
|
+
const ref = useRef(null);
|
|
10
|
+
const { optionProps, isSelected, isFocused, isDisabled } = useOption({ key: item.key }, state, ref);
|
|
11
|
+
const option = item.value;
|
|
12
|
+
return (_jsxs("li", { ...optionProps, ref: ref, className: [
|
|
13
|
+
'ion-combobox-menu__option',
|
|
14
|
+
isSelected ? 'ion-combobox-menu__option--selected' : '',
|
|
15
|
+
isFocused ? 'ion-combobox-menu__option--focused' : '',
|
|
16
|
+
isDisabled ? 'ion-combobox-menu__option--disabled' : '',
|
|
17
|
+
]
|
|
18
|
+
.filter(Boolean)
|
|
19
|
+
.join(' '), "data-focused": isFocused || undefined, "data-selected": isSelected || undefined, "data-disabled": isDisabled || undefined, children: [_jsx("span", { className: "ion-combobox-menu__label", children: item.rendered }), option?.description && (_jsx("span", { className: "ion-combobox-menu__description", children: option.description }))] }));
|
|
20
|
+
}
|
|
21
|
+
/** Breathing room kept between the menu and the viewport edge, in px. */
|
|
22
|
+
const GUTTER = 8;
|
|
23
|
+
/**
|
|
24
|
+
* The listbox, in its own component because it is mounted only while open.
|
|
25
|
+
*
|
|
26
|
+
* Same split as Popover and Drawer, and for the same reason: `usePopover`
|
|
27
|
+
* positions against a ref in an effect that runs when the component CALLING it
|
|
28
|
+
* mounts. Called beside `useComboBoxState` in the parent, that is the moment
|
|
29
|
+
* the input mounts, and the panel is then positioned once, at the origin,
|
|
30
|
+
* before it exists.
|
|
31
|
+
*/
|
|
32
|
+
function ComboboxListBox({ state, listBoxRef, popoverRef, triggerRef, listBoxProps, emptyLabel, }) {
|
|
33
|
+
const { popoverProps } = usePopover({
|
|
34
|
+
triggerRef,
|
|
35
|
+
popoverRef,
|
|
36
|
+
placement: 'bottom start',
|
|
37
|
+
offset: 4,
|
|
38
|
+
/*
|
|
39
|
+
* Non-modal, unlike every other overlay in this system. A modal popover
|
|
40
|
+
* moves focus into itself and hides the rest of the page — correct for a
|
|
41
|
+
* dialog, fatal here, because the user is still typing in the input and
|
|
42
|
+
* focus must never leave it. The input keeps focus and drives the list
|
|
43
|
+
* through `aria-activedescendant`.
|
|
44
|
+
*/
|
|
45
|
+
isNonModal: true,
|
|
46
|
+
}, state);
|
|
47
|
+
const { listBoxProps: innerListBoxProps } = useListBox({ ...listBoxProps, disallowEmptySelection: true }, state, listBoxRef);
|
|
48
|
+
/*
|
|
49
|
+
* The menu matches the field's width rather than hugging its content.
|
|
50
|
+
* `usePopover` sizes to the panel, and a list narrower or wider than the
|
|
51
|
+
* control it belongs to reads as a different element. Measured rather than
|
|
52
|
+
* assumed, because the field is fluid.
|
|
53
|
+
*
|
|
54
|
+
* READ DURING RENDER, NOT FROM A ResizeObserver — and this cost an afternoon.
|
|
55
|
+
*
|
|
56
|
+
* The first version kept the width in state and synced it from a
|
|
57
|
+
* ResizeObserver on the field. It measured correctly and closed the menu:
|
|
58
|
+
* every open flashed a listbox that was gone about a hundred milliseconds
|
|
59
|
+
* later, before any option could take virtual focus. Writing a width onto a
|
|
60
|
+
* positioned popover feeds back into `usePopover`'s own position pass, and
|
|
61
|
+
* its scroll handling closes the overlay. Bisecting the panel down to nothing
|
|
62
|
+
* was the only way to see it — the symptom looked like a keyboard bug.
|
|
63
|
+
*
|
|
64
|
+
* The panel mounts only while open, so the trigger is already laid out by the
|
|
65
|
+
* time this runs. One read, no state, no observer, nothing to feed back.
|
|
66
|
+
*/
|
|
67
|
+
const rect = triggerRef.current?.getBoundingClientRect();
|
|
68
|
+
/*
|
|
69
|
+
* Clamped to what is actually to the right of the trigger. A field running
|
|
70
|
+
* the full bleed of the page sits flush to the viewport edge, and a menu that
|
|
71
|
+
* matched its width exactly would start at the field's left and end past the
|
|
72
|
+
* edge — a horizontal scrollbar, which `useCloseOnScroll` reads as a scroll
|
|
73
|
+
* and closes the menu on. The failure mode was a combobox that could not be
|
|
74
|
+
* opened at all, so the clamp trades a slightly narrow menu for one that
|
|
75
|
+
* exists.
|
|
76
|
+
*/
|
|
77
|
+
const width = rect
|
|
78
|
+
? Math.min(rect.width, document.documentElement.clientWidth - rect.left - GUTTER)
|
|
79
|
+
: undefined;
|
|
80
|
+
const isEmpty = state.collection.size === 0;
|
|
81
|
+
return (_jsx(Overlay, { children: _jsxs("div", { ...popoverProps, ref: popoverRef, className: "ion-combobox-menu", style: { ...popoverProps.style, width }, children: [_jsx(DismissButton, { onDismiss: () => state.close() }), _jsx("ul", { ...mergeProps(innerListBoxProps), ref: listBoxRef, className: "ion-combobox-menu__list", children: [...state.collection].map((item) => (_jsx(ComboboxOptionRow, { item: item, state: state }, item.key))) }), isEmpty && (_jsx("div", { className: "ion-combobox-menu__empty", role: "presentation", children: emptyLabel })), _jsx(DismissButton, { onDismiss: () => state.close() })] }) }));
|
|
82
|
+
}
|
|
83
|
+
/* ---------------------------------------------------------------- combobox */
|
|
84
|
+
/**
|
|
85
|
+
* Combobox — a text field that filters a list, with one selected value.
|
|
86
|
+
*
|
|
87
|
+
* Drawn in Figma as `Combobox` (1370:2359) — three sizes by seven states, the
|
|
88
|
+
* same axes `Input` carries, because the field IS Input's box. The open list is
|
|
89
|
+
* a second Figma component, `Combobox Menu`, with no React export behind it.
|
|
90
|
+
*
|
|
91
|
+
* WHY THIS IS NOT `Select` WITH A SEARCH BOX
|
|
92
|
+
*
|
|
93
|
+
* `Select` wraps a native `<select>`, whose list the browser owns: it cannot be
|
|
94
|
+
* filtered, and its rows can only hold text. Everything above about twenty
|
|
95
|
+
* options needs filtering, and `Select`'s own contract has pointed at "a
|
|
96
|
+
* combobox — not yet in this system" since it was written. This is it.
|
|
97
|
+
*
|
|
98
|
+
* The cost is that everything the native control gave away for free — keyboard
|
|
99
|
+
* handling, the mobile picker, screen-reader semantics — now has to be built.
|
|
100
|
+
* React Aria's `useComboBox` builds it: `role="combobox"` with `aria-expanded`,
|
|
101
|
+
* `aria-controls` and `aria-activedescendant`, arrow keys that move a virtual
|
|
102
|
+
* focus while the real focus stays in the input, and Escape that reverts.
|
|
103
|
+
*
|
|
104
|
+
* FILTERING IS LOCALE-AWARE, NOT `toLowerCase().includes()`
|
|
105
|
+
*
|
|
106
|
+
* `useFilter({ sensitivity: 'base' })` is `Intl.Collator` underneath, so
|
|
107
|
+
* "resume" matches "résumé" and Turkish dotted/dotless I behave the way a
|
|
108
|
+
* Turkish reader expects. The lowercase-and-includes version fails both, and
|
|
109
|
+
* fails them silently in exactly the locales least likely to be tested.
|
|
110
|
+
*/
|
|
111
|
+
export function Combobox({ options, label, description, errorMessage, isInvalid, isDisabled, isReadOnly, size = 'md', placeholder, selectedKey, defaultSelectedKey, onSelectionChange, inputValue, defaultInputValue, onInputChange, allowsCustomValue, menuTrigger = 'input', emptyLabel = 'No matches', buttonLabel = 'Show suggestions', name, className, wrapperClassName, id, }) {
|
|
112
|
+
const { contains } = useFilter({ sensitivity: 'base' });
|
|
113
|
+
/*
|
|
114
|
+
* The description is searchable, but it is NOT part of `textValue`.
|
|
115
|
+
*
|
|
116
|
+
* That distinction cost a test to find. `textValue` does double duty in
|
|
117
|
+
* react-stately: it is what the filter reads AND what the input is set to
|
|
118
|
+
* once an option is chosen. Folding the description into it made "Australia"
|
|
119
|
+
* searchable by "Oceania" and then wrote `Australia Oceania` into the field.
|
|
120
|
+
*
|
|
121
|
+
* So `textValue` stays the label, and the filter is widened instead —
|
|
122
|
+
* keyed by label, which is what a Node carries back to it.
|
|
123
|
+
*/
|
|
124
|
+
const searchable = useMemo(() => {
|
|
125
|
+
const map = new Map();
|
|
126
|
+
for (const o of options) {
|
|
127
|
+
map.set(o.label, [o.label, o.description].filter(Boolean).join(' '));
|
|
128
|
+
}
|
|
129
|
+
return map;
|
|
130
|
+
}, [options]);
|
|
131
|
+
/*
|
|
132
|
+
* Filtering is done HERE, not handed to react-stately's `defaultFilter`.
|
|
133
|
+
*
|
|
134
|
+
* `defaultFilter` is only consulted for an UNCONTROLLED collection — one
|
|
135
|
+
* passed as `defaultItems`. Passed as `items`, as it is here, react-stately
|
|
136
|
+
* takes the list as final and the filter is silently ignored: no error, no
|
|
137
|
+
* warning, and a combobox that lists every option no matter what is typed.
|
|
138
|
+
* Three tests failed on it before the cause was obvious.
|
|
139
|
+
*
|
|
140
|
+
* `items` is the right half of that trade anyway. `defaultItems` snapshots
|
|
141
|
+
* the list on first render, so a caller whose options arrive from a fetch
|
|
142
|
+
* would get an empty combobox forever.
|
|
143
|
+
*/
|
|
144
|
+
const [typed, setTyped] = useState(defaultInputValue ?? '');
|
|
145
|
+
const text = inputValue ?? typed;
|
|
146
|
+
const filtered = useMemo(() => {
|
|
147
|
+
if (!text)
|
|
148
|
+
return options;
|
|
149
|
+
/*
|
|
150
|
+
* An exact label match shows the WHOLE list, which looks like a bug and is
|
|
151
|
+
* the opposite. Selecting an option writes its label into the input, and a
|
|
152
|
+
* filter applied to that would narrow the list to the one row already
|
|
153
|
+
* chosen — so reopening it to change your mind would offer nothing else.
|
|
154
|
+
*/
|
|
155
|
+
if (options.some((o) => o.label === text))
|
|
156
|
+
return options;
|
|
157
|
+
return options.filter((o) => contains(searchable.get(o.label) ?? o.label, text));
|
|
158
|
+
}, [contains, options, searchable, text]);
|
|
159
|
+
const disabledKeys = useMemo(() => options.filter((o) => o.isDisabled).map((o) => o.value), [options]);
|
|
160
|
+
const state = useComboBoxState({
|
|
161
|
+
label: typeof label === 'string' ? label : undefined,
|
|
162
|
+
items: filtered,
|
|
163
|
+
children: (item) => (_jsx(Item, { textValue: item.label, children: item.label }, item.value)),
|
|
164
|
+
disabledKeys,
|
|
165
|
+
menuTrigger,
|
|
166
|
+
/*
|
|
167
|
+
* The list stays mounted with nothing in it so the empty message can be
|
|
168
|
+
* shown. Left off, react-aria closes the popover the moment the filter
|
|
169
|
+
* matches nothing, and the user sees the list vanish with no explanation.
|
|
170
|
+
*/
|
|
171
|
+
allowsEmptyCollection: true,
|
|
172
|
+
allowsCustomValue,
|
|
173
|
+
isDisabled,
|
|
174
|
+
isReadOnly,
|
|
175
|
+
selectedKey,
|
|
176
|
+
defaultSelectedKey,
|
|
177
|
+
onSelectionChange: onSelectionChange
|
|
178
|
+
? (key) => onSelectionChange(key === null ? null : String(key))
|
|
179
|
+
: undefined,
|
|
180
|
+
inputValue,
|
|
181
|
+
defaultInputValue,
|
|
182
|
+
onInputChange: (value) => {
|
|
183
|
+
setTyped(value);
|
|
184
|
+
onInputChange?.(value);
|
|
185
|
+
},
|
|
186
|
+
});
|
|
187
|
+
const inputRef = useRef(null);
|
|
188
|
+
const listBoxRef = useRef(null);
|
|
189
|
+
const popoverRef = useRef(null);
|
|
190
|
+
const buttonRef = useRef(null);
|
|
191
|
+
const boxRef = useRef(null);
|
|
192
|
+
const { buttonProps: triggerProps, inputProps, listBoxProps, labelProps, descriptionProps, errorMessageProps, } = useComboBox({
|
|
193
|
+
/*
|
|
194
|
+
* `id` goes to useComboBox, not to useComboBoxState. The state owns the
|
|
195
|
+
* collection; the hook owns the DOM wiring — the input's id, the label's
|
|
196
|
+
* htmlFor, and the aria-controls that points at the listbox. Setting it
|
|
197
|
+
* on the state is accepted by neither and silently does nothing.
|
|
198
|
+
*/
|
|
199
|
+
id,
|
|
200
|
+
label: typeof label === 'string' ? label : undefined,
|
|
201
|
+
placeholder,
|
|
202
|
+
isDisabled,
|
|
203
|
+
isReadOnly,
|
|
204
|
+
isInvalid,
|
|
205
|
+
inputRef,
|
|
206
|
+
listBoxRef,
|
|
207
|
+
popoverRef,
|
|
208
|
+
buttonRef,
|
|
209
|
+
}, state);
|
|
210
|
+
const { buttonProps } = useButton({
|
|
211
|
+
...triggerProps,
|
|
212
|
+
'aria-label': buttonLabel,
|
|
213
|
+
}, buttonRef);
|
|
214
|
+
const helper = isInvalid && errorMessage ? errorMessage : description;
|
|
215
|
+
const boxClassNames = [
|
|
216
|
+
'ion-input',
|
|
217
|
+
'ion-combobox',
|
|
218
|
+
size !== 'md' ? `ion-input--${size}` : '',
|
|
219
|
+
isInvalid ? 'ion-input--invalid' : '',
|
|
220
|
+
isDisabled ? 'ion-input--disabled' : '',
|
|
221
|
+
className || '',
|
|
222
|
+
]
|
|
223
|
+
.filter(Boolean)
|
|
224
|
+
.join(' ');
|
|
225
|
+
return (_jsxs("div", { className: [
|
|
226
|
+
'ion-field',
|
|
227
|
+
isInvalid ? 'ion-field--error' : '',
|
|
228
|
+
wrapperClassName || '',
|
|
229
|
+
]
|
|
230
|
+
.filter(Boolean)
|
|
231
|
+
.join(' '), children: [label && (_jsx("label", { ...labelProps, className: "ion-field__label", children: label })), _jsxs("div", { ref: boxRef, className: boxClassNames, "data-open": state.isOpen || undefined, "data-invalid": isInvalid || undefined, "data-disabled": isDisabled || undefined, children: [_jsx("input", { ...inputProps, ref: inputRef, className: "ion-input__field" }), name && (_jsx("input", { type: "hidden", name: name, value: state.selectedKey == null ? '' : String(state.selectedKey) })), _jsx("button", { ...buttonProps, "aria-labelledby": undefined, "aria-label": buttonLabel,
|
|
232
|
+
/*
|
|
233
|
+
* The press must not move focus to the button.
|
|
234
|
+
*
|
|
235
|
+
* `useComboBox` closes the list the moment focus leaves the input, so
|
|
236
|
+
* a disclosure button that takes focus opens the list and closes it
|
|
237
|
+
* again within the same gesture. A test caught exactly that: the
|
|
238
|
+
* listbox was found and was gone by the next assertion. Chromium
|
|
239
|
+
* focuses a button on mousedown, so this is not hypothetical.
|
|
240
|
+
*
|
|
241
|
+
* `preventDefault` on mousedown is what stops that — and the caret
|
|
242
|
+
* goes to the field instead, which is where someone pressing a
|
|
243
|
+
* chevron to browse a list wants to type next anyway.
|
|
244
|
+
*/
|
|
245
|
+
onMouseDown: (e) => {
|
|
246
|
+
e.preventDefault();
|
|
247
|
+
inputRef.current?.focus();
|
|
248
|
+
}, ref: buttonRef, type: "button",
|
|
249
|
+
/*
|
|
250
|
+
* Out of the tab order on purpose, and this is the ARIA pattern
|
|
251
|
+
* rather than an oversight. The input already reaches the list with
|
|
252
|
+
* ArrowDown, so a tab stop on the disclosure button would make every
|
|
253
|
+
* combobox in a form cost two tabs to pass and offer nothing the
|
|
254
|
+
* input does not.
|
|
255
|
+
*/
|
|
256
|
+
tabIndex: -1, className: "ion-combobox__button", children: _jsx(ChevronDown, {}) })] }), state.isOpen && (_jsx(ComboboxListBox, { state: state, listBoxRef: listBoxRef, popoverRef: popoverRef, triggerRef: boxRef, listBoxProps: listBoxProps, emptyLabel: emptyLabel })), helper && (_jsx("span", { ...(isInvalid && errorMessage
|
|
257
|
+
? errorMessageProps
|
|
258
|
+
: descriptionProps), className: "ion-field__helper", children: helper }))] }));
|
|
259
|
+
}
|
|
260
|
+
Combobox.displayName = 'Combobox';
|
|
261
|
+
//# sourceMappingURL=Combobox.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Combobox.js","sourceRoot":"","sources":["../../src/components/Combobox.tsx"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import type { AriaModalOverlayProps } from 'react-aria';
|
|
3
|
+
export type DrawerPlacement = 'start' | 'end' | 'top' | 'bottom';
|
|
4
|
+
export type DrawerSize = 'sm' | 'md' | 'lg';
|
|
5
|
+
export interface DrawerProps extends AriaModalOverlayProps {
|
|
6
|
+
/** Whether the drawer is shown. */
|
|
7
|
+
isOpen?: boolean;
|
|
8
|
+
/** Called with the next open state — `false` when the drawer asks to close. */
|
|
9
|
+
onOpenChange?: (isOpen: boolean) => void;
|
|
10
|
+
/**
|
|
11
|
+
* Which edge it enters from. `start` and `end` follow writing direction, so
|
|
12
|
+
* a right-hand drawer in English is a left-hand one in Arabic without a
|
|
13
|
+
* second variant.
|
|
14
|
+
*/
|
|
15
|
+
placement?: DrawerPlacement;
|
|
16
|
+
size?: DrawerSize;
|
|
17
|
+
/** Required: the dialog's accessible name, wired via `aria-labelledby`. */
|
|
18
|
+
title: React.ReactNode;
|
|
19
|
+
/** Supporting copy under the title. */
|
|
20
|
+
description?: React.ReactNode;
|
|
21
|
+
/** Action row, pinned to the bottom edge. */
|
|
22
|
+
footer?: React.ReactNode;
|
|
23
|
+
showClose?: boolean;
|
|
24
|
+
closeLabel?: string;
|
|
25
|
+
children?: React.ReactNode;
|
|
26
|
+
className?: string;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Drawer — a modal panel anchored to an edge.
|
|
30
|
+
*
|
|
31
|
+
* WHY IT IS A MODAL AND NOT A SIDEBAR
|
|
32
|
+
*
|
|
33
|
+
* It takes focus, traps it, closes on Escape and on an outside click, and marks
|
|
34
|
+
* the rest of the page inert — all of `Modal`'s behaviour, differing only in
|
|
35
|
+
* where the panel sits and how it enters. A persistent side panel that does not
|
|
36
|
+
* do those things is layout, not a drawer, and belongs in the page rather than
|
|
37
|
+
* in an overlay.
|
|
38
|
+
*
|
|
39
|
+
* WHEN TO REACH FOR MODAL INSTEAD
|
|
40
|
+
*
|
|
41
|
+
* A drawer suits work that is long or list-shaped — filters, a record's detail,
|
|
42
|
+
* a sequence of settings — because an edge panel can be tall without becoming a
|
|
43
|
+
* square that fights the viewport. A decision, especially a destructive one,
|
|
44
|
+
* belongs in a Modal: it is centred, it is smaller, and it does not invite
|
|
45
|
+
* scrolling past the thing being confirmed.
|
|
46
|
+
*
|
|
47
|
+
* RENDERS NOTHING WHEN CLOSED, and portals when open, for the reasons recorded
|
|
48
|
+
* on Modal: a closed overlay left in the tree is still keyboard-focusable, and
|
|
49
|
+
* an inline one inherits whatever `overflow` and stacking context surrounds it.
|
|
50
|
+
*/
|
|
51
|
+
export declare const Drawer: React.ForwardRefExoticComponent<DrawerProps & React.RefAttributes<HTMLDivElement>>;
|
|
52
|
+
//# sourceMappingURL=Drawer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Drawer.d.ts","sourceRoot":"","sources":["../../src/components/Drawer.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAkD,MAAM,OAAO,CAAC;AASvE,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,YAAY,CAAC;AAExD,MAAM,MAAM,eAAe,GAAG,OAAO,GAAG,KAAK,GAAG,KAAK,GAAG,QAAQ,CAAC;AACjE,MAAM,MAAM,UAAU,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,CAAC;AAE5C,MAAM,WAAW,WAAY,SAAQ,qBAAqB;IACxD,mCAAmC;IACnC,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,+EAA+E;IAC/E,YAAY,CAAC,EAAE,CAAC,MAAM,EAAE,OAAO,KAAK,IAAI,CAAC;IACzC;;;;OAIG;IACH,SAAS,CAAC,EAAE,eAAe,CAAC;IAC5B,IAAI,CAAC,EAAE,UAAU,CAAC;IAClB,2EAA2E;IAC3E,KAAK,EAAE,KAAK,CAAC,SAAS,CAAC;IACvB,uCAAuC;IACvC,WAAW,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IAC9B,6CAA6C;IAC7C,MAAM,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IACzB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IAC3B,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAkGD;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,eAAO,MAAM,MAAM,oFAUjB,CAAC"}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { forwardRef, useRef, useImperativeHandle } from 'react';
|
|
4
|
+
import { Overlay, useModalOverlay, useDialog, useButton, mergeProps, } from 'react-aria';
|
|
5
|
+
import { useOverlayTriggerState } from 'react-stately';
|
|
6
|
+
const CloseGlyph = () => (_jsx("svg", { viewBox: "0 0 24 24", fill: "none", "aria-hidden": "true", focusable: "false", children: _jsx("path", { d: "M6 6l12 12M18 6L6 18", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round" }) }));
|
|
7
|
+
/**
|
|
8
|
+
* The panel. Mounted only while open, which is what makes the hooks work.
|
|
9
|
+
*
|
|
10
|
+
* `useDialog` resolves the title id and moves focus in effects keyed on the
|
|
11
|
+
* panel ref — effects that run when the component CALLING them mounts. Called
|
|
12
|
+
* beside `useOverlayTriggerState` in `Drawer` below, that moment would be when
|
|
13
|
+
* the trigger mounts, with this panel not yet in the DOM: no `aria-labelledby`,
|
|
14
|
+
* and focus never entering the drawer, so Escape never reaches the key handler.
|
|
15
|
+
* Silent in every case. AGENTS.md records the same split for Modal and Popover.
|
|
16
|
+
*/
|
|
17
|
+
function DrawerDialog(props, forwardedRef) {
|
|
18
|
+
const { state, placement = 'end', size = 'md', title, description, footer, showClose = true, closeLabel = 'Close drawer', children, className, } = props;
|
|
19
|
+
const panelRef = useRef(null);
|
|
20
|
+
useImperativeHandle(forwardedRef, () => panelRef.current);
|
|
21
|
+
const { modalProps, underlayProps } = useModalOverlay(props, state, panelRef);
|
|
22
|
+
const { dialogProps, titleProps } = useDialog({ role: 'dialog' }, panelRef);
|
|
23
|
+
const closeRef = useRef(null);
|
|
24
|
+
const { buttonProps } = useButton({ onPress: () => state.close(), 'aria-label': closeLabel }, closeRef);
|
|
25
|
+
const panelClassNames = [
|
|
26
|
+
'ion-drawer',
|
|
27
|
+
`ion-drawer--${placement}`,
|
|
28
|
+
`ion-drawer--${size}`,
|
|
29
|
+
className || '',
|
|
30
|
+
]
|
|
31
|
+
.filter(Boolean)
|
|
32
|
+
.join(' ');
|
|
33
|
+
return (_jsx("div", { ...underlayProps, className: "ion-drawer__scrim", children: _jsxs("div", { ...mergeProps(modalProps, dialogProps), ref: panelRef, className: panelClassNames, children: [_jsxs("div", { className: "ion-drawer__header", children: [_jsxs("div", { className: "ion-drawer__heading", children: [_jsx("h2", { ...titleProps, className: "ion-drawer__title", children: title }), description && (_jsx("p", { className: "ion-drawer__description", children: description }))] }), showClose && (_jsx("button", { ...buttonProps, ref: closeRef, type: "button", className: "ion-drawer__close", children: _jsx(CloseGlyph, {}) }))] }), children && _jsx("div", { className: "ion-drawer__body", children: children }), footer && _jsx("div", { className: "ion-drawer__footer", children: footer })] }) }));
|
|
34
|
+
}
|
|
35
|
+
const DrawerDialogWithRef = forwardRef(DrawerDialog);
|
|
36
|
+
/**
|
|
37
|
+
* Drawer — a modal panel anchored to an edge.
|
|
38
|
+
*
|
|
39
|
+
* WHY IT IS A MODAL AND NOT A SIDEBAR
|
|
40
|
+
*
|
|
41
|
+
* It takes focus, traps it, closes on Escape and on an outside click, and marks
|
|
42
|
+
* the rest of the page inert — all of `Modal`'s behaviour, differing only in
|
|
43
|
+
* where the panel sits and how it enters. A persistent side panel that does not
|
|
44
|
+
* do those things is layout, not a drawer, and belongs in the page rather than
|
|
45
|
+
* in an overlay.
|
|
46
|
+
*
|
|
47
|
+
* WHEN TO REACH FOR MODAL INSTEAD
|
|
48
|
+
*
|
|
49
|
+
* A drawer suits work that is long or list-shaped — filters, a record's detail,
|
|
50
|
+
* a sequence of settings — because an edge panel can be tall without becoming a
|
|
51
|
+
* square that fights the viewport. A decision, especially a destructive one,
|
|
52
|
+
* belongs in a Modal: it is centred, it is smaller, and it does not invite
|
|
53
|
+
* scrolling past the thing being confirmed.
|
|
54
|
+
*
|
|
55
|
+
* RENDERS NOTHING WHEN CLOSED, and portals when open, for the reasons recorded
|
|
56
|
+
* on Modal: a closed overlay left in the tree is still keyboard-focusable, and
|
|
57
|
+
* an inline one inherits whatever `overflow` and stacking context surrounds it.
|
|
58
|
+
*/
|
|
59
|
+
export const Drawer = forwardRef((props, ref) => {
|
|
60
|
+
const state = useOverlayTriggerState(props);
|
|
61
|
+
if (!state.isOpen)
|
|
62
|
+
return null;
|
|
63
|
+
return (_jsx(Overlay, { children: _jsx(DrawerDialogWithRef, { ...props, state: state, ref: ref }) }));
|
|
64
|
+
});
|
|
65
|
+
Drawer.displayName = 'Drawer';
|
|
66
|
+
//# sourceMappingURL=Drawer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Drawer.js","sourceRoot":"","sources":["../../src/components/Drawer.tsx"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
export type FileUploadSize = 'sm' | 'md';
|
|
3
|
+
/** A file the control refused, and the reason, so the caller can say so. */
|
|
4
|
+
export interface RejectedFile {
|
|
5
|
+
file: File;
|
|
6
|
+
reason: 'type' | 'size' | 'count';
|
|
7
|
+
message: string;
|
|
8
|
+
}
|
|
9
|
+
export interface FileUploadProps {
|
|
10
|
+
/** Field label. Also the accessible name of the file input. */
|
|
11
|
+
label?: React.ReactNode;
|
|
12
|
+
/** Helper text below the control. */
|
|
13
|
+
description?: React.ReactNode;
|
|
14
|
+
/** Replaces the helper text when `isInvalid` is set. */
|
|
15
|
+
errorMessage?: React.ReactNode;
|
|
16
|
+
isInvalid?: boolean;
|
|
17
|
+
isDisabled?: boolean;
|
|
18
|
+
/** Matches the Figma `Size` variant when drawn. Height of the drop target. */
|
|
19
|
+
size?: FileUploadSize;
|
|
20
|
+
/** Native `accept` — a comma-separated list of extensions or MIME types. */
|
|
21
|
+
accept?: string;
|
|
22
|
+
/** Allow more than one file. */
|
|
23
|
+
multiple?: boolean;
|
|
24
|
+
/** Reject anything larger, in bytes. */
|
|
25
|
+
maxSize?: number;
|
|
26
|
+
/** Reject once this many files are held. Only meaningful with `multiple`. */
|
|
27
|
+
maxFiles?: number;
|
|
28
|
+
/**
|
|
29
|
+
* The files held. Pass it to control the list; omit and the component keeps
|
|
30
|
+
* its own. A controlled list is the usual case, because the caller is the
|
|
31
|
+
* only thing that knows which uploads have since succeeded or failed.
|
|
32
|
+
*/
|
|
33
|
+
files?: readonly File[];
|
|
34
|
+
/** Fires with the full list after every add or remove, never with a delta. */
|
|
35
|
+
onChange?: (files: File[]) => void;
|
|
36
|
+
/** Fires with anything refused by `accept`, `maxSize` or `maxFiles`. */
|
|
37
|
+
onReject?: (rejected: RejectedFile[]) => void;
|
|
38
|
+
/** Call-to-action inside the drop target. */
|
|
39
|
+
prompt?: React.ReactNode;
|
|
40
|
+
/** Second line inside the drop target — the constraints, in words. */
|
|
41
|
+
hint?: React.ReactNode;
|
|
42
|
+
/** Accessible label for each file's remove button. Receives the file name. */
|
|
43
|
+
removeLabel?: (name: string) => string;
|
|
44
|
+
className?: string;
|
|
45
|
+
wrapperClassName?: string;
|
|
46
|
+
id?: string;
|
|
47
|
+
name?: string;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* FileUpload — a drop target wrapped around a real `<input type="file">`.
|
|
51
|
+
*
|
|
52
|
+
* Drawn in Figma as `File Upload` (1367:2333) — two sizes by five states. The
|
|
53
|
+
* measurements came from `Input` and `EmptyState` rather than being invented:
|
|
54
|
+
* the border, radius and disabled treatment are Input's; the centred
|
|
55
|
+
* icon-over-text stack is EmptyState's. Figma draws the drop zone alone, the
|
|
56
|
+
* same split `Input` makes against `Form Field`.
|
|
57
|
+
*
|
|
58
|
+
* THE INPUT IS THE CONTROL. THE DROP ZONE IS DECORATION.
|
|
59
|
+
*
|
|
60
|
+
* The usual build of this component is a `<div>` with drag handlers and a
|
|
61
|
+
* click that calls `input.click()`. That version cannot be reached by keyboard,
|
|
62
|
+
* has no accessible name, does not participate in a form, and does not work in
|
|
63
|
+
* any environment without a pointer — which includes switch access, voice
|
|
64
|
+
* control and most screen-reader browse modes.
|
|
65
|
+
*
|
|
66
|
+
* Here the file input is a real, focusable, labelled control that is visually
|
|
67
|
+
* hidden but NOT `display: none` — it keeps its place in the tab order and its
|
|
68
|
+
* label. Drag-and-drop is layered on top as an enhancement, and every path it
|
|
69
|
+
* offers is also reachable without it. That ordering is the whole component.
|
|
70
|
+
*
|
|
71
|
+
* VALIDATION IS ADVISORY, NOT SECURITY. `accept` and `maxSize` are checked here
|
|
72
|
+
* so the user finds out immediately instead of after an upload. A server that
|
|
73
|
+
* trusts either one is trusting a value the client chose.
|
|
74
|
+
*/
|
|
75
|
+
export declare function FileUpload({ label, description, errorMessage, isInvalid, isDisabled: isDisabledProp, size, accept, multiple, maxSize, maxFiles, files: controlledFiles, onChange, onReject, prompt, hint, removeLabel, className, wrapperClassName, id: providedId, name: inputName, }: FileUploadProps): React.JSX.Element;
|
|
76
|
+
export declare namespace FileUpload {
|
|
77
|
+
var displayName: string;
|
|
78
|
+
}
|
|
79
|
+
//# sourceMappingURL=FileUpload.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"FileUpload.d.ts","sourceRoot":"","sources":["../../src/components/FileUpload.tsx"],"names":[],"mappings":"AAEA,OAAO,KAA+C,MAAM,OAAO,CAAC;AAEpE,MAAM,MAAM,cAAc,GAAG,IAAI,GAAG,IAAI,CAAC;AAEzC,4EAA4E;AAC5E,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,IAAI,CAAC;IACX,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC;IAClC,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,eAAe;IAC9B,+DAA+D;IAC/D,KAAK,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IACxB,qCAAqC;IACrC,WAAW,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IAC9B,wDAAwD;IACxD,YAAY,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IAC/B,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,8EAA8E;IAC9E,IAAI,CAAC,EAAE,cAAc,CAAC;IACtB,4EAA4E;IAC5E,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,gCAAgC;IAChC,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,wCAAwC;IACxC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,6EAA6E;IAC7E,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB;;;;OAIG;IACH,KAAK,CAAC,EAAE,SAAS,IAAI,EAAE,CAAC;IACxB,8EAA8E;IAC9E,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,IAAI,EAAE,KAAK,IAAI,CAAC;IACnC,wEAAwE;IACxE,QAAQ,CAAC,EAAE,CAAC,QAAQ,EAAE,YAAY,EAAE,KAAK,IAAI,CAAC;IAC9C,6CAA6C;IAC7C,MAAM,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IACzB,sEAAsE;IACtE,IAAI,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IACvB,8EAA8E;IAC9E,WAAW,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAC;IACvC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAmED;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,wBAAgB,UAAU,CAAC,EACzB,KAAK,EACL,WAAW,EACX,YAAY,EACZ,SAAS,EACT,UAAU,EAAE,cAAc,EAC1B,IAAW,EACX,MAAM,EACN,QAAQ,EACR,OAAO,EACP,QAAQ,EACR,KAAK,EAAE,eAAe,EACtB,QAAQ,EACR,QAAQ,EACR,MAAM,EACN,IAAI,EACJ,WAAwC,EACxC,SAAS,EACT,gBAAgB,EAChB,EAAE,EAAE,UAAU,EACd,IAAI,EAAE,SAAS,GAChB,EAAE,eAAe,qBAuPjB;yBA5Qe,UAAU"}
|