@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.
Files changed (288) hide show
  1. package/README.md +135 -28
  2. package/dist/components/Badge/Badge.d.ts +5 -2
  3. package/dist/components/Badge/Badge.d.ts.map +1 -1
  4. package/dist/components/Badge/Badge.js +5 -3
  5. package/dist/components/Badge/Badge.js.map +1 -1
  6. package/dist/components/BarChart/BarChart.d.ts +8 -2
  7. package/dist/components/BarChart/BarChart.d.ts.map +1 -1
  8. package/dist/components/BarChart/BarChart.js +5 -3
  9. package/dist/components/BarChart/BarChart.js.map +1 -1
  10. package/dist/components/Calendar/Calendar.d.ts +17 -1
  11. package/dist/components/Calendar/Calendar.d.ts.map +1 -1
  12. package/dist/components/Calendar/Calendar.js +80 -225
  13. package/dist/components/Calendar/Calendar.js.map +1 -1
  14. package/dist/components/Calendar/CalendarView.d.ts +15 -0
  15. package/dist/components/Calendar/CalendarView.d.ts.map +1 -0
  16. package/dist/components/Calendar/CalendarView.js +255 -0
  17. package/dist/components/Calendar/CalendarView.js.map +1 -0
  18. package/dist/components/Calendar/calendar.types.d.ts +78 -27
  19. package/dist/components/Calendar/calendar.types.d.ts.map +1 -1
  20. package/dist/components/Calendar/calendar.types.js +10 -3
  21. package/dist/components/Calendar/calendar.types.js.map +1 -1
  22. package/dist/components/Calendar/index.d.ts +16 -2
  23. package/dist/components/Calendar/index.d.ts.map +1 -1
  24. package/dist/components/Calendar/index.js +15 -1
  25. package/dist/components/Calendar/index.js.map +1 -1
  26. package/dist/components/ColorDot/ColorDot.d.ts +9 -3
  27. package/dist/components/ColorDot/ColorDot.d.ts.map +1 -1
  28. package/dist/components/ColorDot/ColorDot.js +5 -3
  29. package/dist/components/ColorDot/ColorDot.js.map +1 -1
  30. package/dist/components/DatePicker/DatePicker.d.ts +2 -2
  31. package/dist/components/DatePicker/DatePicker.d.ts.map +1 -1
  32. package/dist/components/DatePicker/DatePicker.js +65 -10
  33. package/dist/components/DatePicker/DatePicker.js.map +1 -1
  34. package/dist/components/DatePicker/datepicker.types.d.ts +34 -36
  35. package/dist/components/DatePicker/datepicker.types.d.ts.map +1 -1
  36. package/dist/components/DatePicker/datepicker.types.js +5 -3
  37. package/dist/components/DatePicker/datepicker.types.js.map +1 -1
  38. package/dist/components/DatePicker/index.d.ts +10 -1
  39. package/dist/components/DatePicker/index.d.ts.map +1 -1
  40. package/dist/components/DatePicker/index.js +8 -0
  41. package/dist/components/DatePicker/index.js.map +1 -1
  42. package/dist/components/DateRangePicker/DateRangePicker.d.ts +3 -2
  43. package/dist/components/DateRangePicker/DateRangePicker.d.ts.map +1 -1
  44. package/dist/components/DateRangePicker/DateRangePicker.js +17 -21
  45. package/dist/components/DateRangePicker/DateRangePicker.js.map +1 -1
  46. package/dist/components/DateRangePicker/daterangepicker.types.d.ts +20 -20
  47. package/dist/components/DateRangePicker/daterangepicker.types.d.ts.map +1 -1
  48. package/dist/components/DateRangePicker/daterangepicker.types.js +6 -0
  49. package/dist/components/DateRangePicker/daterangepicker.types.js.map +1 -1
  50. package/dist/components/DateRangePicker/index.d.ts +8 -1
  51. package/dist/components/DateRangePicker/index.d.ts.map +1 -1
  52. package/dist/components/DateRangePicker/index.js +6 -0
  53. package/dist/components/DateRangePicker/index.js.map +1 -1
  54. package/dist/components/DateTimePicker/DateTimePicker.d.ts +3 -3
  55. package/dist/components/DateTimePicker/DateTimePicker.d.ts.map +1 -1
  56. package/dist/components/DateTimePicker/DateTimePicker.js +30 -12
  57. package/dist/components/DateTimePicker/DateTimePicker.js.map +1 -1
  58. package/dist/components/DateTimePicker/datetimepicker.types.d.ts +20 -17
  59. package/dist/components/DateTimePicker/datetimepicker.types.d.ts.map +1 -1
  60. package/dist/components/DateTimePicker/datetimepicker.types.js +10 -0
  61. package/dist/components/DateTimePicker/datetimepicker.types.js.map +1 -1
  62. package/dist/components/DateTimePicker/index.d.ts +8 -1
  63. package/dist/components/DateTimePicker/index.d.ts.map +1 -1
  64. package/dist/components/DateTimePicker/index.js +6 -0
  65. package/dist/components/DateTimePicker/index.js.map +1 -1
  66. package/dist/components/DateTimeRangePicker/DateTimeRangePicker.d.ts +3 -3
  67. package/dist/components/DateTimeRangePicker/DateTimeRangePicker.d.ts.map +1 -1
  68. package/dist/components/DateTimeRangePicker/DateTimeRangePicker.js +41 -20
  69. package/dist/components/DateTimeRangePicker/DateTimeRangePicker.js.map +1 -1
  70. package/dist/components/DateTimeRangePicker/datetimerangepicker.types.d.ts +22 -18
  71. package/dist/components/DateTimeRangePicker/datetimerangepicker.types.d.ts.map +1 -1
  72. package/dist/components/DateTimeRangePicker/datetimerangepicker.types.js +8 -0
  73. package/dist/components/DateTimeRangePicker/datetimerangepicker.types.js.map +1 -1
  74. package/dist/components/DateTimeRangePicker/index.d.ts +8 -0
  75. package/dist/components/DateTimeRangePicker/index.d.ts.map +1 -1
  76. package/dist/components/DateTimeRangePicker/index.js +7 -0
  77. package/dist/components/DateTimeRangePicker/index.js.map +1 -1
  78. package/dist/components/EmptyState/EmptyState.d.ts +16 -5
  79. package/dist/components/EmptyState/EmptyState.d.ts.map +1 -1
  80. package/dist/components/EmptyState/EmptyState.js +5 -3
  81. package/dist/components/EmptyState/EmptyState.js.map +1 -1
  82. package/dist/components/Field/Field.d.ts +3 -3
  83. package/dist/components/Field/Field.d.ts.map +1 -1
  84. package/dist/components/Field/Field.js +6 -4
  85. package/dist/components/Field/Field.js.map +1 -1
  86. package/dist/components/LineChart/LineChart.d.ts +18 -2
  87. package/dist/components/LineChart/LineChart.d.ts.map +1 -1
  88. package/dist/components/LineChart/LineChart.js +5 -3
  89. package/dist/components/LineChart/LineChart.js.map +1 -1
  90. package/dist/components/Markdown/Markdown.d.ts +2 -4
  91. package/dist/components/Markdown/Markdown.d.ts.map +1 -1
  92. package/dist/components/Markdown/Markdown.js +3 -2
  93. package/dist/components/Markdown/Markdown.js.map +1 -1
  94. package/dist/components/Message/Message.d.ts.map +1 -1
  95. package/dist/components/Message/Message.js +16 -1
  96. package/dist/components/Message/Message.js.map +1 -1
  97. package/dist/components/MetricList/MetricList.d.ts +3 -3
  98. package/dist/components/MetricList/MetricList.d.ts.map +1 -1
  99. package/dist/components/MetricList/MetricList.js +5 -3
  100. package/dist/components/MetricList/MetricList.js.map +1 -1
  101. package/dist/components/Modal/Modal.d.ts +24 -5
  102. package/dist/components/Modal/Modal.d.ts.map +1 -1
  103. package/dist/components/Modal/Modal.js +16 -6
  104. package/dist/components/Modal/Modal.js.map +1 -1
  105. package/dist/components/Modal/components/ModalContext.d.ts +14 -0
  106. package/dist/components/Modal/components/ModalContext.d.ts.map +1 -1
  107. package/dist/components/Modal/components/ModalContext.js.map +1 -1
  108. package/dist/components/Modal/components/ModalHeader.d.ts.map +1 -1
  109. package/dist/components/Modal/components/ModalHeader.js +13 -2
  110. package/dist/components/Modal/components/ModalHeader.js.map +1 -1
  111. package/dist/components/NavBottom/NavBottom.d.ts +2 -1
  112. package/dist/components/NavBottom/NavBottom.d.ts.map +1 -1
  113. package/dist/components/NavBottom/NavBottom.js +6 -4
  114. package/dist/components/NavBottom/NavBottom.js.map +1 -1
  115. package/dist/components/NavSide/NavItem.js +1 -1
  116. package/dist/components/NavSide/NavItem.js.map +1 -1
  117. package/dist/components/NavSide/NavSide.d.ts +2 -1
  118. package/dist/components/NavSide/NavSide.d.ts.map +1 -1
  119. package/dist/components/NavSide/NavSide.js +5 -3
  120. package/dist/components/NavSide/NavSide.js.map +1 -1
  121. package/dist/components/Popover/Popover.d.ts.map +1 -1
  122. package/dist/components/Popover/Popover.js +7 -0
  123. package/dist/components/Popover/Popover.js.map +1 -1
  124. package/dist/components/Popover/components/PopoverBody.d.ts.map +1 -1
  125. package/dist/components/Popover/components/PopoverBody.js +5 -2
  126. package/dist/components/Popover/components/PopoverBody.js.map +1 -1
  127. package/dist/components/Popover/components/PopoverContext.d.ts +1 -0
  128. package/dist/components/Popover/components/PopoverContext.d.ts.map +1 -1
  129. package/dist/components/Popover/components/PopoverContext.js.map +1 -1
  130. package/dist/components/Popover/components/PopoverTrigger.d.ts.map +1 -1
  131. package/dist/components/Popover/components/PopoverTrigger.js +2 -2
  132. package/dist/components/Popover/components/PopoverTrigger.js.map +1 -1
  133. package/dist/components/ProgressBar/ProgressBar.d.ts +2 -1
  134. package/dist/components/ProgressBar/ProgressBar.d.ts.map +1 -1
  135. package/dist/components/ProgressBar/ProgressBar.js +5 -3
  136. package/dist/components/ProgressBar/ProgressBar.js.map +1 -1
  137. package/dist/components/Select/Select.d.ts +11 -2
  138. package/dist/components/Select/Select.d.ts.map +1 -1
  139. package/dist/components/Select/Select.js +64 -18
  140. package/dist/components/Select/Select.js.map +1 -1
  141. package/dist/components/Select/SelectDropdown.d.ts +14 -2
  142. package/dist/components/Select/SelectDropdown.d.ts.map +1 -1
  143. package/dist/components/Select/SelectDropdown.js +19 -4
  144. package/dist/components/Select/SelectDropdown.js.map +1 -1
  145. package/dist/components/Select/SelectTrigger.d.ts +10 -3
  146. package/dist/components/Select/SelectTrigger.d.ts.map +1 -1
  147. package/dist/components/Select/SelectTrigger.js +12 -3
  148. package/dist/components/Select/SelectTrigger.js.map +1 -1
  149. package/dist/components/Select/useSelectKeyboard.d.ts +15 -1
  150. package/dist/components/Select/useSelectKeyboard.d.ts.map +1 -1
  151. package/dist/components/Select/useSelectKeyboard.js +73 -65
  152. package/dist/components/Select/useSelectKeyboard.js.map +1 -1
  153. package/dist/components/Sheet/Sheet.d.ts +19 -5
  154. package/dist/components/Sheet/Sheet.d.ts.map +1 -1
  155. package/dist/components/Sheet/Sheet.js +18 -6
  156. package/dist/components/Sheet/Sheet.js.map +1 -1
  157. package/dist/components/Sheet/components/SheetContext.d.ts +10 -0
  158. package/dist/components/Sheet/components/SheetContext.d.ts.map +1 -1
  159. package/dist/components/Sheet/components/SheetContext.js.map +1 -1
  160. package/dist/components/Sheet/components/SheetHeader.d.ts.map +1 -1
  161. package/dist/components/Sheet/components/SheetHeader.js +11 -2
  162. package/dist/components/Sheet/components/SheetHeader.js.map +1 -1
  163. package/dist/components/Skeleton/Skeleton.d.ts +2 -1
  164. package/dist/components/Skeleton/Skeleton.d.ts.map +1 -1
  165. package/dist/components/Skeleton/Skeleton.js +5 -3
  166. package/dist/components/Skeleton/Skeleton.js.map +1 -1
  167. package/dist/components/Sparkline/Sparkline.d.ts +10 -2
  168. package/dist/components/Sparkline/Sparkline.d.ts.map +1 -1
  169. package/dist/components/Sparkline/Sparkline.js +5 -3
  170. package/dist/components/Sparkline/Sparkline.js.map +1 -1
  171. package/dist/components/StatsCard/StatsCard.d.ts +4 -4
  172. package/dist/components/StatsCard/StatsCard.d.ts.map +1 -1
  173. package/dist/components/StatsCard/StatsCard.js +5 -3
  174. package/dist/components/StatsCard/StatsCard.js.map +1 -1
  175. package/dist/components/Tabs/Tabs.d.ts +2 -2
  176. package/dist/components/Tabs/components/TabsList.d.ts +1 -1
  177. package/dist/components/Tabs/components/TabsList.d.ts.map +1 -1
  178. package/dist/components/Tabs/components/TabsList.js +5 -4
  179. package/dist/components/Tabs/components/TabsList.js.map +1 -1
  180. package/dist/components/Tabs/components/TabsPanels.d.ts +1 -1
  181. package/dist/components/Tabs/components/TabsPanels.d.ts.map +1 -1
  182. package/dist/components/Tabs/components/TabsPanels.js +5 -3
  183. package/dist/components/Tabs/components/TabsPanels.js.map +1 -1
  184. package/dist/components/TagChip/TagChip.d.ts +2 -1
  185. package/dist/components/TagChip/TagChip.d.ts.map +1 -1
  186. package/dist/components/TagChip/TagChip.js +5 -3
  187. package/dist/components/TagChip/TagChip.js.map +1 -1
  188. package/dist/components/TagInput/TagInput.d.ts +1 -1
  189. package/dist/components/TagInput/TagInput.d.ts.map +1 -1
  190. package/dist/components/TagInput/TagInput.js +5 -4
  191. package/dist/components/TagInput/TagInput.js.map +1 -1
  192. package/dist/components/TimePicker/TimePicker.d.ts +2 -2
  193. package/dist/components/TimePicker/TimePicker.d.ts.map +1 -1
  194. package/dist/components/TimePicker/TimePicker.js +27 -11
  195. package/dist/components/TimePicker/TimePicker.js.map +1 -1
  196. package/dist/components/TimePicker/timepicker.types.d.ts +14 -13
  197. package/dist/components/TimePicker/timepicker.types.d.ts.map +1 -1
  198. package/dist/components/TimePicker/timepicker.types.js +4 -3
  199. package/dist/components/TimePicker/timepicker.types.js.map +1 -1
  200. package/dist/date-time/index.d.ts +2 -0
  201. package/dist/date-time/index.d.ts.map +1 -1
  202. package/dist/date-time/index.js +9 -0
  203. package/dist/date-time/index.js.map +1 -1
  204. package/dist/date-time/iso.d.ts +261 -0
  205. package/dist/date-time/iso.d.ts.map +1 -0
  206. package/dist/date-time/iso.js +358 -0
  207. package/dist/date-time/iso.js.map +1 -0
  208. package/dist/date-time/now.d.ts +7 -6
  209. package/dist/date-time/now.d.ts.map +1 -1
  210. package/dist/date-time/now.js +3 -3
  211. package/dist/date-time/now.js.map +1 -1
  212. package/dist/theme/index.d.ts +4 -3
  213. package/dist/theme/index.d.ts.map +1 -1
  214. package/dist/theme/index.js +3 -2
  215. package/dist/theme/index.js.map +1 -1
  216. package/dist/theme/schema.d.ts +70 -32
  217. package/dist/theme/schema.d.ts.map +1 -1
  218. package/dist/theme/schema.js +36 -18
  219. package/dist/theme/schema.js.map +1 -1
  220. package/package.json +9 -15
  221. package/src/components/Badge/Badge.tsx +5 -4
  222. package/src/components/BarChart/BarChart.tsx +25 -30
  223. package/src/components/Calendar/Calendar.tsx +91 -401
  224. package/src/components/Calendar/CalendarView.tsx +445 -0
  225. package/src/components/Calendar/calendar.types.ts +94 -29
  226. package/src/components/Calendar/index.ts +16 -17
  227. package/src/components/ColorDot/ColorDot.tsx +6 -4
  228. package/src/components/DatePicker/DatePicker.tsx +92 -16
  229. package/src/components/DatePicker/datepicker.types.ts +38 -39
  230. package/src/components/DatePicker/index.ts +11 -10
  231. package/src/components/DateRangePicker/DateRangePicker.tsx +17 -21
  232. package/src/components/DateRangePicker/daterangepicker.types.ts +22 -22
  233. package/src/components/DateRangePicker/index.ts +9 -1
  234. package/src/components/DateTimePicker/DateTimePicker.tsx +46 -18
  235. package/src/components/DateTimePicker/datetimepicker.types.ts +22 -18
  236. package/src/components/DateTimePicker/index.ts +9 -1
  237. package/src/components/DateTimeRangePicker/DateTimeRangePicker.tsx +62 -28
  238. package/src/components/DateTimeRangePicker/datetimerangepicker.types.ts +24 -19
  239. package/src/components/DateTimeRangePicker/index.ts +9 -0
  240. package/src/components/EmptyState/EmptyState.tsx +8 -7
  241. package/src/components/Field/Field.tsx +7 -6
  242. package/src/components/LineChart/LineChart.tsx +147 -141
  243. package/src/components/Markdown/Markdown.tsx +4 -3
  244. package/src/components/Message/Message.tsx +17 -1
  245. package/src/components/MetricList/MetricList.tsx +10 -5
  246. package/src/components/Modal/Modal.tsx +119 -87
  247. package/src/components/Modal/components/ModalContext.tsx +14 -0
  248. package/src/components/Modal/components/ModalHeader.tsx +14 -2
  249. package/src/components/NavBottom/NavBottom.tsx +6 -4
  250. package/src/components/NavSide/NavItem.tsx +1 -1
  251. package/src/components/NavSide/NavSide.tsx +5 -3
  252. package/src/components/Popover/Popover.tsx +7 -0
  253. package/src/components/Popover/components/PopoverBody.tsx +6 -1
  254. package/src/components/Popover/components/PopoverContext.tsx +1 -0
  255. package/src/components/Popover/components/PopoverTrigger.tsx +2 -1
  256. package/src/components/ProgressBar/ProgressBar.tsx +32 -28
  257. package/src/components/Select/Select.tsx +76 -19
  258. package/src/components/Select/SelectDropdown.tsx +32 -3
  259. package/src/components/Select/SelectTrigger.tsx +26 -3
  260. package/src/components/Select/useSelectKeyboard.ts +20 -10
  261. package/src/components/Sheet/Sheet.tsx +108 -83
  262. package/src/components/Sheet/components/SheetContext.tsx +10 -0
  263. package/src/components/Sheet/components/SheetHeader.tsx +17 -2
  264. package/src/components/Skeleton/Skeleton.tsx +5 -3
  265. package/src/components/Sparkline/Sparkline.tsx +29 -36
  266. package/src/components/StatsCard/StatsCard.tsx +7 -6
  267. package/src/components/Tabs/components/TabsList.tsx +5 -3
  268. package/src/components/Tabs/components/TabsPanels.tsx +5 -3
  269. package/src/components/TagChip/TagChip.tsx +10 -3
  270. package/src/components/TagInput/TagInput.tsx +5 -3
  271. package/src/components/TimePicker/TimePicker.tsx +31 -13
  272. package/src/components/TimePicker/timepicker.types.ts +14 -13
  273. package/src/date-time/index.ts +61 -0
  274. package/src/date-time/iso.ts +511 -0
  275. package/src/date-time/now.ts +10 -9
  276. package/src/theme/index.ts +11 -3
  277. package/src/theme/schema.ts +82 -54
  278. package/src/theme/tokens.css +807 -0
  279. package/dist/styles.css +0 -2
  280. package/dist/theme.generated.css +0 -438
  281. package/src/styles/base.css +0 -41
  282. package/src/theme/palette.json +0 -100
  283. package/src/theme/palette.schema.json +0 -40
  284. package/src/theme/theme.json +0 -349
  285. package/src/theme/theme.schema.json +0 -876
  286. package/src/theme/typography.json +0 -7
  287. package/src/theme/typography.schema.json +0 -36
  288. 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 }: NavBottomProps) => {
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 }: NavSideProps) => {
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 = ({ completed, total, showFraction = true, label = "Progress", className }: ProgressBarProps) => {
14
- // A caller can hand us anything; the bar must not render an out-of-range
15
- // width (a negative `width` is invalid CSS) or report impossible progress.
16
- const isMeasurable = total > 0;
17
- const clamped = isMeasurable ? Math.min(Math.max(completed, 0), total) : 0;
18
- const percentage = isMeasurable ? (clamped / total) * 100 : 0;
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
- return (
21
- <div className={cn("flex items-center gap-3", className)}>
22
- <div
23
- role="progressbar"
24
- aria-label={label}
25
- aria-valuemin={0}
26
- aria-valuemax={isMeasurable ? total : undefined}
27
- // An indeterminate progressbar omits aria-valuenow; with no positive
28
- // total there is no meaningful progress to report.
29
- aria-valuenow={isMeasurable ? clamped : undefined}
30
- aria-valuetext={isMeasurable ? `${clamped} of ${total}` : undefined}
31
- className="h-2 flex-1 overflow-hidden rounded-full bg-bg-strong"
32
- >
33
- <div className="h-full rounded-full bg-fg-success transition-all" style={{ width: `${percentage}%` }} />
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
- {showFraction && (
36
- <Caption className="shrink-0">
37
- {completed}/{total}
38
- </Caption>
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
- export const Select = <T extends string = string>(props: SelectProps<T>) => {
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
- listboxRef={listboxRef}
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
- listboxRef: WritableRefObject<HTMLDivElement | null>;
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
- listboxRef,
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
- listboxRef.current = node;
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
- <div ref={setReference} className={cn("relative flex w-full items-center", className)}>
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
  />