@neasg/design-system 0.4.20 → 0.4.22

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.
@@ -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, popoverMenuItemFocusClassName, } from "./popover-menu";
10
+ import { PopoverMenuFooter, PopoverMenuFooterAction, PopoverMenuCheckboxItem, 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,9 @@ 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: 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) => {
75
+ return (_jsxs(Popover, { align: align, contentClassName: cn("w-[360px]", contentClassName), defaultOpen: defaultOpen, modal: modal, onOpenChange: handleOpenChange, open: open, side: side, surface: "menu", tabBehavior: "close-at-boundary", 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(PopoverMenuCheckboxItem, { "aria-label": option.ariaLabel, checked: Boolean(option.selected), disabled: option.disabled, onChange: option.onSelect
76
+ ? () => { var _a; return (_a = option.onSelect) === null || _a === void 0 ? void 0 : _a.call(option); }
77
+ : undefined, 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
78
  var _a;
77
79
  const groupSelectedCount = getGroupSelectedCount(group);
78
80
  const selectionLabel = (_a = group.selectionLabel) !== null && _a !== void 0 ? _a : getFilterSelectionLabel(groupSelectedCount);
package/dist/index.d.ts CHANGED
@@ -134,7 +134,7 @@ export type { UseTableSortOptions, UseTableSortReturn, } from "./use-table-sort"
134
134
  export { Table, TableRoot, TableContainer, TableHeader, TableBody, TableFooter, TableHead, TableRow, TableCell, TableCaption, TableRowSkeleton, TABLE_SKELETON_PRESETS, } from "./table";
135
135
  export type { TableColumn, TableColumnWidths, TableProps, TableResizeMode, TableRowExpansionProps, TableRowTone, TableSortDirection, TableSortingProps, TableSkeletonConfig, TableRowSkeletonProps, } from "./table";
136
136
  export type { TableColumnFilter, TableColumnFilterOption, TableDateColumnFilter, TableOptionsColumnFilter, TableTextColumnFilter, } from "./table-column-filter";
137
- export { Tabs, TabsRoot, TabWithDropdown, type TabsItem, type TabsItemLabel, type TabsItemLabelProps, type TabsProps, TabsList, TabsTrigger, TabsContent, type TabWithDropdownProps, useTabsContext, type TabDropdownSection, } from "./tabs";
137
+ export { Tabs, SubTabs, TabsRoot, TabWithDropdown, type TabsItem, type TabsItemLabel, type TabsItemLabelProps, type TabsProps, type SubTabsItem, type SubTabsProps, TabsList, TabsTrigger, TabsContent, type TabWithDropdownProps, useTabsContext, type TabDropdownSection, } from "./tabs";
138
138
  export { Tooltip, TooltipRoot, TooltipTrigger, TooltipContent, TooltipProvider, type TooltipProps, } from "./tooltip";
139
139
  export { Toaster, toast } from "./toaster";
140
140
  export type { ToasterProps } from "./toaster";
package/dist/index.js CHANGED
@@ -69,7 +69,7 @@ export { TableToolbar } from "./table-toolbar";
69
69
  export { TableColumnVisibilityMenu } from "./table-column-visibility";
70
70
  export { useTableSort } from "./use-table-sort";
71
71
  export { Table, TableRoot, TableContainer, TableHeader, TableBody, TableFooter, TableHead, TableRow, TableCell, TableCaption, TableRowSkeleton, TABLE_SKELETON_PRESETS, } from "./table";
72
- export { Tabs, TabsRoot, TabWithDropdown, TabsList, TabsTrigger, TabsContent, useTabsContext, } from "./tabs";
72
+ export { Tabs, SubTabs, TabsRoot, TabWithDropdown, TabsList, TabsTrigger, TabsContent, useTabsContext, } from "./tabs";
73
73
  export { Tooltip, TooltipRoot, TooltipTrigger, TooltipContent, TooltipProvider, } from "./tooltip";
74
74
  export { Toaster, toast } from "./toaster";
75
75
  export { Separator } from "./separator";
@@ -6,9 +6,11 @@ import { XIcon } from "./animated-icons/x";
6
6
  import { Badge } from "./badge";
7
7
  import { Field, FieldDescription, FieldError, FieldLabel } from "./field";
8
8
  import { Checkbox } from "./checkbox";
9
- import { InputControl } from "./input-control";
10
9
  import { cn } from "./lib/utils";
10
+ import { Popover } from "./popover";
11
+ import { PopoverMenuSearch } from "./popover-menu";
11
12
  import { Typography } from "./typography";
13
+ import { SearchHighlight } from "./use-search-highlight";
12
14
  function MultiSelect({ label, options, value, onValueChange, placeholder = "Select options", description, error, invalid = false, disabled = false, hideLabel = false, searchable = false, searchPlaceholder = "Search...", maxVisibleTags = 2, className, triggerClassName, labelClassName, descriptionClassName, errorClassName, id, }) {
13
15
  const generatedId = React.useId();
14
16
  const selectId = id !== null && id !== void 0 ? id : `multi-select-${generatedId}`;
@@ -20,7 +22,6 @@ function MultiSelect({ label, options, value, onValueChange, placeholder = "Sele
20
22
  const isInvalid = invalid || Boolean(error);
21
23
  const [open, setOpen] = React.useState(false);
22
24
  const [searchQuery, setSearchQuery] = React.useState("");
23
- const containerRef = React.useRef(null);
24
25
  const triggerRef = React.useRef(null);
25
26
  const searchInputRef = React.useRef(null);
26
27
  const listboxRef = React.useRef(null);
@@ -67,6 +68,14 @@ function MultiSelect({ label, options, value, onValueChange, placeholder = "Sele
67
68
  const removeOption = React.useCallback((optionValue) => {
68
69
  onValueChange(value.filter((v) => v !== optionValue));
69
70
  }, [value, onValueChange]);
71
+ const handleOpenChange = React.useCallback((nextOpen) => {
72
+ if (disabled)
73
+ return;
74
+ if (nextOpen) {
75
+ openFocusTargetRef.current = searchable ? "search" : "first";
76
+ }
77
+ setOpen(nextOpen);
78
+ }, [disabled, searchable]);
70
79
  React.useEffect(() => {
71
80
  if (!open) {
72
81
  setSearchQuery("");
@@ -82,18 +91,6 @@ function MultiSelect({ label, options, value, onValueChange, placeholder = "Sele
82
91
  });
83
92
  return () => window.cancelAnimationFrame(frame);
84
93
  }, [focusBoundaryOption, open, searchable]);
85
- React.useEffect(() => {
86
- const handleClickOutside = (event) => {
87
- if (containerRef.current &&
88
- !containerRef.current.contains(event.target)) {
89
- setOpen(false);
90
- }
91
- };
92
- if (open) {
93
- document.addEventListener("mousedown", handleClickOutside);
94
- return () => document.removeEventListener("mousedown", handleClickOutside);
95
- }
96
- }, [open]);
97
94
  const handleTriggerKeyDown = React.useCallback((event) => {
98
95
  if (disabled)
99
96
  return;
@@ -165,25 +162,16 @@ function MultiSelect({ label, options, value, onValueChange, placeholder = "Sele
165
162
  var _a;
166
163
  event.preventDefault();
167
164
  (_a = triggerRef.current) === null || _a === void 0 ? void 0 : _a.focus();
168
- }, children: label }), _jsxs("div", { ref: containerRef, className: "relative", children: [_jsxs("div", { ref: triggerRef, role: "combobox", id: selectId, tabIndex: disabled ? -1 : 0, onClick: () => {
169
- if (!disabled) {
170
- if (open) {
171
- setOpen(false);
172
- }
173
- else {
174
- openWithFocus(searchable ? "search" : "first");
175
- }
176
- }
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) => {
184
- const isSelected = value.includes(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));
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 })] }));
165
+ }, children: label }), _jsxs(Popover, { open: disabled ? false : open, onOpenChange: handleOpenChange, align: "start", sideOffset: 4, collisionPadding: 16, surface: "menu", tabBehavior: "close", contentClassName: "flex max-h-[var(--radix-popover-content-available-height)] w-[var(--radix-popover-trigger-width)] min-w-[var(--radix-popover-trigger-width)] flex-col overflow-hidden", onOpenAutoFocus: (event) => event.preventDefault(), trigger: _jsxs("div", { ref: triggerRef, role: "combobox", id: selectId, tabIndex: disabled ? -1 : 0, 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: (event) => {
166
+ event.stopPropagation();
167
+ removeOption(opt.value);
168
+ }, "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
169
+ .slice(maxVisibleTags)
170
+ .map((option) => option.label)
171
+ .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") })] }), children: [searchable ? (_jsx(PopoverMenuSearch, { ref: searchInputRef, value: searchQuery, onChange: (event) => setSearchQuery(event.target.value), onKeyDown: handleSearchKeyDown, placeholder: searchPlaceholder, "aria-label": searchPlaceholder, className: "h-control border-0 bg-transparent px-2 py-0 text-sm shadow-none" })) : 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", onKeyDown: handleListboxKeyDown, children: filteredOptions.length > 0 ? (filteredOptions.map((option) => {
172
+ const isSelected = value.includes(option.value);
173
+ 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 &&
174
+ "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(SearchHighlight, { text: option.label, query: searchQuery }), option.description ? (_jsx(Typography, { as: "span", variant: "caption", children: _jsx(SearchHighlight, { text: option.description, query: searchQuery }) })) : null] })] }, option.value));
175
+ })) : (_jsx("div", { className: "px-2.5 py-1.5 text-center text-sm text-muted-foreground", children: "No options found." })) })] }), showDescription ? (_jsx(FieldDescription, { id: descriptionId, className: descriptionClassName, children: description })) : null, _jsx(FieldError, { id: errorId, className: errorClassName, children: error })] }));
188
176
  }
189
177
  export { MultiSelect };
@@ -0,0 +1,14 @@
1
+ import * as React from "react";
2
+ type PopoverTabBehavior = "contain" | "close" | "close-at-boundary";
3
+ interface UsePopoverTabExitOptions<TTrigger extends HTMLElement, TContent extends HTMLElement> {
4
+ behavior: PopoverTabBehavior;
5
+ triggerRef: React.RefObject<TTrigger>;
6
+ contentRef: React.RefObject<TContent>;
7
+ onExit: () => void;
8
+ }
9
+ declare function usePopoverTabExit<TTrigger extends HTMLElement, TContent extends HTMLElement>({ behavior, triggerRef, contentRef, onExit, }: UsePopoverTabExitOptions<TTrigger, TContent>): {
10
+ handleCloseAutoFocus: (event: Event) => void;
11
+ handleTabKeyDown: (event: React.KeyboardEvent<HTMLElement>) => boolean;
12
+ };
13
+ export { usePopoverTabExit };
14
+ export type { PopoverTabBehavior };
@@ -0,0 +1,64 @@
1
+ "use client";
2
+ import * as React from "react";
3
+ const tabbableSelector = 'a[href], button:not(:disabled), input:not(:disabled):not([type="hidden"]), select:not(:disabled), textarea:not(:disabled), [tabindex]:not([tabindex="-1"])';
4
+ function usePopoverTabExit({ behavior, triggerRef, contentRef, onExit, }) {
5
+ const tabExitPendingRef = React.useRef(false);
6
+ const focusFrameRef = React.useRef(null);
7
+ React.useEffect(() => () => {
8
+ if (focusFrameRef.current !== null) {
9
+ window.cancelAnimationFrame(focusFrameRef.current);
10
+ }
11
+ }, []);
12
+ const handleTabKeyDown = React.useCallback((event) => {
13
+ if (behavior === "contain" ||
14
+ event.defaultPrevented ||
15
+ event.key !== "Tab") {
16
+ return false;
17
+ }
18
+ const trigger = triggerRef.current;
19
+ const content = contentRef.current;
20
+ if (!trigger) {
21
+ return false;
22
+ }
23
+ if (behavior === "close-at-boundary" && content) {
24
+ const contentControls = Array.from(content.querySelectorAll(tabbableSelector)).filter((candidate) => !candidate.closest('[hidden], [aria-hidden="true"], [inert]'));
25
+ const boundaryControl = event.shiftKey
26
+ ? contentControls[0]
27
+ : contentControls[contentControls.length - 1];
28
+ if (document.activeElement !== boundaryControl) {
29
+ return false;
30
+ }
31
+ }
32
+ const candidates = Array.from(document.querySelectorAll(tabbableSelector)).filter((candidate) => {
33
+ const hiddenAncestor = candidate.closest('[hidden], [aria-hidden="true"], [inert]');
34
+ const isHidden = hiddenAncestor &&
35
+ !hiddenAncestor.hasAttribute("data-aria-hidden");
36
+ return (!candidate.hasAttribute("data-radix-focus-guard") &&
37
+ !isHidden &&
38
+ (candidate === trigger ||
39
+ (!trigger.contains(candidate) && !(content === null || content === void 0 ? void 0 : content.contains(candidate)))));
40
+ });
41
+ const triggerIndex = candidates.indexOf(trigger);
42
+ const target = triggerIndex >= 0
43
+ ? candidates[triggerIndex + (event.shiftKey ? -1 : 1)]
44
+ : undefined;
45
+ event.preventDefault();
46
+ event.stopPropagation();
47
+ tabExitPendingRef.current = true;
48
+ onExit();
49
+ focusFrameRef.current = window.requestAnimationFrame(() => {
50
+ (target !== null && target !== void 0 ? target : trigger).focus({ preventScroll: true });
51
+ focusFrameRef.current = null;
52
+ });
53
+ return true;
54
+ }, [behavior, contentRef, onExit, triggerRef]);
55
+ const handleCloseAutoFocus = React.useCallback((event) => {
56
+ if (!tabExitPendingRef.current) {
57
+ return;
58
+ }
59
+ event.preventDefault();
60
+ tabExitPendingRef.current = false;
61
+ }, []);
62
+ return { handleCloseAutoFocus, handleTabKeyDown };
63
+ }
64
+ export { usePopoverTabExit };
@@ -27,7 +27,7 @@ function PopoverMenuItem({ className, children, active, type = "button", ...prop
27
27
  function PopoverMenuCheckboxItem({ className, children, checked = false, description, disabled, onChange, readOnly, ...props }) {
28
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
29
  "bg-primary/10 text-primary hover:bg-primary/20 hover:text-primary focus-within:bg-primary/20 focus-within:text-primary", disabled &&
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] })] }));
30
+ "cursor-not-allowed opacity-50 hover:bg-transparent hover:text-inherit", className), children: [_jsx(Checkbox, { ...props, "data-slot": "popover-menu-item", 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] })] }));
31
31
  }
