@bccampus/ui-components 0.7.2 → 0.8.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.
Files changed (77) hide show
  1. package/.yarn/install-state.gz +0 -0
  2. package/.yarnrc.yml +0 -2
  3. package/dist/_chunks/createLucideIcon.js +11 -10
  4. package/dist/_chunks/index.js +2 -2
  5. package/dist/_chunks/index4.js +2 -2
  6. package/dist/components/index.js +18 -13
  7. package/dist/components/ui/composite/CompositeData.d.ts +9 -8
  8. package/dist/components/ui/composite/CompositeData.js +20 -14
  9. package/dist/components/ui/composite/{FocusProvider/AbstractFocusProvider.d.ts → FocusManager/AbstractFocusManager.d.ts} +2 -19
  10. package/dist/components/ui/composite/{FocusProvider/AbstractFocusProvider.js → FocusManager/AbstractFocusManager.js} +16 -5
  11. package/dist/components/ui/composite/{FocusProvider/ListboxFocusProvider.d.ts → FocusManager/ListboxFocusManager.d.ts} +2 -2
  12. package/dist/components/ui/composite/{FocusProvider/ListboxFocusProvider.js → FocusManager/ListboxFocusManager.js} +3 -3
  13. package/dist/components/ui/composite/FocusManager/index.d.ts +3 -0
  14. package/dist/components/ui/composite/FocusManager/index.js +6 -0
  15. package/dist/components/ui/composite/FocusManager/types.d.ts +20 -0
  16. package/dist/components/ui/composite/FocusManager/types.js +1 -0
  17. package/dist/components/ui/composite/{SelectionProvider/AbstractSelectionProvider.d.ts → SelectionManager/AbstractSelectionManager.d.ts} +5 -20
  18. package/dist/components/ui/composite/{SelectionProvider/AbstractSelectionProvider.js → SelectionManager/AbstractSelectionManager.js} +8 -4
  19. package/dist/components/ui/composite/{SelectionProvider/MultipleSelectionProvider.d.ts → SelectionManager/MultipleSelectionManager.d.ts} +3 -2
  20. package/dist/components/ui/composite/{SelectionProvider/MultipleSelectionProvider.js → SelectionManager/MultipleSelectionManager.js} +4 -3
  21. package/dist/components/ui/composite/{SelectionProvider/SingleSelectionProvider.d.ts → SelectionManager/SingleSelectionManager.d.ts} +3 -2
  22. package/dist/components/ui/composite/{SelectionProvider/SingleSelectionProvider.js → SelectionManager/SingleSelectionManager.js} +6 -5
  23. package/dist/components/ui/composite/SelectionManager/index.d.ts +4 -0
  24. package/dist/components/ui/composite/SelectionManager/index.js +8 -0
  25. package/dist/components/ui/composite/SelectionManager/types.d.ts +19 -0
  26. package/dist/components/ui/composite/SelectionManager/types.js +1 -0
  27. package/dist/components/ui/composite/components/composite-component.d.ts +2 -0
  28. package/dist/components/ui/composite/{composite-component.js → components/composite-component.js} +16 -38
  29. package/dist/components/ui/composite/components/index.d.ts +5 -0
  30. package/dist/components/ui/composite/components/index.js +10 -0
  31. package/dist/components/ui/composite/{listbox.d.ts → components/listbox.d.ts} +1 -1
  32. package/dist/components/ui/composite/{listbox.js → components/listbox.js} +17 -12
  33. package/dist/components/ui/composite/components/types.d.ts +53 -0
  34. package/dist/components/ui/composite/components/types.js +1 -0
  35. package/dist/components/ui/composite/components/utils.d.ts +1 -0
  36. package/dist/components/ui/composite/components/utils.js +8 -0
  37. package/dist/components/ui/composite/create-composite-data.d.ts +80 -0
  38. package/dist/components/ui/composite/create-composite-data.js +22 -0
  39. package/dist/components/ui/composite/index.d.ts +4 -5
  40. package/dist/components/ui/composite/index.js +18 -13
  41. package/dist/components/ui/composite/types.d.ts +5 -67
  42. package/dist/components/ui/navigation-menu.js +1 -1
  43. package/dist/components/ui/popover.js +1 -1
  44. package/dist/hooks/index.d.ts +1 -0
  45. package/dist/hooks/index.js +3 -1
  46. package/eslint.config.js +1 -1
  47. package/package.json +17 -16
  48. package/src/components/ui/composite/CompositeData.ts +38 -30
  49. package/src/components/ui/composite/{FocusProvider/AbstractFocusProvider.ts → FocusManager/AbstractFocusManager.ts} +19 -26
  50. package/src/components/ui/composite/{FocusProvider/ListboxFocusProvider.ts → FocusManager/ListboxFocusManager.ts} +2 -2
  51. package/src/components/ui/composite/FocusManager/index.ts +3 -0
  52. package/src/components/ui/composite/FocusManager/types.ts +23 -0
  53. package/src/components/ui/composite/{SelectionProvider/AbstractSelectionProvider.ts → SelectionManager/AbstractSelectionManager.ts} +13 -28
  54. package/src/components/ui/composite/{SelectionProvider/MultipleSelectionProvider.ts → SelectionManager/MultipleSelectionManager.ts} +4 -2
  55. package/src/components/ui/composite/{SelectionProvider/SingleSelectionProvider.ts → SelectionManager/SingleSelectionManager.ts} +6 -4
  56. package/src/components/ui/composite/SelectionManager/index.ts +4 -0
  57. package/src/components/ui/composite/SelectionManager/types.ts +24 -0
  58. package/src/components/ui/composite/{composite-component.tsx → components/composite-component.tsx} +17 -42
  59. package/src/components/ui/composite/components/index.ts +5 -0
  60. package/src/components/ui/composite/{listbox.tsx → components/listbox.tsx} +16 -11
  61. package/src/components/ui/composite/components/types.ts +66 -0
  62. package/src/components/ui/composite/components/utils.ts +6 -0
  63. package/src/components/ui/composite/create-composite-data.ts +98 -0
  64. package/src/components/ui/composite/index.ts +7 -8
  65. package/src/components/ui/composite/types.ts +5 -86
  66. package/src/hooks/index.ts +4 -3
  67. package/.yarn/releases/yarn-4.10.3.cjs +0 -942
  68. package/dist/components/ui/composite/FocusProvider/index.d.ts +0 -2
  69. package/dist/components/ui/composite/FocusProvider/index.js +0 -6
  70. package/dist/components/ui/composite/SelectionProvider/index.d.ts +0 -3
  71. package/dist/components/ui/composite/SelectionProvider/index.js +0 -8
  72. package/dist/components/ui/composite/composite-component.d.ts +0 -2
  73. package/src/components/ui/composite/FocusProvider/index.ts +0 -2
  74. package/src/components/ui/composite/SelectionProvider/index.ts +0 -3
  75. /package/dist/components/ui/composite/{composite-component-item.d.ts → components/composite-component-item.d.ts} +0 -0
  76. /package/dist/components/ui/composite/{composite-component-item.js → components/composite-component-item.js} +0 -0
  77. /package/src/components/ui/composite/{composite-component-item.tsx → components/composite-component-item.tsx} +0 -0
