@adecore/ui 0.35.3 → 0.35.5

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.
@@ -13,7 +13,7 @@ type AutocompleteProps<ItemValue> = Omit<Autocomplete.Root.Props<ItemValue>, 'op
13
13
  };
14
14
  type DialogProps = Pick<ComponentProps<typeof DialogRoot>, 'open' | 'defaultOpen' | 'onOpenChange' | 'onOpenChangeComplete'>;
15
15
  export type CommandPaletteRootProps<ItemValue> = AutocompleteProps<ItemValue> & DialogProps;
16
- export declare function CommandPaletteRoot<ItemValue>({ open, defaultOpen, onOpenChange, onOpenChangeComplete, autoHighlight, keepHighlight, value, onValueChange, items, children, ...props }: CommandPaletteRootProps<ItemValue>): import("react").JSX.Element;
16
+ export declare function CommandPaletteRoot<ItemValue>({ open, defaultOpen, onOpenChange, onOpenChangeComplete, autoHighlight, keepHighlight, highlightItemOnHover, value, onValueChange, items, children, ...props }: CommandPaletteRootProps<ItemValue>): import("react").JSX.Element;
17
17
  export type CommandPalettePopupProps = Omit<DialogPopupProps, 'size'> & {
18
18
  size?: keyof typeof SIZE;
19
19
  };
@@ -26,7 +26,7 @@ export type CommandPaletteInputProps = Omit<ComponentProps<typeof Autocomplete.I
26
26
  className?: string;
27
27
  };
28
28
  export declare function CommandPaletteInput({ leading, trailing, escapeHint, mono, className, ref, ...props }: CommandPaletteInputProps): import("react").JSX.Element;
29
- export declare function CommandPaletteList({ className, ref, ...props }: ComponentProps<typeof Autocomplete.List>): import("react").JSX.Element;
29
+ export declare function CommandPaletteList({ className, ref, onFocus, ...props }: ComponentProps<typeof Autocomplete.List>): import("react").JSX.Element;
30
30
  export type CommandPaletteFooterProps = useRender.ComponentProps<'div'>;
31
31
  export declare function CommandPaletteFooter({ render, className, ref, ...props }: CommandPaletteFooterProps): import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>>;
32
32
  export {};
@@ -1,5 +1,6 @@
1
1
  import { c as _c } from "react/compiler-runtime";
2
2
  import { createContext, useContext, useLayoutEffect, useRef, useState } from 'react';
3
+ import { flushSync } from 'react-dom';
3
4
  import clsx from 'clsx';
4
5
  import { Search } from 'lucide-react';
5
6
  import { useTranslation } from 'react-i18next';
