@choice-ui/react 1.9.1 → 1.9.3

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.
@@ -2,6 +2,11 @@ import { MenuButton, MenuContextContent, MenuContextItem, MenuContextLabel, Menu
2
2
  import { FloatingFocusManagerProps, Placement } from '@floating-ui/react';
3
3
  import { default as React } from 'react';
4
4
  export interface DropdownProps {
5
+ /**
6
+ * Active index for keyboard navigation (controlled).
7
+ * When provided, the component becomes controlled for activeIndex.
8
+ */
9
+ activeIndex?: number | null;
5
10
  /**
6
11
  * Whether to automatically select the first item in coordinate mode.
7
12
  * @default true
@@ -14,10 +19,21 @@ export interface DropdownProps {
14
19
  */
15
20
  avoidCollisions?: boolean;
16
21
  children?: React.ReactNode;
22
+ /**
23
+ * Disable internal keyboard navigation.
24
+ * Useful when implementing custom keyboard handling externally.
25
+ * @default false
26
+ */
27
+ disableKeyboardNavigation?: boolean;
17
28
  disabledNested?: boolean;
18
29
  focusManagerProps?: Partial<FloatingFocusManagerProps>;
19
30
  matchTriggerWidth?: boolean;
20
31
  offset?: number;
32
+ /**
33
+ * Callback when active index changes.
34
+ * Use with activeIndex prop for controlled keyboard navigation.
35
+ */
36
+ onActiveIndexChange?: (index: number | null) => void;
21
37
  onOpenChange?: (open: boolean) => void;
22
38
  open?: boolean;
23
39
  placement?: Placement;
@@ -12,8 +12,10 @@ const DEFAULT_OFFSET = 4;
12
12
  const DropdownComponent = memo(function DropdownComponent2(props) {
13
13
  const {
14
14
  children,
15
+ activeIndex: controlledActiveIndex,
15
16
  autoSelectFirstItem = true,
16
17
  avoidCollisions = true,
18
+ disableKeyboardNavigation = false,
17
19
  disabledNested = false,
18
20
  offset: offsetDistance = DEFAULT_OFFSET,
19
21
  placement = "bottom-start",
@@ -23,6 +25,7 @@ const DropdownComponent = memo(function DropdownComponent2(props) {
23
25
  selection = false,
24
26
  matchTriggerWidth = false,
25
27
  open: controlledOpen,
28
+ onActiveIndexChange,
26
29
  onOpenChange,
27
30
  triggerRef,
28
31
  triggerSelector,
@@ -39,10 +42,12 @@ const DropdownComponent = memo(function DropdownComponent2(props) {
39
42
  const { scrollRef, elementsRef, labelsRef, selectTimeoutRef } = useMenuBaseRefs();
40
43
  const [isOpen, setIsOpen] = useState(false);
41
44
  const [hasFocusInside, setHasFocusInside] = useState(false);
42
- const [activeIndex, setActiveIndex] = useState(null);
45
+ const [internalActiveIndex, setInternalActiveIndex] = useState(null);
43
46
  const [scrollTop, setScrollTop] = useState(0);
44
47
  const [touch, setTouch] = useState(false);
45
48
  const [isMouseOverMenu, setIsMouseOverMenu] = useState(false);
49
+ const isControlledActiveIndex = controlledActiveIndex !== void 0;
50
+ const activeIndex = isControlledActiveIndex ? controlledActiveIndex : internalActiveIndex;
46
51
  const isCoordinateMode = position !== null && position !== void 0;
47
52
  const isControlledOpen = isCoordinateMode ? controlledOpen || false : controlledOpen === void 0 ? isOpen : controlledOpen;
48
53
  const baseId = useId();
@@ -119,6 +124,12 @@ const DropdownComponent = memo(function DropdownComponent2(props) {
119
124
  middleware,
120
125
  whileElementsMounted: autoUpdate
121
126
  });
127
+ const handleSetActiveIndex = useEventCallback((index) => {
128
+ if (!isControlledActiveIndex) {
129
+ setInternalActiveIndex(index);
130
+ }
131
+ onActiveIndexChange == null ? void 0 : onActiveIndexChange(index);
132
+ });
122
133
  useIsomorphicLayoutEffect(() => {
123
134
  if (position && isCoordinateMode && isControlledOpen && (!lastPositionRef.current || lastPositionRef.current.x !== position.x || lastPositionRef.current.y !== position.y)) {
124
135
  setVirtualPosition(position);
@@ -127,14 +138,14 @@ const DropdownComponent = memo(function DropdownComponent2(props) {
127
138
  }, [position, isCoordinateMode, isControlledOpen, setVirtualPosition]);
128
139
  useEffect(() => {
129
140
  if (isCoordinateMode && isControlledOpen && activeIndex === null && !isMouseOverMenu) {
130
- setActiveIndex(autoSelectFirstItem ? 0 : null);
141
+ handleSetActiveIndex(autoSelectFirstItem ? 0 : null);
131
142
  }
132
- }, [isCoordinateMode, isControlledOpen, activeIndex, isMouseOverMenu, autoSelectFirstItem]);
143
+ }, [isCoordinateMode, isControlledOpen, activeIndex, isMouseOverMenu, autoSelectFirstItem, handleSetActiveIndex]);
133
144
  useEffect(() => {
134
145
  if (isCoordinateMode && !isControlledOpen) {
135
- setActiveIndex(null);
146
+ handleSetActiveIndex(null);
136
147
  }
137
- }, [isCoordinateMode, isControlledOpen]);
148
+ }, [isCoordinateMode, isControlledOpen, handleSetActiveIndex]);
138
149
  const isOpenRef = useRef(isControlledOpen);
139
150
  isOpenRef.current = isControlledOpen;
140
151
  useEffect(() => {
@@ -170,7 +181,7 @@ const DropdownComponent = memo(function DropdownComponent2(props) {
170
181
  escapeKey: true
171
182
  });
172
183
  const handleNavigate = useEventCallback((index) => {
173
- setActiveIndex(index);
184
+ handleSetActiveIndex(index);
174
185
  if (tree && index !== null) {
175
186
  tree.events.emit("navigate", { nodeId, index });
176
187
  }
@@ -180,12 +191,14 @@ const DropdownComponent = memo(function DropdownComponent2(props) {
180
191
  activeIndex,
181
192
  nested: isNested,
182
193
  onNavigate: handleNavigate,
183
- loop: true
194
+ loop: true,
195
+ enabled: !disableKeyboardNavigation
184
196
  });
185
197
  const typeahead = useTypeahead(context, {
186
198
  listRef: labelsRef,
187
- onMatch: isControlledOpen ? setActiveIndex : void 0,
188
- activeIndex
199
+ onMatch: isControlledOpen ? handleSetActiveIndex : void 0,
200
+ activeIndex,
201
+ enabled: !disableKeyboardNavigation
189
202
  });
190
203
  const { getReferenceProps, getFloatingProps, getItemProps } = useInteractions([
191
204
  hover,
@@ -278,7 +291,7 @@ const DropdownComponent = memo(function DropdownComponent2(props) {
278
291
  const contextValue = useMemo(
279
292
  () => ({
280
293
  activeIndex,
281
- setActiveIndex,
294
+ setActiveIndex: handleSetActiveIndex,
282
295
  getItemProps,
283
296
  setHasFocusInside,
284
297
  isOpen: isControlledOpen,
@@ -287,7 +300,7 @@ const DropdownComponent = memo(function DropdownComponent2(props) {
287
300
  close: handleClose,
288
301
  variant
289
302
  }),
290
- [activeIndex, getItemProps, handleClose, isControlledOpen, readOnly, selection, variant]
303
+ [activeIndex, getItemProps, handleClose, handleSetActiveIndex, isControlledOpen, readOnly, selection, variant]
291
304
  );
292
305
  const parentActiveIndex = parent == null ? void 0 : parent.activeIndex;
293
306
  const parentGetItemProps = parent == null ? void 0 : parent.getItemProps;
@@ -67,7 +67,7 @@ interface ListProps extends Omit<HTMLProps<HTMLDivElement>, "size" | "as"> {
67
67
  selection?: boolean;
68
68
  shouldShowReferenceLine?: boolean;
69
69
  size?: "default" | "large";
70
- variant?: "default" | "primary";
70
+ variant?: "default" | "primary" | "dark" | "reset";
71
71
  }
72
72
  interface ListComponentProps extends React.ForwardRefExoticComponent<ListProps & React.RefAttributes<HTMLDivElement>> {
73
73
  Content: typeof ListContent;
@@ -6,7 +6,7 @@ import { tcx } from "../../../../shared/utils/tcx/tcx.js";
6
6
  const ListContent = forwardRef((props, ref) => {
7
7
  const { as: As = "div", children, className, parentId, ...rest } = props;
8
8
  const { isSubListExpanded } = useExpandContext();
9
- const { itemsMap, shouldShowReferenceLine, size } = useStructureContext();
9
+ const { itemsMap, shouldShowReferenceLine, size, variant } = useStructureContext();
10
10
  const level = useMemo(() => {
11
11
  if (!parentId) return 0;
12
12
  let currentLevel = 1;
@@ -32,7 +32,8 @@ const ListContent = forwardRef((props, ref) => {
32
32
  const tv = ListContentTv({
33
33
  showReferenceLine: shouldShowReferenceLine,
34
34
  level: safeLevel,
35
- size
35
+ size,
36
+ variant
36
37
  });
37
38
  return /* @__PURE__ */ jsx(LevelContext.Provider, { value: { level: safeLevel }, children: /* @__PURE__ */ jsx(
38
39
  As,
@@ -1,11 +1,13 @@
1
1
  import { jsx } from "react/jsx-runtime";
2
2
  import { forwardRef } from "react";
3
3
  import { ListDividerTv } from "../tv.js";
4
+ import { useStructureContext } from "../context/list-context.js";
4
5
  import { tcx } from "../../../../shared/utils/tcx/tcx.js";
5
6
  const ListDivider = forwardRef(
6
7
  (props, ref) => {
7
8
  const { className, ...rest } = props;
8
- const tv = ListDividerTv();
9
+ const { variant = "default" } = useStructureContext();
10
+ const tv = ListDividerTv({ variant });
9
11
  return /* @__PURE__ */ jsx(
10
12
  "div",
11
13
  {
@@ -28,7 +28,7 @@ interface StructureContextValue {
28
28
  shouldShowReferenceLine?: boolean;
29
29
  size?: "default" | "large";
30
30
  unregisterItem: (id: string) => void;
31
- variant?: "default" | "primary";
31
+ variant?: "default" | "primary" | "dark" | "reset";
32
32
  }
33
33
  export declare const StructureContext: import('react').Context<StructureContextValue | undefined>;
34
34
  export declare function useStructureContext(): StructureContextValue;
@@ -5,7 +5,7 @@ interface ListProviderProps {
5
5
  selection?: boolean;
6
6
  shouldShowReferenceLine?: boolean;
7
7
  size?: "default" | "large";
8
- variant?: "default" | "primary";
8
+ variant?: "default" | "primary" | "dark" | "reset";
9
9
  }
10
10
  export declare function ListProvider({ children, interactive, shouldShowReferenceLine, selection, variant, size, }: ListProviderProps): import("react/jsx-runtime").JSX.Element;
11
11
  export {};
@@ -12,7 +12,7 @@ export interface ListProps extends Omit<HTMLProps<HTMLDivElement>, "size" | "as"
12
12
  selection?: boolean;
13
13
  shouldShowReferenceLine?: boolean;
14
14
  size?: "default" | "large";
15
- variant?: "default" | "primary";
15
+ variant?: "default" | "primary" | "dark" | "reset";
16
16
  }
17
17
  interface ListComponentProps extends React.ForwardRefExoticComponent<ListProps & React.RefAttributes<HTMLDivElement>> {
18
18
  Content: typeof ListContent;
@@ -28,6 +28,8 @@ export declare const ListItemTv: import('tailwind-variants').TVReturnType<{
28
28
  variant: {
29
29
  default: {};
30
30
  primary: {};
31
+ dark: {};
32
+ reset: {};
31
33
  };
32
34
  size: {
33
35
  default: {
@@ -78,6 +80,8 @@ export declare const ListItemTv: import('tailwind-variants').TVReturnType<{
78
80
  variant: {
79
81
  default: {};
80
82
  primary: {};
83
+ dark: {};
84
+ reset: {};
81
85
  };
82
86
  size: {
83
87
  default: {
@@ -128,6 +132,8 @@ export declare const ListItemTv: import('tailwind-variants').TVReturnType<{
128
132
  variant: {
129
133
  default: {};
130
134
  primary: {};
135
+ dark: {};
136
+ reset: {};
131
137
  };
132
138
  size: {
133
139
  default: {
@@ -167,33 +173,51 @@ export declare const ListLabelTv: import('tailwind-variants').TVReturnType<{
167
173
  };
168
174
  }, undefined, "flex h-6 w-full flex-none items-center gap-2 opacity-50", unknown, unknown, undefined>>;
169
175
  export declare const ListDividerTv: import('tailwind-variants').TVReturnType<{
170
- [key: string]: {
171
- [key: string]: import('tailwind-merge').ClassNameValue | {
172
- root?: import('tailwind-merge').ClassNameValue;
173
- divider?: import('tailwind-merge').ClassNameValue;
176
+ variant: {
177
+ default: {
178
+ divider: string;
174
179
  };
175
- };
176
- } | {
177
- [x: string]: {
178
- [x: string]: import('tailwind-merge').ClassNameValue | {
179
- root?: import('tailwind-merge').ClassNameValue;
180
- divider?: import('tailwind-merge').ClassNameValue;
180
+ primary: {
181
+ divider: string;
182
+ };
183
+ dark: {
184
+ divider: string;
181
185
  };
186
+ reset: {};
182
187
  };
183
- } | {}, {
188
+ }, {
184
189
  root: string;
185
190
  divider: string;
186
191
  }, undefined, {
187
- [key: string]: {
188
- [key: string]: import('tailwind-merge').ClassNameValue | {
189
- root?: import('tailwind-merge').ClassNameValue;
190
- divider?: import('tailwind-merge').ClassNameValue;
192
+ variant: {
193
+ default: {
194
+ divider: string;
195
+ };
196
+ primary: {
197
+ divider: string;
191
198
  };
199
+ dark: {
200
+ divider: string;
201
+ };
202
+ reset: {};
192
203
  };
193
- } | {}, {
204
+ }, {
194
205
  root: string;
195
206
  divider: string;
196
- }, import('tailwind-variants').TVReturnType<unknown, {
207
+ }, import('tailwind-variants').TVReturnType<{
208
+ variant: {
209
+ default: {
210
+ divider: string;
211
+ };
212
+ primary: {
213
+ divider: string;
214
+ };
215
+ dark: {
216
+ divider: string;
217
+ };
218
+ reset: {};
219
+ };
220
+ }, {
197
221
  root: string;
198
222
  divider: string;
199
223
  }, undefined, unknown, unknown, undefined>>;
@@ -206,6 +230,12 @@ export declare const ListContentTv: import('tailwind-variants').TVReturnType<{
206
230
  default: {};
207
231
  large: {};
208
232
  };
233
+ variant: {
234
+ default: {};
235
+ primary: {};
236
+ dark: {};
237
+ reset: {};
238
+ };
209
239
  level: {
210
240
  0: string;
211
241
  1: string;
@@ -223,6 +253,12 @@ export declare const ListContentTv: import('tailwind-variants').TVReturnType<{
223
253
  default: {};
224
254
  large: {};
225
255
  };
256
+ variant: {
257
+ default: {};
258
+ primary: {};
259
+ dark: {};
260
+ reset: {};
261
+ };
226
262
  level: {
227
263
  0: string;
228
264
  1: string;
@@ -240,6 +276,12 @@ export declare const ListContentTv: import('tailwind-variants').TVReturnType<{
240
276
  default: {};
241
277
  large: {};
242
278
  };
279
+ variant: {
280
+ default: {};
281
+ primary: {};
282
+ dark: {};
283
+ reset: {};
284
+ };
243
285
  level: {
244
286
  0: string;
245
287
  1: string;
@@ -46,7 +46,9 @@ const ListItemTv = tcv({
46
46
  },
47
47
  variant: {
48
48
  default: {},
49
- primary: {}
49
+ primary: {},
50
+ dark: {},
51
+ reset: {}
50
52
  },
51
53
  size: {
52
54
  default: {
@@ -91,6 +93,15 @@ const ListItemTv = tcv({
91
93
  shortcut: "text-default-foreground"
92
94
  }
93
95
  },
96
+ {
97
+ disabled: false,
98
+ active: true,
99
+ variant: "dark",
100
+ class: {
101
+ root: "bg-white/10",
102
+ shortcut: "text-white"
103
+ }
104
+ },
94
105
  {
95
106
  size: "default",
96
107
  hasPrefix: false,
@@ -208,9 +219,25 @@ const ListLabelTv = tcv({
208
219
  const ListDividerTv = tcv({
209
220
  slots: {
210
221
  root: "flex h-4 w-full flex-none items-center",
211
- divider: "bg-default-boundary h-px flex-1"
222
+ divider: "h-px flex-1"
212
223
  },
213
- defaultVariants: {}
224
+ variants: {
225
+ variant: {
226
+ default: {
227
+ divider: "bg-default-boundary"
228
+ },
229
+ primary: {
230
+ divider: "bg-default-boundary"
231
+ },
232
+ dark: {
233
+ divider: "bg-white/10"
234
+ },
235
+ reset: {}
236
+ }
237
+ },
238
+ defaultVariants: {
239
+ variant: "default"
240
+ }
214
241
  });
215
242
  const ListContentTv = tcv({
216
243
  base: "group/list flex flex-col gap-1",
@@ -223,6 +250,12 @@ const ListContentTv = tcv({
223
250
  default: {},
224
251
  large: {}
225
252
  },
253
+ variant: {
254
+ default: {},
255
+ primary: {},
256
+ dark: {},
257
+ reset: {}
258
+ },
226
259
  level: {
227
260
  0: "",
228
261
  1: "",
@@ -238,10 +271,17 @@ const ListContentTv = tcv({
238
271
  level: [1, 2, 3, 4, 5],
239
272
  class: [
240
273
  "relative",
241
- "before:absolute before:inset-y-0 before:z-1 before:w-px before:content-['']",
242
- "group-hover/list:before:bg-default-boundary"
274
+ "before:absolute before:inset-y-0 before:z-1 before:w-px before:content-['']"
243
275
  ]
244
276
  },
277
+ {
278
+ variant: ["default", "primary"],
279
+ class: "group-hover/list:before:bg-default-boundary"
280
+ },
281
+ {
282
+ variant: "dark",
283
+ class: "group-hover/list:before:bg-white/10"
284
+ },
245
285
  {
246
286
  size: "default",
247
287
  level: 1,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@choice-ui/react",
3
- "version": "1.9.1",
3
+ "version": "1.9.3",
4
4
  "description": "A desktop-first React UI component library built for professional desktop applications with comprehensive documentation",
5
5
  "sideEffects": false,
6
6
  "type": "module",