@bccampus/ui-components 0.6.1 → 0.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/dist/components/ui/button.d.ts +2 -1
  2. package/dist/components/ui/button.js +11 -2
  3. package/dist/components/ui/composite/CompositeDataItem.js +7 -12
  4. package/dist/components/ui/composite/FocusProvider/AbstractFocusProvider.d.ts +1 -0
  5. package/dist/components/ui/composite/FocusProvider/AbstractFocusProvider.js +1 -4
  6. package/dist/components/ui/composite/FocusProvider/ListboxFocusProvider.js +8 -16
  7. package/dist/components/ui/composite/SelectionProvider/AbstractSelectionProvider.d.ts +8 -0
  8. package/dist/components/ui/composite/SelectionProvider/AbstractSelectionProvider.js +4 -0
  9. package/dist/components/ui/composite/SelectionProvider/MultipleSelectionProvider.js +4 -0
  10. package/dist/components/ui/composite/SelectionProvider/SingleSelectionProvider.js +5 -0
  11. package/dist/components/ui/composite/composite-component-item.d.ts +1 -1
  12. package/dist/components/ui/composite/composite-component-item.js +5 -3
  13. package/dist/components/ui/composite/composite-component.d.ts +1 -1
  14. package/dist/components/ui/composite/composite-component.js +73 -25
  15. package/dist/components/ui/composite/listbox.d.ts +1 -1
  16. package/dist/components/ui/composite/listbox.js +39 -34
  17. package/dist/components/ui/composite/types.d.ts +13 -5
  18. package/dist/hooks/use-keyboard-event.d.ts +3 -3
  19. package/dist/hooks/use-keyboard-event.js +2 -2
  20. package/dist/hooks/use-required-ref.d.ts +1 -0
  21. package/dist/hooks/use-required-ref.js +8 -0
  22. package/package.json +1 -1
  23. package/src/components/ui/button.tsx +14 -3
  24. package/src/components/ui/composite/CompositeDataItem.ts +105 -110
  25. package/src/components/ui/composite/FocusProvider/AbstractFocusProvider.ts +8 -14
  26. package/src/components/ui/composite/FocusProvider/ListboxFocusProvider.ts +15 -29
  27. package/src/components/ui/composite/SelectionProvider/AbstractSelectionProvider.ts +22 -8
  28. package/src/components/ui/composite/SelectionProvider/MultipleSelectionProvider.ts +9 -5
  29. package/src/components/ui/composite/SelectionProvider/SingleSelectionProvider.ts +20 -7
  30. package/src/components/ui/composite/composite-component-item.tsx +3 -1
  31. package/src/components/ui/composite/composite-component.tsx +70 -15
  32. package/src/components/ui/composite/listbox.tsx +39 -36
  33. package/src/components/ui/composite/types.ts +69 -55
  34. package/src/hooks/use-keyboard-event.ts +13 -13
  35. package/src/hooks/use-required-ref.ts +6 -0
@@ -3,9 +3,10 @@ declare const buttonVariants: (props?: ({
3
3
  variant?: "item" | "default" | "secondary" | "outline" | "ghost" | "destructive" | null | undefined;
4
4
  block?: boolean | null | undefined;
5
5
  size?: "text" | "default" | "sm" | "lg" | "icon" | null | undefined;
6
+ icon?: boolean | null | undefined;
6
7
  } & import('class-variance-authority/dist/types').ClassProp) | undefined) => string;
7
8
  export type ButtonProps = React.ComponentProps<"button"> & VariantProps<typeof buttonVariants> & {
8
9
  asChild?: boolean;
9
10
  };
10
- declare function Button({ className, variant, size, block, asChild, ...props }: ButtonProps): import("react").JSX.Element;
11
+ declare function Button({ className, variant, size, block, icon, asChild, ...props }: ButtonProps): import("react").JSX.Element;
11
12
  export { Button, buttonVariants };
@@ -24,8 +24,17 @@ const buttonVariants = cva(
24
24
  lg: "h-10 text-base",
25
25
  icon: "size-9",
26
26
  text: "h-9 text-base"
27
+ },
28
+ icon: {
29
+ true: "py-1 px-1 has-[>svg]:px-1 gap-0 [&_svg:not([class*='size-'])]:size-full",
30
+ false: ""
27
31
  }
