@bccampus/ui-components 0.7.0 → 0.7.2

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 (31) 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 +9 -2
  5. package/dist/components/ui/composite/FocusProvider/AbstractFocusProvider.js +9 -4
  6. package/dist/components/ui/composite/FocusProvider/ListboxFocusProvider.js +8 -16
  7. package/dist/components/ui/composite/SelectionProvider/AbstractSelectionProvider.d.ts +10 -1
  8. package/dist/components/ui/composite/SelectionProvider/AbstractSelectionProvider.js +3 -0
  9. package/dist/components/ui/composite/composite-component-item.d.ts +1 -1
  10. package/dist/components/ui/composite/composite-component-item.js +5 -3
  11. package/dist/components/ui/composite/composite-component.d.ts +1 -1
  12. package/dist/components/ui/composite/composite-component.js +73 -25
  13. package/dist/components/ui/composite/listbox.d.ts +1 -1
  14. package/dist/components/ui/composite/listbox.js +39 -34
  15. package/dist/components/ui/composite/types.d.ts +16 -8
  16. package/dist/hooks/use-keyboard-event.d.ts +3 -3
  17. package/dist/hooks/use-keyboard-event.js +2 -2
  18. package/dist/hooks/use-required-ref.d.ts +1 -0
  19. package/dist/hooks/use-required-ref.js +8 -0
  20. package/package.json +1 -1
  21. package/src/components/ui/button.tsx +14 -3
  22. package/src/components/ui/composite/CompositeDataItem.ts +105 -110
  23. package/src/components/ui/composite/FocusProvider/AbstractFocusProvider.ts +25 -16
  24. package/src/components/ui/composite/FocusProvider/ListboxFocusProvider.ts +15 -29
  25. package/src/components/ui/composite/SelectionProvider/AbstractSelectionProvider.ts +16 -1
  26. package/src/components/ui/composite/composite-component-item.tsx +3 -1
  27. package/src/components/ui/composite/composite-component.tsx +70 -15
  28. package/src/components/ui/composite/listbox.tsx +39 -36
  29. package/src/components/ui/composite/types.ts +70 -56
  30. package/src/hooks/use-keyboard-event.ts +13 -13
  31. package/src/hooks/use-required-ref.ts +6 -0
@@ -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 };
@@ -1,144 +1,139 @@
1
1
  import { omit } from "@/lib/object";
2
- import { map, type PreinitializedMapStore, } from "nanostores";
2
+ import { map, type PreinitializedMapStore } from "nanostores";
3
3
  import type { CompositeDataItemState, CompositeItemKey, CompositeDataItemOptions } from "./types";
4
4
 