32
32
  function PopoverMenuDivider({ className, ...props }) {
33
33
  return _jsx("div", { className: cn("-mx-1 my-1 h-px bg-border", className), ...props });
@@ -49,7 +49,7 @@ function PopoverMenuGroupLabel({ className, children, ...props }) {
49
49
  }
50
50
  const PopoverMenuSearch = React.forwardRef(({ "aria-label": ariaLabel, className, containerClassName, label, placeholder, ...props }, ref) => {
51
51
  var _a, _b;
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 }) }));
52
+ return (_jsx("div", { className: cn("relative -mx-1 -mt-1 mb-1 px-1 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 }) }));
53
53
  });
54
54
  PopoverMenuSearch.displayName = "PopoverMenuSearch";
55
55
  function PopoverMenuScrollArea({ className, children, maxHeight = "max-h-64", ...props }) {
@@ -62,6 +62,8 @@ function PopoverMenuView({ children, className, direction = "forward", viewKey =
62
62
  key: viewKey,
63
63
  children,
64
64
  });
65
+ const activeViewElementRef = React.useRef(null);
66
+ const previousViewKeyRef = React.useRef(viewKey);
65
67
  const exitTimeoutRef = React.useRef(null);
66
68
  if (Object.is(viewKey, activeViewKey)) {
67
69
  activeViewRef.current = { key: viewKey, children };
@@ -99,12 +101,32 @@ function PopoverMenuView({ children, className, direction = "forward", viewKey =
99
101
  }
100
102
  };
101
103
  }, [direction, viewKey]);
104
+ useIsomorphicLayoutEffect(() => {
105
+ var _a;
106
+ if (Object.is(previousViewKeyRef.current, viewKey)) {
107
+ return;
108
+ }
109
+ previousViewKeyRef.current = viewKey;
110
+ const firstControl = (_a = activeViewElementRef.current) === null || _a === void 0 ? void 0 : _a.querySelector('button:not(:disabled), input:not(:disabled):not([type="hidden"]), select:not(:disabled), textarea:not(:disabled), [tabindex]:not([tabindex="-1"])');
111
+ firstControl === null || firstControl === void 0 ? void 0 : firstControl.focus({ preventScroll: true });
112
+ }, [viewKey]);
102
113
  const activeDirection = exitingView === null || exitingView === void 0 ? void 0 : exitingView.direction;
103
114
  const shouldAnimateActive = Boolean(activeDirection);
104
- return (_jsxs("div", { className: cn("grid min-w-0 overflow-hidden", className), ...props, children: [exitingView ? (_jsx("div", { "aria-hidden": "true", className: cn("pointer-events-none col-start-1 row-start-1 min-w-0", exitingView.direction !== "none" &&
115
+ const setExitingViewElement = React.useCallback((element) => {
116
+ if (!element) {
117
+ return;
118
+ }
119
+ element.inert = true;
120
+ element
121
+ .querySelectorAll('a[href], button, input, select, textarea, [contenteditable="true"], [tabindex]')
122
+ .forEach((control) => {
123
+ control.tabIndex = -1;
124
+ });
125
+ }, []);
126
+ return (_jsxs("div", { className: cn("grid min-w-0 overflow-hidden", className), ...props, children: [exitingView ? (_jsx("div", { ref: setExitingViewElement, "aria-hidden": "true", inert: "", "data-popover-menu-view": "exiting", className: cn("pointer-events-none col-start-1 row-start-1 min-w-0", exitingView.direction !== "none" &&
105
127
  "motion-safe:animate-out motion-safe:fade-out-0 motion-safe:duration-300 motion-reduce:animate-none", exitingView.direction === "forward" &&
106
128
  "motion-safe:slide-out-to-left-full", exitingView.direction === "backward" &&
107
- "motion-safe:slide-out-to-right-full"), children: exitingView.children }, `leaving-${exitingView.key}`)) : null, _jsx("div", { className: cn("col-start-1 row-start-1 min-w-0", shouldAnimateActive &&
129
+ "motion-safe:slide-out-to-right-full"), children: exitingView.children }, `leaving-${exitingView.key}`)) : null, _jsx("div", { ref: activeViewElementRef, "data-popover-menu-view": "active", className: cn("col-start-1 row-start-1 min-w-0", shouldAnimateActive &&
108
130
  "motion-safe:animate-in motion-safe:fade-in-0 motion-safe:duration-300 motion-reduce:animate-none", activeDirection === "forward" &&
109
131
  "motion-safe:slide-in-from-right-full", activeDirection === "backward" &&
110
132
  "motion-safe:slide-in-from-left-full"), children: children }, viewKey)] }));
package/dist/popover.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import * as React from "react";
2
2
  import * as PopoverPrimitive from "@radix-ui/react-popover";
3
+ import { type PopoverTabBehavior } from "./popover-focus";
3
4
  import { type TooltipProps } from "./tooltip";
4
5
  import { Typography } from "./typography";
5
6
  type PopoverRootProps = React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Root>;
@@ -10,13 +11,15 @@ export interface PopoverProps extends Pick<PopoverRootProps, "defaultOpen" | "mo
10
11
  /** Visible popover heading. Also labels the popover for assistive technology. */
11
12
  title?: React.ReactNode;
12
13
  surface?: "panel" | "menu";
14
+ /** Controls whether Tab stays within the popover or closes it and continues in the page Tab order. */
15
+ tabBehavior?: PopoverTabBehavior;
13
16
  contentClassName?: string;
14
17
  triggerTooltip?: React.ReactNode;
15
18
  triggerTooltipSide?: TooltipProps["side"];
16
19
  triggerTooltipSideOffset?: TooltipProps["sideOffset"];
17
20
  triggerTooltipClassName?: TooltipProps["contentClassName"];
18
21
  }
19
- declare function Popover({ defaultOpen, modal, onOpenChange, open, trigger, children, title, surface, contentClassName, triggerTooltip, triggerTooltipSide, triggerTooltipSideOffset, triggerTooltipClassName, align, sideOffset, ...contentProps }: PopoverProps): import("react/jsx-runtime").JSX.Element;
22
+ declare function Popover({ defaultOpen, modal, onOpenChange, open, trigger, children, title, surface, tabBehavior, contentClassName, triggerTooltip, triggerTooltipSide, triggerTooltipSideOffset, triggerTooltipClassName, align, sideOffset, ...contentProps }: PopoverProps): import("react/jsx-runtime").JSX.Element;
20
23
  export interface PopoverTitleProps extends Omit<React.ComponentPropsWithoutRef<typeof Typography>, "as" | "asChild" | "variant" | "color"> {
21
24
  }
22
25
  declare function PopoverTitle({ className, children, ...props }: PopoverTitleProps): import("react/jsx-runtime").JSX.Element;
package/dist/popover.js CHANGED
@@ -3,13 +3,33 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import * as React from "react";
4
4
  import { cn } from "./lib/utils";
5
5
  import { PopoverContent, PopoverRoot, PopoverTrigger, } from "./popover-primitive";
6
+ import { usePopoverTabExit, } from "./popover-focus";
6
7
  import { Tooltip } from "./tooltip";
7
8
  import { Typography } from "./typography";
8
- function Popover({ defaultOpen, modal, onOpenChange, open, trigger, children, title, surface = "panel", contentClassName, triggerTooltip, triggerTooltipSide, triggerTooltipSideOffset, triggerTooltipClassName, align = "center", sideOffset = 4, ...contentProps }) {
9
+ function Popover({ defaultOpen, modal, onOpenChange, open, trigger, children, title, surface = "panel", tabBehavior = "contain", contentClassName, triggerTooltip, triggerTooltipSide, triggerTooltipSideOffset, triggerTooltipClassName, align = "center", sideOffset = 4, ...contentProps }) {
9
10
  const generatedTitleId = React.useId();
10
11
  const titleId = title ? `popover-title-${generatedTitleId}` : undefined;
11
- const triggerElement = _jsx(PopoverTrigger, { asChild: true, children: trigger });
12
+ const [internalOpen, setInternalOpen] = React.useState(defaultOpen !== null && defaultOpen !== void 0 ? defaultOpen : false);
13
+ const isControlled = open !== undefined;
14
+ const currentOpen = isControlled ? open : internalOpen;
15
+ const triggerRef = React.useRef(null);
16
+ const contentRef = React.useRef(null);
17
+ const setOpen = React.useCallback((nextOpen) => {
18
+ if (!isControlled) {
19
+ setInternalOpen(nextOpen);
20
+ }
21
+ onOpenChange === null || onOpenChange === void 0 ? void 0 : onOpenChange(nextOpen);
22
+ }, [isControlled, onOpenChange]);
23
+ const { handleCloseAutoFocus, handleTabKeyDown } = usePopoverTabExit({
24
+ behavior: tabBehavior,
25
+ triggerRef,
26
+ contentRef,
27
+ onExit: () => setOpen(false),
28
+ });
29
+ const triggerElement = (_jsx(PopoverTrigger, { ref: triggerRef, asChild: true, children: trigger }));
12
30
  const contentOnKeyDown = contentProps.onKeyDown;
31
+ const contentOnKeyDownCapture = contentProps.onKeyDownCapture;
32
+ const contentOnCloseAutoFocus = contentProps.onCloseAutoFocus;
13
33
  const handleContentKeyDown = React.useCallback((event) => {
14
34
  contentOnKeyDown === null || contentOnKeyDown === void 0 ? void 0 : contentOnKeyDown(event);
15
35
  if (event.defaultPrevented ||
@@ -51,7 +71,15 @@ function Popover({ defaultOpen, modal, onOpenChange, open, trigger, children, ti
51
71
  event.preventDefault();
52
72
  focusTarget.focus({ preventScroll: true });
53
73
  }, [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"
74
+ const handleContentKeyDownCapture = React.useCallback((event) => {
75
+ contentOnKeyDownCapture === null || contentOnKeyDownCapture === void 0 ? void 0 : contentOnKeyDownCapture(event);
76
+ handleTabKeyDown(event);
77
+ }, [contentOnKeyDownCapture, handleTabKeyDown]);
78
+ const handleContentCloseAutoFocus = React.useCallback((event) => {
79
+ contentOnCloseAutoFocus === null || contentOnCloseAutoFocus === void 0 ? void 0 : contentOnCloseAutoFocus(event);
80
+ handleCloseAutoFocus(event);
81
+ }, [contentOnCloseAutoFocus, handleCloseAutoFocus]);
82
+ return (_jsxs(PopoverRoot, { modal: modal, onOpenChange: setOpen, open: currentOpen, children: [triggerTooltip ? (_jsx(Tooltip, { content: triggerTooltip, side: triggerTooltipSide, sideOffset: triggerTooltipSideOffset, contentClassName: triggerTooltipClassName, children: triggerElement })) : (triggerElement), _jsxs(PopoverContent, { ref: contentRef, 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, onCloseAutoFocus: handleContentCloseAutoFocus, onKeyDown: handleContentKeyDown, onKeyDownCapture: handleContentKeyDownCapture, children: [title ? (_jsx(PopoverTitle, { id: titleId, className: cn(surface === "menu"
55
83
  ? "px-2 py-1.5"
56
84
  : "-mx-4 -mt-4 mb-4 border-b px-4 py-3"), children: title })) : null, children] })] }));
57
85
  }
package/dist/select.js CHANGED
@@ -4,6 +4,7 @@ import * as React from "react";
4
4
  import { Field, FieldDescription, FieldError, FieldLabel } from "./field";
5
5
  import { InputControl } from "./input-control";
6
6
  import { cn } from "./lib/utils";
7
+ import { usePopoverTabExit } from "./popover-focus";
7
8
  import { SearchHighlight } from "./use-search-highlight";
8
9
  import { SelectContent, SelectGroup, SelectItem, SelectLabel, SelectRoot, SelectSeparator, SelectTrigger, SelectValue, } from "./select-primitive";
9
10
  function groupOptions(options) {
@@ -42,6 +43,7 @@ function renderHighlightedLabel(label, query) {
42
43
  function Select({ label, options, placeholder = "Select an option", description, error, invalid = false, hideLabel = false, searchable = false, searchPlaceholder = "Search options...", searchEmptyMessage = "No options found.", searchInputAriaLabel = "Search options", className, triggerClassName, contentClassName, labelClassName, descriptionClassName, errorClassName, id, disabled, open, onOpenChange, ...props }) {
43
44
  const generatedId = React.useId();
44
45
  const selectId = id !== null && id !== void 0 ? id : `select-${generatedId}`;
46
+ const triggerRef = React.useRef(null);
45
47
  const searchInputRef = React.useRef(null);
46
48
  const contentRef = React.useRef(null);
47
49
  const [uncontrolledOpen, setUncontrolledOpen] = React.useState(false);
@@ -63,6 +65,22 @@ function Select({ label, options, placeholder = "Select an option", description,
63
65
  : options;
64
66
  const optionGroups = groupOptions(filteredOptions);
65
67
  const hasVisibleOptions = filteredOptions.length > 0;
68
+ const handleOpenChange = React.useCallback((nextOpen) => {
69
+ if (open === undefined) {
70
+ setUncontrolledOpen(nextOpen);
71
+ }
72
+ if (!nextOpen) {
73
+ setSearchQuery("");
74
+ }
75
+ onOpenChange === null || onOpenChange === void 0 ? void 0 : onOpenChange(nextOpen);
76
+ }, [onOpenChange, open]);
77
+ const closeForTab = React.useCallback(() => handleOpenChange(false), [handleOpenChange]);
78
+ const { handleCloseAutoFocus, handleTabKeyDown } = usePopoverTabExit({
79
+ behavior: "close",
80
+ triggerRef,
81
+ contentRef,
82
+ onExit: closeForTab,
83
+ });
66
84
  const focusSearchInput = React.useCallback(() => {
67
85
  const input = searchInputRef.current;
68
86
  if (!input || document.activeElement === input) {
@@ -82,6 +100,9 @@ function Select({ label, options, placeholder = "Select an option", description,
82
100
  option === null || option === void 0 ? void 0 : option.focus({ preventScroll: true });
83
101
  }, [getEnabledOptionElements]);
84
102
  const handleContentKeyDownCapture = React.useCallback((event) => {
103
+ if (handleTabKeyDown(event)) {
104
+ return;
105
+ }
85
106
  if (event.key !== "ArrowUp") {
86
107
  return;
87
108
  }
@@ -94,7 +115,12 @@ function Select({ label, options, placeholder = "Select an option", description,
94
115
  if (searchable) {
95
116
  focusSearchInput();
96
117
  }
97
- }, [focusSearchInput, getEnabledOptionElements, searchable]);
118
+ }, [
119
+ focusSearchInput,
120
+ getEnabledOptionElements,
121
+ handleTabKeyDown,
122
+ searchable,
123
+ ]);
98
124
  React.useEffect(() => {
99
125
  if (!isOpen) {
100
126
  setSearchQuery("");
@@ -127,16 +153,7 @@ function Select({ label, options, placeholder = "Select an option", description,
127
153
  searchable,
128
154
  searchQuery,
129
155
  ]);
130
- const handleOpenChange = React.useCallback((nextOpen) => {
131
- if (open === undefined) {
132
- setUncontrolledOpen(nextOpen);
133
- }
134
- if (!nextOpen) {
135
- setSearchQuery("");
136
- }
137
- onOpenChange === null || onOpenChange === void 0 ? void 0 : onOpenChange(nextOpen);
138
- }, [onOpenChange, open]);
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) => {
156
+ 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: isOpen, onOpenChange: handleOpenChange, ...props, children: [_jsx(SelectTrigger, { ref: triggerRef, 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, onCloseAutoFocus: handleCloseAutoFocus, 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
157
  if (event.key === "ArrowDown" ||
141
158
  event.key === "ArrowUp") {
142
159
  event.preventDefault();
package/dist/tabs.d.ts CHANGED
@@ -70,5 +70,10 @@ export interface TabsProps extends Omit<React.ComponentPropsWithoutRef<typeof Ta
70
70
  triggerClassName?: string;
71
71
  panelClassName?: string;
72
72
  }
73
+ export type SubTabsItem = Omit<TabsItem, "onClose" | "closeLabel" | "sections" | "scrollOffset" | "onSectionSelect" | "activeSection">;
74
+ export interface SubTabsProps extends Omit<TabsProps, "items" | "size"> {
75
+ items: SubTabsItem[];
76
+ }
73
77
  declare function Tabs({ items, defaultValue, value, onValueChange, size, renderPanels, className, listWrapperClassName, listClassName, triggerClassName, panelClassName, ...props }: TabsProps): import("react/jsx-runtime").JSX.Element | null;
74
- export { Tabs, TabsRoot, TabsList, TabsTrigger, TabsContent, TabWithDropdown };
78
+ declare function SubTabs({ className, listWrapperClassName, listClassName, triggerClassName, panelClassName, ...props }: SubTabsProps): import("react/jsx-runtime").JSX.Element;
79
+ export { Tabs, SubTabs, TabsRoot, TabsList, TabsTrigger, TabsContent, TabWithDropdown, };
package/dist/tabs.js CHANGED
@@ -150,4 +150,7 @@ function Tabs({ items, defaultValue, value, onValueChange, size = "default", ren
150
150
  ? items.map((item) => (_jsx(TabsContent, { value: item.value, className: cn("pt-4", panelClassName), children: item.content }, item.value)))
151
151
  : null] }));
152
152
  }
153
- export { Tabs, TabsRoot, TabsList, TabsTrigger, TabsContent, TabWithDropdown };
153
+ function SubTabs({ className, listWrapperClassName, listClassName, triggerClassName, panelClassName, ...props }) {
154
+ return (_jsx(Tabs, { className: cn("space-y-0", className), listWrapperClassName: cn("min-w-0 overflow-x-auto", listWrapperClassName), listClassName: cn("w-full min-w-max justify-start overflow-visible rounded-none border-0 border-b border-[hsl(var(--tab-list-border))] bg-transparent p-0", listClassName), triggerClassName: cn("min-h-9 rounded-none border-b-2 border-transparent bg-transparent px-3 py-2 text-[hsl(var(--tab-trigger-foreground))] shadow-none focus-visible:ring-inset focus-visible:ring-offset-0 data-[state=inactive]:hover:bg-[hsl(var(--tab-trigger-hover-background))] data-[state=active]:border-[hsl(var(--tab-indicator))] data-[state=active]:bg-transparent data-[state=active]:text-[hsl(var(--tab-trigger-active-foreground))] data-[state=active]:shadow-none", triggerClassName), panelClassName: cn("pt-4", panelClassName), ...props }));
155
+ }
156
+ export { Tabs, SubTabs, TabsRoot, TabsList, TabsTrigger, TabsContent, TabWithDropdown, };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@neasg/design-system",
3
- "version": "0.4.20",
3
+ "version": "0.4.22",
4
4
  "description": "NEA shared design system primitives, theme tokens, and Storybook docs.",
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",