@sixthshift/design-system 0.2.1 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +135 -28
- package/dist/components/Badge/Badge.d.ts +5 -2
- package/dist/components/Badge/Badge.d.ts.map +1 -1
- package/dist/components/Badge/Badge.js +5 -3
- package/dist/components/Badge/Badge.js.map +1 -1
- package/dist/components/BarChart/BarChart.d.ts +8 -2
- package/dist/components/BarChart/BarChart.d.ts.map +1 -1
- package/dist/components/BarChart/BarChart.js +5 -3
- package/dist/components/BarChart/BarChart.js.map +1 -1
- package/dist/components/Calendar/Calendar.d.ts +17 -1
- package/dist/components/Calendar/Calendar.d.ts.map +1 -1
- package/dist/components/Calendar/Calendar.js +80 -225
- package/dist/components/Calendar/Calendar.js.map +1 -1
- package/dist/components/Calendar/CalendarView.d.ts +15 -0
- package/dist/components/Calendar/CalendarView.d.ts.map +1 -0
- package/dist/components/Calendar/CalendarView.js +255 -0
- package/dist/components/Calendar/CalendarView.js.map +1 -0
- package/dist/components/Calendar/calendar.types.d.ts +78 -27
- package/dist/components/Calendar/calendar.types.d.ts.map +1 -1
- package/dist/components/Calendar/calendar.types.js +10 -3
- package/dist/components/Calendar/calendar.types.js.map +1 -1
- package/dist/components/Calendar/index.d.ts +16 -2
- package/dist/components/Calendar/index.d.ts.map +1 -1
- package/dist/components/Calendar/index.js +15 -1
- package/dist/components/Calendar/index.js.map +1 -1
- package/dist/components/ColorDot/ColorDot.d.ts +9 -3
- package/dist/components/ColorDot/ColorDot.d.ts.map +1 -1
- package/dist/components/ColorDot/ColorDot.js +5 -3
- package/dist/components/ColorDot/ColorDot.js.map +1 -1
- package/dist/components/DatePicker/DatePicker.d.ts +2 -2
- package/dist/components/DatePicker/DatePicker.d.ts.map +1 -1
- package/dist/components/DatePicker/DatePicker.js +65 -10
- package/dist/components/DatePicker/DatePicker.js.map +1 -1
- package/dist/components/DatePicker/datepicker.types.d.ts +34 -36
- package/dist/components/DatePicker/datepicker.types.d.ts.map +1 -1
- package/dist/components/DatePicker/datepicker.types.js +5 -3
- package/dist/components/DatePicker/datepicker.types.js.map +1 -1
- package/dist/components/DatePicker/index.d.ts +10 -1
- package/dist/components/DatePicker/index.d.ts.map +1 -1
- package/dist/components/DatePicker/index.js +8 -0
- package/dist/components/DatePicker/index.js.map +1 -1
- package/dist/components/DateRangePicker/DateRangePicker.d.ts +3 -2
- package/dist/components/DateRangePicker/DateRangePicker.d.ts.map +1 -1
- package/dist/components/DateRangePicker/DateRangePicker.js +17 -21
- package/dist/components/DateRangePicker/DateRangePicker.js.map +1 -1
- package/dist/components/DateRangePicker/daterangepicker.types.d.ts +20 -20
- package/dist/components/DateRangePicker/daterangepicker.types.d.ts.map +1 -1
- package/dist/components/DateRangePicker/daterangepicker.types.js +6 -0
- package/dist/components/DateRangePicker/daterangepicker.types.js.map +1 -1
- package/dist/components/DateRangePicker/index.d.ts +8 -1
- package/dist/components/DateRangePicker/index.d.ts.map +1 -1
- package/dist/components/DateRangePicker/index.js +6 -0
- package/dist/components/DateRangePicker/index.js.map +1 -1
- package/dist/components/DateTimePicker/DateTimePicker.d.ts +3 -3
- package/dist/components/DateTimePicker/DateTimePicker.d.ts.map +1 -1
- package/dist/components/DateTimePicker/DateTimePicker.js +30 -12
- package/dist/components/DateTimePicker/DateTimePicker.js.map +1 -1
- package/dist/components/DateTimePicker/datetimepicker.types.d.ts +20 -17
- package/dist/components/DateTimePicker/datetimepicker.types.d.ts.map +1 -1
- package/dist/components/DateTimePicker/datetimepicker.types.js +10 -0
- package/dist/components/DateTimePicker/datetimepicker.types.js.map +1 -1
- package/dist/components/DateTimePicker/index.d.ts +8 -1
- package/dist/components/DateTimePicker/index.d.ts.map +1 -1
- package/dist/components/DateTimePicker/index.js +6 -0
- package/dist/components/DateTimePicker/index.js.map +1 -1
- package/dist/components/DateTimeRangePicker/DateTimeRangePicker.d.ts +3 -3
- package/dist/components/DateTimeRangePicker/DateTimeRangePicker.d.ts.map +1 -1
- package/dist/components/DateTimeRangePicker/DateTimeRangePicker.js +41 -20
- package/dist/components/DateTimeRangePicker/DateTimeRangePicker.js.map +1 -1
- package/dist/components/DateTimeRangePicker/datetimerangepicker.types.d.ts +22 -18
- package/dist/components/DateTimeRangePicker/datetimerangepicker.types.d.ts.map +1 -1
- package/dist/components/DateTimeRangePicker/datetimerangepicker.types.js +8 -0
- package/dist/components/DateTimeRangePicker/datetimerangepicker.types.js.map +1 -1
- package/dist/components/DateTimeRangePicker/index.d.ts +8 -0
- package/dist/components/DateTimeRangePicker/index.d.ts.map +1 -1
- package/dist/components/DateTimeRangePicker/index.js +7 -0
- package/dist/components/DateTimeRangePicker/index.js.map +1 -1
- package/dist/components/EmptyState/EmptyState.d.ts +16 -5
- package/dist/components/EmptyState/EmptyState.d.ts.map +1 -1
- package/dist/components/EmptyState/EmptyState.js +5 -3
- package/dist/components/EmptyState/EmptyState.js.map +1 -1
- package/dist/components/Field/Field.d.ts +3 -3
- package/dist/components/Field/Field.d.ts.map +1 -1
- package/dist/components/Field/Field.js +6 -4
- package/dist/components/Field/Field.js.map +1 -1
- package/dist/components/LineChart/LineChart.d.ts +18 -2
- package/dist/components/LineChart/LineChart.d.ts.map +1 -1
- package/dist/components/LineChart/LineChart.js +5 -3
- package/dist/components/LineChart/LineChart.js.map +1 -1
- package/dist/components/Markdown/Markdown.d.ts +2 -4
- package/dist/components/Markdown/Markdown.d.ts.map +1 -1
- package/dist/components/Markdown/Markdown.js +3 -2
- package/dist/components/Markdown/Markdown.js.map +1 -1
- package/dist/components/Message/Message.d.ts.map +1 -1
- package/dist/components/Message/Message.js +16 -1
- package/dist/components/Message/Message.js.map +1 -1
- package/dist/components/MetricList/MetricList.d.ts +3 -3
- package/dist/components/MetricList/MetricList.d.ts.map +1 -1
- package/dist/components/MetricList/MetricList.js +5 -3
- package/dist/components/MetricList/MetricList.js.map +1 -1
- package/dist/components/Modal/Modal.d.ts +24 -5
- package/dist/components/Modal/Modal.d.ts.map +1 -1
- package/dist/components/Modal/Modal.js +16 -6
- package/dist/components/Modal/Modal.js.map +1 -1
- package/dist/components/Modal/components/ModalContext.d.ts +14 -0
- package/dist/components/Modal/components/ModalContext.d.ts.map +1 -1
- package/dist/components/Modal/components/ModalContext.js.map +1 -1
- package/dist/components/Modal/components/ModalHeader.d.ts.map +1 -1
- package/dist/components/Modal/components/ModalHeader.js +13 -2
- package/dist/components/Modal/components/ModalHeader.js.map +1 -1
- package/dist/components/NavBottom/NavBottom.d.ts +2 -1
- package/dist/components/NavBottom/NavBottom.d.ts.map +1 -1
- package/dist/components/NavBottom/NavBottom.js +6 -4
- package/dist/components/NavBottom/NavBottom.js.map +1 -1
- package/dist/components/NavSide/NavItem.js +1 -1
- package/dist/components/NavSide/NavItem.js.map +1 -1
- package/dist/components/NavSide/NavSide.d.ts +2 -1
- package/dist/components/NavSide/NavSide.d.ts.map +1 -1
- package/dist/components/NavSide/NavSide.js +5 -3
- package/dist/components/NavSide/NavSide.js.map +1 -1
- package/dist/components/Popover/Popover.d.ts.map +1 -1
- package/dist/components/Popover/Popover.js +7 -0
- package/dist/components/Popover/Popover.js.map +1 -1
- package/dist/components/Popover/components/PopoverBody.d.ts.map +1 -1
- package/dist/components/Popover/components/PopoverBody.js +5 -2
- package/dist/components/Popover/components/PopoverBody.js.map +1 -1
- package/dist/components/Popover/components/PopoverContext.d.ts +1 -0
- package/dist/components/Popover/components/PopoverContext.d.ts.map +1 -1
- package/dist/components/Popover/components/PopoverContext.js.map +1 -1
- package/dist/components/Popover/components/PopoverTrigger.d.ts.map +1 -1
- package/dist/components/Popover/components/PopoverTrigger.js +2 -2
- package/dist/components/Popover/components/PopoverTrigger.js.map +1 -1
- package/dist/components/ProgressBar/ProgressBar.d.ts +2 -1
- package/dist/components/ProgressBar/ProgressBar.d.ts.map +1 -1
- package/dist/components/ProgressBar/ProgressBar.js +5 -3
- package/dist/components/ProgressBar/ProgressBar.js.map +1 -1
- package/dist/components/Select/Select.d.ts +11 -2
- package/dist/components/Select/Select.d.ts.map +1 -1
- package/dist/components/Select/Select.js +64 -18
- package/dist/components/Select/Select.js.map +1 -1
- package/dist/components/Select/SelectDropdown.d.ts +14 -2
- package/dist/components/Select/SelectDropdown.d.ts.map +1 -1
- package/dist/components/Select/SelectDropdown.js +19 -4
- package/dist/components/Select/SelectDropdown.js.map +1 -1
- package/dist/components/Select/SelectTrigger.d.ts +10 -3
- package/dist/components/Select/SelectTrigger.d.ts.map +1 -1
- package/dist/components/Select/SelectTrigger.js +12 -3
- package/dist/components/Select/SelectTrigger.js.map +1 -1
- package/dist/components/Select/useSelectKeyboard.d.ts +15 -1
- package/dist/components/Select/useSelectKeyboard.d.ts.map +1 -1
- package/dist/components/Select/useSelectKeyboard.js +73 -65
- package/dist/components/Select/useSelectKeyboard.js.map +1 -1
- package/dist/components/Sheet/Sheet.d.ts +19 -5
- package/dist/components/Sheet/Sheet.d.ts.map +1 -1
- package/dist/components/Sheet/Sheet.js +18 -6
- package/dist/components/Sheet/Sheet.js.map +1 -1
- package/dist/components/Sheet/components/SheetContext.d.ts +10 -0
- package/dist/components/Sheet/components/SheetContext.d.ts.map +1 -1
- package/dist/components/Sheet/components/SheetContext.js.map +1 -1
- package/dist/components/Sheet/components/SheetHeader.d.ts.map +1 -1
- package/dist/components/Sheet/components/SheetHeader.js +11 -2
- package/dist/components/Sheet/components/SheetHeader.js.map +1 -1
- package/dist/components/Skeleton/Skeleton.d.ts +2 -1
- package/dist/components/Skeleton/Skeleton.d.ts.map +1 -1
- package/dist/components/Skeleton/Skeleton.js +5 -3
- package/dist/components/Skeleton/Skeleton.js.map +1 -1
- package/dist/components/Sparkline/Sparkline.d.ts +10 -2
- package/dist/components/Sparkline/Sparkline.d.ts.map +1 -1
- package/dist/components/Sparkline/Sparkline.js +5 -3
- package/dist/components/Sparkline/Sparkline.js.map +1 -1
- package/dist/components/StatsCard/StatsCard.d.ts +4 -4
- package/dist/components/StatsCard/StatsCard.d.ts.map +1 -1
- package/dist/components/StatsCard/StatsCard.js +5 -3
- package/dist/components/StatsCard/StatsCard.js.map +1 -1
- package/dist/components/Tabs/Tabs.d.ts +2 -2
- package/dist/components/Tabs/components/TabsList.d.ts +1 -1
- package/dist/components/Tabs/components/TabsList.d.ts.map +1 -1
- package/dist/components/Tabs/components/TabsList.js +5 -4
- package/dist/components/Tabs/components/TabsList.js.map +1 -1
- package/dist/components/Tabs/components/TabsPanels.d.ts +1 -1
- package/dist/components/Tabs/components/TabsPanels.d.ts.map +1 -1
- package/dist/components/Tabs/components/TabsPanels.js +5 -3
- package/dist/components/Tabs/components/TabsPanels.js.map +1 -1
- package/dist/components/TagChip/TagChip.d.ts +2 -1
- package/dist/components/TagChip/TagChip.d.ts.map +1 -1
- package/dist/components/TagChip/TagChip.js +5 -3
- package/dist/components/TagChip/TagChip.js.map +1 -1
- package/dist/components/TagInput/TagInput.d.ts +1 -1
- package/dist/components/TagInput/TagInput.d.ts.map +1 -1
- package/dist/components/TagInput/TagInput.js +5 -4
- package/dist/components/TagInput/TagInput.js.map +1 -1
- package/dist/components/TimePicker/TimePicker.d.ts +2 -2
- package/dist/components/TimePicker/TimePicker.d.ts.map +1 -1
- package/dist/components/TimePicker/TimePicker.js +27 -11
- package/dist/components/TimePicker/TimePicker.js.map +1 -1
- package/dist/components/TimePicker/timepicker.types.d.ts +14 -13
- package/dist/components/TimePicker/timepicker.types.d.ts.map +1 -1
- package/dist/components/TimePicker/timepicker.types.js +4 -3
- package/dist/components/TimePicker/timepicker.types.js.map +1 -1
- package/dist/date-time/index.d.ts +2 -0
- package/dist/date-time/index.d.ts.map +1 -1
- package/dist/date-time/index.js +9 -0
- package/dist/date-time/index.js.map +1 -1
- package/dist/date-time/iso.d.ts +261 -0
- package/dist/date-time/iso.d.ts.map +1 -0
- package/dist/date-time/iso.js +358 -0
- package/dist/date-time/iso.js.map +1 -0
- package/dist/date-time/now.d.ts +7 -6
- package/dist/date-time/now.d.ts.map +1 -1
- package/dist/date-time/now.js +3 -3
- package/dist/date-time/now.js.map +1 -1
- package/dist/theme/index.d.ts +4 -3
- package/dist/theme/index.d.ts.map +1 -1
- package/dist/theme/index.js +3 -2
- package/dist/theme/index.js.map +1 -1
- package/dist/theme/schema.d.ts +70 -32
- package/dist/theme/schema.d.ts.map +1 -1
- package/dist/theme/schema.js +36 -18
- package/dist/theme/schema.js.map +1 -1
- package/package.json +9 -15
- package/src/components/Badge/Badge.tsx +5 -4
- package/src/components/BarChart/BarChart.tsx +25 -30
- package/src/components/Calendar/Calendar.tsx +91 -401
- package/src/components/Calendar/CalendarView.tsx +445 -0
- package/src/components/Calendar/calendar.types.ts +94 -29
- package/src/components/Calendar/index.ts +16 -17
- package/src/components/ColorDot/ColorDot.tsx +6 -4
- package/src/components/DatePicker/DatePicker.tsx +92 -16
- package/src/components/DatePicker/datepicker.types.ts +38 -39
- package/src/components/DatePicker/index.ts +11 -10
- package/src/components/DateRangePicker/DateRangePicker.tsx +17 -21
- package/src/components/DateRangePicker/daterangepicker.types.ts +22 -22
- package/src/components/DateRangePicker/index.ts +9 -1
- package/src/components/DateTimePicker/DateTimePicker.tsx +46 -18
- package/src/components/DateTimePicker/datetimepicker.types.ts +22 -18
- package/src/components/DateTimePicker/index.ts +9 -1
- package/src/components/DateTimeRangePicker/DateTimeRangePicker.tsx +62 -28
- package/src/components/DateTimeRangePicker/datetimerangepicker.types.ts +24 -19
- package/src/components/DateTimeRangePicker/index.ts +9 -0
- package/src/components/EmptyState/EmptyState.tsx +8 -7
- package/src/components/Field/Field.tsx +7 -6
- package/src/components/LineChart/LineChart.tsx +147 -141
- package/src/components/Markdown/Markdown.tsx +4 -3
- package/src/components/Message/Message.tsx +17 -1
- package/src/components/MetricList/MetricList.tsx +10 -5
- package/src/components/Modal/Modal.tsx +119 -87
- package/src/components/Modal/components/ModalContext.tsx +14 -0
- package/src/components/Modal/components/ModalHeader.tsx +14 -2
- package/src/components/NavBottom/NavBottom.tsx +6 -4
- package/src/components/NavSide/NavItem.tsx +1 -1
- package/src/components/NavSide/NavSide.tsx +5 -3
- package/src/components/Popover/Popover.tsx +7 -0
- package/src/components/Popover/components/PopoverBody.tsx +6 -1
- package/src/components/Popover/components/PopoverContext.tsx +1 -0
- package/src/components/Popover/components/PopoverTrigger.tsx +2 -1
- package/src/components/ProgressBar/ProgressBar.tsx +32 -28
- package/src/components/Select/Select.tsx +76 -19
- package/src/components/Select/SelectDropdown.tsx +32 -3
- package/src/components/Select/SelectTrigger.tsx +26 -3
- package/src/components/Select/useSelectKeyboard.ts +20 -10
- package/src/components/Sheet/Sheet.tsx +108 -83
- package/src/components/Sheet/components/SheetContext.tsx +10 -0
- package/src/components/Sheet/components/SheetHeader.tsx +17 -2
- package/src/components/Skeleton/Skeleton.tsx +5 -3
- package/src/components/Sparkline/Sparkline.tsx +29 -36
- package/src/components/StatsCard/StatsCard.tsx +7 -6
- package/src/components/Tabs/components/TabsList.tsx +5 -3
- package/src/components/Tabs/components/TabsPanels.tsx +5 -3
- package/src/components/TagChip/TagChip.tsx +10 -3
- package/src/components/TagInput/TagInput.tsx +5 -3
- package/src/components/TimePicker/TimePicker.tsx +31 -13
- package/src/components/TimePicker/timepicker.types.ts +14 -13
- package/src/date-time/index.ts +61 -0
- package/src/date-time/iso.ts +511 -0
- package/src/date-time/now.ts +10 -9
- package/src/theme/index.ts +11 -3
- package/src/theme/schema.ts +82 -54
- package/src/theme/tokens.css +807 -0
- package/dist/styles.css +0 -2
- package/dist/theme.generated.css +0 -438
- package/src/styles/base.css +0 -41
- package/src/theme/palette.json +0 -100
- package/src/theme/palette.schema.json +0 -40
- package/src/theme/theme.json +0 -349
- package/src/theme/theme.schema.json +0 -876
- package/src/theme/typography.json +0 -7
- package/src/theme/typography.schema.json +0 -36
- package/tailwind.config.ts +0 -261
|
@@ -5,13 +5,25 @@ import { ModalContext } from "./ModalContext";
|
|
|
5
5
|
|
|
6
6
|
export type ModalHeaderProps = React.HTMLAttributes<HTMLDivElement>;
|
|
7
7
|
|
|
8
|
-
export const ModalHeader = React.forwardRef<HTMLDivElement, ModalHeaderProps>(({ className, children, ...props }, ref) => {
|
|
8
|
+
export const ModalHeader = React.forwardRef<HTMLDivElement, ModalHeaderProps>(({ className, children, id, ...props }, ref) => {
|
|
9
9
|
const context = React.useContext(ModalContext);
|
|
10
10
|
const closable = context?.closable;
|
|
11
11
|
const onClose = context?.onClose;
|
|
12
|
+
const registerTitle = context?.registerTitle;
|
|
13
|
+
|
|
14
|
+
// Tell the dialog it has a title to point `aria-labelledby` at, and take that
|
|
15
|
+
// back on unmount so a header removed at runtime doesn't leave a dangling
|
|
16
|
+
// reference behind. A caller-supplied `id` wins over the generated one, which
|
|
17
|
+
// also means the dialog can no longer find it — say so rather than pointing
|
|
18
|
+
// at an id that isn't in the DOM, and let the caller label the Modal itself.
|
|
19
|
+
const ownsTitleId = id === undefined;
|
|
20
|
+
React.useEffect(() => {
|
|
21
|
+
registerTitle?.(ownsTitleId);
|
|
22
|
+
return () => registerTitle?.(false);
|
|
23
|
+
}, [registerTitle, ownsTitleId]);
|
|
12
24
|
|
|
13
25
|
return (
|
|
14
|
-
<div ref={ref} className={cn("flex flex-col space-y-1.5 p-6", closable && "pr-12", className)} {...props}>
|
|
26
|
+
<div ref={ref} id={id ?? context?.titleId} className={cn("flex flex-col space-y-1.5 p-6", closable && "pr-12", className)} {...props}>
|
|
15
27
|
{children}
|
|
16
28
|
{closable && (
|
|
17
29
|
<button
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { useComponents } from "@sixthshift/design-system/components";
|
|
2
2
|
import { cn } from "@sixthshift/design-system/utils";
|
|
3
|
+
import * as React from "react";
|
|
3
4
|
import type { NavItem as NavItemType, NavSection, RenderLinkFn } from "../NavSide";
|
|
4
5
|
|
|
5
6
|
export type NavBottomProps = {
|
|
@@ -15,14 +16,14 @@ export type NavBottomProps = {
|
|
|
15
16
|
className?: string;
|
|
16
17
|
};
|
|
17
18
|
|
|
18
|
-
export const NavBottom = ({ sections, isActive, renderLink, maxItems = 5, className }
|
|
19
|
+
export const NavBottom = React.forwardRef<HTMLElement, NavBottomProps>(({ sections, isActive, renderLink, maxItems = 5, className }, ref) => {
|
|
19
20
|
const { Link } = useComponents();
|
|
20
21
|
const linkRenderer: RenderLinkFn = renderLink ?? (({ to, ...rest }) => <Link href={to} {...rest} />);
|
|
21
22
|
// Flatten all sections and limit to maxItems
|
|
22
23
|
const items = sections.flatMap((section) => section.items).slice(0, maxItems);
|
|
23
24
|
|
|
24
25
|
return (
|
|
25
|
-
<nav className={cn("fixed inset-x-0 bottom-0 z-app-bar bg-bg-brand-strong text-fg-on-brand-strong", className)}>
|
|
26
|
+
<nav ref={ref} className={cn("fixed inset-x-0 bottom-0 z-app-bar bg-bg-brand-strong text-fg-on-brand-strong", className)}>
|
|
26
27
|
<ul className="flex items-center justify-around">
|
|
27
28
|
{items.map((item) => {
|
|
28
29
|
const Icon = item.icon;
|
|
@@ -35,7 +36,7 @@ export const NavBottom = ({ sections, isActive, renderLink, maxItems = 5, classN
|
|
|
35
36
|
className: cn(
|
|
36
37
|
"flex flex-col items-center gap-1 rounded-md px-3 py-2 text-xs",
|
|
37
38
|
"hover:bg-bg-brand-hovered hover:text-fg-on-brand-hovered",
|
|
38
|
-
active && "bg-bg-brand-pressed"
|
|
39
|
+
active && "bg-bg-brand-pressed text-fg-on-brand-pressed"
|
|
39
40
|
),
|
|
40
41
|
children: (
|
|
41
42
|
<>
|
|
@@ -50,4 +51,5 @@ export const NavBottom = ({ sections, isActive, renderLink, maxItems = 5, classN
|
|
|
50
51
|
</ul>
|
|
51
52
|
</nav>
|
|
52
53
|
);
|
|
53
|
-
};
|
|
54
|
+
});
|
|
55
|
+
NavBottom.displayName = "NavBottom";
|
|
@@ -18,7 +18,7 @@ export const NavItem = ({ item, active, expanded, renderLink }: NavItemProps) =>
|
|
|
18
18
|
className: cn(
|
|
19
19
|
"flex items-center gap-3 rounded-md px-2 py-2 font-medium text-sm",
|
|
20
20
|
"hover:bg-bg-brand-hovered hover:text-fg-on-brand-hovered",
|
|
21
|
-
active && "bg-bg-brand-pressed"
|
|
21
|
+
active && "bg-bg-brand-pressed text-fg-on-brand-pressed"
|
|
22
22
|
),
|
|
23
23
|
// Only include title when collapsed (for tooltip)
|
|
24
24
|
...(expanded ? {} : { title: item.label }),
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { useComponents } from "@sixthshift/design-system/components";
|
|
2
2
|
import { cn } from "@sixthshift/design-system/utils";
|
|
3
|
+
import * as React from "react";
|
|
3
4
|
import { NavItem } from "./NavItem";
|
|
4
5
|
import type { NavItem as NavItemType, NavSection, RenderLinkFn } from "./types";
|
|
5
6
|
|
|
@@ -16,11 +17,11 @@ export type NavSideProps = {
|
|
|
16
17
|
className?: string;
|
|
17
18
|
};
|
|
18
19
|
|
|
19
|
-
export const NavSide = ({ sections, expanded = true, isActive, renderLink, className }
|
|
20
|
+
export const NavSide = React.forwardRef<HTMLElement, NavSideProps>(({ sections, expanded = true, isActive, renderLink, className }, ref) => {
|
|
20
21
|
const { Link } = useComponents();
|
|
21
22
|
const linkRenderer: RenderLinkFn = renderLink ?? (({ to, ...rest }) => <Link href={to} {...rest} />);
|
|
22
23
|
return (
|
|
23
|
-
<aside className={cn("flex flex-col bg-bg-brand-strong text-fg-on-brand-strong", expanded ? "w-48" : "w-16", className)}>
|
|
24
|
+
<aside ref={ref} className={cn("flex flex-col bg-bg-brand-strong text-fg-on-brand-strong", expanded ? "w-48" : "w-16", className)}>
|
|
24
25
|
<nav className="flex-1 overflow-y-auto py-3">
|
|
25
26
|
{sections.map((section, sectionIndex) => (
|
|
26
27
|
<div key={section.id}>
|
|
@@ -37,4 +38,5 @@ export const NavSide = ({ sections, expanded = true, isActive, renderLink, class
|
|
|
37
38
|
</nav>
|
|
38
39
|
</aside>
|
|
39
40
|
);
|
|
40
|
-
};
|
|
41
|
+
});
|
|
42
|
+
NavSide.displayName = "NavSide";
|
|
@@ -44,6 +44,12 @@ const PopoverRoot = ({ open: controlledOpen, onOpenChange, defaultOpen = false,
|
|
|
44
44
|
|
|
45
45
|
const id = useId();
|
|
46
46
|
const contentId = `popover-content-${id}`;
|
|
47
|
+
// `Popover.Body` is a `role="dialog"`, and a dialog with no accessible name is
|
|
48
|
+
// announced as just "dialog". There is no title sub-part to read from, so the
|
|
49
|
+
// trigger — which always has a label, because it is what the user clicked —
|
|
50
|
+
// names the popover. A caller passing their own `aria-label`/`aria-labelledby`
|
|
51
|
+
// to `Popover.Body` still wins.
|
|
52
|
+
const triggerId = `popover-trigger-${id}`;
|
|
47
53
|
|
|
48
54
|
const contextValue = {
|
|
49
55
|
open,
|
|
@@ -56,6 +62,7 @@ const PopoverRoot = ({ open: controlledOpen, onOpenChange, defaultOpen = false,
|
|
|
56
62
|
getReferenceProps,
|
|
57
63
|
getFloatingProps,
|
|
58
64
|
contentId,
|
|
65
|
+
triggerId,
|
|
59
66
|
};
|
|
60
67
|
|
|
61
68
|
return <PopoverContext.Provider value={contextValue}>{children}</PopoverContext.Provider>;
|
|
@@ -6,7 +6,11 @@ import { usePopoverContext } from "./PopoverContext";
|
|
|
6
6
|
export type PopoverBodyProps = React.HTMLAttributes<HTMLDivElement>;
|
|
7
7
|
|
|
8
8
|
export const PopoverBody = React.forwardRef<HTMLDivElement, PopoverBodyProps>(({ className, children, ...props }, forwardedRef) => {
|
|
9
|
-
const { open, refs, floatingStyles, getFloatingProps, contentId } = usePopoverContext();
|
|
9
|
+
const { open, refs, floatingStyles, getFloatingProps, contentId, triggerId } = usePopoverContext();
|
|
10
|
+
|
|
11
|
+
// `aria-labelledby` outranks `aria-label` in the name computation, so the
|
|
12
|
+
// trigger only names the dialog when the caller hasn't named it themselves.
|
|
13
|
+
const hasOwnLabel = props["aria-label"] !== undefined || props["aria-labelledby"] !== undefined;
|
|
10
14
|
|
|
11
15
|
if (!open) return null;
|
|
12
16
|
|
|
@@ -20,6 +24,7 @@ export const PopoverBody = React.forwardRef<HTMLDivElement, PopoverBodyProps>(({
|
|
|
20
24
|
}}
|
|
21
25
|
id={contentId}
|
|
22
26
|
role="dialog"
|
|
27
|
+
aria-labelledby={hasOwnLabel ? undefined : triggerId}
|
|
23
28
|
style={floatingStyles}
|
|
24
29
|
className={cn("z-popover rounded-lg border border-border-normal bg-bg-normal p-4 shadow-lg", className)}
|
|
25
30
|
{...getFloatingProps(props)}
|
|
@@ -11,6 +11,7 @@ export type PopoverContextValue = {
|
|
|
11
11
|
getReferenceProps: (props?: React.HTMLProps<HTMLElement>) => Record<string, unknown>;
|
|
12
12
|
getFloatingProps: (props?: React.HTMLProps<HTMLElement>) => Record<string, unknown>;
|
|
13
13
|
contentId: string;
|
|
14
|
+
triggerId: string;
|
|
14
15
|
};
|
|
15
16
|
|
|
16
17
|
export const PopoverContext = createContext<PopoverContextValue | null>(null);
|
|
@@ -8,7 +8,7 @@ export type PopoverTriggerProps = React.ButtonHTMLAttributes<HTMLButtonElement>
|
|
|
8
8
|
};
|
|
9
9
|
|
|
10
10
|
export const PopoverTrigger = React.forwardRef<HTMLButtonElement, PopoverTriggerProps>(({ asChild = false, children, ...props }, forwardedRef) => {
|
|
11
|
-
const { refs, getReferenceProps, contentId, open } = usePopoverContext();
|
|
11
|
+
const { refs, getReferenceProps, contentId, triggerId, open } = usePopoverContext();
|
|
12
12
|
const Comp = asChild ? Slot : "button";
|
|
13
13
|
|
|
14
14
|
return (
|
|
@@ -19,6 +19,7 @@ export const PopoverTrigger = React.forwardRef<HTMLButtonElement, PopoverTrigger
|
|
|
19
19
|
else if (forwardedRef) forwardedRef.current = node;
|
|
20
20
|
}}
|
|
21
21
|
type={asChild ? undefined : "button"}
|
|
22
|
+
id={triggerId}
|
|
22
23
|
aria-haspopup="dialog"
|
|
23
24
|
aria-expanded={open}
|
|
24
25
|
aria-controls={open ? contentId : undefined}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Caption } from "@sixthshift/design-system/caption";
|
|
2
2
|
import { cn } from "@sixthshift/design-system/utils";
|
|
3
|
+
import * as React from "react";
|
|
3
4
|
|
|
4
5
|
export type ProgressBarProps = {
|
|
5
6
|
completed: number;
|
|
@@ -10,33 +11,36 @@ export type ProgressBarProps = {
|
|
|
10
11
|
className?: string;
|
|
11
12
|
};
|
|
12
13
|
|
|
13
|
-
export const ProgressBar =
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
14
|
+
export const ProgressBar = React.forwardRef<HTMLDivElement, ProgressBarProps>(
|
|
15
|
+
({ completed, total, showFraction = true, label = "Progress", className }, ref) => {
|
|
16
|
+
// A caller can hand us anything; the bar must not render an out-of-range
|
|
17
|
+
// width (a negative `width` is invalid CSS) or report impossible progress.
|
|
18
|
+
const isMeasurable = total > 0;
|
|
19
|
+
const clamped = isMeasurable ? Math.min(Math.max(completed, 0), total) : 0;
|
|
20
|
+
const percentage = isMeasurable ? (clamped / total) * 100 : 0;
|
|
19
21
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
22
|
+
return (
|
|
23
|
+
<div ref={ref} className={cn("flex items-center gap-3", className)}>
|
|
24
|
+
<div
|
|
25
|
+
role="progressbar"
|
|
26
|
+
aria-label={label}
|
|
27
|
+
aria-valuemin={0}
|
|
28
|
+
aria-valuemax={isMeasurable ? total : undefined}
|
|
29
|
+
// An indeterminate progressbar omits aria-valuenow; with no positive
|
|
30
|
+
// total there is no meaningful progress to report.
|
|
31
|
+
aria-valuenow={isMeasurable ? clamped : undefined}
|
|
32
|
+
aria-valuetext={isMeasurable ? `${clamped} of ${total}` : undefined}
|
|
33
|
+
className="h-2 flex-1 overflow-hidden rounded-full bg-bg-strong"
|
|
34
|
+
>
|
|
35
|
+
<div className="h-full rounded-full bg-fg-success transition-all" style={{ width: `${percentage}%` }} />
|
|
36
|
+
</div>
|
|
37
|
+
{showFraction && (
|
|
38
|
+
<Caption className="shrink-0">
|
|
39
|
+
{completed}/{total}
|
|
40
|
+
</Caption>
|
|
41
|
+
)}
|
|
34
42
|
</div>
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
)}
|
|
40
|
-
</div>
|
|
41
|
-
);
|
|
42
|
-
};
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
);
|
|
46
|
+
ProgressBar.displayName = "ProgressBar";
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { autoUpdate, flip, offset, shift, size, useFloating } from "@floating-ui/react";
|
|
2
2
|
import { useControllableState } from "@sixthshift/design-system/hooks";
|
|
3
|
-
import { type HTMLAttributes, useCallback, useEffect, useId, useMemo, useRef, useState } from "react";
|
|
4
|
-
import { SelectDropdown } from "./SelectDropdown";
|
|
3
|
+
import { forwardRef, type HTMLAttributes, type ReactElement, type Ref, useCallback, useEffect, useId, useMemo, useRef, useState } from "react";
|
|
4
|
+
import { SelectDropdown, selectOptionId } from "./SelectDropdown";
|
|
5
5
|
import { SelectTriggerButton, SelectTriggerSearch } from "./SelectTrigger";
|
|
6
6
|
import { useSelectKeyboard } from "./useSelectKeyboard";
|
|
7
7
|
|
|
@@ -37,7 +37,13 @@ type SelectMultipleProps<T extends string = string> = SelectBaseProps<T> & {
|
|
|
37
37
|
|
|
38
38
|
export type SelectProps<T extends string = string> = SelectSingleProps<T> | SelectMultipleProps<T>;
|
|
39
39
|
|
|
40
|
-
|
|
40
|
+
/**
|
|
41
|
+
* `forwardRef` erases the generic, so the implementation takes the ref as an
|
|
42
|
+
* ordinary second argument and the exported `Select` re-declares the generic
|
|
43
|
+
* signature over it. Consumers keep `Select<"a" | "b">` inference *and* get a
|
|
44
|
+
* ref; without the cast they would have to choose.
|
|
45
|
+
*/
|
|
46
|
+
const SelectRoot = forwardRef(function SelectRoot<T extends string = string>(props: SelectProps<T>, rootRef: Ref<HTMLDivElement>) {
|
|
41
47
|
const {
|
|
42
48
|
options,
|
|
43
49
|
onBlur,
|
|
@@ -71,6 +77,11 @@ export const Select = <T extends string = string>(props: SelectProps<T>) => {
|
|
|
71
77
|
const [highlightedIndex, setHighlightedIndex] = useState(-1);
|
|
72
78
|
const [searchValue, setSearchValue] = useState("");
|
|
73
79
|
const listboxRef = useRef<HTMLDivElement | null>(null);
|
|
80
|
+
// Also held in state: the listbox is portalled, and FloatingPortal creates its
|
|
81
|
+
// container in an effect, so the node lands a commit after `open` flips. A ref
|
|
82
|
+
// alone is still null when the open effect runs — state re-renders us when the
|
|
83
|
+
// node actually arrives.
|
|
84
|
+
const [listboxNode, setListboxNode] = useState<HTMLDivElement | null>(null);
|
|
74
85
|
const inputRef = useRef<HTMLInputElement>(null);
|
|
75
86
|
const optionRefs = useRef<Map<number, HTMLButtonElement>>(new Map());
|
|
76
87
|
const wasOpenRef = useRef(false);
|
|
@@ -111,6 +122,11 @@ export const Select = <T extends string = string>(props: SelectProps<T>) => {
|
|
|
111
122
|
whileElementsMounted: autoUpdate,
|
|
112
123
|
});
|
|
113
124
|
|
|
125
|
+
const setListbox = useCallback((node: HTMLDivElement | null) => {
|
|
126
|
+
listboxRef.current = node;
|
|
127
|
+
setListboxNode(node);
|
|
128
|
+
}, []);
|
|
129
|
+
|
|
114
130
|
const focusTrigger = useCallback(() => {
|
|
115
131
|
(refs.reference.current as HTMLElement | null)?.focus();
|
|
116
132
|
}, [refs.reference]);
|
|
@@ -142,18 +158,38 @@ export const Select = <T extends string = string>(props: SelectProps<T>) => {
|
|
|
142
158
|
[isMultiple, setMultiValue, setSingleValue]
|
|
143
159
|
);
|
|
144
160
|
|
|
161
|
+
const handleListKeyDown = useSelectKeyboard({
|
|
162
|
+
open,
|
|
163
|
+
searchable,
|
|
164
|
+
searchValue,
|
|
165
|
+
highlightedIndex,
|
|
166
|
+
displayOptions: filteredOptions,
|
|
167
|
+
setOpen,
|
|
168
|
+
setHighlightedIndex,
|
|
169
|
+
setSearchValue,
|
|
170
|
+
handleSelect,
|
|
171
|
+
focusTrigger,
|
|
172
|
+
});
|
|
173
|
+
|
|
145
174
|
const handleTriggerKeyDown = useCallback(
|
|
146
175
|
(event: React.KeyboardEvent<HTMLButtonElement>) => {
|
|
147
176
|
if (disabled || collapsed) return;
|
|
148
177
|
if (!open && (event.key === "ArrowDown" || event.key === "ArrowUp" || event.key === "Enter" || event.key === " ")) {
|
|
149
178
|
event.preventDefault();
|
|
150
179
|
setOpen(true);
|
|
180
|
+
return;
|
|
151
181
|
}
|
|
152
182
|
if (searchable && !open && event.key.length === 1 && !event.ctrlKey && !event.metaKey && !event.altKey) {
|
|
153
183
|
setOpen(true);
|
|
184
|
+
return;
|
|
154
185
|
}
|
|
186
|
+
// Focus normally moves into the listbox as it opens, but it can still be
|
|
187
|
+
// on the trigger — the frame the dropdown mounts in, or after the user
|
|
188
|
+
// shift-tabs back to it. Handle navigation here too rather than dropping
|
|
189
|
+
// the keystroke.
|
|
190
|
+
handleListKeyDown(event);
|
|
155
191
|
},
|
|
156
|
-
[disabled, collapsed, open, setOpen, searchable]
|
|
192
|
+
[disabled, collapsed, open, setOpen, searchable, handleListKeyDown]
|
|
157
193
|
);
|
|
158
194
|
|
|
159
195
|
const handleSearchChange = useCallback((val: string) => {
|
|
@@ -186,6 +222,14 @@ export const Select = <T extends string = string>(props: SelectProps<T>) => {
|
|
|
186
222
|
wasOpenRef.current = open;
|
|
187
223
|
}, [open, filteredOptions, searchable, selectedValues]);
|
|
188
224
|
|
|
225
|
+
// Non-searchable: focus the listbox itself, so arrow keys reach the scoped
|
|
226
|
+
// keydown handler and a screen reader follows aria-activedescendant. The
|
|
227
|
+
// searchable variant keeps focus in its input instead.
|
|
228
|
+
useEffect(() => {
|
|
229
|
+
if (!open || searchable) return;
|
|
230
|
+
listboxNode?.focus();
|
|
231
|
+
}, [open, searchable, listboxNode]);
|
|
232
|
+
|
|
189
233
|
// Scroll highlighted option into view
|
|
190
234
|
useEffect(() => {
|
|
191
235
|
if (open && highlightedIndex >= 0) {
|
|
@@ -215,32 +259,28 @@ export const Select = <T extends string = string>(props: SelectProps<T>) => {
|
|
|
215
259
|
return () => document.removeEventListener("mousedown", handler);
|
|
216
260
|
}, [open, setOpen, refs]);
|
|
217
261
|
|
|
218
|
-
useSelectKeyboard({
|
|
219
|
-
open,
|
|
220
|
-
searchable,
|
|
221
|
-
searchValue,
|
|
222
|
-
highlightedIndex,
|
|
223
|
-
displayOptions: filteredOptions,
|
|
224
|
-
setOpen,
|
|
225
|
-
setHighlightedIndex,
|
|
226
|
-
setSearchValue,
|
|
227
|
-
handleSelect,
|
|
228
|
-
focusTrigger,
|
|
229
|
-
});
|
|
230
|
-
|
|
231
262
|
// The listbox id must be unique per instance: it is referenced by
|
|
232
263
|
// aria-controls, and a hardcoded value meant two Selects on one page
|
|
233
264
|
// produced duplicate ids and cross-wired ARIA.
|
|
234
265
|
const listboxId = useId();
|
|
266
|
+
const activeOptionId = open && highlightedIndex >= 0 && highlightedIndex < filteredOptions.length ? selectOptionId(listboxId, highlightedIndex) : undefined;
|
|
235
267
|
|
|
236
268
|
// Strip mode from DOM props
|
|
237
269
|
const { mode: _, value: _v, defaultValue: _dv, onValueChange: _oc, ...htmlProps } = restProps as Record<string, unknown>;
|
|
238
270
|
|
|
271
|
+
// `role="listbox"` requires an accessible name of its own, and the trigger
|
|
272
|
+
// can't supply one: its label is the current *value*, and in searchable mode
|
|
273
|
+
// it is an input whose name would resolve to whatever has been typed. Use the
|
|
274
|
+
// caller's `aria-label` when there is one, else the placeholder — the closest
|
|
275
|
+
// thing this component has to a field label.
|
|
276
|
+
const listboxLabel = typeof htmlProps["aria-label"] === "string" ? (htmlProps["aria-label"] as string) : placeholder;
|
|
277
|
+
|
|
239
278
|
return (
|
|
240
279
|
<>
|
|
241
280
|
{searchable && open && !collapsed ? (
|
|
242
281
|
<SelectTriggerSearch
|
|
243
282
|
setReference={refs.setReference}
|
|
283
|
+
rootRef={rootRef}
|
|
244
284
|
inputRef={inputRef}
|
|
245
285
|
searchValue={searchValue}
|
|
246
286
|
disabled={disabled}
|
|
@@ -248,12 +288,15 @@ export const Select = <T extends string = string>(props: SelectProps<T>) => {
|
|
|
248
288
|
open={open}
|
|
249
289
|
className={className}
|
|
250
290
|
onSearchChange={handleSearchChange}
|
|
291
|
+
onKeyDown={handleListKeyDown}
|
|
251
292
|
listboxId={listboxId}
|
|
293
|
+
activeOptionId={activeOptionId}
|
|
252
294
|
props={htmlProps as React.InputHTMLAttributes<HTMLInputElement>}
|
|
253
295
|
/>
|
|
254
296
|
) : (
|
|
255
297
|
<SelectTriggerButton
|
|
256
298
|
setReference={refs.setReference}
|
|
299
|
+
rootRef={rootRef}
|
|
257
300
|
open={open}
|
|
258
301
|
disabled={disabled}
|
|
259
302
|
collapsed={collapsed}
|
|
@@ -272,7 +315,9 @@ export const Select = <T extends string = string>(props: SelectProps<T>) => {
|
|
|
272
315
|
<SelectDropdown
|
|
273
316
|
setFloating={refs.setFloating}
|
|
274
317
|
listboxId={listboxId}
|
|
275
|
-
|
|
318
|
+
setListbox={setListbox}
|
|
319
|
+
label={listboxLabel}
|
|
320
|
+
activeOptionId={activeOptionId}
|
|
276
321
|
floatingStyles={floatingStyles}
|
|
277
322
|
displayOptions={filteredOptions}
|
|
278
323
|
highlightedIndex={highlightedIndex}
|
|
@@ -282,8 +327,20 @@ export const Select = <T extends string = string>(props: SelectProps<T>) => {
|
|
|
282
327
|
multiple={isMultiple}
|
|
283
328
|
onSelect={handleSelect}
|
|
284
329
|
onHighlight={setHighlightedIndex}
|
|
330
|
+
onKeyDown={handleListKeyDown}
|
|
285
331
|
/>
|
|
286
332
|
)}
|
|
287
333
|
</>
|
|
288
334
|
);
|
|
289
|
-
};
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
SelectRoot.displayName = "Select";
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* The trigger is the ref target: `register` from a form library and
|
|
341
|
+
* scroll-into-view both want it, and it is a `<div>` in both the button and the
|
|
342
|
+
* searchable variants. It does swap nodes when the searchable trigger replaces
|
|
343
|
+
* the button one, so a callback ref sees null and then the new element — normal
|
|
344
|
+
* for conditional rendering, but worth knowing if you cache the node.
|
|
345
|
+
*/
|
|
346
|
+
export const Select = SelectRoot as <T extends string = string>(props: SelectProps<T> & { ref?: Ref<HTMLDivElement> }) => ReactElement;
|
|
@@ -4,10 +4,22 @@ import { Check } from "lucide-react";
|
|
|
4
4
|
import type React from "react";
|
|
5
5
|
import type { WritableRefObject } from "../../internal/types";
|
|
6
6
|
|
|
7
|
+
/**
|
|
8
|
+
* Options are addressed by id, not by focus: the listbox (or the search input)
|
|
9
|
+
* keeps focus and points `aria-activedescendant` at the highlighted option, so
|
|
10
|
+
* both sides need to agree on the id.
|
|
11
|
+
*/
|
|
12
|
+
export const selectOptionId = (listboxId: string, index: number) => `${listboxId}-option-${index}`;
|
|
13
|
+
|
|
7
14
|
interface SelectDropdownProps<T extends string> {
|
|
8
15
|
setFloating: (node: HTMLElement | null) => void;
|
|
9
16
|
listboxId: string;
|
|
10
|
-
|
|
17
|
+
/** Publishes the listbox node upward — Select focuses it and scrolls it. */
|
|
18
|
+
setListbox: (node: HTMLDivElement | null) => void;
|
|
19
|
+
/** Names the listbox — a bare `role="listbox"` has no accessible name. */
|
|
20
|
+
label: string;
|
|
21
|
+
/** The highlighted option's id, or undefined when nothing is highlighted. */
|
|
22
|
+
activeOptionId: string | undefined;
|
|
11
23
|
floatingStyles: React.CSSProperties;
|
|
12
24
|
displayOptions: readonly { value: T; label: string }[];
|
|
13
25
|
highlightedIndex: number;
|
|
@@ -17,12 +29,15 @@ interface SelectDropdownProps<T extends string> {
|
|
|
17
29
|
multiple: boolean;
|
|
18
30
|
onSelect: (value: T) => void;
|
|
19
31
|
onHighlight: (index: number) => void;
|
|
32
|
+
onKeyDown: (event: React.KeyboardEvent<HTMLElement>) => void;
|
|
20
33
|
}
|
|
21
34
|
|
|
22
35
|
export const SelectDropdown = <T extends string>({
|
|
23
36
|
setFloating,
|
|
24
37
|
listboxId,
|
|
25
|
-
|
|
38
|
+
setListbox,
|
|
39
|
+
label,
|
|
40
|
+
activeOptionId,
|
|
26
41
|
floatingStyles,
|
|
27
42
|
displayOptions,
|
|
28
43
|
highlightedIndex,
|
|
@@ -32,17 +47,24 @@ export const SelectDropdown = <T extends string>({
|
|
|
32
47
|
multiple,
|
|
33
48
|
onSelect,
|
|
34
49
|
onHighlight,
|
|
50
|
+
onKeyDown,
|
|
35
51
|
}: SelectDropdownProps<T>) => (
|
|
36
52
|
<FloatingPortal>
|
|
37
53
|
<div
|
|
38
54
|
ref={(node) => {
|
|
39
55
|
setFloating(node);
|
|
40
|
-
|
|
56
|
+
setListbox(node);
|
|
41
57
|
}}
|
|
42
58
|
id={listboxId}
|
|
43
59
|
role="listbox"
|
|
60
|
+
aria-label={label}
|
|
44
61
|
aria-multiselectable={multiple || undefined}
|
|
62
|
+
aria-activedescendant={activeOptionId}
|
|
63
|
+
// Focus lands here when the dropdown opens (the searchable variant keeps
|
|
64
|
+
// it in the input instead), which is what makes `aria-activedescendant`
|
|
65
|
+
// and the scoped keydown handler work.
|
|
45
66
|
tabIndex={-1}
|
|
67
|
+
onKeyDown={onKeyDown}
|
|
46
68
|
style={floatingStyles}
|
|
47
69
|
className="z-popover max-h-60 overflow-y-auto rounded-md border border-border-normal bg-bg-normal shadow-lg"
|
|
48
70
|
>
|
|
@@ -59,8 +81,15 @@ export const SelectDropdown = <T extends string>({
|
|
|
59
81
|
else optionRefs.current.delete(index);
|
|
60
82
|
}}
|
|
61
83
|
type="button"
|
|
84
|
+
id={selectOptionId(listboxId, index)}
|
|
62
85
|
role="option"
|
|
63
86
|
aria-selected={isSelected}
|
|
87
|
+
// The options are not tab stops and must not steal focus: the
|
|
88
|
+
// listbox holds it and delegates via aria-activedescendant, so a
|
|
89
|
+
// click that moved focus onto the option would strand the keyboard
|
|
90
|
+
// handler with nothing listening.
|
|
91
|
+
tabIndex={-1}
|
|
92
|
+
onMouseDown={(event) => event.preventDefault()}
|
|
64
93
|
onClick={() => onSelect(option.value)}
|
|
65
94
|
onMouseEnter={() => onHighlight(index)}
|
|
66
95
|
className={cn(
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import { cn } from "@sixthshift/design-system/utils";
|
|
2
2
|
import { ChevronDown, X } from "lucide-react";
|
|
3
|
-
import type { HTMLAttributes } from "react";
|
|
3
|
+
import type { HTMLAttributes, Ref } from "react";
|
|
4
|
+
import type { WritableRefObject } from "../../internal/types";
|
|
4
5
|
|
|
5
6
|
interface SelectTriggerButtonProps {
|
|
6
7
|
setReference: (node: HTMLElement | null) => void;
|
|
8
|
+
/** Consumer ref from Select, attached to this trigger's root element. */
|
|
9
|
+
rootRef?: Ref<HTMLDivElement>;
|
|
7
10
|
listboxId: string;
|
|
8
11
|
open: boolean;
|
|
9
12
|
disabled: boolean;
|
|
@@ -19,6 +22,7 @@ interface SelectTriggerButtonProps {
|
|
|
19
22
|
|
|
20
23
|
export const SelectTriggerButton = ({
|
|
21
24
|
setReference,
|
|
25
|
+
rootRef,
|
|
22
26
|
listboxId,
|
|
23
27
|
open,
|
|
24
28
|
disabled,
|
|
@@ -37,7 +41,7 @@ export const SelectTriggerButton = ({
|
|
|
37
41
|
const showClear = showClearButton && !collapsed;
|
|
38
42
|
|
|
39
43
|
return (
|
|
40
|
-
<div className="relative w-full">
|
|
44
|
+
<div ref={rootRef} className="relative w-full">
|
|
41
45
|
<button
|
|
42
46
|
ref={setReference}
|
|
43
47
|
type="button"
|
|
@@ -85,7 +89,11 @@ export const SelectTriggerButton = ({
|
|
|
85
89
|
|
|
86
90
|
interface SelectTriggerSearchProps {
|
|
87
91
|
setReference: (node: HTMLElement | null) => void;
|
|
92
|
+
/** Consumer ref from Select, attached to this trigger's root element. */
|
|
93
|
+
rootRef?: Ref<HTMLDivElement>;
|
|
88
94
|
listboxId: string;
|
|
95
|
+
/** Id of the highlighted option — the input keeps focus and delegates to it. */
|
|
96
|
+
activeOptionId: string | undefined;
|
|
89
97
|
inputRef: React.RefObject<HTMLInputElement | null>;
|
|
90
98
|
searchValue: string;
|
|
91
99
|
disabled: boolean;
|
|
@@ -93,12 +101,15 @@ interface SelectTriggerSearchProps {
|
|
|
93
101
|
open: boolean;
|
|
94
102
|
className: string | undefined;
|
|
95
103
|
onSearchChange: (value: string) => void;
|
|
104
|
+
onKeyDown: (event: React.KeyboardEvent<HTMLElement>) => void;
|
|
96
105
|
props: React.InputHTMLAttributes<HTMLInputElement>;
|
|
97
106
|
}
|
|
98
107
|
|
|
99
108
|
export const SelectTriggerSearch = ({
|
|
100
109
|
setReference,
|
|
110
|
+
rootRef,
|
|
101
111
|
listboxId,
|
|
112
|
+
activeOptionId,
|
|
102
113
|
inputRef,
|
|
103
114
|
searchValue,
|
|
104
115
|
disabled,
|
|
@@ -106,14 +117,25 @@ export const SelectTriggerSearch = ({
|
|
|
106
117
|
open,
|
|
107
118
|
className,
|
|
108
119
|
onSearchChange,
|
|
120
|
+
onKeyDown,
|
|
109
121
|
props,
|
|
110
122
|
}: SelectTriggerSearchProps) => (
|
|
111
|
-
|
|
123
|
+
// floating-ui anchors to this element and the consumer may also want it, so
|
|
124
|
+
// both refs run off one callback rather than one silently winning.
|
|
125
|
+
<div
|
|
126
|
+
ref={(node) => {
|
|
127
|
+
setReference(node);
|
|
128
|
+
if (typeof rootRef === "function") rootRef(node);
|
|
129
|
+
else if (rootRef) (rootRef as WritableRefObject<HTMLDivElement | null>).current = node;
|
|
130
|
+
}}
|
|
131
|
+
className={cn("relative flex w-full items-center", className)}
|
|
132
|
+
>
|
|
112
133
|
<input
|
|
113
134
|
ref={inputRef as React.RefObject<HTMLInputElement>}
|
|
114
135
|
type="text"
|
|
115
136
|
value={searchValue}
|
|
116
137
|
onChange={(e) => onSearchChange(e.target.value)}
|
|
138
|
+
onKeyDown={onKeyDown}
|
|
117
139
|
placeholder={displayLabel}
|
|
118
140
|
disabled={disabled}
|
|
119
141
|
className={cn(
|
|
@@ -122,6 +144,7 @@ export const SelectTriggerSearch = ({
|
|
|
122
144
|
role="combobox"
|
|
123
145
|
aria-expanded={open}
|
|
124
146
|
aria-controls={listboxId}
|
|
147
|
+
aria-activedescendant={activeOptionId}
|
|
125
148
|
aria-autocomplete="list"
|
|
126
149
|
{...props}
|
|
127
150
|
/>
|