5
5
  export class CompositeDataItem<T extends object> implements Iterable<CompositeDataItem<T>> {
6
- #key: CompositeItemKey;
7
- parent: CompositeDataItem<T> | null = null;
8
- children?: CompositeDataItem<T>[];
9
- level: number;
10
-
11
- data: PreinitializedMapStore<T>;
12
- state: PreinitializedMapStore<CompositeDataItemState>;
13
-
14
- pointers: {
15
- left?: CompositeItemKey;
16
- right?: CompositeItemKey;
17
- up?: CompositeItemKey;
18
- down?: CompositeItemKey;
19
- };
20
- childrenProp: string
21
-
22
- constructor(item: T, options: CompositeDataItemOptions<T>, parent: CompositeDataItem<T> | null) {
23
- this.#key = options.getItemKey(item);
24
- this.data = map(omit(item, [options.itemChildrenProp]));
25
- this.state = map({
26
- focused: false,
27
- selected: false,
28
- disabled: false,
29
- ...options.initialState
30
- })
31
- this.pointers = {};
32
- this.parent = parent;
33
- this.level = parent ? parent.level + 1 : 0;
34
-
35
- this.childrenProp = options.itemChildrenProp;
36
-
37
- const children = options.getItemChildren(item);
38
- if (children) {
39
- this.children = children.map(child => {
40
- const childItem = new CompositeDataItem(child, options, this);
41
- return childItem;
42
- })
43
- }
6
+ #key: CompositeItemKey;
7
+ parent: CompositeDataItem<T> | null = null;
8
+ children?: CompositeDataItem<T>[];
9
+ level: number;
10
+
11
+ data: PreinitializedMapStore<T>;
12
+ state: PreinitializedMapStore<CompositeDataItemState>;
13
+
14
+ pointers: {
15
+ left?: CompositeItemKey;
16
+ right?: CompositeItemKey;
17
+ up?: CompositeItemKey;
18
+ down?: CompositeItemKey;
19
+ };
20
+ childrenProp: string;
21
+
22
+ constructor(item: T, options: CompositeDataItemOptions<T>, parent: CompositeDataItem<T> | null) {
23
+ this.#key = options.getItemKey(item);
24
+ this.data = map(omit(item, [options.itemChildrenProp]));
25
+ this.state = map({
26
+ focused: false,
27
+ selected: false,
28
+ disabled: false,
29
+ ...options.initialState,
30
+ });
31
+ this.pointers = {};
32
+ this.parent = parent;
33
+ this.level = parent ? parent.level + 1 : 0;
34
+
35
+ this.childrenProp = options.itemChildrenProp;
36
+
37
+ const children = options.getItemChildren(item);
38
+ if (children) {
39
+ this.children = children.map((child) => {
40
+ const childItem = new CompositeDataItem(child, options, this);
41
+ return childItem;
42
+ });
44
43
  }
44
+ }
45
45
 
46
- get key() {
47
- return this.#key;
48
- }
49
-
50
- *[Symbol.iterator](): Iterator<CompositeDataItem<T>> {
51
- if (this.key !== "ALL") yield this;
52
- if (this.children)
53
- for (const child of this.children)
54
- for (const item of child) yield item;
55
- }
46
+ get key() {
47
+ return this.#key;
48
+ }
56
49
 
57
- toJSON() {
58
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
59
- const json: any = { ...this.data.get() };
50
+ *[Symbol.iterator](): Iterator<CompositeDataItem<T>> {
51
+ if (this.key !== "ALL") yield this;
52
+ if (this.children) for (const child of this.children) for (const item of child) yield item;
53
+ }
60
54
 
61
- if (this.children) {
62
- json[this.childrenProp] = [];
63
- for (const child of this.children)
64
- json[this.childrenProp].push(child.toJSON());
65
- }
55
+ toJSON() {
56
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
57
+ const json: any = { ...this.data.get() };
66
58
 
67
- return json as T;
59
+ if (this.children) {
60
+ json[this.childrenProp] = [];
61
+ for (const child of this.children) json[this.childrenProp].push(child.toJSON());
68
62
  }
69
63
 
70
- descendants() {
71
- if (!this.children) return [];
64
+ return json as T;
65
+ }
72
66
 
73
- const descendants: CompositeDataItem<T>[] = [];
74
- this.children.forEach((child) => {
75
- descendants.push(child);
76
- const childDescendants = child.descendants();
77
- if (childDescendants) descendants.push(...childDescendants);
78
- }
79
- )
67
+ descendants() {
68
+ if (!this.children) return [];
80
69
 
81
- return descendants;
82
- }
70
+ const descendants: CompositeDataItem<T>[] = [];
71
+ this.children.forEach((child) => {
72
+ descendants.push(child);
73
+ const childDescendants = child.descendants();
74
+ if (childDescendants) descendants.push(...childDescendants);
75
+ });
83
76
 
84
- ancestors() {
85
- const ancestors: CompositeDataItem<T>[] = [];
86
- let parent = this.parent;
87
- while (parent) {
88
- ancestors.push(parent);
89
- parent = parent.parent;
90
- }
77
+ return descendants;
78
+ }
91
79
 
92
- return ancestors;
80
+ ancestors() {
81
+ const ancestors: CompositeDataItem<T>[] = [];
82
+ let parent = this.parent;
83
+ while (parent) {
84
+ ancestors.push(parent);
85
+ parent = parent.parent;
93
86
  }
94
87
 
88
+ return ancestors;
89
+ }
95
90
 
96
- toggleSelect(recursive: boolean = false) {
97
- if (this.state.get().selected) this.deselect(recursive);
98
- else this.select(recursive);
99
- }
100
-
101
- select(recursive: boolean = false): CompositeItemKey[] {
102
- this.state.setKey("selected", true);
91
+ toggleSelect(recursive: boolean = false) {
92
+ if (this.state.get().selected) this.deselect(recursive);
93
+ else this.select(recursive);
94
+ }
103
95
 
104
- if (recursive && this.children) {
105
- const selectedChildKeys = this.children.map(child => child.select(true)).flat();
106
- return [this.key, ...selectedChildKeys];
107
- }
96
+ select(recursive: boolean = false): CompositeItemKey[] {
97
+ this.state.setKey("selected", true);
108
98
 
109
- return [this.key]
99
+ if (recursive && this.children) {
100
+ const selectedChildKeys = this.children.map((child) => child.select(true)).flat();
101
+ return [this.key, ...selectedChildKeys];
110
102
  }
111
103
 
112
- deselect(recursive: boolean = false): CompositeItemKey[] {
113
- this.state.setKey("selected", false);
104
+ return [this.key];
105
+ }
114
106
 
115
- if (recursive && this.children) {
116
- const deselectedChildKeys = this.children.map(child => child.deselect(true)).flat();
117
- return [this.key, ...deselectedChildKeys];
118
- }
107
+ deselect(recursive: boolean = false): CompositeItemKey[] {
108
+ this.state.setKey("selected", false);
119
109
 
120
- return [this.key];
110
+ if (recursive && this.children) {
111
+ const deselectedChildKeys = this.children.map((child) => child.deselect(true)).flat();
112
+ return [this.key, ...deselectedChildKeys];
121
113
  }
122
114
 
123
- disable(recursive: boolean = false): CompositeItemKey[] {
124
- this.state.setKey("disabled", true);
115
+ return [this.key];
116
+ }
125
117
 
126
- if (recursive && this.children) {
127
- const disabledChildKeys = this.children.map(child => child.disable(true)).flat();
128
- return [this.key, ...disabledChildKeys];
129
- }
118
+ disable(recursive: boolean = false): CompositeItemKey[] {
119
+ this.state.setKey("disabled", true);
130
120
 
131
- return [this.key]
121
+ if (recursive && this.children) {
122
+ const disabledChildKeys = this.children.map((child) => child.disable(true)).flat();
123
+ return [this.key, ...disabledChildKeys];
132
124
  }
133
125
 
134
- enable(recursive: boolean = false): CompositeItemKey[] {
135
- this.state.setKey("disabled", false);
126
+ return [this.key];
127
+ }
136
128
 
137
- if (recursive && this.children) {
138
- const enabledChildKeys = this.children.map(child => child.enable(true)).flat();
139
- return [this.key, ...enabledChildKeys];
140
- }
129
+ enable(recursive: boolean = false): CompositeItemKey[] {
130
+ this.state.setKey("disabled", false);
141
131
 
142
- return [this.key];
132
+ if (recursive && this.children) {
133
+ const enabledChildKeys = this.children.map((child) => child.enable(true)).flat();
134
+ return [this.key, ...enabledChildKeys];
143
135
  }
136
+
137
+ return [this.key];
138
+ }
144
139
  }
@@ -1,10 +1,15 @@
1
- import { atom, type PreinitializedWritableAtom } from 'nanostores';
2
- import type { CompositeData } from '../CompositeData';
3
- import { CompositeDataItem } from '../CompositeDataItem';
4
- import type { CompositeDataOptions, CompositeItemKey } from '../types';
1
+ import { atom, type PreinitializedWritableAtom } from "nanostores";
2
+ import type { CompositeData } from "../CompositeData";
3
+ import { CompositeDataItem } from "../CompositeDataItem";
4
+ import type { CompositeDataOptions, CompositeItemKey } from "../types";
5
+
6
+ export interface FocusProvider<T extends object> {
7
+ getFocusedItem(): CompositeDataItem<T> | null;
5
8
 
6
- export interface FocusProvider {
7
9
  focus(itemKey: CompositeItemKey): void;
10
+ focus(item: CompositeDataItem<T>): void;
11
+ focus(item: CompositeDataItem<T> | CompositeItemKey | null): void;
12
+ blur(): void;
8
13
 
9
14
  focusUp(): void;
10
15
  focusDown(): void;
@@ -19,7 +24,7 @@ export interface FocusProvider {
19
24
  focusToTypeAheadMatch(): void;
20
25
  }
21
26
 
22
- export abstract class AbstractFocusProvider<T extends object> implements FocusProvider {
27
+ export abstract class AbstractFocusProvider<T extends object> implements FocusProvider<T> {
23
28
  protected data!: CompositeData<T>;
24
29
  protected dataOptions!: CompositeDataOptions<T>;
25
30
 
@@ -31,28 +36,27 @@ export abstract class AbstractFocusProvider<T extends object> implements FocusPr
31
36
  this.data = data;
32
37
  this.dataOptions = dataOptions;
33
38
 
34
-
35
39
  // Set focus pointers
36
40
  const [first, last] = this.setFocusPointers();
37
41
 
38
- // Set initially focused items
39
42
  this.firstFocusableItem = first;
40
- if (this.firstFocusableItem) {
41
- this.firstFocusableItem.state.setKey("focused", true);
42
- this.focusedItem.set(this.firstFocusableItem);
43
- }
44
-
45
43
  this.lastFocusableItem = last;
46
44
  }
47
45
 
48
-
49
46
  isFocusable(itemAtom: CompositeDataItem<T>) {
50
47
  return !itemAtom.state.get().disabled;
51
48
  }
52
49
 
50
+ getFocusedItem() {
51
+ return this.focusedItem.get();
52
+ }
53
+
53
54
  focus(itemKey: CompositeItemKey): void;
54
55
  focus(item: CompositeDataItem<T>): void;
55
- focus(item: CompositeDataItem<T> | CompositeItemKey) {
56
+ focus(item: CompositeDataItem<T> | CompositeItemKey | null): void;
57
+ focus(item: CompositeDataItem<T> | CompositeItemKey | null) {
58
+ if (!item) return;
59
+
56
60
  const _item = item instanceof CompositeDataItem ? item : this.data.lookup.get(item);
57
61
  if (!_item) return;
58
62
 
@@ -66,6 +70,12 @@ export abstract class AbstractFocusProvider<T extends object> implements FocusPr
66
70
  this.focusedItem.set(_item);
67
71
  }
68
72
 
73
+ blur() {
74
+ const _item = this.focusedItem.get();
75
+ if (_item) _item.state.setKey("focused", false);
76
+ this.focusedItem.set(null);
77
+ }
78
+
69
79
  abstract setFocusPointers(): readonly [first: CompositeDataItem<T> | null, last: CompositeDataItem<T> | null];
70
80
 
71
81
  abstract focusUp(): void;
@@ -79,5 +89,4 @@ export abstract class AbstractFocusProvider<T extends object> implements FocusPr
79
89
  abstract focusToFirstInRow(): void;
80
90
  abstract focusToLastInRow(): void;
81
91
  abstract focusToTypeAheadMatch(): void;
82
-
83
92
  }
@@ -1,11 +1,10 @@
1
-
2
1
  import type { CompositeDataItem } from "../CompositeDataItem";
3
2
  import { AbstractFocusProvider } from "./AbstractFocusProvider";
4
3
 
5
4
  export class ListboxFocusProvider<T extends object> extends AbstractFocusProvider<T> {
6
5
  setFocusPointers() {
7
6
  let first: CompositeDataItem<T> | null = null;
8
- let last: CompositeDataItem<T> | null = null
7
+ let last: CompositeDataItem<T> | null = null;
9
8
 
10
9
  const lookupData = [...this.data.lookup];
11
10
  for (let index = 0; index < lookupData.length; index++) {
@@ -30,36 +29,24 @@ export class ListboxFocusProvider<T extends object> extends AbstractFocusProvide
30
29
  }
31
30
 
32
31
  focusUp(): void {
33
- if (this.focusedItem.get()?.pointers.up) {
34
- this.focus(this.focusedItem.get()!.pointers.up!);
35
- }
32
+ const focusedItem = this.focusedItem.get();
33
+ const focusTarget = focusedItem ? focusedItem.pointers.up : this.firstFocusableItem;
34
+ if (focusTarget) this.focus(focusTarget);
36
35
  }
37
36
  focusDown(): void {
38
- if (this.focusedItem.get()?.pointers.down) {
39
- this.focus(this.focusedItem.get()!.pointers.down!);
40
- }
41
- }
42
- focusLeft(): void {
43
- throw new Error("Method not implemented.");
44
- }
45
- focusRight(): void {
46
- throw new Error("Method not implemented.");
47
- }
48
- focusPageUp(): void {
49
- throw new Error("Method not implemented.");
50
- }
51
- focesPageDown(): void {
52
- throw new Error("Method not implemented.");
53
- }
37
+ const focusedItem = this.focusedItem.get();
38
+ const focusTarget = focusedItem ? focusedItem.pointers.down : this.firstFocusableItem;
39
+ if (focusTarget) this.focus(focusTarget);
40
+ }
41
+ focusLeft(): void {}
42
+ focusRight(): void {}
43
+ focusPageUp(): void {}
44
+ focesPageDown(): void {}
54
45
  focusToFirst(): void {
55
- if (this.firstFocusableItem) {
56
- this.focus(this.firstFocusableItem);
57
- }
46
+ this.focus(this.firstFocusableItem);
58
47
  }
59
48
  focusToLast(): void {
60
- if (this.lastFocusableItem) {
61
- this.focus(this.lastFocusableItem);
62
- }
49
+ this.focus(this.lastFocusableItem);
63
50
  }
64
51
  focusToFirstInRow(): void {
65
52
  throw new Error("Method not implemented.");
@@ -70,5 +57,4 @@ export class ListboxFocusProvider<T extends object> extends AbstractFocusProvide
70
57
  focusToTypeAheadMatch(): void {
71
58
  throw new Error("Method not implemented.");
72
59
  }
73
-
74
- }
60
+ }
@@ -3,10 +3,20 @@ import type { CompositeData } from "../CompositeData";
3
3
  import { CompositeDataItem } from "../CompositeDataItem";
4
4
  import type { CompositeDataOptions, CompositeItemKey } from "../types";
5
5
 
6
- export interface SelectionProvider {
6
+ export interface SelectionProvider<T extends object> {
7
+ getSelectedKeys(): Set<CompositeItemKey>;
8
+
7
9
  select(itemKey?: CompositeItemKey, recursive?: boolean): void;
10
+ select(item: CompositeDataItem<T>, recursive?: boolean): void;
11
+ select(item?: CompositeDataItem<T> | CompositeItemKey, recursive?: boolean): void;
12
+
8
13
  deselect(itemKey?: CompositeItemKey, recursive?: boolean): void;
14
+ deselect(item: CompositeDataItem<T>, recursive?: boolean): void;
15
+ deselect(item?: CompositeDataItem<T> | CompositeItemKey, recursive?: boolean): void;
16
+
9
17
  toggleSelect(itemKey?: CompositeItemKey, recursive?: boolean): void;
18
+ toggleSelect(item: CompositeDataItem<T>, recursive?: boolean): void;
19
+ toggleSelect(item: CompositeDataItem<T> | CompositeItemKey, recursive?: boolean): void;
10
20
  }
11
21
 
12
22
  export interface SelectionProviderOptions<T extends object> {
@@ -34,6 +44,10 @@ export abstract class AbstractSelectionProvider<T extends object> {
34
44
  this.dataOptions.selectedKeys.forEach((selectedKey) => this.select(selectedKey));
35
45
  }
36
46
 
47
+ getSelectedKeys() {
48
+ return this.selectedKeys.get();
49
+ }
50
+
37
51
  abstract select(itemKey?: CompositeItemKey, recursive?: boolean): void;
38
52
  abstract select(item: CompositeDataItem<T>, recursive?: boolean): void;
39
53
  abstract select(item?: CompositeDataItem<T> | CompositeItemKey, recursive?: boolean): void;
@@ -44,6 +58,7 @@ export abstract class AbstractSelectionProvider<T extends object> {
44
58
 
45
59
  toggleSelect(itemKey?: CompositeItemKey, recursive?: boolean): void;
46
60
  toggleSelect(item: CompositeDataItem<T>, recursive?: boolean): void;
61
+ toggleSelect(item: CompositeDataItem<T> | CompositeItemKey, recursive?: boolean): void;
47
62
  toggleSelect(item?: CompositeDataItem<T> | CompositeItemKey, recursive: boolean = false) {
48
63
  const _item = item
49
64
  ? item instanceof CompositeDataItem
@@ -10,6 +10,7 @@ export function CompositeComponentItem<T extends object>({
10
10
  itemMouseEventHandler,
11
11
  itemKeyboardEventHandler,
12
12
  render,
13
+ softFocus,
13
14
  }: CompositeItemProps<T>) {
14
15
  const data = useStore(item.data);
15
16
  const state = useStore(item.state);
@@ -29,7 +30,7 @@ export function CompositeComponentItem<T extends object>({
29
30
  role={role}
30
31
  aria-disabled={state.disabled}
31
32
  data-key={item.key}
32
- tabIndex={!state.disabled ? (state.focused ? 0 : -1) : undefined}
33
+ tabIndex={!softFocus && !state.disabled ? (state.focused ? 0 : -1) : undefined}
33
34
  className={className}
34
35
  >
35
36
  {render({ data, key: item.key, level: item.level }, state, handlers)}
@@ -45,6 +46,7 @@ export function CompositeComponentItem<T extends object>({
45
46
  className={className}
46
47
  itemMouseEventHandler={itemMouseEventHandler}
47
48
  itemKeyboardEventHandler={itemKeyboardEventHandler}
49
+ softFocus={softFocus}
48
50
  />
49
51
  ))}
50
52
  </div>
@@ -1,7 +1,9 @@
1
- import { useImperativeHandle, useMemo } from "react";
2
- import type { CompositeProps } from "./types";
1
+ import { useCallback, useEffect, useImperativeHandle, useMemo, type FocusEventHandler } from "react";
2
+ import type { CompositeItemKey, CompositeProps } from "./types";
3
3
  import { CompositeComponentItem } from "./composite-component-item";
4
4
  import { useId } from "@/hooks/use-id";
5
+ import { useRequiredRef } from "@/hooks/use-required-ref";
6
+ import type { CompositeDataItem } from "./CompositeDataItem";
5
7
 
6
8
  const defaultRoles = {
7
9
  listbox: {
@@ -17,6 +19,13 @@ const defaultRoles = {
17
19
  custom: undefined,
18
20
  };
19
21
 
22
+ const isOutsideElement = (parent: Element | null, el: Element | null) => {
23
+ if (parent === el) return false;
24
+
25
+ const nodeId = !!el?.id && CSS.escape(el.id);
26
+ return !nodeId || parent?.querySelector(`#${nodeId}`) === null;
27
+ };
28
+
20
29
  export function CompositeComponent<T extends object>({
21
30
  data,
22
31
  variant,
@@ -26,33 +35,78 @@ export function CompositeComponent<T extends object>({
26
35
  renderItem,
27
36
  className,
28
37
  itemClassName,
29
- ref,
38
+ rootRef,
30
39
  handleRef,
31
40
  id,
32
41
  itemMouseEventHandler,
33
42
  itemKeyboardEventHandler,
43
+ initialFocus = "None",
44
+ softFocus,
34
45
  ...props
35
46
  }: CompositeProps<T>) {
36
47
  const compositeId = useId(id);
48
+ const $rootRef = useRequiredRef(rootRef);
37
49
 
38
- useImperativeHandle(
39
- handleRef,
40
- () => {
41
- return {
42
- focusProvider: data.focusProvider,
43
- selectionProvider: data.selectionProvider,
44
- };
45
- },
46
- [data]
47
- );
50
+ const focusElement = useCallback(() => {
51
+ if (softFocus) return;
52
+
53
+ const itemKey = data.focusProvider.focusedItem.get()?.key;
54
+
55
+ if (itemKey && $rootRef.current) {
56
+ const focusedItemEl = $rootRef.current.querySelector<HTMLElement>(`[data-key="${itemKey}"]`);
57
+ if (focusedItemEl) focusedItemEl.focus();
58
+ }
59
+ }, [softFocus, data.focusProvider.focusedItem, $rootRef]);
60
+
61
+ useImperativeHandle(handleRef, () => {
62
+ return {
63
+ focusProvider: data.focusProvider,
64
+ selectionProvider: data.selectionProvider,
65
+ focusElement,
66
+ };
67
+ }, [data, focusElement]);
48
68
 
49
69
  const roles = useMemo(
50
70
  () => defaultRoles[variant] ?? { rootRole, groupRole, itemRole },
51
- [groupRole, itemRole, rootRole, variant]
71
+ [groupRole, itemRole, rootRole, variant],
52
72
  );
53
73
 
74
+ const intiFocus = useCallback(
75
+ (onlyData: boolean) => {
76
+ let initialFocusItem: CompositeDataItem<T> | CompositeItemKey | null = null;
77
+
78
+ if (initialFocus === "FirstItem") initialFocusItem = data.focusProvider.firstFocusableItem;
79
+ else if (initialFocus === "SelectedItem") {
80
+ const selecedItem = data.selectionProvider?.selectedKeys.get().values().next().value;
81
+ initialFocusItem = selecedItem ?? data.focusProvider.firstFocusableItem;
82
+ }
83
+
84
+ data.focusProvider.focus(initialFocusItem);
85
+ if (!onlyData) focusElement();
86
+ },
87
+ [data.focusProvider, data.selectionProvider?.selectedKeys, focusElement, initialFocus],
88
+ );
89
+
90
+ const onFocusHandler: FocusEventHandler = useCallback(
91
+ (ev) => {
92
+ const fromOut = isOutsideElement($rootRef.current, ev.relatedTarget);
93
+ if (fromOut) intiFocus(false);
94
+ },
95
+ [$rootRef, intiFocus],
96
+ );
97
+
98
+ useEffect(() => intiFocus(true), [intiFocus]);
99
+
54
100
  return (
55
- <div ref={ref} id={compositeId} className={className} tabIndex={-1} role={roles.rootRole} {...props}>
101
+ <div
102
+ ref={$rootRef}
103
+ id={compositeId}
104
+ className={className}
105
+ tabIndex={initialFocus === "None" ? 0 : -1}
106
+ role={roles.rootRole}
107
+ {...props}
108
+ onFocus={onFocusHandler}
109
+ >
56
110
  {[...data].map((item) => (
57
111
  <CompositeComponentItem
58
112
  className={itemClassName}
@@ -64,6 +118,7 @@ export function CompositeComponent<T extends object>({
64
118
  render={renderItem}
65
119
  itemMouseEventHandler={itemMouseEventHandler}
66
120
  itemKeyboardEventHandler={itemKeyboardEventHandler}
121
+ softFocus={softFocus}
67
122
  />
68
123
  ))}
69
124
  </div>