@@ -1,22 +1,24 @@
1
- import type { CompositeDataOptions, CompositeItemKey, CompositeOptions, CompositeProviderOptions } from "./types";
1
+ import type { CompositeDataOptions, CompositeItemKey, CompositeOptions, CompositeManagerOptions } from "./types";
2
2
  import { CompositeDataItem } from "./CompositeDataItem";
3
- import type { AbstractFocusProvider } from "./FocusProvider/AbstractFocusProvider";
4
- import type { AbstractSelectionProvider } from "./SelectionProvider/AbstractSelectionProvider";
3
+ import type { AbstractFocusManager } from "./FocusManager/AbstractFocusManager";
4
+ import type { AbstractSelectionManager } from "./SelectionManager/AbstractSelectionManager";
5
5
  import { union } from "@/lib/set-operations";
6
+ import { atom } from "nanostores";
6
7
 
7
8
  export class CompositeData<T extends object> implements Iterable<CompositeDataItem<T>> {
8
9
  #options: CompositeDataOptions<T>;
10
+ #version = atom(0);
9
11
 
10
12
  lookup: Map<CompositeItemKey, CompositeDataItem<T>> = new Map();
11
13
 
12
- focusProvider: AbstractFocusProvider<T>;
13
- selectionProvider?: AbstractSelectionProvider<T>;
14
+ focusManager: AbstractFocusManager<T>;
15
+ selectionManager?: AbstractSelectionManager<T>;
14
16
 
15
17
  disabledKeys: Set<CompositeItemKey> = new Set();
16
18
 
17
- root!: CompositeDataItem<T>;
19
+ root?: CompositeDataItem<T> = undefined;
18
20
 
19
- constructor(items: T[], providerOptions: CompositeProviderOptions<T>, options?: CompositeOptions) {
21
+ constructor(items: T[] | null, managerOptions: CompositeManagerOptions<T>, options?: CompositeOptions) {
20
22
  this.#options = {
21
23
  itemKeyProp: "key",
22
24
  itemChildrenProp: "children",
@@ -25,19 +27,19 @@ export class CompositeData<T extends object> implements Iterable<CompositeDataIt
25
27
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
26
28
  getItemKey: (item: any) => item[this.#options.itemKeyProp] as CompositeItemKey,
27
29
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
28
- getItemChildren: (item: any) => item[this.#options.itemChildrenProp] ? item[this.#options.itemChildrenProp] as T[] : undefined,
30
+ getItemChildren: (item: any) =>
31
+ item[this.#options.itemChildrenProp] ? (item[this.#options.itemChildrenProp] as T[]) : undefined,
29
32
 
30
- ...options
33
+ ...options,
31
34
  };
32
35
 
33
- this.focusProvider = providerOptions.focusProvider;
34
- this.selectionProvider = providerOptions.selectionProvider;
35
-
36
- this.init(items);
36
+ this.focusManager = managerOptions.focusManager;
37
+ this.selectionManager = managerOptions.selectionManager;
38
+ if (items) this.setItems(items);
37
39
  }
38
40
 
39
41
  *[Symbol.iterator](): Iterator<CompositeDataItem<T>> {
40
- if (this.root.children) {
42
+ if (this.root?.children) {
41
43
  for (const node of this.root.children) {
42
44
  yield node;
43
45
  }
@@ -45,33 +47,41 @@ export class CompositeData<T extends object> implements Iterable<CompositeDataIt
45
47
  }
46
48
 
47
49
  toJSON() {
50
+ if (!this.root) return [] as T[];
51
+
48
52
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
49
- const json: any = this.root.toJSON()
53
+ const json: any = this.root.toJSON();
50
54
 
51
55
  return json[this.#options.itemChildrenProp] as T[];
52
56
  }
53
57
 
58
+ get version() {
59
+ return this.#version;
60
+ }
54
61
 
55
- init(items: T[]) {
62
+ setItems(items: T[]) {
56
63
  this.root = new CompositeDataItem(
57
64
  { [this.#options.itemKeyProp]: "ALL", [this.#options.itemChildrenProp]: items } as T,
58
65
  this.#options,
59
- null
66
+ null,
60
67
  );
61
68
 
62
69
  // Build item lookup table
63
- this.lookup = new Map([...this.root].map(item => [item.key, item]));
70
+ this.lookup = new Map([...this.root].map((item) => [item.key, item]));
64
71
 
65
- // Set initially disabled items
66
- this.#options.disabledKeys.forEach(disabledKey => this.#disable(disabledKey));
72
+ // Set default disabled items
73
+ this.#options.disabledKeys.forEach((disabledKey) => this.#disable(disabledKey));
67
74
 
68
- if (this.selectionProvider)
69
- this.selectionProvider.init(this, this.#options);
75
+ if (this.selectionManager) this.selectionManager.init(this, this.#options);
70
76
 
71
- this.focusProvider.init(this, this.#options);
77
+ this.focusManager.init(this, this.#options);
78
+
79
+ this.#version.set(this.#version.value + 1);
72
80
  }
73
81
 
74
- item(key: CompositeItemKey) { return this.lookup.get(key) };
82
+ item(key: CompositeItemKey) {
83
+ return this.lookup.get(key);
84
+ }
75
85
 
76
86
  descendants(itemKey: CompositeItemKey) {
77
87
  const item = this.lookup.get(itemKey);
@@ -97,14 +107,12 @@ export class CompositeData<T extends object> implements Iterable<CompositeDataIt
97
107
  this.disabledKeys = union(this.disabledKeys, disabledKeys);
98
108
  }
99
109
 
100
-
101
110
  updateItem(key: CompositeItemKey, data: Partial<T> | ((item: T) => Partial<T>)) {
102
111
  const item = this.lookup.get(key);
103
112
  if (item) {
104
113
  const newData = {
105
- ...item.data.get(), ...(
106
- (typeof data === "function") ? data(item.data.get()) : data
107
- )
114
+ ...item.data.get(),
115
+ ...(typeof data === "function" ? data(item.data.get()) : data),
108
116
  };
109
117
 
110
118
  const newKey = this.#options.getItemKey(newData);
@@ -116,8 +124,8 @@ export class CompositeData<T extends object> implements Iterable<CompositeDataIt
116
124
  }
117
125
 
118
126
  #updateIfSelectedItem(key: CompositeItemKey) {
119
- if (this.selectionProvider && this.selectionProvider.selectedKeys.get().has(key)) {
120
- this.selectionProvider.selectedKeys.set(new Set([...this.selectionProvider.selectedKeys.get()]));
127
+ if (this.selectionManager && this.selectionManager.selectedKeys.get().has(key)) {
128
+ this.selectionManager.selectedKeys.set(new Set([...this.selectionManager.selectedKeys.get()]));
121
129
  }
122
130
  }
123
131
  }
@@ -2,29 +2,9 @@ import { atom, type PreinitializedWritableAtom } from "nanostores";
2
2
  import type { CompositeData } from "../CompositeData";
3
3
  import { CompositeDataItem } from "../CompositeDataItem";
4
4
  import type { CompositeDataOptions, CompositeItemKey } from "../types";
5
+ import type { IFocusManager } from "./types";
5
6
 
6
- export interface FocusProvider<T extends object> {
7
- getFocusedItem(): CompositeDataItem<T> | null;
8
-
9
- focus(itemKey: CompositeItemKey): void;
10
- focus(item: CompositeDataItem<T>): void;
11
- focus(item: CompositeDataItem<T> | CompositeItemKey | null): void;
12
- blur(): void;
13
-
14
- focusUp(): void;
15
- focusDown(): void;
16
- focusLeft(): void;
17
- focusRight(): void;
18
- focusPageUp(): void;
19
- focesPageDown(): void;
20
- focusToFirst(): void;
21
- focusToLast(): void;
22
- focusToFirstInRow(): void;
23
- focusToLastInRow(): void;
24
- focusToTypeAheadMatch(): void;
25
- }
26
-
27
- export abstract class AbstractFocusProvider<T extends object> implements FocusProvider<T> {
7
+ export abstract class AbstractFocusManager<T extends object> implements IFocusManager<T> {
28
8
  protected data!: CompositeData<T>;
29
9
  protected dataOptions!: CompositeDataOptions<T>;
30
10
 
@@ -41,6 +21,19 @@ export abstract class AbstractFocusProvider<T extends object> implements FocusPr
41
21
 
42
22
  this.firstFocusableItem = first;
43
23
  this.lastFocusableItem = last;
24
+
25
+ // Restore previos state
26
+ const prevFocusedItem = this.focusedItem.get();
27
+ if (prevFocusedItem) {
28
+ const _item = this.data.lookup.get(prevFocusedItem.key);
29
+ if (!_item) {
30
+ this.focusedItem.set(null);
31
+ return;
32
+ }
33
+
34
+ _item.state.setKey("focused", true);
35
+ this.focusedItem.set(_item);
36
+ }
44
37
  }
45
38
 
46
39
  isFocusable(itemAtom: CompositeDataItem<T>) {
@@ -60,10 +53,10 @@ export abstract class AbstractFocusProvider<T extends object> implements FocusPr
60
53
  const _item = item instanceof CompositeDataItem ? item : this.data.lookup.get(item);
61
54
  if (!_item) return;
62
55
 
63
- if (this.focusedItem.get()) {
64
- if (_item.key === this.focusedItem.get()?.key) return;
65
-
66
- this.focusedItem.get()!.state.setKey("focused", false);
56
+ const focusedItem = this.focusedItem.get();
57
+ if (focusedItem) {
58
+ if (_item.key === focusedItem.key) return;
59
+ focusedItem.state.setKey("focused", false);
67
60
  }
68
61
 
69
62
  _item.state.setKey("focused", true);
@@ -1,7 +1,7 @@
1
1
  import type { CompositeDataItem } from "../CompositeDataItem";
2
- import { AbstractFocusProvider } from "./AbstractFocusProvider";
2
+ import { AbstractFocusManager } from "./AbstractFocusManager";
3
3
 
4
- export class ListboxFocusProvider<T extends object> extends AbstractFocusProvider<T> {
4
+ export class ListboxFocusManager<T extends object> extends AbstractFocusManager<T> {
5
5
  setFocusPointers() {
6
6
  let first: CompositeDataItem<T> | null = null;
7
7
  let last: CompositeDataItem<T> | null = null;
@@ -0,0 +1,3 @@
1
+ export * from "./AbstractFocusManager";
2
+ export * from "./ListboxFocusManager";
3
+ export type * from "./types";
@@ -0,0 +1,23 @@
1
+ import type { CompositeDataItem } from "../CompositeDataItem";
2
+ import type { CompositeItemKey } from "../types";
3
+
4
+ export interface IFocusManager<T extends object> {
5
+ getFocusedItem(): CompositeDataItem<T> | null;
6
+
7
+ focus(itemKey: CompositeItemKey): void;
8
+ focus(item: CompositeDataItem<T>): void;
9
+ focus(item: CompositeDataItem<T> | CompositeItemKey | null): void;
10
+ blur(): void;
11
+
12
+ focusUp(): void;
13
+ focusDown(): void;
14
+ focusLeft(): void;
15
+ focusRight(): void;
16
+ focusPageUp(): void;
17
+ focesPageDown(): void;
18
+ focusToFirst(): void;
19
+ focusToLast(): void;
20
+ focusToFirstInRow(): void;
21
+ focusToLastInRow(): void;
22
+ focusToTypeAheadMatch(): void;
23
+ }
@@ -2,37 +2,17 @@ import { atom, type PreinitializedWritableAtom } from "nanostores";
2
2
  import type { CompositeData } from "../CompositeData";
3
3
  import { CompositeDataItem } from "../CompositeDataItem";
4
4
  import type { CompositeDataOptions, CompositeItemKey } from "../types";
5
+ import type { SelectionManager, SelectionManagerOptions } from "./types";
5
6
 
6
- export interface SelectionProvider<T extends object> {
7
- getSelectedKeys(): Set<CompositeItemKey>;
8
-
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
-
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
-
17
- toggleSelect(itemKey?: CompositeItemKey, recursive?: boolean): void;
18
- toggleSelect(item: CompositeDataItem<T>, recursive?: boolean): void;
19
- toggleSelect(item: CompositeDataItem<T> | CompositeItemKey, recursive?: boolean): void;
20
- }
21
-
22
- export interface SelectionProviderOptions<T extends object> {
23
- onSelect?: (item: T) => void;
24
- onDeselect?: (item: T) => void;
25
- onChange?: (item: Set<CompositeItemKey>) => void;
26
- }
27
-
28
- export abstract class AbstractSelectionProvider<T extends object> {
7
+ export abstract class AbstractSelectionManager<T extends object> implements SelectionManager<T> {
29
8
  protected data!: CompositeData<T>;
30
9
  protected dataOptions!: CompositeDataOptions<T>;
10
+ protected abstract multiselect: boolean;
31
11
 
32
12
  selectedKeys: PreinitializedWritableAtom<Set<CompositeItemKey>> = atom(new Set());
33
- options?: SelectionProviderOptions<T>;
13
+ options?: SelectionManagerOptions<T>;
34
14
 
35
- constructor(options?: SelectionProviderOptions<T>) {
15
+ constructor(options?: SelectionManagerOptions<T>) {
36
16
  this.options = options;
37
17
  }
38
18
 
@@ -40,8 +20,13 @@ export abstract class AbstractSelectionProvider<T extends object> {
40
20
  this.data = data;
41
21
  this.dataOptions = dataOptions;
42
22
 
43
- // Set initially selected items : SelectionProvider
44
- this.dataOptions.selectedKeys.forEach((selectedKey) => this.select(selectedKey));
23
+ if (this.data.version.get() > 0) {
24
+ // Restore previos state
25
+ this.selectedKeys.get().forEach((key) => this.select(key));
26
+ } else {
27
+ // Set default selected items
28
+ this.dataOptions.selectedKeys.forEach((selectedKey) => this.select(selectedKey));
29
+ }
45
30
  }
46
31
 
47
32
  getSelectedKeys() {
@@ -64,7 +49,7 @@ export abstract class AbstractSelectionProvider<T extends object> {
64
49
  ? item instanceof CompositeDataItem
65
50
  ? item
66
51
  : this.data.lookup.get(item)
67
- : this.data.focusProvider.focusedItem.get();
52
+ : this.data.focusManager.focusedItem.get();
68
53
  if (!_item) return;
69
54
 
70
55
  if (_item.state.get().selected) this.deselect(_item, recursive);
@@ -1,9 +1,11 @@
1
1
  import { CompositeDataItem } from "../CompositeDataItem";
2
2
  import type { CompositeItemKey } from "../types";
3
- import { AbstractSelectionProvider } from "./AbstractSelectionProvider";
3
+ import { AbstractSelectionManager } from "./AbstractSelectionManager";
4
4
  import { difference, union } from "@/lib/set-operations";
5
5
 
6
- export class MultipleSelectionProvider<T extends object> extends AbstractSelectionProvider<T> {
6
+ export class MultipleSelectionManager<T extends object> extends AbstractSelectionManager<T> {
7
+ protected multiselect: boolean = true;
8
+
7
9
  select(itemKey: CompositeItemKey, recursive?: boolean): void;
8
10
  select(item: CompositeDataItem<T>, recursive?: boolean): void;
9
11
  select(item: CompositeDataItem<T> | CompositeItemKey, recursive: boolean = false) {
@@ -2,10 +2,12 @@
2
2
 
3
3
  import { CompositeDataItem } from "../CompositeDataItem";
4
4
  import type { CompositeItemKey } from "../types";
5
- import { AbstractSelectionProvider } from "./AbstractSelectionProvider";
5
+ import { AbstractSelectionManager } from "./AbstractSelectionManager";
6
6
  import { difference } from "@/lib/set-operations";
7
7
 
8
- export class SingleSelectionProvider<T extends object> extends AbstractSelectionProvider<T> {
8
+ export class SingleSelectionManager<T extends object> extends AbstractSelectionManager<T> {
9
+ protected multiselect: boolean = false;
10
+
9
11
  select(itemKey?: CompositeItemKey, recursive?: boolean): void;
10
12
  select(item: CompositeDataItem<T>, recursive?: boolean): void;
11
13
  select(item?: CompositeDataItem<T> | CompositeItemKey, _recursive: boolean = false) {
@@ -13,7 +15,7 @@ export class SingleSelectionProvider<T extends object> extends AbstractSelection
13
15
  ? item instanceof CompositeDataItem
14
16
  ? item
15
17
  : this.data.lookup.get(item)
16
- : this.data.focusProvider.focusedItem.get();
18
+ : this.data.focusManager.focusedItem.get();
17
19
  if (!_item) return;
18
20
 
19
21
  this.selectedKeys.get().forEach((selectedKey) => {
@@ -38,7 +40,7 @@ export class SingleSelectionProvider<T extends object> extends AbstractSelection
38
40
  ? item instanceof CompositeDataItem
39
41
  ? item
40
42
  : this.data.lookup.get(item)
41
- : this.data.focusProvider.focusedItem.get();
43
+ : this.data.focusManager.focusedItem.get();
42
44
  if (!_item) return;
43
45
 
44
46
  const deselectedKeys = _item.deselect(false);
@@ -0,0 +1,4 @@
1
+ export * from "./AbstractSelectionManager";
2
+ export * from "./SingleSelectionManager";
3
+ export * from "./MultipleSelectionManager";
4
+ export type * from "./types";
@@ -0,0 +1,24 @@
1
+ import type { CompositeDataItem } from "../CompositeDataItem";
2
+ import type { CompositeItemKey } from "../types";
3
+
4
+ export interface SelectionManager<T extends object> {
5
+ getSelectedKeys(): Set<CompositeItemKey>;
6
+
7
+ select(itemKey?: CompositeItemKey, recursive?: boolean): void;
8
+ select(item: CompositeDataItem<T>, recursive?: boolean): void;
9
+ select(item?: CompositeDataItem<T> | CompositeItemKey, recursive?: boolean): void;
10
+
11
+ deselect(itemKey?: CompositeItemKey, recursive?: boolean): void;
12
+ deselect(item: CompositeDataItem<T>, recursive?: boolean): void;
13
+ deselect(item?: CompositeDataItem<T> | CompositeItemKey, recursive?: boolean): void;
14
+
15
+ toggleSelect(itemKey?: CompositeItemKey, recursive?: boolean): void;
16
+ toggleSelect(item: CompositeDataItem<T>, recursive?: boolean): void;
17
+ toggleSelect(item: CompositeDataItem<T> | CompositeItemKey, recursive?: boolean): void;
18
+ }
19
+
20
+ export interface SelectionManagerOptions<T extends object> {
21
+ onSelect?: (item: T) => void;
22
+ onDeselect?: (item: T) => void;
23
+ onChange?: (selectedKeys: Set<CompositeItemKey>) => void;
24
+ }
@@ -1,34 +1,14 @@
1
- import { useCallback, useEffect, useImperativeHandle, useMemo, type FocusEventHandler } from "react";
2
- import type { CompositeItemKey, CompositeProps } from "./types";
1
+ import { useCallback, useEffect, useImperativeHandle, type FocusEventHandler } from "react";
2
+ import type { CompositeItemKey } from "../types";
3
+ import type { CompositeProps } from "./types.ts";
3
4
  import { CompositeComponentItem } from "./composite-component-item";
4
5
  import { useId } from "@/hooks/use-id";
5
6
  import { useRequiredRef } from "@/hooks/use-required-ref";
6
- import type { CompositeDataItem } from "./CompositeDataItem";
7
-
8
- const defaultRoles = {
9
- listbox: {
10
- rootRole: "listbox",
11
- groupRole: "group",
12
- itemRole: "option",
13
- },
14
- grid: {
15
- rootRole: "grid",
16
- groupRole: "rowgroup",
17
- itemRole: "row",
18
- },
19
- custom: undefined,
20
- };
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
- };
7
+ import type { CompositeDataItem } from "../CompositeDataItem";
8
+ import { isOutsideElement } from "./utils";
28
9
 
29
10
  export function CompositeComponent<T extends object>({
30
11
  data,
31
- variant,
32
12
  rootRole,
33
13
  itemRole,
34
14
  groupRole,
@@ -50,41 +30,36 @@ export function CompositeComponent<T extends object>({
50
30
  const focusElement = useCallback(() => {
51
31
  if (softFocus) return;
52
32
 
53
- const itemKey = data.focusProvider.focusedItem.get()?.key;
33
+ const itemKey = data.focusManager.focusedItem.get()?.key;
54
34
 
55
35
  if (itemKey && $rootRef.current) {
56
36
  const focusedItemEl = $rootRef.current.querySelector<HTMLElement>(`[data-key="${itemKey}"]`);
57
37
  if (focusedItemEl) focusedItemEl.focus();
58
38
  }
59
- }, [softFocus, data.focusProvider.focusedItem, $rootRef]);
39
+ }, [softFocus, data.focusManager.focusedItem, $rootRef]);
60
40
 
61
41
  useImperativeHandle(handleRef, () => {
62
42
  return {
63
- focusProvider: data.focusProvider,
64
- selectionProvider: data.selectionProvider,
43
+ focusManager: data.focusManager,
44
+ selectionManager: data.selectionManager,
65
45
  focusElement,
66
46
  };
67
47
  }, [data, focusElement]);
68
48
 
69
- const roles = useMemo(
70
- () => defaultRoles[variant] ?? { rootRole, groupRole, itemRole },
71
- [groupRole, itemRole, rootRole, variant],
72
- );
73
-
74
49
  const intiFocus = useCallback(
75
50
  (onlyData: boolean) => {
76
51
  let initialFocusItem: CompositeDataItem<T> | CompositeItemKey | null = null;
77
52
 
78
- if (initialFocus === "FirstItem") initialFocusItem = data.focusProvider.firstFocusableItem;
53
+ if (initialFocus === "FirstItem") initialFocusItem = data.focusManager.firstFocusableItem;
79
54
  else if (initialFocus === "SelectedItem") {
80
- const selecedItem = data.selectionProvider?.selectedKeys.get().values().next().value;
81
- initialFocusItem = selecedItem ?? data.focusProvider.firstFocusableItem;
55
+ const selecedItem = data.selectionManager?.selectedKeys.get().values().next().value;
56
+ initialFocusItem = selecedItem ?? data.focusManager.firstFocusableItem;
82
57
  }
83
58
 
84
- data.focusProvider.focus(initialFocusItem);
59
+ data.focusManager.focus(initialFocusItem);
85
60
  if (!onlyData) focusElement();
86
61
  },
87
- [data.focusProvider, data.selectionProvider?.selectedKeys, focusElement, initialFocus],
62
+ [data.focusManager, data.selectionManager?.selectedKeys, focusElement, initialFocus],
88
63
  );
89
64
 
90
65
  const onFocusHandler: FocusEventHandler = useCallback(
@@ -103,7 +78,7 @@ export function CompositeComponent<T extends object>({
103
78
  id={compositeId}
104
79
  className={className}
105
80
  tabIndex={initialFocus === "None" ? 0 : -1}
106
- role={roles.rootRole}
81
+ role={rootRole}
107
82
  {...props}
108
83
  onFocus={onFocusHandler}
109
84
  >
@@ -111,8 +86,8 @@ export function CompositeComponent<T extends object>({
111
86
  <CompositeComponentItem
112
87
  className={itemClassName}
113
88
  id={`${compositeId}-${item.key}`}
114
- role={roles.itemRole}
115
- groupRole={roles.groupRole}
89
+ role={itemRole}
90
+ groupRole={groupRole}
116
91
  item={item}
117
92
  key={item.key}
118
93
  render={renderItem}
@@ -0,0 +1,5 @@
1
+ export * from "./composite-component.tsx";
2
+ export * from "./composite-component-item.tsx";
3
+ export * from "./listbox.tsx";
4
+ export * from "./utils.ts";
5
+ export type * from "./types";
@@ -1,15 +1,17 @@
1
1
  import { CompositeComponent } from "./composite-component";
2
2
  import { useCallback } from "react";
3
- import { CompositeDataItem } from "./CompositeDataItem";
3
+ import { CompositeDataItem } from "../CompositeDataItem";
4
4
  import type { BaseCompositeProps } from "./types";
5
5
  import { useKeyboardEvent } from "@/hooks/use-keyboard-event";
6
6
  import { useRequiredRef } from "@/hooks/use-required-ref";
7
+ import { cn } from "@/lib";
7
8
 
8
9
  export function Listbox<T extends object>({
9
10
  data,
10
11
  rootRef,
11
12
  handleRef,
12
13
  initialFocus = "SelectedItem",
14
+ className,
13
15
  ...props
14
16
  }: BaseCompositeProps<T>) {
15
17
  const $handleRef = useRequiredRef(handleRef);
@@ -17,47 +19,50 @@ export function Listbox<T extends object>({
17
19
  const handleKeyboardEvent = useKeyboardEvent(
18
20
  {
19
21
  ArrowUp: () => {
20
- data.focusProvider.focusUp();
22
+ data.focusManager.focusUp();
21
23
  $handleRef.current?.focusElement();
22
24
  },
23
25
  ArrowDown: () => {
24
- data.focusProvider.focusDown();
26
+ data.focusManager.focusDown();
25
27
  $handleRef.current?.focusElement();
26
28
  },
27
29
  Home: () => {
28
- data.focusProvider.focusToFirst();
30
+ data.focusManager.focusToFirst();
29
31
  $handleRef.current?.focusElement();
30
32
  },
31
33
  End: () => {
32
- data.focusProvider.focusToLast();
34
+ data.focusManager.focusToLast();
33
35
  $handleRef.current?.focusElement();
34
36
  },
35
37
  Space: () => {
36
- data.selectionProvider?.toggleSelect();
38
+ data.selectionManager?.toggleSelect();
37
39
  $handleRef.current?.focusElement();
38
40
  },
39
41
  },
40
- [$handleRef, data.focusProvider],
42
+ [$handleRef, data.focusManager],
41
43
  );
42
44
 
43
45
  const handleItemMouseEvent = useCallback(
44
46
  (item: CompositeDataItem<T>) => {
45
- data.focusProvider.focus(item.key);
46
- data.selectionProvider?.toggleSelect(item);
47
+ data.focusManager.focus(item.key);
48
+ data.selectionManager?.toggleSelect(item);
47
49
  $handleRef.current?.focusElement();
48
50
  },
49
- [$handleRef, data.focusProvider, data.selectionProvider],
51
+ [$handleRef, data.focusManager, data.selectionManager],
50
52
  );
51
53
 
52
54
  return (
53
55
  <CompositeComponent
54
56
  rootRef={rootRef}
55
57
  handleRef={$handleRef}
56
- variant="listbox"
58
+ rootRole="listbox"
59
+ groupRole="group"
60
+ itemRole="option"
57
61
  data={data}
58
62
  initialFocus={initialFocus}
59
63
  onKeyDown={handleKeyboardEvent}
60
64
  itemMouseEventHandler={handleItemMouseEvent}
65
+ className={cn("flex flex-col gap-1", className)}
61
66
  {...props}
62
67
  />
63
68
  );
@@ -0,0 +1,66 @@
1
+ import type { AriaRole, KeyboardEventHandler, MouseEventHandler, ReactNode } from "react";
2
+ import type { CompositeData } from "../CompositeData";
3
+ import type { CompositeDataItemState, CompositeItemKey } from "../types";
4
+ import type { IFocusManager } from "../FocusManager";
5
+ import type { SelectionManager } from "../SelectionManager";
6
+ import type { CompositeDataItem } from "../CompositeDataItem";
7
+
8
+ export type InitialFocusTarget = "None" | "LastFocusedItem" | "SelectedItem" | "FirstItem";
9
+
10
+ export interface BaseCompositeProps<T extends object> extends React.ComponentPropsWithoutRef<"div"> {
11
+ data: CompositeData<T>;
12
+ className?: string;
13
+ rootRef?: React.RefObject<HTMLDivElement | null>;
14
+ handleRef?: React.RefObject<CompositeHandle<T> | null>;
15
+
16
+ renderItem: CompositeItemRenderFn<T>;
17
+ itemClassName?: string;
18
+
19
+ initialFocus?: InitialFocusTarget;
20
+ /**
21
+ * Set `item.focused = true`, but not focus on the HTMLElement
22
+ */
23
+ softFocus?: boolean;
24
+ }
25
+
26
+ export interface CompositeProps<T extends object> extends BaseCompositeProps<T>, CompositeItemEventHandlerFunctions<T> {
27
+ rootRole: AriaRole;
28
+ itemRole: AriaRole;
29
+ groupRole: AriaRole;
30
+ }
31
+
32
+ export interface CompositeHandle<T extends object> {
33
+ focusManager: IFocusManager<T>;
34
+ focusElement: () => void;
35
+ selectionManager?: SelectionManager<T>;
36
+ }
37
+
38
+ export type CompositeItemRenderFn<T extends object> = (
39
+ item: { data: T; level: number; key: CompositeItemKey },
40
+ state: CompositeDataItemState,
41
+ eventHandlers: CompositeEventHandlers,
42
+ ) => ReactNode;
43
+
44
+ export interface CompositeItemProps<T extends object> extends CompositeItemEventHandlerFunctions<T> {
45
+ id: string;
46
+ className?: string;
47
+ role?: AriaRole;
48
+ groupRole?: AriaRole;
49
+
50
+ item: CompositeDataItem<T>;
51
+
52
+ remove?: () => void;
53
+ render: CompositeItemRenderFn<T>;
54
+
55
+ softFocus?: boolean;
56
+ }
57
+
58
+ export interface CompositeEventHandlers {
59
+ mouseEventHandler?: MouseEventHandler<HTMLElement>;
60
+ keyboardEventHandler?: KeyboardEventHandler<HTMLElement>;
61
+ }
62
+
63
+ export interface CompositeItemEventHandlerFunctions<T extends object> {
64
+ itemMouseEventHandler?: (itemAtom: CompositeDataItem<T>) => void;
65
+ itemKeyboardEventHandler?: (itemAtom: CompositeDataItem<T>) => void;
66
+ }
@@ -0,0 +1,6 @@
1
+ export const isOutsideElement = (parent: Element | null, el: Element | null) => {
2
+ if (parent === el) return false;
3
+
4
+ const nodeId = !!el?.id && CSS.escape(el.id);
5
+ return !nodeId || parent?.querySelector(`#${nodeId}`) === null;
6
+ };