@evinvest/uikit 0.21.0 → 0.22.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 CHANGED
@@ -253,6 +253,31 @@ through hydration. Three parts of the kit make that hold:
253
253
  opens. An item rendered by a component of yours is learnt when it mounts (on
254
254
  the first open), and the trigger updates then; pass `SelectValue` children to
255
255
  show something else outright.
256
+ - **`Select` keeps focus where a native one would.** Focus moves into the
257
+ list and back out. The list is portaled, so where it goes is the kit's job,
258
+ not the DOM order's:
259
+ - **Opening** (a click, Enter, Space or the arrows on the trigger) focuses the
260
+ chosen option, else the first, without scrolling the page; the list alone
261
+ scrolls it into view.
262
+ - **In the list**, the arrows, Home and End move between options (read from
263
+ the DOM, so groups, labels, separators and items rendered by your own
264
+ components all count); typed letters jump to the next option that starts
265
+ with them; Enter or Space chooses.
266
+ - **Closing** by a key or a choice (Escape, Tab, Enter) hands focus back to
267
+ the trigger; Tab then goes on to the next field — inside a `Dialog` or
268
+ `Drawer` it wraps within the modal as its trap does. A click elsewhere
269
+ closes it and leaves focus where the click put it: the trigger is never
270
+ focused on the way out, so its blur (a field's validation) does not fire.
271
+ - **Escape** closes the topmost overlay only: the list, not the `Dialog` or
272
+ `Drawer` around it (dismissable layers stack), and a click in the list is
273
+ not "outside" the modal that holds its trigger.
274
+ - The trigger carries `aria-haspopup="listbox"` and, while open,
275
+ `aria-controls`; the list is named by the trigger's label.
276
+
277
+ The open list carries the trigger's width as `--select-trigger-width`:
278
+ `SelectContent className="min-w-(--select-trigger-width)"` keeps it at least
279
+ as wide as its field. It posts nothing by itself; `FormSelect` in
280
+ `@evinvest/kitstart/react` pairs it with a form value.
256
281
 
257
282
  ```tsx
258
283
  <form action="/lead" method="post">
@@ -0,0 +1,74 @@
1
+ "use client";
2
+ import * as React from "react";
3
+ import { focusCandidates } from "../primitives/focus-scope.js";
4
+ const TYPEAHEAD_MS = 500;
5
+ function listOptions(list) {
6
+ return Array.from(list?.querySelectorAll("[role='option']:not([data-disabled])") ?? []);
7
+ }
8
+ function landOnChosen(list) {
9
+ const options = listOptions(list);
10
+ const target = options.find((o) => o.getAttribute("aria-selected") === "true") ?? options[0];
11
+ if (!target) return;
12
+ target.focus({ preventScroll: true });
13
+ target.scrollIntoView?.({ block: "nearest" });
14
+ }
15
+ function useListboxKeys(listRef) {
16
+ const typed = React.useRef({ text: "", at: 0 });
17
+ return React.useCallback(
18
+ (event) => {
19
+ const options = listOptions(listRef.current);
20
+ if (options.length === 0) return false;
21
+ const current = options.indexOf(document.activeElement);
22
+ const move = (index) => {
23
+ event.preventDefault();
24
+ options[(index + options.length) % options.length]?.focus();
25
+ return true;
26
+ };
27
+ switch (event.key) {
28
+ case "ArrowDown":
29
+ return move(current + 1);
30
+ case "ArrowUp":
31
+ return move(current < 0 ? -1 : current - 1);
32
+ case "Home":
33
+ return move(0);
34
+ case "End":
35
+ return move(options.length - 1);
36
+ }
37
+ if (event.key.length !== 1 || event.key === " " || event.ctrlKey || event.metaKey || event.altKey) return false;
38
+ const now = Date.now();
39
+ const t = typed.current;
40
+ t.text = now - t.at > TYPEAHEAD_MS ? event.key.toLowerCase() : t.text + event.key.toLowerCase();
41
+ t.at = now;
42
+ const repeated = t.text.split("").every((c) => c === t.text[0]);
43
+ const query = repeated ? t.text[0] : t.text;
44
+ const from = repeated ? current + 1 : Math.max(current, 0);
45
+ for (let i = 0; i < options.length; i++) {
46
+ const index = (from + i) % options.length;
47
+ if ((options[index].textContent ?? "").trim().toLowerCase().startsWith(query)) return move(index);
48
+ }
49
+ event.preventDefault();
50
+ return true;
51
+ },
52
+ [listRef]
53
+ );
54
+ }
55
+ function tabFromTrigger(event, trigger) {
56
+ if (!trigger) return;
57
+ const modal = trigger.closest("[aria-modal='true']");
58
+ if (!modal) {
59
+ trigger.focus();
60
+ return;
61
+ }
62
+ event.preventDefault();
63
+ const stops = focusCandidates(modal).filter((el) => el === trigger || el.offsetWidth > 0 || el.offsetHeight > 0);
64
+ const at = stops.indexOf(trigger);
65
+ const next = stops[(at + (event.shiftKey ? -1 : 1) + stops.length) % stops.length];
66
+ (at < 0 || !next ? trigger : next).focus();
67
+ }
68
+ export {
69
+ landOnChosen,
70
+ listOptions,
71
+ tabFromTrigger,
72
+ useListboxKeys
73
+ };
74
+ //# sourceMappingURL=select-listbox.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/components/select-listbox.ts"],"sourcesContent":["\"use client\";\n\nimport * as React from \"react\";\nimport { focusCandidates } from \"../primitives/focus-scope\";\n\n// Keyboard and focus for `SelectContent`, kept out of the component itself.\n// The options are read from the DOM, never counted from `children`: a\n// `SelectGroup`, a `SelectLabel` or an item behind a component of the\n// caller's is invisible to `Children`, and an index counted there points at\n// the wrong row.\n\nconst TYPEAHEAD_MS = 500;\n\nexport function listOptions(list: HTMLElement | null): HTMLElement[] {\n return Array.from(list?.querySelectorAll<HTMLElement>(\"[role='option']:not([data-disabled])\") ?? []);\n}\n\n/**\n * Opening lands on the chosen option (else the first), as a native select\n * does. Focus without scrolling — the page must not jump on the frame the\n * list is placed — then the list alone brings that option into view.\n */\nexport function landOnChosen(list: HTMLElement | null): void {\n const options = listOptions(list);\n const target = options.find((o) => o.getAttribute(\"aria-selected\") === \"true\") ?? options[0];\n if (!target) return;\n target.focus({ preventScroll: true });\n target.scrollIntoView?.({ block: \"nearest\" });\n}\n\n/** Arrows, Home, End and type-ahead over the options; `true` when the key was one of them. */\nexport function useListboxKeys(listRef: React.RefObject<HTMLElement | null>) {\n const typed = React.useRef({ text: \"\", at: 0 });\n return React.useCallback(\n (event: React.KeyboardEvent): boolean => {\n const options = listOptions(listRef.current);\n if (options.length === 0) return false;\n const current = options.indexOf(document.activeElement as HTMLElement);\n const move = (index: number) => {\n event.preventDefault();\n // A plain focus: the browser scrolls the list to follow the arrows.\n options[(index + options.length) % options.length]?.focus();\n return true;\n };\n switch (event.key) {\n case \"ArrowDown\":\n return move(current + 1);\n case \"ArrowUp\":\n return move(current < 0 ? -1 : current - 1);\n case \"Home\":\n return move(0);\n case \"End\":\n return move(options.length - 1);\n }\n // Space chooses (the option's own handler), as Enter does: it is not type-ahead.\n if (event.key.length !== 1 || event.key === \" \" || event.ctrlKey || event.metaKey || event.altKey) return false;\n const now = Date.now();\n const t = typed.current;\n t.text = now - t.at > TYPEAHEAD_MS ? event.key.toLowerCase() : t.text + event.key.toLowerCase();\n t.at = now;\n // One letter repeated cycles through the options that start with it.\n const repeated = t.text.split(\"\").every((c) => c === t.text[0]);\n const query = repeated ? t.text[0]! : t.text;\n const from = repeated ? current + 1 : Math.max(current, 0);\n for (let i = 0; i < options.length; i++) {\n const index = (from + i) % options.length;\n if ((options[index]!.textContent ?? \"\").trim().toLowerCase().startsWith(query)) return move(index);\n }\n event.preventDefault();\n return true;\n },\n [listRef],\n );\n}\n\n/**\n * Tab from the open list goes on from the trigger. The list is portaled to the\n * end of the document, so the browser's own Tab would leave from there: focus\n * goes back to the trigger first and the browser's Tab carries on from it.\n * Inside a modal (a Dialog, a Drawer) that is not enough — the trap listens on\n * the modal, never sees a key from the portal, and would let a Tab from its\n * last field out — so there the next field is found here, wrapping as the\n * trap would.\n */\nexport function tabFromTrigger(event: React.KeyboardEvent, trigger: HTMLElement | null): void {\n if (!trigger) return;\n const modal = trigger.closest<HTMLElement>(\"[aria-modal='true']\");\n if (!modal) {\n trigger.focus();\n return;\n }\n event.preventDefault();\n const stops = focusCandidates(modal).filter((el) => el === trigger || el.offsetWidth > 0 || el.offsetHeight > 0);\n const at = stops.indexOf(trigger);\n const next = stops[(at + (event.shiftKey ? -1 : 1) + stops.length) % stops.length];\n (at < 0 || !next ? trigger : next).focus();\n}\n"],"mappings":";AAEA,YAAY,WAAW;AACvB,SAAS,uBAAuB;AAQhC,MAAM,eAAe;AAEd,SAAS,YAAY,MAAyC;AACnE,SAAO,MAAM,KAAK,MAAM,iBAA8B,sCAAsC,KAAK,CAAC,CAAC;AACrG;AAOO,SAAS,aAAa,MAAgC;AAC3D,QAAM,UAAU,YAAY,IAAI;AAChC,QAAM,SAAS,QAAQ,KAAK,CAAC,MAAM,EAAE,aAAa,eAAe,MAAM,MAAM,KAAK,QAAQ,CAAC;AAC3F,MAAI,CAAC,OAAQ;AACb,SAAO,MAAM,EAAE,eAAe,KAAK,CAAC;AACpC,SAAO,iBAAiB,EAAE,OAAO,UAAU,CAAC;AAC9C;AAGO,SAAS,eAAe,SAA8C;AAC3E,QAAM,QAAQ,MAAM,OAAO,EAAE,MAAM,IAAI,IAAI,EAAE,CAAC;AAC9C,SAAO,MAAM;AAAA,IACX,CAAC,UAAwC;AACvC,YAAM,UAAU,YAAY,QAAQ,OAAO;AAC3C,UAAI,QAAQ,WAAW,EAAG,QAAO;AACjC,YAAM,UAAU,QAAQ,QAAQ,SAAS,aAA4B;AACrE,YAAM,OAAO,CAAC,UAAkB;AAC9B,cAAM,eAAe;AAErB,iBAAS,QAAQ,QAAQ,UAAU,QAAQ,MAAM,GAAG,MAAM;AAC1D,eAAO;AAAA,MACT;AACA,cAAQ,MAAM,KAAK;AAAA,QACjB,KAAK;AACH,iBAAO,KAAK,UAAU,CAAC;AAAA,QACzB,KAAK;AACH,iBAAO,KAAK,UAAU,IAAI,KAAK,UAAU,CAAC;AAAA,QAC5C,KAAK;AACH,iBAAO,KAAK,CAAC;AAAA,QACf,KAAK;AACH,iBAAO,KAAK,QAAQ,SAAS,CAAC;AAAA,MAClC;AAEA,UAAI,MAAM,IAAI,WAAW,KAAK,MAAM,QAAQ,OAAO,MAAM,WAAW,MAAM,WAAW,MAAM,OAAQ,QAAO;AAC1G,YAAM,MAAM,KAAK,IAAI;AACrB,YAAM,IAAI,MAAM;AAChB,QAAE,OAAO,MAAM,EAAE,KAAK,eAAe,MAAM,IAAI,YAAY,IAAI,EAAE,OAAO,MAAM,IAAI,YAAY;AAC9F,QAAE,KAAK;AAEP,YAAM,WAAW,EAAE,KAAK,MAAM,EAAE,EAAE,MAAM,CAAC,MAAM,MAAM,EAAE,KAAK,CAAC,CAAC;AAC9D,YAAM,QAAQ,WAAW,EAAE,KAAK,CAAC,IAAK,EAAE;AACxC,YAAM,OAAO,WAAW,UAAU,IAAI,KAAK,IAAI,SAAS,CAAC;AACzD,eAAS,IAAI,GAAG,IAAI,QAAQ,QAAQ,KAAK;AACvC,cAAM,SAAS,OAAO,KAAK,QAAQ;AACnC,aAAK,QAAQ,KAAK,EAAG,eAAe,IAAI,KAAK,EAAE,YAAY,EAAE,WAAW,KAAK,EAAG,QAAO,KAAK,KAAK;AAAA,MACnG;AACA,YAAM,eAAe;AACrB,aAAO;AAAA,IACT;AAAA,IACA,CAAC,OAAO;AAAA,EACV;AACF;AAWO,SAAS,eAAe,OAA4B,SAAmC;AAC5F,MAAI,CAAC,QAAS;AACd,QAAM,QAAQ,QAAQ,QAAqB,qBAAqB;AAChE,MAAI,CAAC,OAAO;AACV,YAAQ,MAAM;AACd;AAAA,EACF;AACA,QAAM,eAAe;AACrB,QAAM,QAAQ,gBAAgB,KAAK,EAAE,OAAO,CAAC,OAAO,OAAO,WAAW,GAAG,cAAc,KAAK,GAAG,eAAe,CAAC;AAC/G,QAAM,KAAK,MAAM,QAAQ,OAAO;AAChC,QAAM,OAAO,OAAO,MAAM,MAAM,WAAW,KAAK,KAAK,MAAM,UAAU,MAAM,MAAM;AACjF,GAAC,KAAK,KAAK,CAAC,OAAO,UAAU,MAAM,MAAM;AAC3C;","names":[]}
@@ -7,12 +7,12 @@ import { useControllableState } from "../primitives/use-controllable-state.js";
7
7
  import { useFloating } from "../primitives/use-floating.js";
8
8
  import { useDismissableLayer } from "../primitives/dismissable-layer.js";
9
9
  import { usePresence } from "../primitives/presence.js";
10
- import { useRovingFocus } from "../primitives/use-roving-focus.js";
11
10
  import { mergeRefs } from "../primitives/merge-refs.js";
12
11
  import { Portal } from "../primitives/portal.js";
13
12
  import { walkElements } from "../primitives/walk-elements.js";
14
13
  import { useFieldControlId } from "./field-context.js";
15
14
  import { SelectChevron } from "./select-chevron.js";
15
+ import { landOnChosen, tabFromTrigger, useListboxKeys } from "./select-listbox.js";
16
16
  const SelectContext = React.createContext(null);
17
17
  function useSelect() {
18
18
  const ctx = React.useContext(SelectContext);
@@ -39,6 +39,15 @@ function Select({
39
39
  ...onOpenChange ? { onChange: onOpenChange } : {}
40
40
  });
41
41
  const anchorRef = React.useRef(null);
42
+ const contentId = React.useId();
43
+ const restoreFocusRef = React.useRef(false);
44
+ const close = React.useCallback(
45
+ (restoreFocus) => {
46
+ restoreFocusRef.current = restoreFocus;
47
+ setOpen(false);
48
+ },
49
+ [setOpen]
50
+ );
42
51
  const declared = React.useMemo(() => collectItemLabels(children), [children]);
43
52
  const mounted = React.useRef(/* @__PURE__ */ new Map());
44
53
  const [, bump] = React.useReducer((n) => n + 1, 0);
@@ -59,7 +68,10 @@ function Select({
59
68
  setValue,
60
69
  open: isOpen,
61
70
  setOpen,
71
+ close,
72
+ restoreFocusRef,
62
73
  anchorRef,
74
+ contentId,
63
75
  labelOf,
64
76
  registerLabel
65
77
  },
@@ -90,11 +102,12 @@ function SelectTrigger({
90
102
  className,
91
103
  size = "md",
92
104
  onClick,
105
+ onKeyDown,
93
106
  children,
94
107
  id,
95
108
  ...props
96
109
  }) {
97
- const { open, setOpen, anchorRef } = useSelect();
110
+ const { open, setOpen, close, anchorRef, contentId } = useSelect();
98
111
  return /* @__PURE__ */ jsxs(
99
112
  "button",
100
113
  {
@@ -104,14 +117,25 @@ function SelectTrigger({
104
117
  "data-slot": "select-trigger",
105
118
  "data-size": size,
106
119
  "data-state": open ? "open" : "closed",
120
+ "aria-haspopup": "listbox",
107
121
  "aria-expanded": open,
122
+ "aria-controls": open ? contentId : void 0,
108
123
  ref: anchorRef,
109
124
  onClick: (e) => {
110
125
  onClick?.(e);
111
- setOpen(!open);
126
+ if (open) close(true);
127
+ else setOpen(true);
128
+ },
129
+ onKeyDown: (e) => {
130
+ onKeyDown?.(e);
131
+ if (!e.defaultPrevented && !open && (e.key === "ArrowDown" || e.key === "ArrowUp")) {
132
+ e.preventDefault();
133
+ setOpen(true);
134
+ }
112
135
  },
113
136
  className: cn(
114
- "border-input data-[placeholder]:text-ink-soft [&_svg:not([class*='text-'])]:text-ink-soft focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-accent-error/20 aria-invalid:border-accent-error flex w-fit items-center justify-between gap-2 rounded-[var(--control-radius)] border bg-transparent px-3 py-2 text-sm whitespace-nowrap shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
137
+ // The placeholder mark is on `SelectValue`'s span, not on this button.
138
+ "border-input [&_[data-placeholder]]:text-ink-soft [&_svg:not([class*='text-'])]:text-ink-soft focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-accent-error/20 aria-invalid:border-accent-error flex w-fit items-center justify-between gap-2 rounded-[var(--control-radius)] border bg-transparent px-3 py-2 text-sm whitespace-nowrap shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
115
139
  selectTriggerSizeClasses[size],
116
140
  className
117
141
  ),
@@ -145,8 +169,16 @@ function SelectContent({
145
169
  children,
146
170
  ...props
147
171
  }) {
148
- const { open, setOpen, anchorRef } = useSelect();
172
+ const { open, setOpen, close, restoreFocusRef, anchorRef, contentId } = useSelect();
149
173
  const { isPresent, ref: presRef } = usePresence(open);
174
+ const listRef = React.useRef(null);
175
+ const [name, setName] = React.useState({});
176
+ React.useLayoutEffect(() => {
177
+ const anchor = anchorRef.current;
178
+ if (!open || !(anchor instanceof HTMLElement)) return;
179
+ listRef.current?.style.setProperty("--select-trigger-width", `${anchor.offsetWidth}px`);
180
+ setName(listName(anchor));
181
+ }, [open, anchorRef]);
150
182
  const { floatingRef, style, side } = useFloating({
151
183
  anchorRef,
152
184
  open,
@@ -156,72 +188,79 @@ function SelectContent({
156
188
  });
157
189
  const dismissRef = useDismissableLayer({
158
190
  enabled: open,
159
- onDismiss: () => setOpen(false),
191
+ onDismiss: (event) => close(event.type === "keydown"),
160
192
  exclude: [anchorRef]
161
193
  });
162
- const items = React.Children.toArray(children).filter(React.isValidElement);
163
- const { activeIndex, setActiveIndex, onKeyDown } = useRovingFocus({
164
- count: items.length,
165
- orientation: "vertical"
166
- });
194
+ const onListKey = useListboxKeys(listRef);
195
+ React.useEffect(() => {
196
+ if (open) landOnChosen(listRef.current);
197
+ }, [open]);
198
+ React.useEffect(() => {
199
+ if (open || !restoreFocusRef.current) return;
200
+ restoreFocusRef.current = false;
201
+ anchorRef.current?.focus();
202
+ }, [open, anchorRef, restoreFocusRef]);
167
203
  if (!isPresent) return null;
168
204
  return /* @__PURE__ */ jsx(Portal, { children: /* @__PURE__ */ jsx(
169
205
  "div",
170
206
  {
171
207
  role: "listbox",
208
+ id: contentId,
209
+ ...name,
172
210
  "data-slot": "select-content",
173
211
  "data-state": open ? "open" : "closed",
174
212
  "data-side": side,
175
- ref: mergeRefs(floatingRef, dismissRef, presRef),
213
+ ref: mergeRefs(floatingRef, dismissRef, presRef, listRef),
176
214
  style,
177
215
  tabIndex: -1,
178
- onKeyDown,
216
+ onKeyDown: (e) => {
217
+ if (onListKey(e)) return;
218
+ if (e.key === "Escape") {
219
+ e.stopPropagation();
220
+ close(true);
221
+ } else if (e.key === "Tab") {
222
+ tabFromTrigger(e, anchorRef.current);
223
+ setOpen(false);
224
+ }
225
+ },
179
226
  className: cn(
180
227
  "bg-popover text-ink data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 relative z-50 max-h-96 min-w-[8rem] origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border border-border shadow-md",
181
228
  className
182
229
  ),
183
230
  ...props,
184
- children: /* @__PURE__ */ jsx("div", { className: "p-1", children: items.map(
185
- (child, index) => React.cloneElement(child, {
186
- __index: index,
187
- __active: index === activeIndex,
188
- __setActive: setActiveIndex
189
- })
190
- ) })
231
+ children: /* @__PURE__ */ jsx("div", { className: "p-1", children })
191
232
  }
192
233
  ) });
193
234
  }
194
- function SelectItem({
195
- className,
196
- value,
197
- textValue,
198
- children,
199
- __index = 0,
200
- __active = false,
201
- __setActive,
202
- ...props
203
- }) {
204
- const { value: selectedValue, setValue, setOpen, registerLabel } = useSelect();
235
+ function listName(trigger) {
236
+ const by = trigger.getAttribute("aria-labelledby");
237
+ if (by) return { "aria-labelledby": by };
238
+ const label = trigger.labels?.[0];
239
+ if (label?.id) return { "aria-labelledby": label.id };
240
+ const text = label?.textContent?.trim() || trigger.getAttribute("aria-label");
241
+ return text ? { "aria-label": text } : {};
242
+ }
243
+ function SelectItem({ className, value, textValue, children, ...props }) {
244
+ const { value: selectedValue, setValue, close, registerLabel } = useSelect();
205
245
  const selected = selectedValue === value;
206
246
  const label = itemLabel(value, textValue, children);
207
247
  React.useEffect(() => registerLabel(value, label), [registerLabel, value, label]);
248
+ const choose = () => {
249
+ setValue(value);
250
+ close(true);
251
+ };
208
252
  return /* @__PURE__ */ jsxs(
209
253
  "div",
210
254
  {
211
255
  role: "option",
212
256
  "data-slot": "select-item",
213
257
  "aria-selected": selected,
214
- tabIndex: __active ? 0 : -1,
215
- onFocus: () => __setActive?.(__index),
216
- onClick: () => {
217
- setValue(value);
218
- setOpen(false);
219
- },
258
+ tabIndex: -1,
259
+ onClick: choose,
220
260
  onKeyDown: (e) => {
221
261
  if (e.key === "Enter" || e.key === " ") {
222
262
  e.preventDefault();
223
- setValue(value);
224
- setOpen(false);
263
+ choose();
225
264
  }
226
265
  },
227
266
  className: cn(
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/components/select.tsx"],"sourcesContent":["\"use client\";\n\nimport * as React from \"react\";\nimport { cn } from \"../lib/cn\";\nimport { selectTriggerSizeClasses, type SelectTriggerSize } from \"../generated/select\";\nimport { useControllableState } from \"../primitives/use-controllable-state\";\nimport { useFloating } from \"../primitives/use-floating\";\nimport { useDismissableLayer } from \"../primitives/dismissable-layer\";\nimport { usePresence } from \"../primitives/presence\";\nimport { useRovingFocus } from \"../primitives/use-roving-focus\";\nimport { mergeRefs } from \"../primitives/merge-refs\";\nimport { Portal } from \"../primitives/portal\";\nimport { walkElements } from \"../primitives/walk-elements\";\nimport { useFieldControlId } from \"./field-context\";\nimport { SelectChevron } from \"./select-chevron\";\n\nexport type { SelectTriggerSize };\n\ninterface SelectContextValue {\n value: string;\n setValue: (next: string) => void;\n open: boolean;\n setOpen: (next: boolean) => void;\n anchorRef: React.RefObject<HTMLElement | null>;\n /** What `SelectValue` shows for a value: the matching `SelectItem`'s children. */\n labelOf: (value: string) => string;\n registerLabel: (value: string, label: string) => void;\n}\n\nconst SelectContext = React.createContext<SelectContextValue | null>(null);\n\nfunction useSelect(): SelectContextValue {\n const ctx = React.useContext(SelectContext);\n if (!ctx) throw new Error(\"Select parts must be used within <Select>\");\n return ctx;\n}\n\nexport interface SelectProps {\n value?: string;\n defaultValue?: string;\n onValueChange?: (value: string) => void;\n open?: boolean;\n defaultOpen?: boolean;\n onOpenChange?: (open: boolean) => void;\n children?: React.ReactNode;\n}\n\nexport function Select({\n value,\n defaultValue = \"\",\n onValueChange,\n open,\n defaultOpen = false,\n onOpenChange,\n children,\n}: SelectProps) {\n const [currentValue, setValue] = useControllableState<string>({\n ...(value !== undefined ? { value } : {}),\n defaultValue,\n ...(onValueChange ? { onChange: onValueChange } : {}),\n });\n const [isOpen, setOpen] = useControllableState<boolean>({\n ...(open !== undefined ? { value: open } : {}),\n defaultValue: defaultOpen,\n ...(onOpenChange ? { onChange: onOpenChange } : {}),\n });\n const anchorRef = React.useRef<HTMLElement | null>(null);\n // The items live in a closed popover, so they are not mounted when the\n // trigger first renders — on the server least of all. Their labels are read\n // off the element tree instead; items hidden behind a component of the\n // caller's register once they have mounted (i.e. after the first open), and\n // a label learnt that way re-renders the trigger once.\n const declared = React.useMemo(() => collectItemLabels(children), [children]);\n const mounted = React.useRef(new Map<string, string>());\n const [, bump] = React.useReducer((n: number) => n + 1, 0);\n const labelOf = (v: string) => declared.get(v) ?? mounted.current.get(v) ?? v;\n const registerLabel = React.useCallback(\n (v: string, label: string) => {\n if (mounted.current.get(v) === label) return;\n mounted.current.set(v, label);\n if (!declared.has(v)) bump();\n },\n [declared],\n );\n return (\n <SelectContext.Provider\n value={{\n value: currentValue,\n setValue,\n open: isOpen,\n setOpen,\n anchorRef,\n labelOf,\n registerLabel,\n }}\n >\n {children}\n </SelectContext.Provider>\n );\n}\n\nfunction collectItemLabels(node: React.ReactNode): Map<string, string> {\n const labels = new Map<string, string>();\n walkElements(node, (element) => {\n if (element.type !== SelectItem) return true;\n const { value, textValue, children } = element.props as Partial<SelectItemProps>;\n if (typeof value === \"string\") labels.set(value, itemLabel(value, textValue, children));\n return false;\n });\n return labels;\n}\n\n/**\n * What the trigger shows for an item: its `textValue`, else the text of its\n * children. Text, not the children themselves — an item's markup rendered a\n * second time in the trigger would duplicate any id inside it.\n */\nfunction itemLabel(value: string, textValue: string | undefined, children: unknown): string {\n return textValue ?? (textOf(children).trim() || value);\n}\n\nfunction textOf(node: unknown): string {\n if (typeof node === \"string\" || typeof node === \"number\") return String(node);\n if (Array.isArray(node)) return node.map(textOf).join(\"\");\n if (React.isValidElement<{ children?: unknown }>(node)) return textOf(node.props.children);\n return \"\";\n}\n\nexport interface SelectTriggerProps extends React.ComponentProps<\"button\"> {\n size?: SelectTriggerSize;\n}\n\nexport function SelectTrigger({\n className,\n size = \"md\",\n onClick,\n children,\n id,\n ...props\n}: SelectTriggerProps) {\n const { open, setOpen, anchorRef } = useSelect();\n return (\n <button\n type=\"button\"\n role=\"combobox\"\n id={useFieldControlId(id)}\n data-slot=\"select-trigger\"\n data-size={size}\n data-state={open ? \"open\" : \"closed\"}\n aria-expanded={open}\n ref={anchorRef as React.Ref<HTMLButtonElement>}\n onClick={(e) => {\n onClick?.(e);\n setOpen(!open);\n }}\n className={cn(\n \"border-input data-[placeholder]:text-ink-soft [&_svg:not([class*='text-'])]:text-ink-soft focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-accent-error/20 aria-invalid:border-accent-error flex w-fit items-center justify-between gap-2 rounded-[var(--control-radius)] border bg-transparent px-3 py-2 text-sm whitespace-nowrap shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4\",\n selectTriggerSizeClasses[size],\n className,\n )}\n {...(props as Record<string, unknown>)}\n >\n {children}\n <SelectChevron className=\"size-4 opacity-50\" />\n </button>\n );\n}\n\nexport interface SelectValueProps extends React.ComponentProps<\"span\"> {\n placeholder?: string;\n}\n\n/**\n * The chosen option's label — the `textValue` of the `SelectItem` whose `value`\n * matches, else the text of its children — or `placeholder` while nothing is\n * chosen. `children`, when given,\n * replace the label outright.\n */\nexport function SelectValue({ className, placeholder, children, ...props }: SelectValueProps) {\n const { value, labelOf } = useSelect();\n const isEmpty = value === \"\";\n return (\n <span\n data-slot=\"select-value\"\n {...(isEmpty ? { \"data-placeholder\": \"true\" } : {})}\n className={className}\n {...(props as Record<string, unknown>)}\n >\n {isEmpty ? placeholder : (children ?? labelOf(value))}\n </span>\n );\n}\n\nexport interface SelectContentProps extends React.ComponentProps<\"div\"> {\n side?: \"top\" | \"right\" | \"bottom\" | \"left\";\n align?: \"start\" | \"center\" | \"end\";\n sideOffset?: number;\n}\n\nexport function SelectContent({\n className,\n side: sideProp = \"bottom\",\n align = \"start\",\n sideOffset = 4,\n children,\n ...props\n}: SelectContentProps) {\n const { open, setOpen, anchorRef } = useSelect();\n const { isPresent, ref: presRef } = usePresence(open);\n const { floatingRef, style, side } = useFloating({\n anchorRef,\n open,\n side: sideProp,\n align,\n offset: sideOffset,\n });\n const dismissRef = useDismissableLayer({\n enabled: open,\n onDismiss: () => setOpen(false),\n exclude: [anchorRef],\n });\n const items = React.Children.toArray(children).filter(React.isValidElement);\n const { activeIndex, setActiveIndex, onKeyDown } = useRovingFocus({\n count: items.length,\n orientation: \"vertical\",\n });\n if (!isPresent) return null;\n return (\n <Portal>\n <div\n role=\"listbox\"\n data-slot=\"select-content\"\n data-state={open ? \"open\" : \"closed\"}\n data-side={side}\n ref={mergeRefs(floatingRef, dismissRef, presRef)}\n style={style}\n tabIndex={-1}\n onKeyDown={onKeyDown}\n className={cn(\n \"bg-popover text-ink data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 relative z-50 max-h-96 min-w-[8rem] origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border border-border shadow-md\",\n className,\n )}\n {...(props as Record<string, unknown>)}\n >\n <div className=\"p-1\">\n {items.map((child, index) =>\n React.cloneElement(child as React.ReactElement<SelectItemContext>, {\n __index: index,\n __active: index === activeIndex,\n __setActive: setActiveIndex,\n }),\n )}\n </div>\n </div>\n </Portal>\n );\n}\n\ninterface SelectItemContext {\n __index?: number;\n __active?: boolean;\n __setActive?: (i: number) => void;\n}\n\nexport interface SelectItemProps\n extends React.ComponentProps<\"div\">,\n SelectItemContext {\n value: string;\n /** What the trigger shows once this item is chosen; defaults to the text of `children`. */\n textValue?: string;\n}\n\nexport function SelectItem({\n className,\n value,\n textValue,\n children,\n __index = 0,\n __active = false,\n __setActive,\n ...props\n}: SelectItemProps) {\n const { value: selectedValue, setValue, setOpen, registerLabel } = useSelect();\n const selected = selectedValue === value;\n const label = itemLabel(value, textValue, children);\n React.useEffect(() => registerLabel(value, label), [registerLabel, value, label]);\n return (\n <div\n role=\"option\"\n data-slot=\"select-item\"\n aria-selected={selected}\n tabIndex={__active ? 0 : -1}\n onFocus={() => __setActive?.(__index)}\n onClick={() => {\n setValue(value);\n setOpen(false);\n }}\n onKeyDown={(e) => {\n if (e.key === \"Enter\" || e.key === \" \") {\n e.preventDefault();\n setValue(value);\n setOpen(false);\n }\n }}\n className={cn(\n \"focus:bg-hover focus:text-ink [&_svg:not([class*='text-'])]:text-ink-soft relative flex w-full cursor-default items-center gap-2 rounded-sm py-1.5 pr-8 pl-2 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2\",\n className,\n )}\n {...(props as Record<string, unknown>)}\n >\n {selected ? (\n <span className=\"absolute right-2 flex size-3.5 items-center justify-center\">\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n width=\"24\"\n height=\"24\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth=\"2\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n className=\"size-4\"\n aria-hidden=\"true\"\n >\n <path d=\"M20 6 9 17l-5-5\" />\n </svg>\n </span>\n ) : null}\n <span>{children}</span>\n </div>\n );\n}\n\nexport function SelectGroup({ className, ...props }: React.ComponentProps<\"div\">) {\n return <div role=\"group\" data-slot=\"select-group\" className={className} {...props} />;\n}\n\nexport function SelectLabel({ className, ...props }: React.ComponentProps<\"div\">) {\n return (\n <div\n data-slot=\"select-label\"\n className={cn(\"text-ink-soft px-2 py-1.5 text-xs\", className)}\n {...props}\n />\n );\n}\n\nexport function SelectSeparator({ className, ...props }: React.ComponentProps<\"div\">) {\n return (\n <div\n data-slot=\"select-separator\"\n className={cn(\"bg-border pointer-events-none -mx-1 my-1 h-px\", className)}\n {...props}\n />\n );\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/components/select.tsx"],"sourcesContent":["\"use client\";\n\nimport * as React from \"react\";\nimport { cn } from \"../lib/cn\";\nimport { selectTriggerSizeClasses, type SelectTriggerSize } from \"../generated/select\";\nimport { useControllableState } from \"../primitives/use-controllable-state\";\nimport { useFloating } from \"../primitives/use-floating\";\nimport { useDismissableLayer } from \"../primitives/dismissable-layer\";\nimport { usePresence } from \"../primitives/presence\";\nimport { mergeRefs } from \"../primitives/merge-refs\";\nimport { Portal } from \"../primitives/portal\";\nimport { walkElements } from \"../primitives/walk-elements\";\nimport { useFieldControlId } from \"./field-context\";\nimport { SelectChevron } from \"./select-chevron\";\nimport { landOnChosen, tabFromTrigger, useListboxKeys } from \"./select-listbox\";\n\nexport type { SelectTriggerSize };\n\ninterface SelectContextValue {\n value: string;\n setValue: (next: string) => void;\n open: boolean;\n setOpen: (next: boolean) => void;\n /**\n * Closes the list. `restoreFocus`: hand focus back to the trigger — after a\n * key or a choice, never after a click elsewhere, where it would land on the\n * trigger for a moment and fire its blur (a field's validation) on the way out.\n */\n close: (restoreFocus: boolean) => void;\n /** Set by `close(true)`, read once the list is gone. */\n restoreFocusRef: React.RefObject<boolean>;\n anchorRef: React.RefObject<HTMLElement | null>;\n /** The listbox's id, for the trigger's `aria-controls`. */\n contentId: string;\n /** What `SelectValue` shows for a value: the matching `SelectItem`'s children. */\n labelOf: (value: string) => string;\n registerLabel: (value: string, label: string) => void;\n}\n\nconst SelectContext = React.createContext<SelectContextValue | null>(null);\n\nfunction useSelect(): SelectContextValue {\n const ctx = React.useContext(SelectContext);\n if (!ctx) throw new Error(\"Select parts must be used within <Select>\");\n return ctx;\n}\n\nexport interface SelectProps {\n value?: string;\n defaultValue?: string;\n onValueChange?: (value: string) => void;\n open?: boolean;\n defaultOpen?: boolean;\n onOpenChange?: (open: boolean) => void;\n children?: React.ReactNode;\n}\n\nexport function Select({\n value,\n defaultValue = \"\",\n onValueChange,\n open,\n defaultOpen = false,\n onOpenChange,\n children,\n}: SelectProps) {\n const [currentValue, setValue] = useControllableState<string>({\n ...(value !== undefined ? { value } : {}),\n defaultValue,\n ...(onValueChange ? { onChange: onValueChange } : {}),\n });\n const [isOpen, setOpen] = useControllableState<boolean>({\n ...(open !== undefined ? { value: open } : {}),\n defaultValue: defaultOpen,\n ...(onOpenChange ? { onChange: onOpenChange } : {}),\n });\n const anchorRef = React.useRef<HTMLElement | null>(null);\n const contentId = React.useId();\n const restoreFocusRef = React.useRef(false);\n const close = React.useCallback(\n (restoreFocus: boolean) => {\n restoreFocusRef.current = restoreFocus;\n setOpen(false);\n },\n [setOpen],\n );\n // The items live in a closed popover, so they are not mounted when the\n // trigger first renders — on the server least of all. Their labels are read\n // off the element tree instead; items hidden behind a component of the\n // caller's register once they have mounted (i.e. after the first open), and\n // a label learnt that way re-renders the trigger once.\n const declared = React.useMemo(() => collectItemLabels(children), [children]);\n const mounted = React.useRef(new Map<string, string>());\n const [, bump] = React.useReducer((n: number) => n + 1, 0);\n const labelOf = (v: string) => declared.get(v) ?? mounted.current.get(v) ?? v;\n const registerLabel = React.useCallback(\n (v: string, label: string) => {\n if (mounted.current.get(v) === label) return;\n mounted.current.set(v, label);\n if (!declared.has(v)) bump();\n },\n [declared],\n );\n return (\n <SelectContext.Provider\n value={{\n value: currentValue,\n setValue,\n open: isOpen,\n setOpen,\n close,\n restoreFocusRef,\n anchorRef,\n contentId,\n labelOf,\n registerLabel,\n }}\n >\n {children}\n </SelectContext.Provider>\n );\n}\n\nfunction collectItemLabels(node: React.ReactNode): Map<string, string> {\n const labels = new Map<string, string>();\n walkElements(node, (element) => {\n if (element.type !== SelectItem) return true;\n const { value, textValue, children } = element.props as Partial<SelectItemProps>;\n if (typeof value === \"string\") labels.set(value, itemLabel(value, textValue, children));\n return false;\n });\n return labels;\n}\n\n/**\n * What the trigger shows for an item: its `textValue`, else the text of its\n * children. Text, not the children themselves — an item's markup rendered a\n * second time in the trigger would duplicate any id inside it.\n */\nfunction itemLabel(value: string, textValue: string | undefined, children: unknown): string {\n return textValue ?? (textOf(children).trim() || value);\n}\n\nfunction textOf(node: unknown): string {\n if (typeof node === \"string\" || typeof node === \"number\") return String(node);\n if (Array.isArray(node)) return node.map(textOf).join(\"\");\n if (React.isValidElement<{ children?: unknown }>(node)) return textOf(node.props.children);\n return \"\";\n}\n\nexport interface SelectTriggerProps extends React.ComponentProps<\"button\"> {\n size?: SelectTriggerSize;\n}\n\nexport function SelectTrigger({\n className,\n size = \"md\",\n onClick,\n onKeyDown,\n children,\n id,\n ...props\n}: SelectTriggerProps) {\n const { open, setOpen, close, anchorRef, contentId } = useSelect();\n return (\n <button\n type=\"button\"\n role=\"combobox\"\n id={useFieldControlId(id)}\n data-slot=\"select-trigger\"\n data-size={size}\n data-state={open ? \"open\" : \"closed\"}\n aria-haspopup=\"listbox\"\n aria-expanded={open}\n aria-controls={open ? contentId : undefined}\n ref={anchorRef as React.Ref<HTMLButtonElement>}\n onClick={(e) => {\n onClick?.(e);\n if (open) close(true);\n else setOpen(true);\n }}\n onKeyDown={(e) => {\n onKeyDown?.(e);\n // A native select opens on the arrows too; Enter and Space already click the button.\n if (!e.defaultPrevented && !open && (e.key === \"ArrowDown\" || e.key === \"ArrowUp\")) {\n e.preventDefault();\n setOpen(true);\n }\n }}\n className={cn(\n // The placeholder mark is on `SelectValue`'s span, not on this button.\n \"border-input [&_[data-placeholder]]:text-ink-soft [&_svg:not([class*='text-'])]:text-ink-soft focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-accent-error/20 aria-invalid:border-accent-error flex w-fit items-center justify-between gap-2 rounded-[var(--control-radius)] border bg-transparent px-3 py-2 text-sm whitespace-nowrap shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4\",\n selectTriggerSizeClasses[size],\n className,\n )}\n {...(props as Record<string, unknown>)}\n >\n {children}\n <SelectChevron className=\"size-4 opacity-50\" />\n </button>\n );\n}\n\nexport interface SelectValueProps extends React.ComponentProps<\"span\"> {\n placeholder?: string;\n}\n\n/**\n * The chosen option's label — the `textValue` of the `SelectItem` whose `value`\n * matches, else the text of its children — or `placeholder` while nothing is\n * chosen. `children`, when given,\n * replace the label outright.\n */\nexport function SelectValue({ className, placeholder, children, ...props }: SelectValueProps) {\n const { value, labelOf } = useSelect();\n const isEmpty = value === \"\";\n return (\n <span\n data-slot=\"select-value\"\n {...(isEmpty ? { \"data-placeholder\": \"true\" } : {})}\n className={className}\n {...(props as Record<string, unknown>)}\n >\n {isEmpty ? placeholder : (children ?? labelOf(value))}\n </span>\n );\n}\n\nexport interface SelectContentProps extends React.ComponentProps<\"div\"> {\n side?: \"top\" | \"right\" | \"bottom\" | \"left\";\n align?: \"start\" | \"center\" | \"end\";\n sideOffset?: number;\n}\n\nexport function SelectContent({\n className,\n side: sideProp = \"bottom\",\n align = \"start\",\n sideOffset = 4,\n children,\n ...props\n}: SelectContentProps) {\n const { open, setOpen, close, restoreFocusRef, anchorRef, contentId } = useSelect();\n const { isPresent, ref: presRef } = usePresence(open);\n const listRef = React.useRef<HTMLDivElement | null>(null);\n const [name, setName] = React.useState<{ \"aria-labelledby\"?: string; \"aria-label\"?: string }>({});\n // Before placement measures the list: the trigger's width as\n // `--select-trigger-width` (for `min-w-(--select-trigger-width)`), written to\n // the element like the floating offset, and the list's name — the trigger's\n // label, as the combobox is named.\n React.useLayoutEffect(() => {\n const anchor = anchorRef.current;\n if (!open || !(anchor instanceof HTMLElement)) return;\n listRef.current?.style.setProperty(\"--select-trigger-width\", `${anchor.offsetWidth}px`);\n setName(listName(anchor));\n }, [open, anchorRef]);\n const { floatingRef, style, side } = useFloating({\n anchorRef,\n open,\n side: sideProp,\n align,\n offset: sideOffset,\n });\n const dismissRef = useDismissableLayer({\n enabled: open,\n onDismiss: (event) => close(event.type === \"keydown\"),\n exclude: [anchorRef],\n });\n const onListKey = useListboxKeys(listRef);\n React.useEffect(() => {\n if (open) landOnChosen(listRef.current);\n }, [open]);\n React.useEffect(() => {\n if (open || !restoreFocusRef.current) return;\n restoreFocusRef.current = false;\n anchorRef.current?.focus();\n }, [open, anchorRef, restoreFocusRef]);\n if (!isPresent) return null;\n return (\n <Portal>\n <div\n role=\"listbox\"\n id={contentId}\n {...name}\n data-slot=\"select-content\"\n data-state={open ? \"open\" : \"closed\"}\n data-side={side}\n ref={mergeRefs(floatingRef, dismissRef, presRef, listRef)}\n style={style}\n tabIndex={-1}\n onKeyDown={(e) => {\n if (onListKey(e)) return;\n if (e.key === \"Escape\") {\n // The list's Escape is the list's: a Drawer or Dialog around it\n // hears the key through React's tree, portal or not.\n e.stopPropagation();\n close(true);\n } else if (e.key === \"Tab\") {\n tabFromTrigger(e, anchorRef.current);\n setOpen(false);\n }\n }}\n className={cn(\n \"bg-popover text-ink data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 relative z-50 max-h-96 min-w-[8rem] origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border border-border shadow-md\",\n className,\n )}\n {...(props as Record<string, unknown>)}\n >\n <div className=\"p-1\">{children}</div>\n </div>\n </Portal>\n );\n}\n\n/** The trigger's own name for the list: its `aria-labelledby`, else its `<label>`. */\nfunction listName(trigger: HTMLElement): { \"aria-labelledby\"?: string; \"aria-label\"?: string } {\n const by = trigger.getAttribute(\"aria-labelledby\");\n if (by) return { \"aria-labelledby\": by };\n const label = (trigger as HTMLButtonElement).labels?.[0];\n if (label?.id) return { \"aria-labelledby\": label.id };\n const text = label?.textContent?.trim() || trigger.getAttribute(\"aria-label\");\n return text ? { \"aria-label\": text } : {};\n}\n\nexport interface SelectItemProps extends React.ComponentProps<\"div\"> {\n value: string;\n /** What the trigger shows once this item is chosen; defaults to the text of `children`. */\n textValue?: string;\n}\n\nexport function SelectItem({ className, value, textValue, children, ...props }: SelectItemProps) {\n const { value: selectedValue, setValue, close, registerLabel } = useSelect();\n const selected = selectedValue === value;\n const label = itemLabel(value, textValue, children);\n React.useEffect(() => registerLabel(value, label), [registerLabel, value, label]);\n const choose = () => {\n setValue(value);\n close(true);\n };\n return (\n <div\n role=\"option\"\n data-slot=\"select-item\"\n aria-selected={selected}\n tabIndex={-1}\n onClick={choose}\n onKeyDown={(e) => {\n if (e.key === \"Enter\" || e.key === \" \") {\n e.preventDefault();\n choose();\n }\n }}\n className={cn(\n \"focus:bg-hover focus:text-ink [&_svg:not([class*='text-'])]:text-ink-soft relative flex w-full cursor-default items-center gap-2 rounded-sm py-1.5 pr-8 pl-2 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2\",\n className,\n )}\n {...(props as Record<string, unknown>)}\n >\n {selected ? (\n <span className=\"absolute right-2 flex size-3.5 items-center justify-center\">\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n width=\"24\"\n height=\"24\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth=\"2\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n className=\"size-4\"\n aria-hidden=\"true\"\n >\n <path d=\"M20 6 9 17l-5-5\" />\n </svg>\n </span>\n ) : null}\n <span>{children}</span>\n </div>\n );\n}\n\nexport function SelectGroup({ className, ...props }: React.ComponentProps<\"div\">) {\n return <div role=\"group\" data-slot=\"select-group\" className={className} {...props} />;\n}\n\nexport function SelectLabel({ className, ...props }: React.ComponentProps<\"div\">) {\n return (\n <div\n data-slot=\"select-label\"\n className={cn(\"text-ink-soft px-2 py-1.5 text-xs\", className)}\n {...props}\n />\n );\n}\n\nexport function SelectSeparator({ className, ...props }: React.ComponentProps<\"div\">) {\n return (\n <div\n data-slot=\"select-separator\"\n className={cn(\"bg-border pointer-events-none -mx-1 my-1 h-px\", className)}\n {...props}\n />\n );\n}\n"],"mappings":"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","names":[]}
package/dist/index.d.ts CHANGED
@@ -71,16 +71,27 @@ interface PortalProviderProps {
71
71
  declare function PortalProvider({ container, children }: PortalProviderProps): React.JSX.Element;
72
72
  declare function Portal({ children, container }: PortalProps): React.ReactPortal | null;
73
73
 
74
+ /** Why a layer is being dismissed: the event itself, for a caller that treats keys and pointers apart. */
75
+ type DismissEvent = PointerEvent | KeyboardEvent;
74
76
  /**
75
77
  * Calls `onDismiss` on Escape or on a pointer-down outside the layer — the
76
78
  * dep-light core of every overlay's "click away to close". Pass refs to any
77
79
  * nodes that should NOT count as outside (e.g. the trigger), via `exclude`.
78
80
  *
81
+ * Layers stack, in the order they opened: Escape dismisses only the topmost
82
+ * layer on this stack, and a pointer-down inside a layer opened above this one
83
+ * is not outside it — so a Select's list inside a Dialog closes the list, not
84
+ * the Dialog. The stack knows only the overlays built on this hook: a
85
+ * `Drawer` or `CommandDialog` (Escape in their own React `onKeyDown`) is not
86
+ * on it, and a menu that also closes on Escape in its React `onKeyDown` lets
87
+ * the key bubble through React's tree to what holds it. Moving those onto the
88
+ * stack is a follow-up.
89
+ *
79
90
  * Mirrors the dismiss behaviour Rust expresses with a full-screen backdrop.
80
91
  */
81
92
  declare function useDismissableLayer(opts: {
82
93
  enabled: boolean;
83
- onDismiss: () => void;
94
+ onDismiss: (event: DismissEvent) => void;
84
95
  exclude?: Array<React.RefObject<Element | null>>;
85
96
  }): React.RefObject<HTMLDivElement | null>;
86
97
 
@@ -1401,7 +1412,7 @@ declare function Select({ value, defaultValue, onValueChange, open, defaultOpen,
1401
1412
  interface SelectTriggerProps extends React.ComponentProps<"button"> {
1402
1413
  size?: SelectTriggerSize;
1403
1414
  }
1404
- declare function SelectTrigger({ className, size, onClick, children, id, ...props }: SelectTriggerProps): React.JSX.Element;
1415
+ declare function SelectTrigger({ className, size, onClick, onKeyDown, children, id, ...props }: SelectTriggerProps): React.JSX.Element;
1405
1416
  interface SelectValueProps extends React.ComponentProps<"span"> {
1406
1417
  placeholder?: string;
1407
1418
  }
@@ -1418,17 +1429,12 @@ interface SelectContentProps extends React.ComponentProps<"div"> {
1418
1429
  sideOffset?: number;
1419
1430
  }
1420
1431
  declare function SelectContent({ className, side: sideProp, align, sideOffset, children, ...props }: SelectContentProps): React.JSX.Element | null;
1421
- interface SelectItemContext {
1422
- __index?: number;
1423
- __active?: boolean;
1424
- __setActive?: (i: number) => void;
1425
- }
1426
- interface SelectItemProps extends React.ComponentProps<"div">, SelectItemContext {
1432
+ interface SelectItemProps extends React.ComponentProps<"div"> {
1427
1433
  value: string;
1428
1434
  /** What the trigger shows once this item is chosen; defaults to the text of `children`. */
1429
1435
  textValue?: string;
1430
1436
  }
1431
- declare function SelectItem({ className, value, textValue, children, __index, __active, __setActive, ...props }: SelectItemProps): React.JSX.Element;
1437
+ declare function SelectItem({ className, value, textValue, children, ...props }: SelectItemProps): React.JSX.Element;
1432
1438
  declare function SelectGroup({ className, ...props }: React.ComponentProps<"div">): React.JSX.Element;
1433
1439
  declare function SelectLabel({ className, ...props }: React.ComponentProps<"div">): React.JSX.Element;
1434
1440
  declare function SelectSeparator({ className, ...props }: React.ComponentProps<"div">): React.JSX.Element;
@@ -1855,4 +1861,4 @@ declare function OrderFormSubmit({ side, className, ...props }: OrderFormSubmitP
1855
1861
  /** The open-orders pane with nothing in it. */
1856
1862
  declare function OpenOrdersEmpty({ className, ...props }: React.ComponentProps<"div">): React.JSX.Element;
1857
1863
 
1858
- export { type Accent, Accordion, AccordionContent, AccordionItem, type AccordionItemProps, type AccordionProps, AccordionTrigger, Alert, AlertDescription, AlertDialog, AlertDialogAction, type AlertDialogActionProps, AlertDialogCancel, type AlertDialogCancelProps, AlertDialogContent, type AlertDialogContentProps, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogOverlay, type AlertDialogOverlayProps, type AlertDialogProps, AlertDialogTitle, AlertDialogTrigger, type AlertDialogTriggerProps, type AlertProps, AlertTitle, type AlertVariant, type Align, Avatar, AvatarFallback, AvatarImage, Badge, type BadgeProps, type BadgeVariant, type BookSide, Breadcrumb, BreadcrumbEllipsis, BreadcrumbItem, BreadcrumbLink, type BreadcrumbLinkProps, BreadcrumbList, BreadcrumbPage, BreadcrumbSeparator, Button, ButtonGroup, type ButtonGroupOrientation, type ButtonGroupProps, ButtonGroupSeparator, ButtonGroupText, type ButtonGroupTextProps, type ButtonProps, type ButtonSize, type ButtonVariant, type ButtonVariantsOptions, Calendar, type CalendarProps, Card, CardAction, CardContent, CardDescription, CardFooter, CardHeader, CardTitle, Carousel, CarouselContent, CarouselEdgeFade, CarouselItem, CarouselNext, CarouselPrevious, type CarouselProps, type ChartConfig, ChartContainer, type ChartItem, ChartLegend, ChartLegendContent, ChartStyle, ChartTooltip, ChartTooltipContent, Check, Checkbox, type CheckboxProps, Collapsible, CollapsibleContent, type CollapsibleProps, CollapsibleTrigger, Command, CommandDialog, type CommandDialogProps, CommandEmpty, CommandGroup, type CommandGroupProps, CommandInput, CommandItem, type CommandItemProps, CommandList, type CommandProps, CommandSeparator, CommandShortcut, Container, type ContainerProps, ContextMenu, ContextMenuCheckboxItem, type ContextMenuCheckboxItemProps, ContextMenuContent, ContextMenuGroup, ContextMenuItem, type ContextMenuItemProps, ContextMenuLabel, type ContextMenuLabelProps, ContextMenuPortal, type ContextMenuProps, ContextMenuRadioGroup, type ContextMenuRadioGroupProps, ContextMenuRadioItem, type ContextMenuRadioItemProps, ContextMenuSeparator, ContextMenuShortcut, ContextMenuSub, ContextMenuSubContent, type ContextMenuSubProps, ContextMenuSubTrigger, type ContextMenuSubTriggerProps, ContextMenuTrigger, type ContextMenuTriggerProps, DateTimePicker, type DateTimePickerLabels, type DateTimePickerProps, Dialog, DialogClose, type DialogCloseProps, DialogContent, type DialogContentProps, DialogDescription, DialogFooter, DialogHeader, DialogOverlay, type DialogOverlayProps, DialogPortal, type DialogPortalProps, type DialogProps, DialogTitle, DialogTrigger, type DialogTriggerProps, Display, Drawer, DrawerClose, type DrawerCloseProps, DrawerContent, DrawerDescription, type DrawerDirection, DrawerFooter, DrawerHeader, DrawerOverlay, type DrawerProps, DrawerTitle, DrawerTrigger, type DrawerTriggerProps, DropdownMenu, DropdownMenuCheckboxItem, type DropdownMenuCheckboxItemProps, DropdownMenuContent, type DropdownMenuContentProps, DropdownMenuGroup, DropdownMenuItem, type DropdownMenuItemProps, DropdownMenuLabel, type DropdownMenuLabelProps, DropdownMenuPortal, type DropdownMenuProps, DropdownMenuRadioGroup, type DropdownMenuRadioGroupProps, DropdownMenuRadioItem, type DropdownMenuRadioItemProps, DropdownMenuSeparator, DropdownMenuShortcut, DropdownMenuSub, DropdownMenuSubContent, type DropdownMenuSubProps, DropdownMenuSubTrigger, type DropdownMenuSubTriggerProps, DropdownMenuTrigger, type DropdownMenuTriggerProps, Empty, EmptyContent, EmptyDescription, EmptyHeader, EmptyMedia, type EmptyMediaProps, type EmptyMediaVariant, EmptyTitle, Eyebrow, Field, FieldContent, FieldDescription, FieldError, FieldGroup, FieldLabel, FieldLegend, type FieldOrientation, FieldSeparator, FieldSet, FieldTitle, type FloatingResult, Footer, type FooterLink, type FooterLinkGroup, type FooterOffice, type FooterProps, Forbidden, Form, FormControl, FormDescription, FormItem, FormLabel, FormMessage, HoverCard, HoverCardContent, type HoverCardContentProps, type HoverCardProps, HoverCardTrigger, type HoverCardTriggerProps, type HoverIntentHandlers, InfoTip, InfoTipContent, type InfoTipContentProps, type InfoTipProps, InfoTipTrigger, type InfoTipTriggerProps, Input, InputGroup, InputGroupAddon, type InputGroupAddonAlign, type InputGroupAddonProps, InputGroupButton, type InputGroupButtonProps, type InputGroupButtonSize, InputGroupInput, InputGroupText, InputGroupTextarea, InputOTP, InputOTPGroup, type InputOTPProps, InputOTPSeparator, InputOTPSlot, type InputOTPSlotProps, type InputProps, type InputSize, Item, ItemActions, ItemContent, ItemDescription, ItemFooter, ItemGroup, ItemHeader, ItemMedia, type ItemMediaProps, type ItemMediaVariant, type ItemProps, ItemSeparator, type ItemSize, ItemTitle, type ItemVariant, Kbd, KbdGroup, Label, Logo, type LogoProps, Menubar, MenubarCheckboxItem, MenubarContent, MenubarGroup, MenubarItem, MenubarLabel, MenubarMenu, type MenubarMenuProps, MenubarRadioGroup, type MenubarRadioGroupProps, MenubarRadioItem, MenubarSeparator, MenubarShortcut, MenubarSub, MenubarSubContent, type MenubarSubProps, MenubarSubTrigger, MenubarTrigger, NativeSelect, NativeSelectGroup, NativeSelectOption, type NativeSelectProps, type NativeSelectSize, NavigationMenu, NavigationMenuContent, NavigationMenuIndicator, NavigationMenuItem, type NavigationMenuItemProps, NavigationMenuLink, NavigationMenuList, NavigationMenuTrigger, NavigationMenuViewport, NotFound, OpenOrdersEmpty, OrderBook, OrderBookHead, type OrderBookHeadProps, OrderBookRow, type OrderBookRowProps, OrderBookSpread, OrderForm, OrderFormRow, type OrderFormRowProps, OrderFormSubmit, type OrderFormSubmitProps, type OrderSide, Pagination, PaginationContent, PaginationEllipsis, PaginationItem, PaginationLink, type PaginationLinkProps, PaginationNext, PaginationPrevious, type Polarity, Popover, PopoverAnchor, type PopoverAnchorProps, PopoverContent, type PopoverContentProps, type PopoverProps, PopoverTrigger, type PopoverTriggerProps, Portal, type PortalProps, PortalProvider, type PortalProviderProps, Progress, type ProgressProps, Prose, RadioGroup, RadioGroupItem, type RadioGroupItemProps, type RadioGroupProps, type ResizableDirection, ResizableHandle, type ResizableHandleProps, ResizablePanel, ResizablePanelGroup, type ResizablePanelGroupProps, type ResizablePanelProps, ScrollArea, ScrollBar, type ScrollBarOrientation, type ScrollBarProps, Section, SectionDescriptor, type SectionDescriptorProps, SectionHead, type SectionHeadProps, type SectionProps, Select, SelectContent, type SelectContentProps, SelectGroup, SelectItem, type SelectItemProps, SelectLabel, type SelectProps, SelectSeparator, SelectTrigger, type SelectTriggerProps, type SelectTriggerSize, SelectValue, type SelectValueProps, Separator, type SeparatorOrientation, type SeparatorProps, ServerError, type ServerErrorProps, Sheet, SheetClose, type SheetCloseProps, SheetContent, type SheetContentProps, SheetDescription, SheetFooter, SheetHeader, SheetOverlay, type SheetOverlayProps, type SheetProps, SheetTitle, SheetTrigger, type SheetTriggerProps, type Side, Sidebar, SidebarContent, SidebarFooter, SidebarGroup, SidebarGroupContent, SidebarGroupLabel, type SidebarGroupLabelProps, SidebarHeader, SidebarInset, SidebarMenu, SidebarMenuButton, type SidebarMenuButtonProps, type SidebarMenuButtonSize, SidebarMenuItem, type SidebarProps, SidebarProvider, type SidebarProviderProps, SidebarRail, SidebarSeparator, SidebarTrigger, Skeleton, Slider, type SliderProps, Slot, type SlotProps, Spinner, Stat, type StatProps, type StatusLinkData, type StatusPageProps, StatusScreen, type StatusScreenProps, type Surface, Switch, type SwitchProps, Table, TableBody, TableCaption, TableCell, TableFooter, TableHead, TableHeader, TableRow, Tabs, TabsContent, type TabsContentProps, TabsList, type TabsProps, TabsTrigger, type TabsTriggerProps, Terminal, type TerminalArea, TerminalChart, TerminalPane, TerminalPaneBody, TerminalPaneHeader, type TerminalPaneProps, TerminalTicker, Textarea, type TextareaProps, type TextareaSize, TickerStat, type TickerStatProps, type Toast, type ToastFn, type ToastOptions, type ToastPosition, type ToastState, type ToastVariant, Toaster, type ToasterProps, Toggle, ToggleGroup, ToggleGroupItem, type ToggleGroupItemProps, type ToggleGroupProps, type ToggleProps, type ToggleSize, type ToggleVariant, type ToggleVariantsOptions, Tooltip, TooltipContent, type TooltipContentProps, type TooltipProps, TooltipProvider, type TooltipProviderProps, TooltipTrigger, type TooltipTriggerProps, TradesTapeRow, type TradesTapeRowProps, buttonVariants, cn, mergeRefs, navigationMenuTriggerStyle, statusCtaClass, toast, toggleVariants, useControllableState, useDismissableLayer, useFloating, useFocusScope, useHoverIntent, usePresence, useRovingFocus, useSidebar };
1864
+ export { type Accent, Accordion, AccordionContent, AccordionItem, type AccordionItemProps, type AccordionProps, AccordionTrigger, Alert, AlertDescription, AlertDialog, AlertDialogAction, type AlertDialogActionProps, AlertDialogCancel, type AlertDialogCancelProps, AlertDialogContent, type AlertDialogContentProps, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogOverlay, type AlertDialogOverlayProps, type AlertDialogProps, AlertDialogTitle, AlertDialogTrigger, type AlertDialogTriggerProps, type AlertProps, AlertTitle, type AlertVariant, type Align, Avatar, AvatarFallback, AvatarImage, Badge, type BadgeProps, type BadgeVariant, type BookSide, Breadcrumb, BreadcrumbEllipsis, BreadcrumbItem, BreadcrumbLink, type BreadcrumbLinkProps, BreadcrumbList, BreadcrumbPage, BreadcrumbSeparator, Button, ButtonGroup, type ButtonGroupOrientation, type ButtonGroupProps, ButtonGroupSeparator, ButtonGroupText, type ButtonGroupTextProps, type ButtonProps, type ButtonSize, type ButtonVariant, type ButtonVariantsOptions, Calendar, type CalendarProps, Card, CardAction, CardContent, CardDescription, CardFooter, CardHeader, CardTitle, Carousel, CarouselContent, CarouselEdgeFade, CarouselItem, CarouselNext, CarouselPrevious, type CarouselProps, type ChartConfig, ChartContainer, type ChartItem, ChartLegend, ChartLegendContent, ChartStyle, ChartTooltip, ChartTooltipContent, Check, Checkbox, type CheckboxProps, Collapsible, CollapsibleContent, type CollapsibleProps, CollapsibleTrigger, Command, CommandDialog, type CommandDialogProps, CommandEmpty, CommandGroup, type CommandGroupProps, CommandInput, CommandItem, type CommandItemProps, CommandList, type CommandProps, CommandSeparator, CommandShortcut, Container, type ContainerProps, ContextMenu, ContextMenuCheckboxItem, type ContextMenuCheckboxItemProps, ContextMenuContent, ContextMenuGroup, ContextMenuItem, type ContextMenuItemProps, ContextMenuLabel, type ContextMenuLabelProps, ContextMenuPortal, type ContextMenuProps, ContextMenuRadioGroup, type ContextMenuRadioGroupProps, ContextMenuRadioItem, type ContextMenuRadioItemProps, ContextMenuSeparator, ContextMenuShortcut, ContextMenuSub, ContextMenuSubContent, type ContextMenuSubProps, ContextMenuSubTrigger, type ContextMenuSubTriggerProps, ContextMenuTrigger, type ContextMenuTriggerProps, DateTimePicker, type DateTimePickerLabels, type DateTimePickerProps, Dialog, DialogClose, type DialogCloseProps, DialogContent, type DialogContentProps, DialogDescription, DialogFooter, DialogHeader, DialogOverlay, type DialogOverlayProps, DialogPortal, type DialogPortalProps, type DialogProps, DialogTitle, DialogTrigger, type DialogTriggerProps, type DismissEvent, Display, Drawer, DrawerClose, type DrawerCloseProps, DrawerContent, DrawerDescription, type DrawerDirection, DrawerFooter, DrawerHeader, DrawerOverlay, type DrawerProps, DrawerTitle, DrawerTrigger, type DrawerTriggerProps, DropdownMenu, DropdownMenuCheckboxItem, type DropdownMenuCheckboxItemProps, DropdownMenuContent, type DropdownMenuContentProps, DropdownMenuGroup, DropdownMenuItem, type DropdownMenuItemProps, DropdownMenuLabel, type DropdownMenuLabelProps, DropdownMenuPortal, type DropdownMenuProps, DropdownMenuRadioGroup, type DropdownMenuRadioGroupProps, DropdownMenuRadioItem, type DropdownMenuRadioItemProps, DropdownMenuSeparator, DropdownMenuShortcut, DropdownMenuSub, DropdownMenuSubContent, type DropdownMenuSubProps, DropdownMenuSubTrigger, type DropdownMenuSubTriggerProps, DropdownMenuTrigger, type DropdownMenuTriggerProps, Empty, EmptyContent, EmptyDescription, EmptyHeader, EmptyMedia, type EmptyMediaProps, type EmptyMediaVariant, EmptyTitle, Eyebrow, Field, FieldContent, FieldDescription, FieldError, FieldGroup, FieldLabel, FieldLegend, type FieldOrientation, FieldSeparator, FieldSet, FieldTitle, type FloatingResult, Footer, type FooterLink, type FooterLinkGroup, type FooterOffice, type FooterProps, Forbidden, Form, FormControl, FormDescription, FormItem, FormLabel, FormMessage, HoverCard, HoverCardContent, type HoverCardContentProps, type HoverCardProps, HoverCardTrigger, type HoverCardTriggerProps, type HoverIntentHandlers, InfoTip, InfoTipContent, type InfoTipContentProps, type InfoTipProps, InfoTipTrigger, type InfoTipTriggerProps, Input, InputGroup, InputGroupAddon, type InputGroupAddonAlign, type InputGroupAddonProps, InputGroupButton, type InputGroupButtonProps, type InputGroupButtonSize, InputGroupInput, InputGroupText, InputGroupTextarea, InputOTP, InputOTPGroup, type InputOTPProps, InputOTPSeparator, InputOTPSlot, type InputOTPSlotProps, type InputProps, type InputSize, Item, ItemActions, ItemContent, ItemDescription, ItemFooter, ItemGroup, ItemHeader, ItemMedia, type ItemMediaProps, type ItemMediaVariant, type ItemProps, ItemSeparator, type ItemSize, ItemTitle, type ItemVariant, Kbd, KbdGroup, Label, Logo, type LogoProps, Menubar, MenubarCheckboxItem, MenubarContent, MenubarGroup, MenubarItem, MenubarLabel, MenubarMenu, type MenubarMenuProps, MenubarRadioGroup, type MenubarRadioGroupProps, MenubarRadioItem, MenubarSeparator, MenubarShortcut, MenubarSub, MenubarSubContent, type MenubarSubProps, MenubarSubTrigger, MenubarTrigger, NativeSelect, NativeSelectGroup, NativeSelectOption, type NativeSelectProps, type NativeSelectSize, NavigationMenu, NavigationMenuContent, NavigationMenuIndicator, NavigationMenuItem, type NavigationMenuItemProps, NavigationMenuLink, NavigationMenuList, NavigationMenuTrigger, NavigationMenuViewport, NotFound, OpenOrdersEmpty, OrderBook, OrderBookHead, type OrderBookHeadProps, OrderBookRow, type OrderBookRowProps, OrderBookSpread, OrderForm, OrderFormRow, type OrderFormRowProps, OrderFormSubmit, type OrderFormSubmitProps, type OrderSide, Pagination, PaginationContent, PaginationEllipsis, PaginationItem, PaginationLink, type PaginationLinkProps, PaginationNext, PaginationPrevious, type Polarity, Popover, PopoverAnchor, type PopoverAnchorProps, PopoverContent, type PopoverContentProps, type PopoverProps, PopoverTrigger, type PopoverTriggerProps, Portal, type PortalProps, PortalProvider, type PortalProviderProps, Progress, type ProgressProps, Prose, RadioGroup, RadioGroupItem, type RadioGroupItemProps, type RadioGroupProps, type ResizableDirection, ResizableHandle, type ResizableHandleProps, ResizablePanel, ResizablePanelGroup, type ResizablePanelGroupProps, type ResizablePanelProps, ScrollArea, ScrollBar, type ScrollBarOrientation, type ScrollBarProps, Section, SectionDescriptor, type SectionDescriptorProps, SectionHead, type SectionHeadProps, type SectionProps, Select, SelectContent, type SelectContentProps, SelectGroup, SelectItem, type SelectItemProps, SelectLabel, type SelectProps, SelectSeparator, SelectTrigger, type SelectTriggerProps, type SelectTriggerSize, SelectValue, type SelectValueProps, Separator, type SeparatorOrientation, type SeparatorProps, ServerError, type ServerErrorProps, Sheet, SheetClose, type SheetCloseProps, SheetContent, type SheetContentProps, SheetDescription, SheetFooter, SheetHeader, SheetOverlay, type SheetOverlayProps, type SheetProps, SheetTitle, SheetTrigger, type SheetTriggerProps, type Side, Sidebar, SidebarContent, SidebarFooter, SidebarGroup, SidebarGroupContent, SidebarGroupLabel, type SidebarGroupLabelProps, SidebarHeader, SidebarInset, SidebarMenu, SidebarMenuButton, type SidebarMenuButtonProps, type SidebarMenuButtonSize, SidebarMenuItem, type SidebarProps, SidebarProvider, type SidebarProviderProps, SidebarRail, SidebarSeparator, SidebarTrigger, Skeleton, Slider, type SliderProps, Slot, type SlotProps, Spinner, Stat, type StatProps, type StatusLinkData, type StatusPageProps, StatusScreen, type StatusScreenProps, type Surface, Switch, type SwitchProps, Table, TableBody, TableCaption, TableCell, TableFooter, TableHead, TableHeader, TableRow, Tabs, TabsContent, type TabsContentProps, TabsList, type TabsProps, TabsTrigger, type TabsTriggerProps, Terminal, type TerminalArea, TerminalChart, TerminalPane, TerminalPaneBody, TerminalPaneHeader, type TerminalPaneProps, TerminalTicker, Textarea, type TextareaProps, type TextareaSize, TickerStat, type TickerStatProps, type Toast, type ToastFn, type ToastOptions, type ToastPosition, type ToastState, type ToastVariant, Toaster, type ToasterProps, Toggle, ToggleGroup, ToggleGroupItem, type ToggleGroupItemProps, type ToggleGroupProps, type ToggleProps, type ToggleSize, type ToggleVariant, type ToggleVariantsOptions, Tooltip, TooltipContent, type TooltipContentProps, type TooltipProps, TooltipProvider, type TooltipProviderProps, TooltipTrigger, type TooltipTriggerProps, TradesTapeRow, type TradesTapeRowProps, buttonVariants, cn, mergeRefs, navigationMenuTriggerStyle, statusCtaClass, toast, toggleVariants, useControllableState, useDismissableLayer, useFloating, useFocusScope, useHoverIntent, usePresence, useRovingFocus, useSidebar };
@@ -1,32 +1,45 @@
1
1
  "use client";
2
2
  import * as React from "react";
3
+ const stack = [];
4
+ function isInsideHigherLayer(own, target) {
5
+ const index = stack.indexOf(own);
6
+ if (index < 0) return false;
7
+ return stack.slice(index + 1).some((layer) => layer.current?.contains(target) ?? false);
8
+ }
3
9
  function useDismissableLayer(opts) {
4
10
  const { enabled, onDismiss, exclude } = opts;
5
11
  const ref = React.useRef(null);
6
12
  const onDismissRef = React.useRef(onDismiss);
13
+ const excludeRef = React.useRef(exclude);
7
14
  React.useEffect(() => {
8
15
  onDismissRef.current = onDismiss;
16
+ excludeRef.current = exclude;
9
17
  });
10
18
  React.useEffect(() => {
11
19
  if (!enabled) return;
20
+ stack.push(ref);
12
21
  function onPointerDown(event) {
13
22
  const target = event.target;
14
- if (ref.current && target && ref.current.contains(target)) return;
15
- for (const r of exclude ?? []) {
16
- if (r.current && target && r.current.contains(target)) return;
23
+ if (!target) return;
24
+ if (ref.current && ref.current.contains(target)) return;
25
+ for (const r of excludeRef.current ?? []) {
26
+ if (r.current && r.current.contains(target)) return;
17
27
  }
18
- onDismissRef.current();
28
+ if (isInsideHigherLayer(ref, target)) return;
29
+ onDismissRef.current(event);
19
30
  }
20
31
  function onKeyDown(event) {
21
- if (event.key === "Escape") onDismissRef.current();
32
+ if (event.key === "Escape" && stack[stack.length - 1] === ref) onDismissRef.current(event);
22
33
  }
23
34
  document.addEventListener("pointerdown", onPointerDown, true);
24
35
  document.addEventListener("keydown", onKeyDown);
25
36
  return () => {
37
+ const index = stack.indexOf(ref);
38
+ if (index >= 0) stack.splice(index, 1);
26
39
  document.removeEventListener("pointerdown", onPointerDown, true);
27
40
  document.removeEventListener("keydown", onKeyDown);
28
41
  };
29
- }, [enabled, exclude]);
42
+ }, [enabled]);
30
43
  return ref;
31
44
  }
32
45
  export {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/primitives/dismissable-layer.tsx"],"sourcesContent":["\"use client\";\n\nimport * as React from \"react\";\n\n/**\n * Calls `onDismiss` on Escape or on a pointer-down outside the layer — the\n * dep-light core of every overlay's \"click away to close\". Pass refs to any\n * nodes that should NOT count as outside (e.g. the trigger), via `exclude`.\n *\n * Mirrors the dismiss behaviour Rust expresses with a full-screen backdrop.\n */\nexport function useDismissableLayer(opts: {\n enabled: boolean;\n onDismiss: () => void;\n exclude?: Array<React.RefObject<Element | null>>;\n}): React.RefObject<HTMLDivElement | null> {\n const { enabled, onDismiss, exclude } = opts;\n const ref = React.useRef<HTMLDivElement | null>(null);\n const onDismissRef = React.useRef(onDismiss);\n React.useEffect(() => {\n onDismissRef.current = onDismiss;\n });\n\n React.useEffect(() => {\n if (!enabled) return;\n\n function onPointerDown(event: PointerEvent) {\n const target = event.target as Node | null;\n if (ref.current && target && ref.current.contains(target)) return;\n for (const r of exclude ?? []) {\n if (r.current && target && r.current.contains(target)) return;\n }\n onDismissRef.current();\n }\n\n function onKeyDown(event: KeyboardEvent) {\n if (event.key === \"Escape\") onDismissRef.current();\n }\n\n document.addEventListener(\"pointerdown\", onPointerDown, true);\n document.addEventListener(\"keydown\", onKeyDown);\n return () => {\n document.removeEventListener(\"pointerdown\", onPointerDown, true);\n document.removeEventListener(\"keydown\", onKeyDown);\n };\n }, [enabled, exclude]);\n\n return ref;\n}\n"],"mappings":";AAEA,YAAY,WAAW;AAShB,SAAS,oBAAoB,MAIO;AACzC,QAAM,EAAE,SAAS,WAAW,QAAQ,IAAI;AACxC,QAAM,MAAM,MAAM,OAA8B,IAAI;AACpD,QAAM,eAAe,MAAM,OAAO,SAAS;AAC3C,QAAM,UAAU,MAAM;AACpB,iBAAa,UAAU;AAAA,EACzB,CAAC;AAED,QAAM,UAAU,MAAM;AACpB,QAAI,CAAC,QAAS;AAEd,aAAS,cAAc,OAAqB;AAC1C,YAAM,SAAS,MAAM;AACrB,UAAI,IAAI,WAAW,UAAU,IAAI,QAAQ,SAAS,MAAM,EAAG;AAC3D,iBAAW,KAAK,WAAW,CAAC,GAAG;AAC7B,YAAI,EAAE,WAAW,UAAU,EAAE,QAAQ,SAAS,MAAM,EAAG;AAAA,MACzD;AACA,mBAAa,QAAQ;AAAA,IACvB;AAEA,aAAS,UAAU,OAAsB;AACvC,UAAI,MAAM,QAAQ,SAAU,cAAa,QAAQ;AAAA,IACnD;AAEA,aAAS,iBAAiB,eAAe,eAAe,IAAI;AAC5D,aAAS,iBAAiB,WAAW,SAAS;AAC9C,WAAO,MAAM;AACX,eAAS,oBAAoB,eAAe,eAAe,IAAI;AAC/D,eAAS,oBAAoB,WAAW,SAAS;AAAA,IACnD;AAAA,EACF,GAAG,CAAC,SAAS,OAAO,CAAC;AAErB,SAAO;AACT;","names":[]}
1
+ {"version":3,"sources":["../../src/primitives/dismissable-layer.tsx"],"sourcesContent":["\"use client\";\n\nimport * as React from \"react\";\n\ntype LayerRef = React.RefObject<HTMLDivElement | null>;\n\n// The open layers, oldest first. An overlay opened inside another (a Select in\n// a Dialog, a Popover in a Drawer) is portaled out of its parent's subtree, so\n// \"outside the parent\" would include it: the stack is what tells a nested\n// layer from the rest of the page.\nconst stack: LayerRef[] = [];\n\nfunction isInsideHigherLayer(own: LayerRef, target: Node): boolean {\n const index = stack.indexOf(own);\n // Not on the stack (a listener outliving its cleanup): nothing is above it.\n if (index < 0) return false;\n return stack.slice(index + 1).some((layer) => layer.current?.contains(target) ?? false);\n}\n\n/** Why a layer is being dismissed: the event itself, for a caller that treats keys and pointers apart. */\nexport type DismissEvent = PointerEvent | KeyboardEvent;\n\n/**\n * Calls `onDismiss` on Escape or on a pointer-down outside the layer — the\n * dep-light core of every overlay's \"click away to close\". Pass refs to any\n * nodes that should NOT count as outside (e.g. the trigger), via `exclude`.\n *\n * Layers stack, in the order they opened: Escape dismisses only the topmost\n * layer on this stack, and a pointer-down inside a layer opened above this one\n * is not outside it — so a Select's list inside a Dialog closes the list, not\n * the Dialog. The stack knows only the overlays built on this hook: a\n * `Drawer` or `CommandDialog` (Escape in their own React `onKeyDown`) is not\n * on it, and a menu that also closes on Escape in its React `onKeyDown` lets\n * the key bubble through React's tree to what holds it. Moving those onto the\n * stack is a follow-up.\n *\n * Mirrors the dismiss behaviour Rust expresses with a full-screen backdrop.\n */\nexport function useDismissableLayer(opts: {\n enabled: boolean;\n onDismiss: (event: DismissEvent) => void;\n exclude?: Array<React.RefObject<Element | null>>;\n}): React.RefObject<HTMLDivElement | null> {\n const { enabled, onDismiss, exclude } = opts;\n const ref = React.useRef<HTMLDivElement | null>(null);\n const onDismissRef = React.useRef(onDismiss);\n // Callers pass `exclude` inline, a new array each render: read through a ref,\n // or the effect would re-run and move this layer to the top of the stack.\n const excludeRef = React.useRef(exclude);\n React.useEffect(() => {\n onDismissRef.current = onDismiss;\n excludeRef.current = exclude;\n });\n\n React.useEffect(() => {\n if (!enabled) return;\n stack.push(ref);\n\n function onPointerDown(event: PointerEvent) {\n const target = event.target as Node | null;\n if (!target) return;\n if (ref.current && ref.current.contains(target)) return;\n for (const r of excludeRef.current ?? []) {\n if (r.current && r.current.contains(target)) return;\n }\n if (isInsideHigherLayer(ref, target)) return;\n onDismissRef.current(event);\n }\n\n function onKeyDown(event: KeyboardEvent) {\n if (event.key === \"Escape\" && stack[stack.length - 1] === ref) onDismissRef.current(event);\n }\n\n document.addEventListener(\"pointerdown\", onPointerDown, true);\n document.addEventListener(\"keydown\", onKeyDown);\n return () => {\n const index = stack.indexOf(ref);\n if (index >= 0) stack.splice(index, 1);\n document.removeEventListener(\"pointerdown\", onPointerDown, true);\n document.removeEventListener(\"keydown\", onKeyDown);\n };\n }, [enabled]);\n\n return ref;\n}\n"],"mappings":";AAEA,YAAY,WAAW;AAQvB,MAAM,QAAoB,CAAC;AAE3B,SAAS,oBAAoB,KAAe,QAAuB;AACjE,QAAM,QAAQ,MAAM,QAAQ,GAAG;AAE/B,MAAI,QAAQ,EAAG,QAAO;AACtB,SAAO,MAAM,MAAM,QAAQ,CAAC,EAAE,KAAK,CAAC,UAAU,MAAM,SAAS,SAAS,MAAM,KAAK,KAAK;AACxF;AAqBO,SAAS,oBAAoB,MAIO;AACzC,QAAM,EAAE,SAAS,WAAW,QAAQ,IAAI;AACxC,QAAM,MAAM,MAAM,OAA8B,IAAI;AACpD,QAAM,eAAe,MAAM,OAAO,SAAS;AAG3C,QAAM,aAAa,MAAM,OAAO,OAAO;AACvC,QAAM,UAAU,MAAM;AACpB,iBAAa,UAAU;AACvB,eAAW,UAAU;AAAA,EACvB,CAAC;AAED,QAAM,UAAU,MAAM;AACpB,QAAI,CAAC,QAAS;AACd,UAAM,KAAK,GAAG;AAEd,aAAS,cAAc,OAAqB;AAC1C,YAAM,SAAS,MAAM;AACrB,UAAI,CAAC,OAAQ;AACb,UAAI,IAAI,WAAW,IAAI,QAAQ,SAAS,MAAM,EAAG;AACjD,iBAAW,KAAK,WAAW,WAAW,CAAC,GAAG;AACxC,YAAI,EAAE,WAAW,EAAE,QAAQ,SAAS,MAAM,EAAG;AAAA,MAC/C;AACA,UAAI,oBAAoB,KAAK,MAAM,EAAG;AACtC,mBAAa,QAAQ,KAAK;AAAA,IAC5B;AAEA,aAAS,UAAU,OAAsB;AACvC,UAAI,MAAM,QAAQ,YAAY,MAAM,MAAM,SAAS,CAAC,MAAM,IAAK,cAAa,QAAQ,KAAK;AAAA,IAC3F;AAEA,aAAS,iBAAiB,eAAe,eAAe,IAAI;AAC5D,aAAS,iBAAiB,WAAW,SAAS;AAC9C,WAAO,MAAM;AACX,YAAM,QAAQ,MAAM,QAAQ,GAAG;AAC/B,UAAI,SAAS,EAAG,OAAM,OAAO,OAAO,CAAC;AACrC,eAAS,oBAAoB,eAAe,eAAe,IAAI;AAC/D,eAAS,oBAAoB,WAAW,SAAS;AAAA,IACnD;AAAA,EACF,GAAG,CAAC,OAAO,CAAC;AAEZ,SAAO;AACT;","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@evinvest/uikit",
3
- "version": "0.21.0",
3
+ "version": "0.22.0",
4
4
  "description": "Dep-light React UI kit: shadcn-semantics components with zero @radix-ui / cva, mirroring the ev_lib::uikit Rust feature. Ships the shared design tokens.",
5
5
  "license": "BlueOak-1.0.0",
6
6
  "publishConfig": {