28
32
  },
33
+ compoundVariants: [
34
+ { size: ["sm"], icon: true, className: "size-8" },
35
+ { size: ["default", "icon", "text"], icon: true, className: "size-9" },
36
+ { size: ["lg"], icon: true, className: "size-10" }
37
+ ],
29
38
  defaultVariants: {
30
39
  size: "default",
31
40
  block: false,
@@ -33,9 +42,9 @@ const buttonVariants = cva(
33
42
  }
34
43
  }
35
44
  );
36
- function Button({ className, variant, size, block, asChild = false, ...props }) {
45
+ function Button({ className, variant, size, block, icon, asChild = false, ...props }) {
37
46
  const Comp = asChild ? Slot : "button";
38
- return /* @__PURE__ */ jsx(Comp, { "data-slot": "button", className: cn(buttonVariants({ size, block, variant }), className), ...props });
47
+ return /* @__PURE__ */ jsx(Comp, { "data-slot": "button", className: cn(buttonVariants({ size, block, variant, icon }), className), ...props });
39
48
  }
40
49
  export {
41
50
  Button,
@@ -35,29 +35,24 @@ class CompositeDataItem {
35
35
  }
36
36
  *[Symbol.iterator]() {
37
37
  if (this.key !== "ALL") yield this;
38
- if (this.children)
39
- for (const child of this.children)
40
- for (const item of child) yield item;
38
+ if (this.children) for (const child of this.children) for (const item of child) yield item;
41
39
  }
42
40
  toJSON() {
43
41
  const json = { ...this.data.get() };
44
42
  if (this.children) {
45
43
  json[this.childrenProp] = [];
46
- for (const child of this.children)
47
- json[this.childrenProp].push(child.toJSON());
44
+ for (const child of this.children) json[this.childrenProp].push(child.toJSON());
48
45
  }
49
46
  return json;
50
47
  }
51
48
  descendants() {
52
49
  if (!this.children) return [];
53
50
  const descendants = [];
54
- this.children.forEach(
55
- (child) => {
56
- descendants.push(child);
57
- const childDescendants = child.descendants();
58
- if (childDescendants) descendants.push(...childDescendants);
59
- }
60
- );
51
+ this.children.forEach((child) => {
52
+ descendants.push(child);
53
+ const childDescendants = child.descendants();
54
+ if (childDescendants) descendants.push(...childDescendants);
55
+ });
61
56
  return descendants;
62
57
  }
63
58
  ancestors() {
@@ -26,6 +26,7 @@ export declare abstract class AbstractFocusProvider<T extends object> implements
26
26
  isFocusable(itemAtom: CompositeDataItem<T>): boolean;
27
27
  focus(itemKey: CompositeItemKey): void;
28
28
  focus(item: CompositeDataItem<T>): void;
29
+ focus(item: CompositeDataItem<T> | CompositeItemKey | null): void;
29
30
  abstract setFocusPointers(): readonly [first: CompositeDataItem<T> | null, last: CompositeDataItem<T> | null];
30
31
  abstract focusUp(): void;
31
32
  abstract focusDown(): void;
@@ -11,16 +11,13 @@ class AbstractFocusProvider {
11
11
  this.dataOptions = dataOptions;
12
12
  const [first, last] = this.setFocusPointers();
13
13
  this.firstFocusableItem = first;
14
- if (this.firstFocusableItem) {
15
- this.firstFocusableItem.state.setKey("focused", true);
16
- this.focusedItem.set(this.firstFocusableItem);
17
- }
18
14
  this.lastFocusableItem = last;
19
15
  }
20
16
  isFocusable(itemAtom) {
21
17
  return !itemAtom.state.get().disabled;
22
18
  }
23
19
  focus(item) {
20
+ if (!item) return;
24
21
  const _item = item instanceof CompositeDataItem ? item : this.data.lookup.get(item);
25
22
  if (!_item) return;
26
23
  if (this.focusedItem.get()) {
@@ -21,36 +21,28 @@ class ListboxFocusProvider extends AbstractFocusProvider {
21
21
  return [first, last];
22
22
  }
23
23
  focusUp() {
24
- if (this.focusedItem.get()?.pointers.up) {
25
- this.focus(this.focusedItem.get().pointers.up);
26
- }
24
+ const focusedItem = this.focusedItem.get();
25
+ const focusTarget = focusedItem ? focusedItem.pointers.up : this.firstFocusableItem;
26
+ if (focusTarget) this.focus(focusTarget);
27
27
  }
28
28
  focusDown() {
29
- if (this.focusedItem.get()?.pointers.down) {
30
- this.focus(this.focusedItem.get().pointers.down);
31
- }
29
+ const focusedItem = this.focusedItem.get();
30
+ const focusTarget = focusedItem ? focusedItem.pointers.down : this.firstFocusableItem;
31
+ if (focusTarget) this.focus(focusTarget);
32
32
  }
33
33
  focusLeft() {
34
- throw new Error("Method not implemented.");
35
34
  }
36
35
  focusRight() {
37
- throw new Error("Method not implemented.");
38
36
  }
39
37
  focusPageUp() {
40
- throw new Error("Method not implemented.");
41
38
  }
42
39
  focesPageDown() {
43
- throw new Error("Method not implemented.");
44
40
  }
45
41
  focusToFirst() {
46
- if (this.firstFocusableItem) {
47
- this.focus(this.firstFocusableItem);
48
- }
42
+ this.focus(this.firstFocusableItem);
49
43
  }
50
44
  focusToLast() {
51
- if (this.lastFocusableItem) {
52
- this.focus(this.lastFocusableItem);
53
- }
45
+ this.focus(this.lastFocusableItem);
54
46
  }
55
47
  focusToFirstInRow() {
56
48
  throw new Error("Method not implemented.");
@@ -7,10 +7,17 @@ export interface SelectionProvider {
7
7
  deselect(itemKey?: CompositeItemKey, recursive?: boolean): void;
8
8
  toggleSelect(itemKey?: CompositeItemKey, recursive?: boolean): void;
9
9
  }
10
+ export interface SelectionProviderOptions<T extends object> {
11
+ onSelect?: (item: T) => void;
12
+ onDeselect?: (item: T) => void;
13
+ onChange?: (item: Set<CompositeItemKey>) => void;
14
+ }
10
15
  export declare abstract class AbstractSelectionProvider<T extends object> {
11
16
  protected data: CompositeData<T>;
12
17
  protected dataOptions: CompositeDataOptions<T>;
13
18
  selectedKeys: PreinitializedWritableAtom<Set<CompositeItemKey>>;
19
+ options?: SelectionProviderOptions<T>;
20
+ constructor(options?: SelectionProviderOptions<T>);
14
21
  init(data: CompositeData<T>, dataOptions: CompositeDataOptions<T>): void;
15
22
  abstract select(itemKey?: CompositeItemKey, recursive?: boolean): void;
16
23
  abstract select(item: CompositeDataItem<T>, recursive?: boolean): void;
@@ -20,4 +27,5 @@ export declare abstract class AbstractSelectionProvider<T extends object> {
20
27
  abstract deselect(item?: CompositeDataItem<T> | CompositeItemKey, recursive?: boolean): void;
21
28
  toggleSelect(itemKey?: CompositeItemKey, recursive?: boolean): void;
22
29
  toggleSelect(item: CompositeDataItem<T>, recursive?: boolean): void;
30
+ toggleSelect(item: CompositeDataItem<T> | CompositeItemKey, recursive?: boolean): void;
23
31
  }
@@ -4,6 +4,10 @@ class AbstractSelectionProvider {
4
4
  data;
5
5
  dataOptions;
6
6
  selectedKeys = atom(/* @__PURE__ */ new Set());
7
+ options;
8
+ constructor(options) {
9
+ this.options = options;
10
+ }
7
11
  init(data, dataOptions) {
8
12
  this.data = data;
9
13
  this.dataOptions = dataOptions;
@@ -6,13 +6,17 @@ class MultipleSelectionProvider extends AbstractSelectionProvider {
6
6
  const _item = item instanceof CompositeDataItem ? item : this.data.lookup.get(item);
7
7
  if (!_item) return;
8
8
  const selectedKeys = _item.select(recursive);
9
+ this.options?.onSelect?.(_item.data.get());
9
10
  this.selectedKeys.set(union(this.selectedKeys.get(), selectedKeys));
11
+ this.options?.onChange?.(this.selectedKeys.get());
10
12
  }
11
13
  deselect(item, recursive = false) {
12
14
  const _item = item instanceof CompositeDataItem ? item : this.data.lookup.get(item);
13
15
  if (!_item) return;
14
16
  const deselectedKeys = _item.deselect(recursive);
17
+ this.options?.onDeselect?.(_item.data.get());
15
18
  this.selectedKeys.set(difference(this.selectedKeys.get(), deselectedKeys));
19
+ this.options?.onChange?.(this.selectedKeys.get());
16
20
  }
17
21
  }
18
22
  export {
@@ -9,15 +9,20 @@ class SingleSelectionProvider extends AbstractSelectionProvider {
9
9
  const _item2 = this.data.lookup.get(selectedKey);
10
10
  if (!_item2) return;
11
11
  _item2.deselect(false);
12
+ this.options?.onDeselect?.(_item2.data.get());
12
13
  });
13
14
  const selectedKeys = _item.select(false);
15
+ this.options?.onSelect?.(_item.data.get());
14
16
  this.selectedKeys.set(new Set(selectedKeys));
17
+ this.options?.onChange?.(this.selectedKeys.get());
15
18
  }
16
19
  deselect(item, _recursive = false) {
17
20
  const _item = item ? item instanceof CompositeDataItem ? item : this.data.lookup.get(item) : this.data.focusProvider.focusedItem.get();
18
21
  if (!_item) return;
19
22
  const deselectedKeys = _item.deselect(false);
23
+ this.options?.onDeselect?.(_item.data.get());
20
24
  this.selectedKeys.set(difference(this.selectedKeys.get(), deselectedKeys));
25
+ this.options?.onChange?.(this.selectedKeys.get());
21
26
  }
22
27
  }
23
28
  export {
@@ -1,2 +1,2 @@
1
1
  import { CompositeItemProps } from './types';
2
- export declare function CompositeComponentItem<T extends object>({ id, className, item, role, itemMouseEventHandler, itemKeyboardEventHandler, render, }: CompositeItemProps<T>): import("react").JSX.Element;
2
+ export declare function CompositeComponentItem<T extends object>({ id, className, item, role, itemMouseEventHandler, itemKeyboardEventHandler, render, softFocus, }: CompositeItemProps<T>): import("react").JSX.Element;
@@ -8,7 +8,8 @@ function CompositeComponentItem({
8
8
  role,
9
9
  itemMouseEventHandler,
10
10
  itemKeyboardEventHandler,
11
- render
11
+ render,
12
+ softFocus
12
13
  }) {
13
14
  const data = useStore(item.data);
14
15
  const state = useStore(item.state);
@@ -26,7 +27,7 @@ function CompositeComponentItem({
26
27
  role,
27
28
  "aria-disabled": state.disabled,
28
29
  "data-key": item.key,
29
- tabIndex: !state.disabled ? state.focused ? 0 : -1 : void 0,
30
+ tabIndex: !softFocus && !state.disabled ? state.focused ? 0 : -1 : void 0,
30
31
  className,
31
32
  children: [
32
33
  render({ data, key: item.key, level: item.level }, state, handlers),
@@ -39,7 +40,8 @@ function CompositeComponentItem({
39
40
  render,
40
41
  className,
41
42
  itemMouseEventHandler,
42
- itemKeyboardEventHandler
43
+ itemKeyboardEventHandler,
44
+ softFocus
43
45
  },
44
46
  child.key
45
47
  ))
@@ -1,2 +1,2 @@
1
1
  import { CompositeProps } from './types';
2
- export declare function CompositeComponent<T extends object>({ data, variant, rootRole, itemRole, groupRole, renderItem, className, itemClassName, ref, handleRef, id, itemMouseEventHandler, itemKeyboardEventHandler, ...props }: CompositeProps<T>): import("react").JSX.Element;
2
+ export declare function CompositeComponent<T extends object>({ data, variant, rootRole, itemRole, groupRole, renderItem, className, itemClassName, rootRef, handleRef, id, itemMouseEventHandler, itemKeyboardEventHandler, initialFocus, softFocus, ...props }: CompositeProps<T>): import("react").JSX.Element;
@@ -1,7 +1,8 @@
1
1
  import { jsx } from "react/jsx-runtime";
2
- import { useImperativeHandle, useMemo } from "react";
2
+ import { useCallback, useImperativeHandle, useMemo, useEffect } from "react";
3
3
  import { CompositeComponentItem } from "./composite-component-item.js";
4
4
  import { useId } from "../../../hooks/use-id.js";
5
+ import { useRequiredRef } from "../../../hooks/use-required-ref.js";
5
6
  const defaultRoles = {
6
7
  listbox: {
7
8
  rootRole: "listbox",
@@ -15,6 +16,11 @@ const defaultRoles = {
15
16
  },
16
17
  custom: void 0
17
18
  };
19
+ const isOutsideElement = (parent, el) => {
20
+ if (parent === el) return false;
21
+ const nodeId = !!el?.id && CSS.escape(el.id);
22
+ return !nodeId || parent?.querySelector(`#${nodeId}`) === null;
23
+ };
18
24
  function CompositeComponent({
19
25
  data,
20
26
  variant,
@@ -24,42 +30,84 @@ function CompositeComponent({
24
30
  renderItem,
25
31
  className,
26
32
  itemClassName,
27
- ref,
33
+ rootRef,
28
34
  handleRef,
29
35
  id,
30
36
  itemMouseEventHandler,
31
37
  itemKeyboardEventHandler,
38
+ initialFocus = "None",
39
+ softFocus,
32
40
  ...props
33
41
  }) {
34
42
  const compositeId = useId(id);
35
- useImperativeHandle(
36
- handleRef,
37
- () => {
38
- return {
39
- focusProvider: data.focusProvider,
40
- selectionProvider: data.selectionProvider
41
- };
42
- },
43
- [data]
44
- );
43
+ const $rootRef = useRequiredRef(rootRef);
44
+ const focusElement = useCallback(() => {
45
+ if (softFocus) return;
46
+ const itemKey = data.focusProvider.focusedItem.get()?.key;
47
+ if (itemKey && $rootRef.current) {
48
+ const focusedItemEl = $rootRef.current.querySelector(`[data-key="${itemKey}"]`);
49
+ if (focusedItemEl) focusedItemEl.focus();
50
+ }
51
+ }, [softFocus, data.focusProvider.focusedItem, $rootRef]);
52
+ useImperativeHandle(handleRef, () => {
53
+ return {
54
+ focusProvider: data.focusProvider,
55
+ selectionProvider: data.selectionProvider,
56
+ focusElement
57
+ };
58
+ }, [data, focusElement]);
45
59
  const roles = useMemo(
46
60
  () => defaultRoles[variant] ?? { rootRole, groupRole, itemRole },
47
61
  [groupRole, itemRole, rootRole, variant]
48
62
  );
49
- return /* @__PURE__ */ jsx("div", { ref, id: compositeId, className, tabIndex: -1, role: roles.rootRole, ...props, children: [...data].map((item) => /* @__PURE__ */ jsx(
50
- CompositeComponentItem,
51
- {
52
- className: itemClassName,
53
- id: `${compositeId}-${item.key}`,
54
- role: roles.itemRole,
55
- groupRole: roles.groupRole,
56
- item,
57
- render: renderItem,
58
- itemMouseEventHandler,
59
- itemKeyboardEventHandler
63
+ const intiFocus = useCallback(
64
+ (onlyData) => {
65
+ let initialFocusItem = null;
66
+ if (initialFocus === "FirstItem") initialFocusItem = data.focusProvider.firstFocusableItem;
67
+ else if (initialFocus === "SelectedItem") {
68
+ const selecedItem = data.selectionProvider?.selectedKeys.get().values().next().value;
69
+ initialFocusItem = selecedItem ?? data.focusProvider.firstFocusableItem;
70
+ }
71
+ data.focusProvider.focus(initialFocusItem);
72
+ if (!onlyData) focusElement();
60
73
  },
61
- item.key
62
- )) });
74
+ [data.focusProvider, data.selectionProvider?.selectedKeys, focusElement, initialFocus]
75
+ );
76
+ const onFocusHandler = useCallback(
77
+ (ev) => {
78
+ const fromOut = isOutsideElement($rootRef.current, ev.relatedTarget);
79
+ if (fromOut) intiFocus(false);
80
+ },
81
+ [$rootRef, intiFocus]
82
+ );
83
+ useEffect(() => intiFocus(true), [intiFocus]);
84
+ return /* @__PURE__ */ jsx(
85
+ "div",
86
+ {
87
+ ref: $rootRef,
88
+ id: compositeId,
89
+ className,
90
+ tabIndex: initialFocus === "None" ? 0 : -1,
91
+ role: roles.rootRole,
92
+ ...props,
93
+ onFocus: onFocusHandler,
94
+ children: [...data].map((item) => /* @__PURE__ */ jsx(
95
+ CompositeComponentItem,
96
+ {
97
+ className: itemClassName,
98
+ id: `${compositeId}-${item.key}`,
99
+ role: roles.itemRole,
100
+ groupRole: roles.groupRole,
101
+ item,
102
+ render: renderItem,
103
+ itemMouseEventHandler,
104
+ itemKeyboardEventHandler,
105
+ softFocus
106
+ },
107
+ item.key
108
+ ))
109
+ }
110
+ );
63
111
  }
64
112
  export {
65
113
  CompositeComponent
@@ -1,2 +1,2 @@
1
1
  import { BaseCompositeProps } from './types';
2
- export declare function Listbox<T extends object>({ data, ...props }: BaseCompositeProps<T>): import("react").JSX.Element;
2
+ export declare function Listbox<T extends object>({ data, rootRef, handleRef, initialFocus, ...props }: BaseCompositeProps<T>): import("react").JSX.Element;
@@ -1,53 +1,58 @@
1
1
  import { jsx } from "react/jsx-runtime";
2
2
  import { CompositeComponent } from "./composite-component.js";
3
- import { useRef, useCallback } from "react";
3
+ import { useCallback } from "react";
4
4
  import "nanostores";
5
5
  import { useKeyboardEvent } from "../../../hooks/use-keyboard-event.js";
6
- function Listbox({ data, ...props }) {
7
- const compositeRef = useRef(null);
8
- const focusElement = useCallback(() => {
9
- const itemKey = data.focusProvider.focusedItem.get()?.key;
10
- if (itemKey && compositeRef.current) {
11
- const focusedItemEl = compositeRef.current.querySelector(`[data-key="${itemKey}"]`);
12
- if (focusedItemEl) focusedItemEl.focus();
13
- }
14
- }, [data]);
15
- const handleKeyboardEvent = useKeyboardEvent({
16
- ArrowUp: () => {
17
- data.focusProvider.focusUp();
18
- focusElement();
19
- },
20
- ArrowDown: () => {
21
- data.focusProvider.focusDown();
22
- focusElement();
23
- },
24
- Home: () => {
25
- data.focusProvider.focusToFirst();
26
- focusElement();
27
- },
28
- End: () => {
29
- data.focusProvider.focusToLast();
30
- focusElement();
6
+ import { useRequiredRef } from "../../../hooks/use-required-ref.js";
7
+ function Listbox({
8
+ data,
9
+ rootRef,
10
+ handleRef,
11
+ initialFocus = "SelectedItem",
12
+ ...props
13
+ }) {
14
+ const $handleRef = useRequiredRef(handleRef);
15
+ const handleKeyboardEvent = useKeyboardEvent(
16
+ {
17
+ ArrowUp: () => {
18
+ data.focusProvider.focusUp();
19
+ $handleRef.current?.focusElement();
20
+ },
21
+ ArrowDown: () => {
22
+ data.focusProvider.focusDown();
23
+ $handleRef.current?.focusElement();
24
+ },
25
+ Home: () => {
26
+ data.focusProvider.focusToFirst();
27
+ $handleRef.current?.focusElement();
28
+ },
29
+ End: () => {
30
+ data.focusProvider.focusToLast();
31
+ $handleRef.current?.focusElement();
32
+ },
33
+ Space: () => {
34
+ data.selectionProvider?.toggleSelect();
35
+ $handleRef.current?.focusElement();
36
+ }
31
37
  },
32
- Space: () => {
33
- data.selectionProvider?.toggleSelect();
34
- focusElement();
35
- }
36
- });
38
+ [$handleRef, data.focusProvider]
39
+ );
37
40
  const handleItemMouseEvent = useCallback(
38
41
  (item) => {
39
42
  data.focusProvider.focus(item.key);
40
43
  data.selectionProvider?.toggleSelect(item);
41
- focusElement();
44
+ $handleRef.current?.focusElement();
42
45
  },
43
- [data.focusProvider, data.selectionProvider, focusElement]
46
+ [$handleRef, data.focusProvider, data.selectionProvider]
44
47
  );
45
48
  return /* @__PURE__ */ jsx(
46
49
  CompositeComponent,
47
50
  {
48
- ref: compositeRef,
51
+ rootRef,
52
+ handleRef: $handleRef,
49
53
  variant: "listbox",
50
54
  data,
55
+ initialFocus,
51
56
  onKeyDown: handleKeyboardEvent,
52
57
  itemMouseEventHandler: handleItemMouseEvent,
53
58
  ...props
@@ -5,21 +5,22 @@ import { AbstractSelectionProvider, SelectionProvider } from './SelectionProvide
5
5
  import { CompositeData } from './CompositeData';
6
6
  export type CompositeItemKey = string | number;
7
7
  export type CompositeRoles = {
8
- variant: 'listbox';
8
+ variant: "listbox";
9
9
  rootRole?: never;
10
10
  itemRole?: never;
11
11
  groupRole?: never;
12
12
  } | {
13
- variant: 'grid';
13
+ variant: "grid";
14
14
  rootRole?: never;
15
15
  itemRole?: never;
16
16
  groupRole?: never;
17
17
  } | {
18
- variant: 'custom';
18
+ variant: "custom";
19
19
  rootRole: AriaRole;
20
20
  itemRole: AriaRole;
21
21
  groupRole: AriaRole;
22
22
  };
23
+ export type InitialFocusTarget = "None" | "LastFocusedItem" | "SelectedItem" | "FirstItem";
23
24
  export interface CompositeOptions {
24
25
  disabledKeys?: CompositeItemKey[];
25
26
  selectedKeys?: CompositeItemKey[];
@@ -55,14 +56,20 @@ export interface CompositeItemEventHandlerFunctions<T extends object> {
55
56
  export interface BaseCompositeProps<T extends object> extends React.ComponentPropsWithoutRef<"div"> {
56
57
  data: CompositeData<T>;
57
58
  className?: string;
58
- ref?: React.Ref<HTMLDivElement>;
59
- handleRef?: React.Ref<CompositeHandle>;
59
+ rootRef?: React.RefObject<HTMLDivElement | null>;
60
+ handleRef?: React.RefObject<CompositeHandle | null>;
60
61
  renderItem: CompositeItemRenderFn<T>;
61
62
  itemClassName?: string;
63
+ initialFocus?: InitialFocusTarget;
64
+ /**
65
+ * Set `item.focused = true`, but not focus on the HTMLElement
66
+ */
67
+ softFocus?: boolean;
62
68
  }
63
69
  export type CompositeProps<T extends object> = BaseCompositeProps<T> & CompositeItemEventHandlerFunctions<T> & CompositeRoles;
64
70
  export interface CompositeHandle {
65
71
  focusProvider: FocusProvider;
72
+ focusElement: () => void;
66
73
  selectionProvider?: SelectionProvider;
67
74
  }
68
75
  export type CompositeItemRenderFn<T extends object> = (item: {
@@ -78,5 +85,6 @@ export interface CompositeItemProps<T extends object> extends CompositeItemEvent
78
85
  item: CompositeDataItem<T>;
79
86
  remove?: () => void;
80
87
  render: CompositeItemRenderFn<T>;
88
+ softFocus?: boolean;
81
89
  }
82
90
  export {};
@@ -3,7 +3,7 @@ interface KeyBindings {
3
3
  [sequence: string]: (event: KeyboardEvent) => void;
4
4
  }
5
5
  interface UseKeyboardEventOptions {
6
- eventKeyProp: 'key' | 'code';
6
+ eventKeyProp: "key" | "code";
7
7
  }
8
8
  export declare function keyboardEventHandler(bindings: KeyBindings, options?: UseKeyboardEventOptions): KeyboardEventHandler;
9
9
  /**
@@ -44,7 +44,7 @@ export declare function keyboardEventHandler(bindings: KeyBindings, options?: Us
44
44
  * 'escape': clearInput,
45
45
  * 'ctrl+c': clearInput,
46
46
  * 'ctrl + shift + c': deleteAll,
47
- * });
47
+ * },[]);
48
48
  *
49
49
  * return (
50
50
  * <input
@@ -55,5 +55,5 @@ export declare function keyboardEventHandler(bindings: KeyBindings, options?: Us
55
55
  * }
56
56
  * ```
57
57
  */
58
- export declare function useKeyboardEvent(bindings: KeyBindings, options?: UseKeyboardEventOptions): KeyboardEventHandler;
58
+ export declare function useKeyboardEvent(bindings: KeyBindings, deps: React.DependencyList, options?: UseKeyboardEventOptions): KeyboardEventHandler;
59
59
  export {};
@@ -43,8 +43,8 @@ function keyboardEventHandler(bindings, options = defaultOptions) {
43
43
  }
44
44
  };
45
45
  }
46
- function useKeyboardEvent(bindings, options) {
47
- return useMemo(() => keyboardEventHandler(bindings, options), [bindings, options]);
46
+ function useKeyboardEvent(bindings, deps, options) {
47
+ return useMemo(() => keyboardEventHandler(bindings, options), [deps]);
48
48
  }
49
49
  export {
50
50
  keyboardEventHandler,
@@ -0,0 +1 @@
1
+ export declare function useRequiredRef<T>(ref?: React.RefObject<T | null>): import('react').RefObject<T | null>;
@@ -0,0 +1,8 @@
1
+ import { useRef } from "react";
2
+ function useRequiredRef(ref) {
3
+ const emptyRef = useRef(null);
4
+ return ref ?? emptyRef;
5
+ }
6
+ export {
7
+ useRequiredRef
8
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bccampus/ui-components",
3
- "version": "0.6.1",
3
+ "version": "0.7.1",
4
4
  "type": "module",
5
5
  "packageManager": "yarn@4.10.3",
6
6
  "exports": {
@@ -30,13 +30,22 @@ const buttonVariants = cva(
30
30
  icon: "size-9",
31
31
  text: "h-9 text-base",
32
32
  },
33
+ icon: {
34
+ true: "py-1 px-1 has-[>svg]:px-1 gap-0 [&_svg:not([class*='size-'])]:size-full",
35
+ false: "",
36
+ },
33
37
  },
38
+ compoundVariants: [
39
+ { size: ["sm"], icon: true, className: "size-8" },
40
+ { size: ["default", "icon", "text"], icon: true, className: "size-9" },
41
+ { size: ["lg"], icon: true, className: "size-10" },
42
+ ],
34
43
  defaultVariants: {
35
44
  size: "default",
36
45
  block: false,
37
46
  variant: "default",
38
47
  },
39
- }
48
+ },
40
49
  );
41
50
 
42
51
  export type ButtonProps = React.ComponentProps<"button"> &
@@ -44,10 +53,12 @@ export type ButtonProps = React.ComponentProps<"button"> &
44
53
  asChild?: boolean;
45
54
  };
46
55
 
47
- function Button({ className, variant, size, block, asChild = false, ...props }: ButtonProps) {
56
+ function Button({ className, variant, size, block, icon, asChild = false, ...props }: ButtonProps) {
48
57
  const Comp = asChild ? Slot : "button";
49
58
 
50
- return <Comp data-slot="button" className={cn(buttonVariants({ size, block, variant }), className)} {...props} />;
59
+ return (
60
+ <Comp data-slot="button" className={cn(buttonVariants({ size, block, variant, icon }), className)} {...props} />
61
+ );
51
62
  }
52
63
 
53
64
  export { Button, buttonVariants };