@codapet/design-system 0.8.6 → 0.8.7
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/AGENTS.md +2 -0
- package/dist/index.d.mts +26 -2
- package/dist/index.mjs +92 -19
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/AGENTS.md
CHANGED
|
@@ -369,6 +369,8 @@ There are no other public exports, and no subpath imports: only
|
|
|
369
369
|
- **Wrong default Button look** → you forgot `variant="primary"`/`size="md"` are not the same as shadcn's defaults; passing nothing gives you `primary` + `lg`.
|
|
370
370
|
- **Toast looks generic** → you imported `toast` from `'sonner'` instead of from `@codapet/design-system`.
|
|
371
371
|
- **Modal doesn't bottom-sheet on mobile** → you used `Dialog` instead of `SmartDialog`.
|
|
372
|
+
- **`AsyncAutocomplete` keeps stale results while refetching** → `loading` only replaces the list when there is nothing else to show. Once results are visible, a further keystroke leaves them in place and marks the listbox `aria-busy` plus `data-busy`, so the panel does not flash back to the spinner on every character. Style the refresh with `classNames={{ listbox: 'data-[busy=true]:opacity-60' }}` if you want visible feedback.
|
|
373
|
+
- **`AsyncAutocomplete` warns about a missing accessible name** → a combobox must be named, and a `placeholder` does not name one (ARIA practices are explicit about this). Pass `label` for a visible label — it renders a real `<label htmlFor>`, unlike `SearchInput`/`MultiSelectFreeText`, whose `label` prop is a styled `<div>` and is *not* programmatically associated — or pass `inputProps={{ 'aria-label': '…' }}` when the design has no visible label. `mandatory` sets `aria-required` and hides the asterisk from assistive tech.
|
|
372
374
|
- **`AsyncAutocomplete` fires a request per keystroke** → it deliberately does not debounce. Debounce in your fetch layer (`usePlacesService({ debounce: 300 })`, your own hook) so cancellation and out-of-order responses stay with the code that owns them.
|
|
373
375
|
- **`AsyncAutocomplete` shows results that don't match what you typed** → it renders `options` verbatim by design, because the server already filtered. If your list is static, use `SearchableSelect`/`DropdownSelect`, which filter client-side.
|
|
374
376
|
- **Styling `AsyncAutocomplete`** → it does **not** follow the flat `*ClassName` convention that `DateInput`/`TimeInput` use, because it has twenty-odd stylable slots. `className` is the root wrapper; everything else lives in one `classNames` object: `field`, `input`, `leftIcon`, `clearButton`, `content`, `listbox`, `option`, `optionHighlighted`, `optionDisabled`, `optionIcon`, `optionLabel`, `optionDescription`, `loading`, `loadingSpinner`, `empty`, plus `sheetOverlay`, `sheetContent`, `sheetHeader`, `sheetCloseButton`, `sheetInput`, `sheetList`. All merge through `cn`, so `{ content: 'max-h-[420px]' }` replaces the built-in max-height rather than stacking with it. A single row can also carry its own `className` via the option object, and rows expose `data-highlighted` / `data-disabled` for parent-level selectors. Use `renderOption` to change row *structure*, `classNames` to change its *look*.
|
package/dist/index.d.mts
CHANGED
|
@@ -140,6 +140,8 @@ type AsyncAutocompleteInputProps = Omit<React$1.ComponentPropsWithoutRef<'input'
|
|
|
140
140
|
interface AsyncAutocompleteClassNames {
|
|
141
141
|
/** Wrapper around the input — also the popover's anchor. */
|
|
142
142
|
field?: string;
|
|
143
|
+
/** The visible label element. */
|
|
144
|
+
label?: string;
|
|
143
145
|
/** The text input itself. */
|
|
144
146
|
input?: string;
|
|
145
147
|
/** Left icon wrapper inside the input. */
|
|
@@ -202,7 +204,14 @@ interface AsyncAutocompleteProps<TData = unknown> {
|
|
|
202
204
|
* `formatted_address`) or clear the field. Drive `value` yourself.
|
|
203
205
|
*/
|
|
204
206
|
onSelect: (option: AsyncAutocompleteOption<TData>) => void;
|
|
205
|
-
/**
|
|
207
|
+
/**
|
|
208
|
+
* A request is in flight.
|
|
209
|
+
*
|
|
210
|
+
* The spinner row only replaces the list when there is nothing else to show.
|
|
211
|
+
* If results are already on screen they stay put and the listbox is marked
|
|
212
|
+
* `aria-busy` / `data-busy` instead — so typing another character does not
|
|
213
|
+
* flash the panel back to "Searching…".
|
|
214
|
+
*/
|
|
206
215
|
loading?: boolean;
|
|
207
216
|
/** Controlled input text. */
|
|
208
217
|
value?: string;
|
|
@@ -217,6 +226,17 @@ interface AsyncAutocompleteProps<TData = unknown> {
|
|
|
217
226
|
*/
|
|
218
227
|
onClear?: () => void;
|
|
219
228
|
placeholder?: string;
|
|
229
|
+
/**
|
|
230
|
+
* Visible label, rendered above the field and wired to it with `htmlFor`.
|
|
231
|
+
*
|
|
232
|
+
* A combobox **must** have an accessible name — the ARIA practices are
|
|
233
|
+
* explicit that a placeholder is not one. Provide either this, or an
|
|
234
|
+
* `aria-label`/`aria-labelledby` through `inputProps`. In development the
|
|
235
|
+
* component warns when neither is present.
|
|
236
|
+
*/
|
|
237
|
+
label?: string;
|
|
238
|
+
/** Appends a red asterisk to `label`. */
|
|
239
|
+
mandatory?: boolean;
|
|
220
240
|
/** Decorative leading icon — `<MapPin />`, `<Stethoscope />`, … */
|
|
221
241
|
leftIcon?: React$1.ReactNode;
|
|
222
242
|
/** Fixed height: `sm` 40px · `md` 48px (default) · `lg` 56px. */
|
|
@@ -241,6 +261,10 @@ interface AsyncAutocompleteProps<TData = unknown> {
|
|
|
241
261
|
* for stacked two-line rows, avatars, or bold match highlighting — the
|
|
242
262
|
* wrapper, `role="option"`, ids, highlight background and click/keyboard
|
|
243
263
|
* wiring stay with the component.
|
|
264
|
+
*
|
|
265
|
+
* Keep the content non-interactive: it renders inside `role="option"`, and a
|
|
266
|
+
* nested button or link there is both invalid and unreachable by keyboard,
|
|
267
|
+
* since the row is driven by `aria-activedescendant` rather than focus.
|
|
244
268
|
*/
|
|
245
269
|
renderOption?: (option: AsyncAutocompleteOption<TData>, state: AsyncAutocompleteOptionState) => React$1.ReactNode;
|
|
246
270
|
/** Default leading icon for rows without their own `option.icon`. */
|
|
@@ -280,7 +304,7 @@ interface AsyncAutocompleteProps<TData = unknown> {
|
|
|
280
304
|
/** Per-slot class overrides. See `AsyncAutocompleteClassNames`. */
|
|
281
305
|
classNames?: AsyncAutocompleteClassNames;
|
|
282
306
|
}
|
|
283
|
-
declare function AsyncAutocomplete<TData = unknown>({ options, onSearch, onSelect, loading, value: valueProp, defaultValue, onValueChange, onClear, placeholder, leftIcon, size, error, disabled, inputRef, inputProps, mobileVariant, renderOption, optionIcon, emptyMessage, emptyState, loadingMessage, clearIcon, clearLabel, sheetTitle, sheetCloseIcon, sheetCloseLabel, open: openProp, defaultOpen, onOpenChange, idPrefix, className, classNames }: AsyncAutocompleteProps<TData>): react_jsx_runtime.JSX.Element;
|
|
307
|
+
declare function AsyncAutocomplete<TData = unknown>({ options, onSearch, onSelect, loading, value: valueProp, defaultValue, onValueChange, onClear, placeholder, label, mandatory, leftIcon, size, error, disabled, inputRef, inputProps, mobileVariant, renderOption, optionIcon, emptyMessage, emptyState, loadingMessage, clearIcon, clearLabel, sheetTitle, sheetCloseIcon, sheetCloseLabel, open: openProp, defaultOpen, onOpenChange, idPrefix, className, classNames }: AsyncAutocompleteProps<TData>): react_jsx_runtime.JSX.Element;
|
|
284
308
|
|
|
285
309
|
interface TextareaProps extends Omit<React$1.ComponentProps<'textarea'>, 'size'> {
|
|
286
310
|
error?: boolean;
|
package/dist/index.mjs
CHANGED
|
@@ -664,6 +664,8 @@ function AsyncAutocomplete({
|
|
|
664
664
|
onValueChange,
|
|
665
665
|
onClear,
|
|
666
666
|
placeholder,
|
|
667
|
+
label,
|
|
668
|
+
mandatory = false,
|
|
667
669
|
leftIcon,
|
|
668
670
|
size = "md",
|
|
669
671
|
error = false,
|
|
@@ -702,6 +704,13 @@ function AsyncAutocomplete({
|
|
|
702
704
|
const sheetInputRef = React8.useRef(null);
|
|
703
705
|
const optionRefs = React8.useRef([]);
|
|
704
706
|
const isMobile = useIsMobile();
|
|
707
|
+
const hasAccessibleName = !!label || !!inputProps?.["aria-label"] || !!inputProps?.["aria-labelledby"];
|
|
708
|
+
React8.useEffect(() => {
|
|
709
|
+
if (process.env.NODE_ENV === "production" || hasAccessibleName) return;
|
|
710
|
+
console.warn(
|
|
711
|
+
"[AsyncAutocomplete] No accessible name. Pass `label`, or an `aria-label`/`aria-labelledby` via `inputProps`. A `placeholder` does not name a combobox."
|
|
712
|
+
);
|
|
713
|
+
}, [hasAccessibleName]);
|
|
705
714
|
const isControlledValue = valueProp !== void 0;
|
|
706
715
|
const isControlledOpen = openProp !== void 0;
|
|
707
716
|
const query = isControlledValue ? valueProp : internalValue;
|
|
@@ -796,13 +805,13 @@ function AsyncAutocomplete({
|
|
|
796
805
|
if (highlightedIndex < 0) return;
|
|
797
806
|
optionRefs.current[highlightedIndex]?.scrollIntoView({ block: "nearest" });
|
|
798
807
|
}, [highlightedIndex]);
|
|
799
|
-
const
|
|
800
|
-
const
|
|
808
|
+
const hasOptions = options.length > 0;
|
|
809
|
+
const showLoadingRow = loading && !hasOptions;
|
|
810
|
+
const showEmpty = !loading && !hasOptions && query.trim() !== "";
|
|
811
|
+
const isRefreshing = loading && hasOptions;
|
|
812
|
+
const hasBody = showLoadingRow || hasOptions || showEmpty;
|
|
801
813
|
const popoverOpen = isOpen && !useSheet && hasBody;
|
|
802
|
-
const dismissIgnoreRefs = React8.useMemo(
|
|
803
|
-
() => [contentRef, fieldRef],
|
|
804
|
-
[]
|
|
805
|
-
);
|
|
814
|
+
const dismissIgnoreRefs = React8.useMemo(() => [contentRef, fieldRef], []);
|
|
806
815
|
useDismissOnScroll(popoverOpen, closePanel, dismissIgnoreRefs);
|
|
807
816
|
const handleKeyDown = (event) => {
|
|
808
817
|
if (useSheet && !isOpen && (event.key === "Enter" || event.key === " " || event.key === "ArrowDown")) {
|
|
@@ -890,7 +899,16 @@ function AsyncAutocomplete({
|
|
|
890
899
|
),
|
|
891
900
|
children: [
|
|
892
901
|
option.label,
|
|
893
|
-
option.description && /* @__PURE__ */ jsx8(
|
|
902
|
+
option.description && /* @__PURE__ */ jsx8(
|
|
903
|
+
"span",
|
|
904
|
+
{
|
|
905
|
+
className: cn(
|
|
906
|
+
"text-gray-subtle",
|
|
907
|
+
classNames?.optionDescription
|
|
908
|
+
),
|
|
909
|
+
children: `, ${option.description}`
|
|
910
|
+
}
|
|
911
|
+
)
|
|
894
912
|
]
|
|
895
913
|
}
|
|
896
914
|
)
|
|
@@ -904,22 +922,71 @@ function AsyncAutocomplete({
|
|
|
904
922
|
{
|
|
905
923
|
role: "listbox",
|
|
906
924
|
id: listboxId,
|
|
925
|
+
"aria-busy": loading || void 0,
|
|
926
|
+
"data-busy": isRefreshing || void 0,
|
|
907
927
|
className: cn("flex flex-col", classNames?.listbox),
|
|
908
|
-
children:
|
|
909
|
-
|
|
910
|
-
|
|
928
|
+
children: showLoadingRow ? (
|
|
929
|
+
// `role="listbox"` may only own `option` and `group` children, so the
|
|
930
|
+
// status rows are disabled options: still announced, never selectable,
|
|
931
|
+
// and skipped by keyboard nav since they are not in `options`.
|
|
932
|
+
/* @__PURE__ */ jsxs5(
|
|
933
|
+
"div",
|
|
911
934
|
{
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
)
|
|
935
|
+
role: "option",
|
|
936
|
+
"aria-disabled": "true",
|
|
937
|
+
"aria-selected": "false",
|
|
938
|
+
className: cn(messageClassName, classNames?.loading),
|
|
939
|
+
children: [
|
|
940
|
+
/* @__PURE__ */ jsx8(
|
|
941
|
+
Loader2,
|
|
942
|
+
{
|
|
943
|
+
className: cn(
|
|
944
|
+
"size-[16px] animate-spin text-gray-icon-light",
|
|
945
|
+
classNames?.loadingSpinner
|
|
946
|
+
)
|
|
947
|
+
}
|
|
948
|
+
),
|
|
949
|
+
loadingMessage
|
|
950
|
+
]
|
|
916
951
|
}
|
|
917
|
-
)
|
|
918
|
-
|
|
919
|
-
|
|
952
|
+
)
|
|
953
|
+
) : showEmpty ? /* @__PURE__ */ jsx8(
|
|
954
|
+
"div",
|
|
955
|
+
{
|
|
956
|
+
role: "option",
|
|
957
|
+
"aria-disabled": "true",
|
|
958
|
+
"aria-selected": "false",
|
|
959
|
+
className: cn(!emptyState && messageClassName, classNames?.empty),
|
|
960
|
+
children: emptyState ?? emptyMessage
|
|
961
|
+
}
|
|
962
|
+
) : options.map(renderRow)
|
|
920
963
|
}
|
|
921
964
|
);
|
|
922
965
|
const activeDescendant = highlightedIndex >= 0 ? optionDomId(highlightedIndex) : void 0;
|
|
966
|
+
const labelNode = label ? /* @__PURE__ */ jsxs5(
|
|
967
|
+
"label",
|
|
968
|
+
{
|
|
969
|
+
htmlFor: `${prefix}-input`,
|
|
970
|
+
"data-slot": "async-autocomplete-label",
|
|
971
|
+
className: cn(
|
|
972
|
+
"flex items-center font-sans font-medium text-[14px] leading-[20px] text-vibrant-text-details",
|
|
973
|
+
classNames?.label
|
|
974
|
+
),
|
|
975
|
+
children: [
|
|
976
|
+
label,
|
|
977
|
+
mandatory && // Decorative: the requirement is conveyed by aria-required on the
|
|
978
|
+
// input, so screen readers say "City, required" rather than "City star".
|
|
979
|
+
/* @__PURE__ */ jsx8(
|
|
980
|
+
"span",
|
|
981
|
+
{
|
|
982
|
+
"aria-hidden": "true",
|
|
983
|
+
className: "ml-0.5 text-[14px] leading-[20px] text-error-surface-default",
|
|
984
|
+
children: "*"
|
|
985
|
+
}
|
|
986
|
+
)
|
|
987
|
+
]
|
|
988
|
+
}
|
|
989
|
+
) : null;
|
|
923
990
|
const field = /* @__PURE__ */ jsx8("div", { ref: fieldRef, className: cn("relative w-full", classNames?.field), children: /* @__PURE__ */ jsx8(
|
|
924
991
|
Input,
|
|
925
992
|
{
|
|
@@ -933,6 +1000,7 @@ function AsyncAutocomplete({
|
|
|
933
1000
|
"aria-expanded": useSheet ? sheetIsOpen : popoverOpen,
|
|
934
1001
|
"aria-controls": popoverOpen ? listboxId : void 0,
|
|
935
1002
|
"aria-autocomplete": useSheet ? void 0 : "list",
|
|
1003
|
+
"aria-required": mandatory || void 0,
|
|
936
1004
|
"aria-activedescendant": popoverOpen ? activeDescendant : void 0,
|
|
937
1005
|
size,
|
|
938
1006
|
error,
|
|
@@ -971,8 +1039,13 @@ function AsyncAutocomplete({
|
|
|
971
1039
|
"div",
|
|
972
1040
|
{
|
|
973
1041
|
"data-slot": "async-autocomplete",
|
|
974
|
-
className: cn(
|
|
1042
|
+
className: cn(
|
|
1043
|
+
"flex w-full flex-col",
|
|
1044
|
+
labelNode && "gap-[8px]",
|
|
1045
|
+
className
|
|
1046
|
+
),
|
|
975
1047
|
children: [
|
|
1048
|
+
labelNode,
|
|
976
1049
|
/* @__PURE__ */ jsx8(PopoverPrimitive.Anchor, { asChild: true, children: field }),
|
|
977
1050
|
/* @__PURE__ */ jsx8(PopoverPrimitive.Portal, { children: /* @__PURE__ */ jsx8(
|
|
978
1051
|
PopoverPrimitive.Content,
|
|
@@ -1076,7 +1149,7 @@ function AsyncAutocomplete({
|
|
|
1076
1149
|
"aria-controls": listboxId,
|
|
1077
1150
|
"aria-autocomplete": "list",
|
|
1078
1151
|
"aria-activedescendant": activeDescendant,
|
|
1079
|
-
"aria-label": sheetTitle,
|
|
1152
|
+
"aria-label": label ?? sheetTitle,
|
|
1080
1153
|
size,
|
|
1081
1154
|
placeholder,
|
|
1082
1155
|
leftIcon,
|