@@ -24,156 +25,91 @@ const SIZE = {
24
25
  * is Base UI's autocomplete, so the query, the items and their filter work as in a combobox. The
25
26
  * first row is highlighted and stays so while the pointer leaves the list, which is what Enter runs.
26
27
  */
27
- export function CommandPaletteRoot(t0) {
28
- const $ = _c(33);
29
- let children;
30
- let defaultOpen;
31
- let items;
32
- let onOpenChange;
33
- let onOpenChangeComplete;
34
- let onValueChange;
35
- let open;
36
- let props;
37
- let t1;
38
- let t2;
39
- let value;
40
- if ($[0] !== t0) {
41
- ({
42
- open,
43
- defaultOpen,
44
- onOpenChange,
45
- onOpenChangeComplete,
46
- autoHighlight: t1,
47
- keepHighlight: t2,
48
- value,
49
- onValueChange,
50
- items,
51
- children,
52
- ...props
53
- } = t0);
54
- $[0] = t0;
55
- $[1] = children;
56
- $[2] = defaultOpen;
57
- $[3] = items;
58
- $[4] = onOpenChange;
59
- $[5] = onOpenChangeComplete;
60
- $[6] = onValueChange;
61
- $[7] = open;
62
- $[8] = props;
63
- $[9] = t1;
64
- $[10] = t2;
65
- $[11] = value;
66
- } else {
67
- children = $[1];
68
- defaultOpen = $[2];
69
- items = $[3];
70
- onOpenChange = $[4];
71
- onOpenChangeComplete = $[5];
72
- onValueChange = $[6];
73
- open = $[7];
74
- props = $[8];
75
- t1 = $[9];
76
- t2 = $[10];
77
- value = $[11];
78
- }
79
- const autoHighlight = t1 === undefined ? "always" : t1;
80
- const keepHighlight = t2 === undefined ? true : t2;
28
+ export function CommandPaletteRoot({
29
+ open,
30
+ defaultOpen,
31
+ onOpenChange,
32
+ onOpenChangeComplete,
33
+ autoHighlight = 'always',
34
+ keepHighlight = true,
35
+ highlightItemOnHover = true,
36
+ value,
37
+ onValueChange,
38
+ items,
39
+ children,
40
+ ...props
41
+ }) {
81
42
  const inputRef = useRef(null);
82
43
  const listRef = useRef(null);
44
+ const [focusHighlight, setFocusHighlight] = useState(false);
45
+ /*
46
+ * Base UI moves the highlight to the first row for a typed query only. For a query the app sets,
47
+ * the list is empty for one commit that is never painted: Base UI drops the highlight then and
48
+ * puts it on the first row of what comes back.
49
+ */
83
50
  const [settledValue, setSettledValue] = useState(value);
84
51
  const resetting = value !== undefined && value !== settledValue;
85
- let t3;
86
- let t4;
87
- if ($[12] !== resetting || $[13] !== value) {
88
- t3 = () => {
89
- if (!resetting) {
90
- return;
52
+ useLayoutEffect(() => {
53
+ if (!resetting) {
54
+ return;
55
+ }
56
+ if (listRef.current !== null) {
57
+ listRef.current.scrollTop = 0;
58
+ }
59
+ // The rows may come back only after Base UI has seen the empty list in a commit of its own.
60
+ // oxlint-disable-next-line react/set-state-in-effect
61
+ setSettledValue(value);
62
+ }, [resetting, value]);
63
+ const highlight = row => {
64
+ if (row.hasAttribute('data-highlighted')) {
65
+ return;
66
+ }
67
+ // Base UI has no imperative highlight action and disables highlighting on item focus.
68
+ // Enable pointer navigation just for this event when the app has disabled hover highlighting.
69
+ if (!highlightItemOnHover) {
70
+ flushSync(() => setFocusHighlight(true));
71
+ }
72
+ try {
73
+ row.dispatchEvent(new MouseEvent('mousemove', {
74
+ bubbles: true,
75
+ movementX: 1
76
+ }));
77
+ } finally {
78
+ if (!highlightItemOnHover) {
79
+ flushSync(() => setFocusHighlight(false));
91
80
  }
92
- if (listRef.current !== null) {
93
- listRef.current.scrollTop = 0;
94
- }
95
- setSettledValue(value);
96
- };
97
- t4 = [resetting, value];
98
- $[12] = resetting;
99
- $[13] = value;
100
- $[14] = t3;
101
- $[15] = t4;
102
- } else {
103
- t3 = $[14];
104
- t4 = $[15];
105
- }
106
- useLayoutEffect(t3, t4);
107
- let t5;
108
- if ($[16] === Symbol.for("react.memo_cache_sentinel")) {
109
- t5 = {
110
- input: inputRef,
111
- list: listRef
112
- };
113
- $[16] = t5;
114
- } else {
115
- t5 = $[16];
116
- }
117
- let t6;
118
- if ($[17] !== onValueChange) {
119
- t6 = (next, details) => {
120
- if (details.reason === "input-change") {
121
- setSettledValue(next);
122
- }
123
- onValueChange?.(next, details);
124
- };
125
- $[17] = onValueChange;
126
- $[18] = t6;
127
- } else {
128
- t6 = $[18];
129
- }
130
- const t7 = resetting ? NO_ITEMS : items;
131
- let t8;
132
- if ($[19] !== autoHighlight || $[20] !== children || $[21] !== keepHighlight || $[22] !== props || $[23] !== t6 || $[24] !== t7 || $[25] !== value) {
133
- t8 = /*#__PURE__*/_jsx(PaletteRefsContext, {
134
- value: t5,
81
+ }
82
+ };
83
+ return /*#__PURE__*/_jsx(DialogRoot, {
84
+ open: open,
85
+ defaultOpen: defaultOpen,
86
+ onOpenChange: onOpenChange,
87
+ onOpenChangeComplete: onOpenChangeComplete,
88
+ children: /*#__PURE__*/_jsx(PaletteRefsContext, {
89
+ value: {
90
+ input: inputRef,
91
+ list: listRef,
92
+ highlight
93
+ },
135
94
  children: /*#__PURE__*/_jsx(Autocomplete.Root, {
136
95
  ...props,
137
96
  value: value,
138
- onValueChange: t6,
139
- items: t7,
97
+ onValueChange: (next, details) => {
98
+ if (details.reason === 'input-change') {
99
+ setSettledValue(next);
100
+ }
101
+ onValueChange?.(next, details);
102
+ },
103
+ items: resetting ? NO_ITEMS : items,
140
104
  inline: true,
141
105
  open: true,
142
106
  autoHighlight: autoHighlight,
143
107
  keepHighlight: keepHighlight,
108
+ highlightItemOnHover: highlightItemOnHover || focusHighlight,
144
109
  children: children
145
110
  })
146
- });
147
- $[19] = autoHighlight;
148
- $[20] = children;
149
- $[21] = keepHighlight;
150
- $[22] = props;
151
- $[23] = t6;
152
- $[24] = t7;
153
- $[25] = value;
154
- $[26] = t8;
155
- } else {
156
- t8 = $[26];
157
- }
158
- let t9;
159
- if ($[27] !== defaultOpen || $[28] !== onOpenChange || $[29] !== onOpenChangeComplete || $[30] !== open || $[31] !== t8) {
160
- t9 = /*#__PURE__*/_jsx(DialogRoot, {
161
- open: open,
162
- defaultOpen: defaultOpen,
163
- onOpenChange: onOpenChange,
164
- onOpenChangeComplete: onOpenChangeComplete,
165
- children: t8
166
- });
167
- $[27] = defaultOpen;
168
- $[28] = onOpenChange;
169
- $[29] = onOpenChangeComplete;
170
- $[30] = open;
171
- $[31] = t8;
172
- $[32] = t9;
173
- } else {
174
- t9 = $[32];
175
- }
176
- return t9;
111
+ })
112
+ });
177
113
  }
178
114
  /*
179
115
  * The dialog's surface. It hangs from a fixed line near the top instead of the middle: the list
@@ -369,24 +305,28 @@ export function CommandPaletteInput(t0) {
369
305
  }
370
306
  /* The rows. It scrolls on its own, so the field and the footer stay put. */
371
307
  export function CommandPaletteList(t0) {
372
- const $ = _c(16);
308
+ const $ = _c(21);
373
309
  let className;
310
+ let onFocus;
374
311
  let props;
375
312
  let ref;
376
313
  if ($[0] !== t0) {
377
314
  ({
378
315
  className,
379
316
  ref,
317
+ onFocus,
380
318
  ...props
381
319
  } = t0);
382
320
  $[0] = t0;
383
321
  $[1] = className;
384
- $[2] = props;
385
- $[3] = ref;
322
+ $[2] = onFocus;
323
+ $[3] = props;
324
+ $[4] = ref;
386
325
  } else {
387
326
  className = $[1];
388
- props = $[2];
389
- ref = $[3];
327
+ onFocus = $[2];
328
+ props = $[3];
329
+ ref = $[4];
390
330
  }
391
331
  const {
392
332
  t
@@ -394,47 +334,68 @@ export function CommandPaletteList(t0) {
394
334
  const refs = useContext(PaletteRefsContext);
395
335
  const t1 = refs?.list;
396
336
  let t2;
397
- if ($[4] !== ref || $[5] !== t1) {
337
+ if ($[5] !== ref || $[6] !== t1) {
398
338
  t2 = mergeRefs(ref, t1);
399
- $[4] = ref;
400
- $[5] = t1;
401
- $[6] = t2;
339
+ $[5] = ref;
340
+ $[6] = t1;
341
+ $[7] = t2;
402
342
  } else {
403
- t2 = $[6];
343
+ t2 = $[7];
404
344
  }
405
345
  let t3;
406
- if ($[7] !== t) {
346
+ if ($[8] !== t) {
407
347
  t3 = t("palette.results");
408
- $[7] = t;
409
- $[8] = t3;
348
+ $[8] = t;
349
+ $[9] = t3;
410
350
  } else {
411
- t3 = $[8];
351
+ t3 = $[9];
412
352
  }
413
353
  let t4;
414
- if ($[9] !== className) {
354
+ if ($[10] !== className) {
415
355
  t4 = withClass("max-h-[50vh] min-h-0 overflow-y-auto overscroll-contain scroll-py-1.5 not-empty:p-1.5", className);
416
- $[9] = className;
417
- $[10] = t4;
356
+ $[10] = className;
357
+ $[11] = t4;
418
358
  } else {
419
- t4 = $[10];
359
+ t4 = $[11];
420
360
  }
421
361
  let t5;
422
- if ($[11] !== props || $[12] !== t2 || $[13] !== t3 || $[14] !== t4) {
423
- t5 = /*#__PURE__*/_jsx(Autocomplete.List, {
362
+ if ($[12] !== onFocus || $[13] !== refs) {
363
+ t5 = event => {
364
+ onFocus?.(event);
365
+ const row = event.target.closest("[role=\"option\"]");
366
+ if (row === null || !event.currentTarget.contains(row) || refs?.input.current == null) {
367
+ return;
368
+ }
369
+ refs.highlight(row);
370
+ refs.input.current.focus({
371
+ preventScroll: true
372
+ });
373
+ };
374
+ $[12] = onFocus;
375
+ $[13] = refs;
376
+ $[14] = t5;
377
+ } else {
378
+ t5 = $[14];
379
+ }
380
+ let t6;
381
+ if ($[15] !== props || $[16] !== t2 || $[17] !== t3 || $[18] !== t4 || $[19] !== t5) {
382
+ t6 = /*#__PURE__*/_jsx(Autocomplete.List, {
424
383
  ref: t2,
425
384
  "aria-label": t3,
426
385
  className: t4,
427
- ...props
386
+ ...props,
387
+ onFocus: t5
428
388
  });
429
- $[11] = props;
430
- $[12] = t2;
431
- $[13] = t3;
432
- $[14] = t4;
433
- $[15] = t5;
389
+ $[15] = props;
390
+ $[16] = t2;
391
+ $[17] = t3;
392
+ $[18] = t4;
393
+ $[19] = t5;
394
+ $[20] = t6;
434
395
  } else {
435
- t5 = $[15];
396
+ t6 = $[20];
436
397
  }
437
- return t5;
398
+ return t6;
438
399
  }
439
400
  /* Under the list: the keys that work here, a count, an action that leaves the palette. */
440
401
  export function CommandPaletteFooter(t0) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adecore/ui",
3
- "version": "0.35.3",
3
+ "version": "0.35.5",
4
4
  "description": "React components, a theme, formatters and a settings dialog for desktop-grade apps on React 19, Base UI and Tailwind 4.",
5
5
  "license": "FSL-1.1-MIT",
6
6
  "author": "Bas Milius",
@@ -1,4 +1,5 @@
1
1
  import { createContext, useContext, useLayoutEffect, useRef, useState, type ComponentProps, type ReactNode, type RefObject } from 'react';
2
+ import { flushSync } from 'react-dom';
2
3
  import clsx from 'clsx';
3
4
  import { Search } from 'lucide-react';
4
5
  import { useTranslation } from 'react-i18next';
@@ -16,6 +17,7 @@ interface PaletteRefs {
16
17
  input: RefObject<HTMLInputElement | null>;
17
18
  /* For the root to scroll back to the top when the app sets the query. */
18
19
  list: RefObject<HTMLDivElement | null>;
20
+ highlight(row: Element): void;
19
21
  }
20
22
 
21
23
  const PaletteRefsContext = createContext<PaletteRefs | null>(null);
@@ -52,6 +54,7 @@ export function CommandPaletteRoot<ItemValue>({
52
54
  onOpenChangeComplete,
53
55
  autoHighlight = 'always',
54
56
  keepHighlight = true,
57
+ highlightItemOnHover = true,
55
58
  value,
56
59
  onValueChange,
57
60
  items,
@@ -60,6 +63,7 @@ export function CommandPaletteRoot<ItemValue>({
60
63
  }: CommandPaletteRootProps<ItemValue>) {
61
64
  const inputRef = useRef<HTMLInputElement>(null);
62
65
  const listRef = useRef<HTMLDivElement>(null);
66
+ const [focusHighlight, setFocusHighlight] = useState(false);
63
67
  /*
64
68
  * Base UI moves the highlight to the first row for a typed query only. For a query the app sets,
65
69
  * the list is empty for one commit that is never painted: Base UI drops the highlight then and
@@ -80,9 +84,27 @@ export function CommandPaletteRoot<ItemValue>({
80
84
  setSettledValue(value);
81
85
  }, [resetting, value]);
82
86
 
87
+ const highlight = (row: Element): void => {
88
+ if (row.hasAttribute('data-highlighted')) {
89
+ return;
90
+ }
91
+ // Base UI has no imperative highlight action and disables highlighting on item focus.
92
+ // Enable pointer navigation just for this event when the app has disabled hover highlighting.
93
+ if (!highlightItemOnHover) {
94
+ flushSync(() => setFocusHighlight(true));
95
+ }
96
+ try {
97
+ row.dispatchEvent(new MouseEvent('mousemove', { bubbles: true, movementX: 1 }));
98
+ } finally {
99
+ if (!highlightItemOnHover) {
100
+ flushSync(() => setFocusHighlight(false));
101
+ }
102
+ }
103
+ };
104
+
83
105
  return (
84
106
  <DialogRoot open={open} defaultOpen={defaultOpen} onOpenChange={onOpenChange} onOpenChangeComplete={onOpenChangeComplete}>
85
- <PaletteRefsContext value={{ input: inputRef, list: listRef }}>
107
+ <PaletteRefsContext value={{ input: inputRef, list: listRef, highlight }}>
86
108
  {/* Its props take a flat list or groups; only its overloads split the two, which a generic caller cannot pick between. */}
87
109
  <Autocomplete.Root<ItemValue>
88
110
  {...props}
@@ -98,6 +120,7 @@ export function CommandPaletteRoot<ItemValue>({
98
120
  open
99
121
  autoHighlight={autoHighlight}
100
122
  keepHighlight={keepHighlight}
123
+ highlightItemOnHover={highlightItemOnHover || focusHighlight}
101
124
  >
102
125
  {children}
103
126
  </Autocomplete.Root>
@@ -159,7 +182,7 @@ export function CommandPaletteInput({ leading, trailing, escapeHint = true, mono
159
182
  }
160
183
 
161
184
  /* The rows. It scrolls on its own, so the field and the footer stay put. */
162
- export function CommandPaletteList({ className, ref, ...props }: ComponentProps<typeof Autocomplete.List>) {
185
+ export function CommandPaletteList({ className, ref, onFocus, ...props }: ComponentProps<typeof Autocomplete.List>) {
163
186
  const { t } = useTranslation('ui');
164
187
  const refs = useContext(PaletteRefsContext);
165
188
  return (
@@ -168,6 +191,15 @@ export function CommandPaletteList({ className, ref, ...props }: ComponentProps<
168
191
  aria-label={t('palette.results')}
169
192
  className={withClass('max-h-[50vh] min-h-0 overflow-y-auto overscroll-contain scroll-py-1.5 not-empty:p-1.5', className)}
170
193
  {...props}
194
+ onFocus={(event) => {
195
+ onFocus?.(event);
196
+ const row = event.target.closest('[role="option"]');
197
+ if (row === null || !event.currentTarget.contains(row) || refs?.input.current == null) {
198
+ return;
199
+ }
200
+ refs.highlight(row);
201
+ refs.input.current.focus({ preventScroll: true });
202
+ }}
171
203
  />
172
204
  );
173
205
  }