@neasg/design-system 0.4.19 → 0.4.20

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.
@@ -4,6 +4,7 @@ import { AvatarProfile } from "./avatar";
4
4
  import { Button } from "./button";
5
5
  import { cn } from "./lib/utils";
6
6
  import { Popover } from "./popover";
7
+ import { popoverMenuItemFocusClassName } from "./popover-menu";
7
8
  import { Typography } from "./typography";
8
9
  function AvatarProfilePopover({ trigger, children, contentClassName, align = "start", sideOffset = 4, ...props }) {
9
10
  return (_jsx(Popover, { ...props, trigger: trigger, surface: "menu", align: align, sideOffset: sideOffset, contentClassName: cn("max-h-[calc(100vh-1rem)] w-80 max-w-[calc(100vw-1rem)] overflow-visible", contentClassName), children: children }));
@@ -21,7 +22,7 @@ function AvatarProfilePopoverFooter({ className, ...props }) {
21
22
  return (_jsx("div", { "data-slot": "avatar-profile-popover-footer", className: cn("-mx-1 -mb-1 border-t p-1.5", className), ...props }));
22
23
  }
23
24
  function AvatarProfilePopoverFooterAction({ className, tone = "default", type = "button", ...props }) {
24
- return (_jsx(Button, { type: type, variant: "ghost", size: "sm", className: cn("h-auto min-h-8 w-full justify-start px-2 py-1.5 text-left whitespace-normal focus-visible:ring-inset focus-visible:ring-1 focus-visible:ring-offset-0", tone === "destructive" &&
25
+ return (_jsx(Button, { type: type, variant: "ghost", size: "sm", className: cn("h-auto min-h-8 w-full justify-start px-2 py-1.5 text-left whitespace-normal", popoverMenuItemFocusClassName, tone === "destructive" &&
25
26
  "text-destructive hover:bg-destructive/10 hover:text-destructive", className), ...props }));
26
27
  }
27
28
  export { AvatarProfilePopover, AvatarProfilePopoverContent, AvatarProfilePopoverHeader, AvatarProfilePopoverSection, AvatarProfilePopoverFooter, AvatarProfilePopoverFooterAction, };
@@ -7,7 +7,7 @@ import { Button } from "./button";
7
7
  import { CountBadge } from "./count-badge";
8
8
  import { cn } from "./lib/utils";
9
9
  import { Popover } from "./popover";
10
- import { PopoverMenuFooter, PopoverMenuFooterAction, PopoverMenuGroup, PopoverMenuGroupLabel, PopoverMenuItem, PopoverMenuScrollArea, PopoverMenuSearch, PopoverMenuSection, PopoverMenuView, } from "./popover-menu";
10
+ import { PopoverMenuFooter, PopoverMenuFooterAction, PopoverMenuGroup, PopoverMenuGroupLabel, PopoverMenuItem, PopoverMenuScrollArea, PopoverMenuSearch, PopoverMenuSection, PopoverMenuView, popoverMenuItemFocusClassName, } from "./popover-menu";
11
11
  import { Typography } from "./typography";
12
12
  function getText(value) {
13
13
  if (typeof value === "string" || typeof value === "number") {
@@ -72,7 +72,7 @@ function FilterPopover({ align = "start", ariaLabel, contentClassName, defaultOp
72
72
  onOpenChange === null || onOpenChange === void 0 ? void 0 : onOpenChange(nextOpen);
73
73
  };
74
74
  const resolvedTitle = (_c = title !== null && title !== void 0 ? title : directGroup === null || directGroup === void 0 ? void 0 : directGroup.label) !== null && _c !== void 0 ? _c : (!activeGroup ? triggerLabel : undefined);
75
- return (_jsxs(Popover, { align: align, contentClassName: cn("w-[360px]", contentClassName), defaultOpen: defaultOpen, modal: modal, onOpenChange: handleOpenChange, open: open, side: side, surface: "menu", trigger: trigger !== null && trigger !== void 0 ? trigger : defaultTrigger, title: resolvedTitle, "aria-label": (_e = (_d = ariaLabel !== null && ariaLabel !== void 0 ? ariaLabel : getText(resolvedTitle)) !== null && _d !== void 0 ? _d : getText(triggerLabel)) !== null && _e !== void 0 ? _e : "Filters", children: [_jsx(PopoverMenuView, { className: cn("-mx-1 -mt-1 px-1 pt-1", viewClassName), direction: transitionDirection, viewKey: activeGroup ? `group-${activeGroup.id}` : "filter-groups", children: activeGroup ? (_jsxs(_Fragment, { children: [!directGroup ? (_jsx("div", { className: "-mx-1 -mt-1 border-b p-1", children: _jsxs(Button, { type: "button", variant: "ghost", size: "sm", className: "w-full justify-start", onClick: showFilterGroups, children: [_jsx(ChevronLeft, { className: "h-4 w-4" }), activeGroup.label] }) })) : null, activeGroup.searchable ? (_jsx(PopoverMenuSearch, { value: query, onChange: (event) => setQuery(event.target.value), placeholder: (_f = activeGroup.searchPlaceholder) !== null && _f !== void 0 ? _f : `Search ${getGroupLabel(activeGroup).toLowerCase()}...`, "aria-label": (_g = activeGroup.searchPlaceholder) !== null && _g !== void 0 ? _g : `Search ${getGroupLabel(activeGroup).toLowerCase()}` })) : null, _jsx(PopoverMenuScrollArea, { maxHeight: maxHeight, children: _jsx(PopoverMenuSection, { children: filteredOptions.length ? (filteredOptions.map((option) => (_jsx(PopoverMenuItem, { active: option.selected, "aria-label": option.ariaLabel, "aria-pressed": option.selected, disabled: option.disabled, onClick: option.onSelect, children: option.label }, option.id)))) : (_jsx(Typography, { as: "p", variant: "caption", className: "px-2 py-2", children: (_h = activeGroup.emptyLabel) !== null && _h !== void 0 ? _h : emptyLabel })) }) })] })) : (_jsx(PopoverMenuScrollArea, { maxHeight: maxHeight, children: _jsxs(PopoverMenuGroup, { children: [_jsx(PopoverMenuGroupLabel, { children: groupListLabel }), _jsx(PopoverMenuSection, { children: groups.map((group) => {
75
+ return (_jsxs(Popover, { align: align, contentClassName: cn("w-[360px]", contentClassName), defaultOpen: defaultOpen, modal: modal, onOpenChange: handleOpenChange, open: open, side: side, surface: "menu", trigger: trigger !== null && trigger !== void 0 ? trigger : defaultTrigger, title: resolvedTitle, "aria-label": (_e = (_d = ariaLabel !== null && ariaLabel !== void 0 ? ariaLabel : getText(resolvedTitle)) !== null && _d !== void 0 ? _d : getText(triggerLabel)) !== null && _e !== void 0 ? _e : "Filters", children: [_jsx(PopoverMenuView, { className: cn("-mx-1 -mt-1 px-1 pt-1", viewClassName), direction: transitionDirection, viewKey: activeGroup ? `group-${activeGroup.id}` : "filter-groups", children: activeGroup ? (_jsxs(_Fragment, { children: [!directGroup ? (_jsx("div", { className: "-mx-1 -mt-1 border-b p-1", children: _jsxs(Button, { type: "button", variant: "ghost", size: "sm", className: cn("w-full justify-start", popoverMenuItemFocusClassName), onClick: showFilterGroups, children: [_jsx(ChevronLeft, { className: "h-4 w-4" }), activeGroup.label] }) })) : null, activeGroup.searchable ? (_jsx(PopoverMenuSearch, { value: query, onChange: (event) => setQuery(event.target.value), placeholder: (_f = activeGroup.searchPlaceholder) !== null && _f !== void 0 ? _f : `Search ${getGroupLabel(activeGroup).toLowerCase()}...`, "aria-label": (_g = activeGroup.searchPlaceholder) !== null && _g !== void 0 ? _g : `Search ${getGroupLabel(activeGroup).toLowerCase()}` })) : null, _jsx(PopoverMenuScrollArea, { maxHeight: maxHeight, children: _jsx(PopoverMenuSection, { children: filteredOptions.length ? (filteredOptions.map((option) => (_jsx(PopoverMenuItem, { active: option.selected, "aria-label": option.ariaLabel, "aria-pressed": option.selected, disabled: option.disabled, onClick: option.onSelect, children: option.label }, option.id)))) : (_jsx(Typography, { as: "p", variant: "caption", className: "px-2 py-2", children: (_h = activeGroup.emptyLabel) !== null && _h !== void 0 ? _h : emptyLabel })) }) })] })) : (_jsx(PopoverMenuScrollArea, { maxHeight: maxHeight, children: _jsxs(PopoverMenuGroup, { children: [!resolvedTitle ? (_jsx(PopoverMenuGroupLabel, { children: groupListLabel })) : null, _jsx(PopoverMenuSection, { children: groups.map((group) => {
76
76
  var _a;
77
77
  const groupSelectedCount = getGroupSelectedCount(group);
78
78
  const selectionLabel = (_a = group.selectionLabel) !== null && _a !== void 0 ? _a : getFilterSelectionLabel(groupSelectedCount);
@@ -9,7 +9,7 @@ const filterToggleVariants = cva("inline-flex shrink-0 cursor-pointer select-non
9
9
  muted: "",
10
10
  },
11
11
  selected: {
12
- true: "border-transparent bg-primary/10 text-primary",
12
+ true: "border-transparent bg-primary/10 text-primary hover:bg-primary/20 focus-visible:bg-primary/20",
13
13
  false: "border-transparent bg-muted text-muted-foreground hover:bg-muted/80",
14
14
  },
15
15
  },
@@ -1,6 +1,7 @@
1
1
  "use client";
2
2
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
3
  import * as React from "react";
4
+ import { ChevronDownIcon } from "./animated-icons/chevron-down";
4
5
  import { XIcon } from "./animated-icons/x";
5
6
  import { Badge } from "./badge";
6
7
  import { Field, FieldDescription, FieldError, FieldLabel } from "./field";
@@ -22,6 +23,8 @@ function MultiSelect({ label, options, value, onValueChange, placeholder = "Sele
22
23
  const containerRef = React.useRef(null);
23
24
  const triggerRef = React.useRef(null);
24
25
  const searchInputRef = React.useRef(null);
26
+ const listboxRef = React.useRef(null);
27
+ const openFocusTargetRef = React.useRef(searchable ? "search" : "first");
25
28
  const normalizedQuery = searchQuery.trim().toLowerCase();
26
29
  const filteredOptions = searchable && normalizedQuery
27
30
  ? options.filter((opt) => {
@@ -33,6 +36,28 @@ function MultiSelect({ label, options, value, onValueChange, placeholder = "Sele
33
36
  const selectedLabels = value
34
37
  .map((v) => options.find((opt) => opt.value === v))
35
38
  .filter(Boolean);
39
+ const getEnabledOptionElements = React.useCallback(() => {
40
+ var _a, _b;
41
+ return Array.from((_b = (_a = listboxRef.current) === null || _a === void 0 ? void 0 : _a.querySelectorAll('[role="option"]:not(:disabled)')) !== null && _b !== void 0 ? _b : []);
42
+ }, []);
43
+ const focusBoundaryOption = React.useCallback((boundary) => {
44
+ const enabledOptions = getEnabledOptionElements();
45
+ const option = boundary === "first"
46
+ ? enabledOptions[0]
47
+ : enabledOptions[enabledOptions.length - 1];
48
+ option === null || option === void 0 ? void 0 : option.focus({ preventScroll: true });
49
+ }, [getEnabledOptionElements]);
50
+ const openWithFocus = React.useCallback((target) => {
51
+ openFocusTargetRef.current = target;
52
+ setOpen(true);
53
+ }, []);
54
+ const closeAndFocusTrigger = React.useCallback(() => {
55
+ setOpen(false);
56
+ window.requestAnimationFrame(() => {
57
+ var _a;
58
+ (_a = triggerRef.current) === null || _a === void 0 ? void 0 : _a.focus({ preventScroll: true });
59
+ });
60
+ }, []);
36
61
  const toggleOption = React.useCallback((optionValue) => {
37
62
  const next = value.includes(optionValue)
38
63
  ? value.filter((v) => v !== optionValue)
@@ -47,14 +72,16 @@ function MultiSelect({ label, options, value, onValueChange, placeholder = "Sele
47
72
  setSearchQuery("");
48
73
  return;
49
74
  }
50
- if (searchable) {
51
- const frame = window.requestAnimationFrame(() => {
52
- var _a;
75
+ const frame = window.requestAnimationFrame(() => {
76
+ var _a;
77
+ if (openFocusTargetRef.current === "search" && searchable) {
53
78
  (_a = searchInputRef.current) === null || _a === void 0 ? void 0 : _a.focus();
54
- });
55
- return () => window.cancelAnimationFrame(frame);
56
- }
57
- }, [open, searchable]);
79
+ return;
80
+ }
81
+ focusBoundaryOption(openFocusTargetRef.current === "last" ? "last" : "first");
82
+ });
83
+ return () => window.cancelAnimationFrame(frame);
84
+ }, [focusBoundaryOption, open, searchable]);
58
85
  React.useEffect(() => {
59
86
  const handleClickOutside = (event) => {
60
87
  if (containerRef.current &&
@@ -72,33 +99,91 @@ function MultiSelect({ label, options, value, onValueChange, placeholder = "Sele
72
99
  return;
73
100
  if (event.key === "Enter" || event.key === " ") {
74
101
  event.preventDefault();
75
- setOpen((prev) => !prev);
102
+ if (open) {
103
+ setOpen(false);
104
+ }
105
+ else {
106
+ openWithFocus(searchable ? "search" : "first");
107
+ }
76
108
  }
77
109
  else if (event.key === "ArrowDown") {
78
110
  event.preventDefault();
79
- setOpen(true);
111
+ openWithFocus("first");
112
+ }
113
+ else if (event.key === "ArrowUp") {
114
+ event.preventDefault();
115
+ openWithFocus("last");
80
116
  }
81
117
  else if (event.key === "Escape") {
82
- setOpen(false);
118
+ event.preventDefault();
119
+ closeAndFocusTrigger();
120
+ }
121
+ }, [closeAndFocusTrigger, disabled, open, openWithFocus, searchable]);
122
+ const handleSearchKeyDown = React.useCallback((event) => {
123
+ if (event.key === "ArrowDown" || event.key === "ArrowUp") {
124
+ event.preventDefault();
125
+ focusBoundaryOption(event.key === "ArrowDown" ? "first" : "last");
126
+ return;
127
+ }
128
+ if (event.key === "Escape") {
129
+ event.preventDefault();
130
+ closeAndFocusTrigger();
131
+ }
132
+ }, [closeAndFocusTrigger, focusBoundaryOption]);
133
+ const handleListboxKeyDown = React.useCallback((event) => {
134
+ var _a, _b;
135
+ const enabledOptions = getEnabledOptionElements();
136
+ const currentIndex = enabledOptions.findIndex((option) => option === document.activeElement);
137
+ if (event.key === "Escape") {
138
+ event.preventDefault();
139
+ closeAndFocusTrigger();
140
+ return;
141
+ }
142
+ if (event.key === "Home" || event.key === "End") {
143
+ event.preventDefault();
144
+ focusBoundaryOption(event.key === "Home" ? "first" : "last");
145
+ return;
146
+ }
147
+ if (event.key !== "ArrowDown" && event.key !== "ArrowUp") {
148
+ return;
149
+ }
150
+ event.preventDefault();
151
+ if (event.key === "ArrowUp" && currentIndex <= 0 && searchable) {
152
+ (_a = searchInputRef.current) === null || _a === void 0 ? void 0 : _a.focus({ preventScroll: true });
153
+ return;
83
154
  }
84
- }, [disabled]);
155
+ const offset = event.key === "ArrowDown" ? 1 : -1;
156
+ const nextIndex = Math.min(Math.max(currentIndex + offset, 0), enabledOptions.length - 1);
157
+ (_b = enabledOptions[nextIndex]) === null || _b === void 0 ? void 0 : _b.focus({ preventScroll: true });
158
+ }, [
159
+ closeAndFocusTrigger,
160
+ focusBoundaryOption,
161
+ getEnabledOptionElements,
162
+ searchable,
163
+ ]);
85
164
  return (_jsxs(Field, { className: className, "data-disabled": disabled ? "true" : undefined, "data-invalid": isInvalid ? "true" : undefined, children: [_jsx(FieldLabel, { id: labelId, htmlFor: selectId, className: cn(hideLabel && "sr-only", labelClassName), onClick: (event) => {
86
165
  var _a;
87
166
  event.preventDefault();
88
167
  (_a = triggerRef.current) === null || _a === void 0 ? void 0 : _a.focus();
89
- }, children: label }), _jsxs("div", { ref: containerRef, className: "relative", children: [_jsx("div", { ref: triggerRef, role: "combobox", id: selectId, tabIndex: disabled ? -1 : 0, onClick: () => {
168
+ }, children: label }), _jsxs("div", { ref: containerRef, className: "relative", children: [_jsxs("div", { ref: triggerRef, role: "combobox", id: selectId, tabIndex: disabled ? -1 : 0, onClick: () => {
90
169
  if (!disabled) {
91
- setOpen((prev) => !prev);
170
+ if (open) {
171
+ setOpen(false);
172
+ }
173
+ else {
174
+ openWithFocus(searchable ? "search" : "first");
175
+ }
92
176
  }
93
- }, onKeyDown: handleTriggerKeyDown, "aria-expanded": open, "aria-controls": listboxId, "aria-haspopup": "listbox", "aria-invalid": isInvalid ? "true" : undefined, "aria-disabled": disabled ? "true" : undefined, "aria-labelledby": labelId, "aria-describedby": [descriptionId, errorId].filter(Boolean).join(" ") || undefined, className: cn("flex h-control w-full cursor-pointer items-center gap-1.5 overflow-hidden rounded-control border border-input bg-background px-2.5 py-1.5 text-sm ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2", isInvalid ? "border-destructive" : "border-input", disabled && "cursor-not-allowed opacity-50", triggerClassName), children: selectedLabels.length > 0 ? (_jsxs(_Fragment, { children: [selectedLabels.slice(0, maxVisibleTags).map((opt) => (_jsxs(Badge, { variant: "secondary", className: "max-w-[10rem] shrink-0 justify-start gap-1 overflow-visible text-sm font-medium", children: [_jsx("span", { className: "min-w-0 truncate", children: opt.label }), _jsx("button", { type: "button", className: "inline-flex cursor-pointer items-center justify-center rounded-sm text-muted-foreground hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring", onClick: (e) => {
94
- e.stopPropagation();
95
- removeOption(opt.value);
96
- }, "aria-label": `Remove ${opt.label}`, children: _jsx(XIcon, { size: 12 }) })] }, opt.value))), selectedLabels.length > maxVisibleTags ? (_jsxs(Badge, { variant: "secondary", className: "shrink-0 text-sm font-medium", title: selectedLabels
97
- .slice(maxVisibleTags)
98
- .map((o) => o.label)
99
- .join(", "), children: ["+", selectedLabels.length - maxVisibleTags] })) : null] })) : (_jsx("span", { className: "text-muted-foreground", children: placeholder })) }), open ? (_jsxs("div", { className: "absolute z-50 mt-1 w-full rounded-overlay border bg-popover shadow-md", children: [searchable ? (_jsx("div", { className: "border-b px-2 py-1", children: _jsx(InputControl, { ref: searchInputRef, value: searchQuery, onChange: (e) => setSearchQuery(e.target.value), placeholder: searchPlaceholder, className: "h-8 border-0 bg-transparent px-2 py-0 text-sm shadow-none focus-visible:ring-0", onKeyDown: (e) => e.stopPropagation() }) })) : null, _jsx("div", { id: listboxId, role: "listbox", "aria-label": typeof label === "string" ? label : "Options", "aria-multiselectable": "true", className: "max-h-64 overflow-y-auto p-1", children: filteredOptions.length > 0 ? (filteredOptions.map((option) => {
177
+ }, onKeyDown: handleTriggerKeyDown, "aria-expanded": open, "aria-controls": listboxId, "aria-haspopup": "listbox", "aria-invalid": isInvalid ? "true" : undefined, "aria-disabled": disabled ? "true" : undefined, "aria-labelledby": labelId, "aria-describedby": [descriptionId, errorId].filter(Boolean).join(" ") || undefined, className: cn("relative flex h-control w-full cursor-pointer items-center gap-1.5 overflow-hidden rounded-control border border-input bg-background px-2.5 py-1.5 pr-8 text-sm ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2", isInvalid ? "border-destructive" : "border-input", disabled && "cursor-not-allowed opacity-50", triggerClassName), children: [selectedLabels.length > 0 ? (_jsxs(_Fragment, { children: [selectedLabels.slice(0, maxVisibleTags).map((opt) => (_jsxs(Badge, { variant: "secondary", className: "max-w-[10rem] shrink-0 justify-start gap-1 overflow-visible text-sm font-medium", children: [_jsx("span", { className: "min-w-0 truncate", children: opt.label }), _jsx("button", { type: "button", className: "inline-flex cursor-pointer items-center justify-center rounded-sm text-muted-foreground hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring", onClick: (e) => {
178
+ e.stopPropagation();
179
+ removeOption(opt.value);
180
+ }, "aria-label": `Remove ${opt.label}`, children: _jsx(XIcon, { size: 12 }) })] }, opt.value))), selectedLabels.length > maxVisibleTags ? (_jsxs(Badge, { variant: "secondary", className: "shrink-0 text-sm font-medium", title: selectedLabels
181
+ .slice(maxVisibleTags)
182
+ .map((o) => o.label)
183
+ .join(", "), children: ["+", selectedLabels.length - maxVisibleTags] })) : null] })) : (_jsx("span", { className: "text-muted-foreground", children: placeholder })), _jsx(ChevronDownIcon, { "aria-hidden": "true", size: 16, className: cn("pointer-events-none absolute right-2.5 top-1/2 -translate-y-1/2 shrink-0 text-muted-foreground transition-transform duration-150", open && "rotate-180") })] }), open ? (_jsxs("div", { className: "absolute z-50 mt-1 w-full rounded-overlay border bg-popover shadow-md", children: [searchable ? (_jsx("div", { className: "border-b p-1", children: _jsx(InputControl, { ref: searchInputRef, value: searchQuery, onChange: (e) => setSearchQuery(e.target.value), placeholder: searchPlaceholder, className: "h-control border-0 bg-transparent px-2 py-0 text-sm shadow-none", onKeyDown: handleSearchKeyDown }) })) : null, _jsx("div", { ref: listboxRef, id: listboxId, role: "listbox", "aria-label": typeof label === "string" ? label : "Options", "aria-multiselectable": "true", className: "max-h-64 overflow-y-auto p-1", onKeyDown: handleListboxKeyDown, children: filteredOptions.length > 0 ? (filteredOptions.map((option) => {
100
184
  const isSelected = value.includes(option.value);
101
- return (_jsxs("button", { type: "button", role: "option", "aria-selected": isSelected, disabled: option.disabled, onClick: () => toggleOption(option.value), className: cn("flex min-h-8 w-full cursor-pointer items-center gap-1.5 rounded-sm px-2 py-1.5 text-left text-sm outline-none transition-colors hover:bg-accent hover:text-accent-foreground disabled:pointer-events-none disabled:opacity-50", isSelected && "bg-accent"), children: [_jsx(Checkbox, { checked: isSelected, disabled: option.disabled, tabIndex: -1, readOnly: true, "aria-hidden": "true", className: "mt-0.5" }), _jsxs("div", { className: "flex flex-col gap-0.5", children: [_jsx("span", { children: option.label }), option.description ? (_jsx(Typography, { as: "span", variant: "caption", children: option.description })) : null] })] }, option.value));
185
+ return (_jsxs("button", { type: "button", role: "option", "aria-selected": isSelected, disabled: option.disabled, tabIndex: -1, onClick: () => toggleOption(option.value), className: cn("flex min-h-8 w-full cursor-pointer items-center gap-1.5 rounded-sm px-2 py-1.5 text-left text-sm outline-none transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground disabled:pointer-events-none disabled:opacity-50", isSelected &&
186
+ "bg-primary/10 text-primary hover:bg-primary/20 hover:text-primary focus:bg-primary/20 focus:text-primary"), children: [_jsx(Checkbox, { checked: isSelected, disabled: option.disabled, tabIndex: -1, readOnly: true, "aria-hidden": "true", className: "mt-0.5" }), _jsxs("div", { className: "flex flex-col gap-0.5", children: [_jsx("span", { children: option.label }), option.description ? (_jsx(Typography, { as: "span", variant: "caption", children: option.description })) : null] })] }, option.value));
102
187
  })) : (_jsx("div", { className: "px-2.5 py-1.5 text-center text-sm text-muted-foreground", children: "No options found." })) })] })) : null] }), showDescription ? (_jsx(FieldDescription, { id: descriptionId, className: descriptionClassName, children: description })) : null, _jsx(FieldError, { id: errorId, className: errorClassName, children: error })] }));
103
188
  }
104
189
  export { MultiSelect };
@@ -4,6 +4,8 @@ import { type CheckboxProps } from "./checkbox";
4
4
  import { type InputProps } from "./input";
5
5
  import { PopoverContent } from "./popover-primitive";
6
6
  import { Typography } from "./typography";
7
+ declare const popoverMenuItemFocusClassName = "rounded-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring focus-visible:ring-offset-0";
8
+ declare const popoverMenuItemFocusWithinClassName = "rounded-sm focus-within:ring-2 focus-within:ring-inset focus-within:ring-ring";
7
9
  export interface PopoverMenuHeaderProps extends Omit<React.ComponentPropsWithoutRef<typeof Typography>, "as" | "asChild" | "variant" | "color"> {
8
10
  }
9
11
  export interface PopoverMenuContentProps extends React.ComponentPropsWithoutRef<typeof PopoverContent> {
@@ -56,4 +58,4 @@ export interface PopoverMenuViewProps extends React.HTMLAttributes<HTMLDivElemen
56
58
  viewKey?: React.Key;
57
59
  }
58
60
  declare function PopoverMenuView({ children, className, direction, viewKey, ...props }: PopoverMenuViewProps): import("react/jsx-runtime").JSX.Element;
59
- export { PopoverMenuContent, PopoverMenuHeader, PopoverMenuSection, PopoverMenuItem, PopoverMenuCheckboxItem, PopoverMenuDivider, PopoverMenuFooter, PopoverMenuFooterAction, PopoverMenuFooterMeta, PopoverMenuGroup, PopoverMenuGroupLabel, PopoverMenuSearch, PopoverMenuScrollArea, PopoverMenuView, };
61
+ export { PopoverMenuContent, PopoverMenuHeader, PopoverMenuSection, PopoverMenuItem, PopoverMenuCheckboxItem, PopoverMenuDivider, PopoverMenuFooter, PopoverMenuFooterAction, PopoverMenuFooterMeta, PopoverMenuGroup, PopoverMenuGroupLabel, PopoverMenuSearch, PopoverMenuScrollArea, PopoverMenuView, popoverMenuItemFocusClassName, popoverMenuItemFocusWithinClassName, };
@@ -10,6 +10,8 @@ import { PopoverContent } from "./popover-primitive";
10
10
  import { Typography } from "./typography";
11
11
  const POPOVER_MENU_VIEW_TRANSITION_MS = 300;
12
12
  const useIsomorphicLayoutEffect = typeof window === "undefined" ? React.useEffect : React.useLayoutEffect;
13
+ const popoverMenuItemFocusClassName = "rounded-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring focus-visible:ring-offset-0";
14
+ const popoverMenuItemFocusWithinClassName = "rounded-sm focus-within:ring-2 focus-within:ring-inset focus-within:ring-ring";
13
15
  const PopoverMenuContent = React.forwardRef(({ className, ...props }, ref) => (_jsx(PopoverContent, { ref: ref, className: cn("overflow-hidden p-1", className), ...props })));
14
16
  PopoverMenuContent.displayName = "PopoverMenuContent";
15
17
  function PopoverMenuHeader({ className, children, ...props }) {
@@ -19,11 +21,12 @@ function PopoverMenuSection({ className, children, ...props }) {
19
21
  return (_jsx("div", { className: cn("space-y-0", className), ...props, children: children }));
20
22
  }
21
23
  function PopoverMenuItem({ className, children, active, type = "button", ...props }) {
22
- return (_jsxs(Button, { type: type, variant: "ghost", className: cn("h-auto min-h-8 w-full justify-start gap-2 rounded-sm px-2 py-2 text-left font-normal whitespace-normal focus-visible:ring-inset focus-visible:ring-1 focus-visible:ring-offset-0", active &&
23
- "bg-accent text-accent-foreground hover:bg-accent hover:text-accent-foreground", className), ...props, children: [children, active ? _jsx(Check, { "aria-hidden": "true", className: "ml-auto h-4 w-4 shrink-0" }) : null] }));
24
+ return (_jsxs(Button, { type: type, variant: "ghost", "data-slot": "popover-menu-item", className: cn("h-auto min-h-8 w-full justify-start gap-2 px-2 py-2 text-left font-normal whitespace-normal", popoverMenuItemFocusClassName, active &&
25
+ "bg-primary/10 text-primary hover:bg-primary/20 hover:text-primary focus-visible:bg-primary/20 focus-visible:text-primary", className), ...props, children: [children, active ? _jsx(Check, { "aria-hidden": "true", className: "ml-auto h-4 w-4 shrink-0" }) : null] }));
24
26
  }
25
27
  function PopoverMenuCheckboxItem({ className, children, checked = false, description, disabled, onChange, readOnly, ...props }) {
26
- return (_jsxs("label", { className: cn("flex h-auto min-h-8 w-full cursor-pointer items-start gap-1.5 rounded-sm px-2 py-2 text-left text-popover-foreground outline-none transition-colors hover:bg-accent hover:text-accent-foreground focus-within:bg-accent focus-within:text-accent-foreground", checked && "bg-accent text-accent-foreground", disabled &&
28
+ return (_jsxs("label", { className: cn("flex h-auto min-h-8 w-full cursor-pointer items-start gap-1.5 rounded-sm px-2 py-2 text-left text-popover-foreground outline-none transition-colors hover:bg-accent hover:text-accent-foreground focus-within:bg-accent focus-within:text-accent-foreground", popoverMenuItemFocusWithinClassName, checked &&
29
+ "bg-primary/10 text-primary hover:bg-primary/20 hover:text-primary focus-within:bg-primary/20 focus-within:text-primary", disabled &&
27
30
  "cursor-not-allowed opacity-50 hover:bg-transparent hover:text-inherit", className), children: [_jsx(Checkbox, { ...props, checked: checked, disabled: disabled, readOnly: readOnly !== null && readOnly !== void 0 ? readOnly : !onChange, onChange: onChange, className: "mt-0.5" }), _jsxs("span", { className: "min-w-0 flex-1", children: [_jsx(Typography, { as: "span", variant: "bodySm", className: "block", children: children }), description ? (_jsx(Typography, { as: "span", variant: "caption", className: "mt-0.5 block", children: description })) : null] })] }));
28
31
  }
29
32
  function PopoverMenuDivider({ className, ...props }) {
@@ -33,7 +36,7 @@ function PopoverMenuFooter({ className, children, inset = "menu", ...props }) {
33
36
  return (_jsx("div", { className: cn("flex items-center gap-2 border-t p-1.5", inset === "menu" && "-mx-1 -mb-1", inset === "flush" && "m-0", className), ...props, children: children }));
34
37
  }
35
38
  function PopoverMenuFooterAction({ className, type = "button", ...props }) {
36
- return (_jsx(Button, { type: type, variant: "ghost", size: "sm", className: cn("h-auto min-h-8 w-full justify-start px-2 py-1.5 text-left whitespace-normal focus-visible:ring-inset focus-visible:ring-1 focus-visible:ring-offset-0", className), ...props }));
39
+ return (_jsx(Button, { type: type, variant: "ghost", size: "sm", className: cn("h-auto min-h-8 w-full justify-start px-2 py-1.5 text-left whitespace-normal", popoverMenuItemFocusClassName, className), ...props }));
37
40
  }
38
41
  function PopoverMenuFooterMeta({ className, children, ...props }) {
39
42
  return (_jsx(Typography, { as: "span", variant: "bodySm", color: "muted", className: cn("shrink-0 px-2 font-medium", className), ...props, children: children }));
@@ -46,7 +49,7 @@ function PopoverMenuGroupLabel({ className, children, ...props }) {
46
49
  }
47
50
  const PopoverMenuSearch = React.forwardRef(({ "aria-label": ariaLabel, className, containerClassName, label, placeholder, ...props }, ref) => {
48
51
  var _a, _b;
49
- return (_jsx("div", { className: cn("relative -mx-1 -mt-1 mb-1 px-2 py-1 after:absolute after:inset-x-0 after:bottom-0 after:h-px after:bg-border after:content-['']", containerClassName), children: _jsx(Input, { ref: ref, label: (_b = (_a = label !== null && label !== void 0 ? label : ariaLabel) !== null && _a !== void 0 ? _a : placeholder) !== null && _b !== void 0 ? _b : "Search", hideLabel: true, className: "gap-0", inputClassName: className, "aria-label": ariaLabel, placeholder: placeholder, ...props }) }));
52
+ return (_jsx("div", { className: cn("relative -mx-1 -mt-1 mb-1 px-2 py-1 after:absolute after:inset-x-0 after:bottom-0 after:h-px after:bg-border after:content-['']", containerClassName), children: _jsx(Input, { ref: ref, "data-popover-menu-search": "true", label: (_b = (_a = label !== null && label !== void 0 ? label : ariaLabel) !== null && _a !== void 0 ? _a : placeholder) !== null && _b !== void 0 ? _b : "Search", hideLabel: true, className: "gap-0", inputClassName: className, "aria-label": ariaLabel, placeholder: placeholder, ...props }) }));
50
53
  });
51
54
  PopoverMenuSearch.displayName = "PopoverMenuSearch";
52
55
  function PopoverMenuScrollArea({ className, children, maxHeight = "max-h-64", ...props }) {
@@ -106,4 +109,4 @@ function PopoverMenuView({ children, className, direction = "forward", viewKey =
106
109
  "motion-safe:slide-in-from-right-full", activeDirection === "backward" &&
107
110
  "motion-safe:slide-in-from-left-full"), children: children }, viewKey)] }));
108
111
  }
109
- export { PopoverMenuContent, PopoverMenuHeader, PopoverMenuSection, PopoverMenuItem, PopoverMenuCheckboxItem, PopoverMenuDivider, PopoverMenuFooter, PopoverMenuFooterAction, PopoverMenuFooterMeta, PopoverMenuGroup, PopoverMenuGroupLabel, PopoverMenuSearch, PopoverMenuScrollArea, PopoverMenuView, };
112
+ export { PopoverMenuContent, PopoverMenuHeader, PopoverMenuSection, PopoverMenuItem, PopoverMenuCheckboxItem, PopoverMenuDivider, PopoverMenuFooter, PopoverMenuFooterAction, PopoverMenuFooterMeta, PopoverMenuGroup, PopoverMenuGroupLabel, PopoverMenuSearch, PopoverMenuScrollArea, PopoverMenuView, popoverMenuItemFocusClassName, popoverMenuItemFocusWithinClassName, };
package/dist/popover.js CHANGED
@@ -9,7 +9,49 @@ function Popover({ defaultOpen, modal, onOpenChange, open, trigger, children, ti
9
9
  const generatedTitleId = React.useId();
10
10
  const titleId = title ? `popover-title-${generatedTitleId}` : undefined;
11
11
  const triggerElement = _jsx(PopoverTrigger, { asChild: true, children: trigger });
12
- return (_jsxs(PopoverRoot, { defaultOpen: defaultOpen, modal: modal, onOpenChange: onOpenChange, open: open, children: [triggerTooltip ? (_jsx(Tooltip, { content: triggerTooltip, side: triggerTooltipSide, sideOffset: triggerTooltipSideOffset, contentClassName: triggerTooltipClassName, children: triggerElement })) : (triggerElement), _jsxs(PopoverContent, { align: align, sideOffset: sideOffset, "aria-labelledby": titleId !== null && titleId !== void 0 ? titleId : contentProps["aria-labelledby"], className: cn(surface === "menu" && "overflow-hidden p-1", contentClassName), ...contentProps, children: [title ? (_jsx(PopoverTitle, { id: titleId, className: cn(surface === "menu"
12
+ const contentOnKeyDown = contentProps.onKeyDown;
13
+ const handleContentKeyDown = React.useCallback((event) => {
14
+ contentOnKeyDown === null || contentOnKeyDown === void 0 ? void 0 : contentOnKeyDown(event);
15
+ if (event.defaultPrevented ||
16
+ !["ArrowDown", "ArrowUp", "Home", "End"].includes(event.key)) {
17
+ return;
18
+ }
19
+ const menuItems = Array.from(event.currentTarget.querySelectorAll('[data-slot="popover-menu-item"]:not(:disabled)')).filter((item) => !item.closest('[aria-hidden="true"]'));
20
+ if (menuItems.length === 0) {
21
+ return;
22
+ }
23
+ const currentIndex = menuItems.findIndex((item) => item === document.activeElement);
24
+ const searchInput = event.currentTarget.querySelector('[data-popover-menu-search="true"]');
25
+ let focusTarget;
26
+ if (event.key === "Home") {
27
+ focusTarget = menuItems[0];
28
+ }
29
+ else if (event.key === "End") {
30
+ focusTarget = menuItems[menuItems.length - 1];
31
+ }
32
+ else if (document.activeElement === searchInput) {
33
+ focusTarget =
34
+ event.key === "ArrowDown"
35
+ ? menuItems[0]
36
+ : menuItems[menuItems.length - 1];
37
+ }
38
+ else if (currentIndex >= 0) {
39
+ if (event.key === "ArrowUp" && currentIndex === 0 && searchInput) {
40
+ focusTarget = searchInput;
41
+ }
42
+ else {
43
+ const offset = event.key === "ArrowDown" ? 1 : -1;
44
+ const nextIndex = Math.min(Math.max(currentIndex + offset, 0), menuItems.length - 1);
45
+ focusTarget = menuItems[nextIndex];
46
+ }
47
+ }
48
+ if (!focusTarget) {
49
+ return;
50
+ }
51
+ event.preventDefault();
52
+ focusTarget.focus({ preventScroll: true });
53
+ }, [contentOnKeyDown]);
54
+ return (_jsxs(PopoverRoot, { defaultOpen: defaultOpen, modal: modal, onOpenChange: onOpenChange, open: open, children: [triggerTooltip ? (_jsx(Tooltip, { content: triggerTooltip, side: triggerTooltipSide, sideOffset: triggerTooltipSideOffset, contentClassName: triggerTooltipClassName, children: triggerElement })) : (triggerElement), _jsxs(PopoverContent, { align: align, sideOffset: sideOffset, "aria-labelledby": titleId !== null && titleId !== void 0 ? titleId : contentProps["aria-labelledby"], className: cn(surface === "menu" && "overflow-hidden p-1", contentClassName), ...contentProps, onKeyDown: handleContentKeyDown, children: [title ? (_jsx(PopoverTitle, { id: titleId, className: cn(surface === "menu"
13
55
  ? "px-2 py-1.5"
14
56
  : "-mx-4 -mt-4 mb-4 border-b px-4 py-3"), children: title })) : null, children] })] }));
15
57
  }
@@ -7,6 +7,7 @@ import { Button } from "./button";
7
7
  import { cn } from "./lib/utils";
8
8
  import { Link } from "./link";
9
9
  import { Popover } from "./popover";
10
+ import { popoverMenuItemFocusClassName } from "./popover-menu";
10
11
  import { Typography } from "./typography";
11
12
  function hasSectionNavChildren(item) {
12
13
  return Array.isArray(item.items) && item.items.length > 0;
@@ -28,7 +29,7 @@ const SectionNavRow = React.forwardRef(({ item, active, surface, hasChildren = f
28
29
  const isLink = Boolean(item.href) && !item.disabled && !hasChildren;
29
30
  const className = cn(surface === "list"
30
31
  ? "w-full justify-start gap-2 rounded-control px-2.5 text-sm font-medium shadow-none"
31
- : "h-auto min-h-8 w-full justify-start gap-2 rounded-sm px-2 py-1.5 text-left text-sm font-normal shadow-none whitespace-normal", active && !item.disabled
32
+ : cn("h-auto min-h-8 w-full justify-start gap-2 px-2 py-1.5 text-left text-sm font-normal shadow-none whitespace-normal", popoverMenuItemFocusClassName), active && !item.disabled
32
33
  ? surface === "list"
33
34
  ? "bg-background hover:bg-background"
34
35
  : "bg-accent text-accent-foreground hover:bg-accent hover:text-accent-foreground"
@@ -42,14 +43,14 @@ const SectionNavRow = React.forwardRef(({ item, active, surface, hasChildren = f
42
43
  hasChildren,
43
44
  });
44
45
  if (isLink) {
45
- return (_jsx(Button, { ref: ref, asChild: true, variant: "ghost", disabled: item.disabled, className: cn(className, triggerClassName), "aria-current": active ? "page" : undefined, ...buttonProps, children: _jsx(Link, { variant: "inherit", href: item.href, target: item.target, rel: item.rel, onClick: (event) => {
46
+ return (_jsx(Button, { ref: ref, asChild: true, variant: "ghost", disabled: item.disabled, "data-slot": surface === "menu" ? "popover-menu-item" : undefined, className: cn(className, triggerClassName), "aria-current": active ? "page" : undefined, ...buttonProps, children: _jsx(Link, { variant: "inherit", href: item.href, target: item.target, rel: item.rel, onClick: (event) => {
46
47
  var _a;
47
48
  externalOnClick === null || externalOnClick === void 0 ? void 0 : externalOnClick(event);
48
49
  onSelect === null || onSelect === void 0 ? void 0 : onSelect();
49
50
  (_a = item.onClick) === null || _a === void 0 ? void 0 : _a.call(item);
50
51
  }, children: content }) }));
51
52
  }
52
- return (_jsx(Button, { ref: ref, type: "button", variant: "ghost", disabled: item.disabled, className: cn(className, triggerClassName), "aria-current": active && !hasChildren ? "page" : undefined, "aria-expanded": hasChildren ? ariaExpanded : undefined, "aria-haspopup": hasChildren ? "menu" : undefined, ...buttonProps, onClick: hasChildren
53
+ return (_jsx(Button, { ref: ref, type: "button", variant: "ghost", disabled: item.disabled, "data-slot": surface === "menu" ? "popover-menu-item" : undefined, className: cn(className, triggerClassName), "aria-current": active && !hasChildren ? "page" : undefined, "aria-expanded": hasChildren ? ariaExpanded : undefined, "aria-haspopup": hasChildren ? "menu" : undefined, ...buttonProps, onClick: hasChildren
53
54
  ? externalOnClick
54
55
  : (event) => {
55
56
  var _a;
@@ -27,7 +27,7 @@ const SelectContent = React.forwardRef(({ className, children, header, position
27
27
  SelectContent.displayName = SelectPrimitive.Content.displayName;
28
28
  const SelectLabel = React.forwardRef(({ className, ...props }, ref) => (_jsx(SelectPrimitive.Label, { ref: ref, "data-slot": "select-label", className: cn("px-2 py-1.5 text-xs text-muted-foreground", className), ...props })));
29
29
  SelectLabel.displayName = SelectPrimitive.Label.displayName;
30
- const SelectItem = React.forwardRef(({ className, children, description, ...props }, ref) => (_jsxs(SelectPrimitive.Item, { ref: ref, "data-slot": "select-item", className: cn("relative flex min-h-8 w-full cursor-pointer select-none items-center gap-1.5 rounded-sm py-1.5 pl-2 pr-8 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50", className), ...props, children: [_jsx("span", { "data-slot": "select-item-indicator", className: "absolute right-2 flex h-3.5 w-3.5 items-center justify-center", children: _jsx(SelectPrimitive.ItemIndicator, { children: _jsx(Check, { className: "h-4 w-4" }) }) }), _jsxs("div", { className: "flex flex-col gap-0.5", children: [_jsx(SelectPrimitive.ItemText, { className: "break-words font-normal", children: children }), description ? (_jsx(Typography, { as: "span", variant: "caption", children: description })) : null] })] })));
30
+ const SelectItem = React.forwardRef(({ className, children, description, ...props }, ref) => (_jsxs(SelectPrimitive.Item, { ref: ref, "data-slot": "select-item", className: cn("relative flex min-h-8 w-full cursor-pointer select-none items-center gap-1.5 rounded-sm py-1.5 pl-2 pr-8 text-sm outline-none hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground data-[state=checked]:bg-primary/10 data-[state=checked]:text-primary data-[state=checked]:hover:bg-primary/20 data-[state=checked]:hover:text-primary data-[state=checked]:focus:bg-primary/20 data-[state=checked]:focus:text-primary data-[disabled]:pointer-events-none data-[disabled]:opacity-50", className), ...props, children: [_jsx("span", { "data-slot": "select-item-indicator", className: "absolute right-2 flex h-3.5 w-3.5 items-center justify-center", children: _jsx(SelectPrimitive.ItemIndicator, { children: _jsx(Check, { className: "h-4 w-4" }) }) }), _jsxs("div", { className: "flex flex-col gap-0.5", children: [_jsx(SelectPrimitive.ItemText, { className: "break-words font-normal", children: children }), description ? (_jsx(Typography, { as: "span", variant: "caption", children: description })) : null] })] })));
31
31
  SelectItem.displayName = SelectPrimitive.Item.displayName;
32
32
  const SelectSeparator = React.forwardRef(({ className, ...props }, ref) => (_jsx(SelectPrimitive.Separator, { ref: ref, "data-slot": "select-separator", className: cn("-mx-1 my-1 h-px bg-border", className), ...props })));
33
33
  SelectSeparator.displayName = SelectPrimitive.Separator.displayName;
package/dist/select.js CHANGED
@@ -43,6 +43,7 @@ function Select({ label, options, placeholder = "Select an option", description,
43
43
  const generatedId = React.useId();
44
44
  const selectId = id !== null && id !== void 0 ? id : `select-${generatedId}`;
45
45
  const searchInputRef = React.useRef(null);
46
+ const contentRef = React.useRef(null);
46
47
  const [uncontrolledOpen, setUncontrolledOpen] = React.useState(false);
47
48
  const [searchQuery, setSearchQuery] = React.useState("");
48
49
  const errorId = error ? `${selectId}-error` : undefined;
@@ -69,6 +70,31 @@ function Select({ label, options, placeholder = "Select an option", description,
69
70
  }
70
71
  input.focus({ preventScroll: true });
71
72
  }, []);
73
+ const getEnabledOptionElements = React.useCallback(() => {
74
+ var _a, _b;
75
+ return Array.from((_b = (_a = contentRef.current) === null || _a === void 0 ? void 0 : _a.querySelectorAll('[data-slot="select-item"]:not([data-disabled])')) !== null && _b !== void 0 ? _b : []);
76
+ }, []);
77
+ const focusBoundaryOption = React.useCallback((boundary) => {
78
+ const enabledOptions = getEnabledOptionElements();
79
+ const option = boundary === "first"
80
+ ? enabledOptions[0]
81
+ : enabledOptions[enabledOptions.length - 1];
82
+ option === null || option === void 0 ? void 0 : option.focus({ preventScroll: true });
83
+ }, [getEnabledOptionElements]);
84
+ const handleContentKeyDownCapture = React.useCallback((event) => {
85
+ if (event.key !== "ArrowUp") {
86
+ return;
87
+ }
88
+ const firstOption = getEnabledOptionElements()[0];
89
+ if (document.activeElement !== firstOption) {
90
+ return;
91
+ }
92
+ event.preventDefault();
93
+ event.stopPropagation();
94
+ if (searchable) {
95
+ focusSearchInput();
96
+ }
97
+ }, [focusSearchInput, getEnabledOptionElements, searchable]);
72
98
  React.useEffect(() => {
73
99
  if (!isOpen) {
74
100
  setSearchQuery("");
@@ -110,7 +136,17 @@ function Select({ label, options, placeholder = "Select an option", description,
110
136
  }
111
137
  onOpenChange === null || onOpenChange === void 0 ? void 0 : onOpenChange(nextOpen);
112
138
  }, [onOpenChange, open]);
113
- return (_jsxs(Field, { className: className, "data-disabled": disabled ? "true" : undefined, "data-invalid": isInvalid ? "true" : undefined, children: [_jsx(FieldLabel, { htmlFor: selectId, className: cn(hideLabel && "sr-only", labelClassName), children: label }), _jsxs(SelectRoot, { disabled: disabled, open: open, onOpenChange: handleOpenChange, ...props, children: [_jsx(SelectTrigger, { id: selectId, className: triggerClassName, "aria-invalid": isInvalid ? "true" : undefined, "aria-describedby": [descriptionId, errorId].filter(Boolean).join(" ") || undefined, children: _jsx(SelectValue, { placeholder: placeholder }) }), _jsx(SelectContent, { className: contentClassName, header: searchable ? (_jsx(InputControl, { ref: searchInputRef, value: searchQuery, onChange: (event) => setSearchQuery(event.target.value), placeholder: searchPlaceholder, "aria-label": searchInputAriaLabel, autoComplete: "off", className: "h-control border-0 bg-transparent px-2 py-0 text-sm shadow-none focus-visible:border-0 focus-visible:ring-0", onKeyDown: (event) => {
139
+ return (_jsxs(Field, { className: className, "data-disabled": disabled ? "true" : undefined, "data-invalid": isInvalid ? "true" : undefined, children: [_jsx(FieldLabel, { htmlFor: selectId, className: cn(hideLabel && "sr-only", labelClassName), children: label }), _jsxs(SelectRoot, { disabled: disabled, open: open, onOpenChange: handleOpenChange, ...props, children: [_jsx(SelectTrigger, { id: selectId, className: triggerClassName, "aria-invalid": isInvalid ? "true" : undefined, "aria-describedby": [descriptionId, errorId].filter(Boolean).join(" ") || undefined, children: _jsx(SelectValue, { placeholder: placeholder }) }), _jsx(SelectContent, { ref: contentRef, className: contentClassName, onKeyDownCapture: handleContentKeyDownCapture, header: searchable ? (_jsx(InputControl, { ref: searchInputRef, value: searchQuery, onChange: (event) => setSearchQuery(event.target.value), placeholder: searchPlaceholder, "aria-label": searchInputAriaLabel, autoComplete: "off", className: "h-control border-0 bg-transparent px-2 py-0 text-sm shadow-none", onKeyDown: (event) => {
140
+ if (event.key === "ArrowDown" ||
141
+ event.key === "ArrowUp") {
142
+ event.preventDefault();
143
+ event.stopPropagation();
144
+ focusBoundaryOption(event.key === "ArrowDown" ? "first" : "last");
145
+ return;
146
+ }
147
+ if (event.key === "Escape" || event.key === "Tab") {
148
+ return;
149
+ }
114
150
  event.stopPropagation();
115
151
  } })) : undefined, viewportClassName: searchable ? "max-h-64" : undefined, children: hasVisibleOptions ? (optionGroups.map((group, groupIndex) => {
116
152
  var _a;
@@ -23,14 +23,14 @@ function getOptionsFilterSummary(filter) {
23
23
  }
24
24
  return ((_c = (_b = filter.options.find((option) => option.value === filter.values[0])) === null || _b === void 0 ? void 0 : _b.label) !== null && _c !== void 0 ? _c : filter.values[0]);
25
25
  }
26
- const TableFilterTrigger = React.forwardRef(({ active, disabled, label, children, ...props }, ref) => (_jsx(FilterToggle, { ref: ref, type: "button", disabled: disabled, selected: active, "aria-label": `Filter by ${label}`, "aria-pressed": active, className: cn("h-9 w-full min-w-0 justify-start overflow-hidden px-2 text-sm font-normal", !active &&
26
+ const TableFilterTrigger = React.forwardRef(({ active, disabled, label, children, ...props }, ref) => (_jsx(FilterToggle, { ref: ref, type: "button", disabled: disabled, selected: active, "aria-label": `Filter by ${label}${active ? ", active" : ""}`, className: cn("h-9 w-full min-w-0 justify-start overflow-hidden px-2 text-sm font-normal", !active &&
27
27
  "bg-transparent text-muted-foreground hover:bg-accent hover:text-accent-foreground"), icon: _jsx(FilterIcon, { "aria-hidden": "true", className: "shrink-0", size: 16 }), ...props, children: _jsx("span", { className: "min-w-0 flex-1 truncate text-left", children: children }) })));
28
28
  TableFilterTrigger.displayName = "TableFilterTrigger";
29
29
  function TextColumnFilter({ filter, label, }) {
30
30
  var _a, _b, _c;
31
31
  const active = isTableColumnFilterActive(filter);
32
32
  const accessibleLabel = (_a = filter.label) !== null && _a !== void 0 ? _a : label;
33
- return (_jsxs(Popover, { align: "start", surface: "menu", "aria-label": `${accessibleLabel} filter`, title: `Filter by ${accessibleLabel}`, trigger: _jsx(TableFilterTrigger, { active: active, disabled: filter.disabled, label: accessibleLabel, children: active ? filter.value : (_b = filter.placeholder) !== null && _b !== void 0 ? _b : "Filter" }), children: [_jsx(PopoverMenuSearch, { autoFocus: true, value: filter.value, onChange: (event) => filter.onValueChange(event.target.value), placeholder: (_c = filter.placeholder) !== null && _c !== void 0 ? _c : `Search ${accessibleLabel}`, "aria-label": `Filter ${accessibleLabel}`, disabled: filter.disabled }), active ? (_jsx(PopoverMenuFooter, { children: _jsx(PopoverMenuFooterAction, { onClick: () => filter.onValueChange(""), children: "Clear filter" }) })) : null] }));
33
+ return (_jsxs(Popover, { align: "start", surface: "menu", "aria-label": `${accessibleLabel} filter`, title: `Filter by ${accessibleLabel}`, trigger: _jsx(TableFilterTrigger, { active: active, disabled: filter.disabled, label: accessibleLabel, children: active ? filter.value : (_b = filter.placeholder) !== null && _b !== void 0 ? _b : "Filter" }), children: [_jsx(PopoverMenuSearch, { autoFocus: true, value: filter.value, onChange: (event) => filter.onValueChange(event.target.value), placeholder: (_c = filter.placeholder) !== null && _c !== void 0 ? _c : `Search ${accessibleLabel}`, "aria-label": `Filter ${accessibleLabel}`, disabled: filter.disabled, containerClassName: "after:hidden" }), active ? (_jsx(PopoverMenuFooter, { children: _jsx(PopoverMenuFooterAction, { onClick: () => filter.onValueChange(""), children: "Clear filter" }) })) : null] }));
34
34
  }
35
35
  function OptionsColumnFilter({ filter, label, }) {
36
36
  var _a, _b, _c, _d;
@@ -46,7 +46,7 @@ function OptionsColumnFilter({ filter, label, }) {
46
46
  return (_jsx(FilterPopover, { directSingleGroup: true, ariaLabel: `${accessibleLabel} filter`, title: `Filter by ${accessibleLabel}`, trigger: _jsx(TableFilterTrigger, { active: active, disabled: filter.disabled, label: accessibleLabel, children: getOptionsFilterSummary(filter) }), groups: [
47
47
  {
48
48
  id: `table-column-${accessibleLabel}`,
49
- label: `Filter by ${accessibleLabel}`,
49
+ label: accessibleLabel,
50
50
  searchable,
51
51
  searchPlaceholder: (_c = filter.searchPlaceholder) !== null && _c !== void 0 ? _c : "Search options",
52
52
  emptyLabel: (_d = filter.emptyMessage) !== null && _d !== void 0 ? _d : "No options found",
@@ -65,7 +65,7 @@ function DateColumnFilter({ filter, label, }) {
65
65
  var _a, _b;
66
66
  const active = isTableColumnFilterActive(filter);
67
67
  const accessibleLabel = (_a = filter.label) !== null && _a !== void 0 ? _a : label;
68
- return (_jsx(DateInput, { label: `Filter by ${accessibleLabel}`, hideLabel: true, value: filter.value, onValueChange: filter.onValueChange, placeholder: (_b = filter.placeholder) !== null && _b !== void 0 ? _b : "Pick a date", disabled: filter.disabled, minDate: filter.minDate, maxDate: filter.maxDate, clearable: true, clearLabel: `Clear ${accessibleLabel} filter`, className: "gap-0", triggerClassName: cn(filterToggleVariants({ selected: active }), "h-9 w-full min-w-0 justify-start text-sm font-normal shadow-none", active ? "pl-2 pr-16" : "pl-2 pr-9", !active &&
68
+ return (_jsx(DateInput, { label: `Filter by ${accessibleLabel}${active ? ", active" : ""}`, hideLabel: true, value: filter.value, onValueChange: filter.onValueChange, placeholder: (_b = filter.placeholder) !== null && _b !== void 0 ? _b : "Pick a date", disabled: filter.disabled, minDate: filter.minDate, maxDate: filter.maxDate, clearable: true, clearLabel: `Clear ${accessibleLabel} filter`, className: "gap-0", triggerClassName: cn(filterToggleVariants({ selected: active }), "h-9 w-full min-w-0 justify-start text-sm font-normal shadow-none", active ? "pl-2 pr-16" : "pl-2 pr-9", !active &&
69
69
  "border-0 bg-transparent text-muted-foreground hover:bg-accent hover:text-accent-foreground") }));
70
70
  }
71
71
  function TableColumnFilterControl({ filter, label, }) {
package/dist/tabs.js CHANGED
@@ -7,7 +7,7 @@ import { ChevronDownIcon } from "./animated-icons/chevron-down";
7
7
  import { XIcon } from "./animated-icons/x";
8
8
  import { cn } from "./lib/utils";
9
9
  import { Popover } from "./popover";
10
- import { PopoverMenuSection } from "./popover-menu";
10
+ import { PopoverMenuSection, popoverMenuItemFocusClassName, } from "./popover-menu";
11
11
  const TabsContext = React.createContext(null);
12
12
  const getTabValueSelector = (value) => {
13
13
  const escapedValue = typeof CSS !== "undefined" && typeof CSS.escape === "function"
@@ -82,7 +82,7 @@ function TabTriggerWithDropdown({ item, triggerClassName, contentClassName, }) {
82
82
  setInternalActiveSection((_b = (_a = sections[0]) === null || _a === void 0 ? void 0 : _a.id) !== null && _b !== void 0 ? _b : null);
83
83
  }
84
84
  }, [isTabSelected, item.activeSection, sections]);
85
- return (_jsx(Popover, { open: open, onOpenChange: setOpen, surface: "menu", align: "start", contentClassName: cn("min-w-[12rem]", contentClassName), trigger: _jsxs(TabsTrigger, { value: item.value, disabled: item.disabled, "data-state": isTabSelected ? "active" : "inactive", "data-tab-value": item.value, className: cn("flex items-center gap-1", triggerClassName), children: [renderTabsItemLabel(item.label, isTabSelected), _jsx(ChevronDownIcon, { size: 12, className: cn("transition-transform duration-200", open && "rotate-180") })] }), children: _jsx(PopoverMenuSection, { className: "space-y-1", children: sections.map((section) => (_jsxs("button", { type: "button", className: cn("flex min-h-fit w-full cursor-pointer items-start gap-2 rounded-sm px-3 py-2 text-left text-sm font-normal leading-5 text-popover-foreground outline-none transition-colors hover:bg-accent hover:text-accent-foreground focus-visible:bg-accent focus-visible:text-accent-foreground focus-visible:ring-1 focus-visible:ring-inset focus-visible:ring-ring", isTabSelected &&
85
+ return (_jsx(Popover, { open: open, onOpenChange: setOpen, surface: "menu", align: "start", contentClassName: cn("min-w-[12rem]", contentClassName), trigger: _jsxs(TabsTrigger, { value: item.value, disabled: item.disabled, "data-state": isTabSelected ? "active" : "inactive", "data-tab-value": item.value, className: cn("flex items-center gap-1", triggerClassName), children: [renderTabsItemLabel(item.label, isTabSelected), _jsx(ChevronDownIcon, { size: 12, className: cn("transition-transform duration-200", open && "rotate-180") })] }), children: _jsx(PopoverMenuSection, { className: "space-y-1", children: sections.map((section) => (_jsxs("button", { type: "button", "data-slot": "popover-menu-item", className: cn("flex min-h-fit w-full cursor-pointer items-start gap-2 px-3 py-2 text-left text-sm font-normal leading-5 text-popover-foreground outline-none transition-colors hover:bg-accent hover:text-accent-foreground focus-visible:bg-accent focus-visible:text-accent-foreground", popoverMenuItemFocusClassName, isTabSelected &&
86
86
  activeSection === section.id &&
87
87
  "bg-accent text-accent-foreground hover:bg-accent hover:text-accent-foreground"), onClick: () => handleSectionClick(section.id), children: [_jsx("span", { className: "block min-w-0 flex-1 whitespace-normal break-words leading-5", children: section.label }), isTabSelected && activeSection === section.id ? (_jsx(Check, { "aria-hidden": "true", className: "mt-0.5 h-4 w-4 shrink-0" })) : null] }, section.id))) }) }));
88
88
  }
@@ -41,6 +41,6 @@ function useSearchHighlight(text, query) {
41
41
  }
42
42
  function SearchHighlight({ text, query, className, markClassName, ...props }) {
43
43
  const segments = useSearchHighlight(text, query);
44
- return (_jsx("span", { className: className, ...props, children: segments.map((segment, index) => segment.matched ? (_jsx("mark", { className: cn("rounded-[2px] bg-[hsl(var(--status-warning))] px-0.5 font-medium text-[hsl(var(--status-warning-foreground))]", markClassName), children: segment.text }, `${segment.text}-${index}`)) : (_jsx(React.Fragment, { children: segment.text }, `${segment.text}-${index}`))) }));
44
+ return (_jsx("span", { className: className, ...props, children: segments.map((segment, index) => segment.matched ? (_jsx("mark", { className: cn("rounded-[2px] bg-primary/10 px-0.5 font-medium text-primary dark:bg-primary/50 dark:text-foreground dark:ring-1 dark:ring-inset dark:ring-[hsl(var(--status-success-foreground)/0.55)]", markClassName), children: segment.text }, `${segment.text}-${index}`)) : (_jsx(React.Fragment, { children: segment.text }, `${segment.text}-${index}`))) }));
45
45
  }
46
46
  export { SearchHighlight, useSearchHighlight };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@neasg/design-system",
3
- "version": "0.4.19",
3
+ "version": "0.4.20",
4
4
  "description": "NEA shared design system primitives, theme tokens, and Storybook docs.",
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",