@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.
- package/.yarn/install-state.gz +0 -0
- package/.yarnrc.yml +0 -2
- package/dist/_chunks/createLucideIcon.js +11 -10
- package/dist/_chunks/index.js +2 -2
- package/dist/_chunks/index4.js +2 -2
- package/dist/components/index.js +18 -13
- package/dist/components/ui/composite/CompositeData.d.ts +9 -8
- package/dist/components/ui/composite/CompositeData.js +20 -14
- package/dist/components/ui/composite/{FocusProvider/AbstractFocusProvider.d.ts → FocusManager/AbstractFocusManager.d.ts} +2 -19
- package/dist/components/ui/composite/{FocusProvider/AbstractFocusProvider.js → FocusManager/AbstractFocusManager.js} +16 -5
- package/dist/components/ui/composite/{FocusProvider/ListboxFocusProvider.d.ts → FocusManager/ListboxFocusManager.d.ts} +2 -2
- package/dist/components/ui/composite/{FocusProvider/ListboxFocusProvider.js → FocusManager/ListboxFocusManager.js} +3 -3
- package/dist/components/ui/composite/FocusManager/index.d.ts +3 -0
- package/dist/components/ui/composite/FocusManager/index.js +6 -0
- package/dist/components/ui/composite/FocusManager/types.d.ts +20 -0
- package/dist/components/ui/composite/FocusManager/types.js +1 -0
- package/dist/components/ui/composite/{SelectionProvider/AbstractSelectionProvider.d.ts → SelectionManager/AbstractSelectionManager.d.ts} +5 -20
- package/dist/components/ui/composite/{SelectionProvider/AbstractSelectionProvider.js → SelectionManager/AbstractSelectionManager.js} +8 -4
- package/dist/components/ui/composite/{SelectionProvider/MultipleSelectionProvider.d.ts → SelectionManager/MultipleSelectionManager.d.ts} +3 -2
- package/dist/components/ui/composite/{SelectionProvider/MultipleSelectionProvider.js → SelectionManager/MultipleSelectionManager.js} +4 -3
- package/dist/components/ui/composite/{SelectionProvider/SingleSelectionProvider.d.ts → SelectionManager/SingleSelectionManager.d.ts} +3 -2
- package/dist/components/ui/composite/{SelectionProvider/SingleSelectionProvider.js → SelectionManager/SingleSelectionManager.js} +6 -5
- package/dist/components/ui/composite/SelectionManager/index.d.ts +4 -0
- package/dist/components/ui/composite/SelectionManager/index.js +8 -0
- package/dist/components/ui/composite/SelectionManager/types.d.ts +19 -0
- package/dist/components/ui/composite/SelectionManager/types.js +1 -0
- package/dist/components/ui/composite/components/composite-component.d.ts +2 -0
- package/dist/components/ui/composite/{composite-component.js → components/composite-component.js} +16 -38
- package/dist/components/ui/composite/components/index.d.ts +5 -0
- package/dist/components/ui/composite/components/index.js +10 -0
- package/dist/components/ui/composite/{listbox.d.ts → components/listbox.d.ts} +1 -1
- package/dist/components/ui/composite/{listbox.js → components/listbox.js} +17 -12
- package/dist/components/ui/composite/components/types.d.ts +53 -0
- package/dist/components/ui/composite/components/types.js +1 -0
- package/dist/components/ui/composite/components/utils.d.ts +1 -0
- package/dist/components/ui/composite/components/utils.js +8 -0
- package/dist/components/ui/composite/create-composite-data.d.ts +80 -0
- package/dist/components/ui/composite/create-composite-data.js +22 -0
- package/dist/components/ui/composite/index.d.ts +4 -5
- package/dist/components/ui/composite/index.js +18 -13
- package/dist/components/ui/composite/types.d.ts +5 -67
- package/dist/components/ui/navigation-menu.js +1 -1
- package/dist/components/ui/popover.js +1 -1
- package/dist/hooks/index.d.ts +1 -0
- package/dist/hooks/index.js +3 -1
- package/eslint.config.js +1 -1
- package/package.json +17 -16
- package/src/components/ui/composite/CompositeData.ts +38 -30
- package/src/components/ui/composite/{FocusProvider/AbstractFocusProvider.ts → FocusManager/AbstractFocusManager.ts} +19 -26
- package/src/components/ui/composite/{FocusProvider/ListboxFocusProvider.ts → FocusManager/ListboxFocusManager.ts} +2 -2
- package/src/components/ui/composite/FocusManager/index.ts +3 -0
- package/src/components/ui/composite/FocusManager/types.ts +23 -0
- package/src/components/ui/composite/{SelectionProvider/AbstractSelectionProvider.ts → SelectionManager/AbstractSelectionManager.ts} +13 -28
- package/src/components/ui/composite/{SelectionProvider/MultipleSelectionProvider.ts → SelectionManager/MultipleSelectionManager.ts} +4 -2
- package/src/components/ui/composite/{SelectionProvider/SingleSelectionProvider.ts → SelectionManager/SingleSelectionManager.ts} +6 -4
- package/src/components/ui/composite/SelectionManager/index.ts +4 -0
- package/src/components/ui/composite/SelectionManager/types.ts +24 -0
- package/src/components/ui/composite/{composite-component.tsx → components/composite-component.tsx} +17 -42
- package/src/components/ui/composite/components/index.ts +5 -0
- package/src/components/ui/composite/{listbox.tsx → components/listbox.tsx} +16 -11
- package/src/components/ui/composite/components/types.ts +66 -0
- package/src/components/ui/composite/components/utils.ts +6 -0
- package/src/components/ui/composite/create-composite-data.ts +98 -0
- package/src/components/ui/composite/index.ts +7 -8
- package/src/components/ui/composite/types.ts +5 -86
- package/src/hooks/index.ts +4 -3
- package/.yarn/releases/yarn-4.10.3.cjs +0 -942
- package/dist/components/ui/composite/FocusProvider/index.d.ts +0 -2
- package/dist/components/ui/composite/FocusProvider/index.js +0 -6
- package/dist/components/ui/composite/SelectionProvider/index.d.ts +0 -3
- package/dist/components/ui/composite/SelectionProvider/index.js +0 -8
- package/dist/components/ui/composite/composite-component.d.ts +0 -2
- package/src/components/ui/composite/FocusProvider/index.ts +0 -2
- package/src/components/ui/composite/SelectionProvider/index.ts +0 -3
- /package/dist/components/ui/composite/{composite-component-item.d.ts → components/composite-component-item.d.ts} +0 -0
- /package/dist/components/ui/composite/{composite-component-item.js → components/composite-component-item.js} +0 -0
- /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,
|
|
1
|
+
import type { CompositeDataOptions, CompositeItemKey, CompositeOptions, CompositeManagerOptions } from "./types";
|
|
2
2
|
import { CompositeDataItem } from "./CompositeDataItem";
|
|
3
|
-
import type {
|
|
4
|
-
import type {
|
|
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
|
-
|
|
13
|
-
|
|
14
|
+
focusManager: AbstractFocusManager<T>;
|
|
15
|
+
selectionManager?: AbstractSelectionManager<T>;
|
|
14
16
|
|
|
15
17
|
disabledKeys: Set<CompositeItemKey> = new Set();
|
|
16
18
|
|
|
17
|
-
root
|
|
19
|
+
root?: CompositeDataItem<T> = undefined;
|
|
18
20
|
|
|
19
|
-
constructor(items: T[],
|
|
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) =>
|
|
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.
|
|
34
|
-
this.
|
|
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
|
|
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
|
-
|
|
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
|
|
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.
|
|
69
|
-
this.selectionProvider.init(this, this.#options);
|
|
75
|
+
if (this.selectionManager) this.selectionManager.init(this, this.#options);
|
|
70
76
|
|
|
71
|
-
this.
|
|
77
|
+
this.focusManager.init(this, this.#options);
|
|
78
|
+
|
|
79
|
+
this.#version.set(this.#version.value + 1);
|
|
72
80
|
}
|
|
73
81
|
|
|
74
|
-
item(key: CompositeItemKey) {
|
|
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
|
-
|
|
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.
|
|
120
|
-
this.
|
|
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
|
|
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
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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 {
|
|
2
|
+
import { AbstractFocusManager } from "./AbstractFocusManager";
|
|
3
3
|
|
|
4
|
-
export class
|
|
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,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
|
|
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?:
|
|
13
|
+
options?: SelectionManagerOptions<T>;
|
|
34
14
|
|
|
35
|
-
constructor(options?:
|
|
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
|
-
|
|
44
|
-
|
|
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.
|
|
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 {
|
|
3
|
+
import { AbstractSelectionManager } from "./AbstractSelectionManager";
|
|
4
4
|
import { difference, union } from "@/lib/set-operations";
|
|
5
5
|
|
|
6
|
-
export class
|
|
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 {
|
|
5
|
+
import { AbstractSelectionManager } from "./AbstractSelectionManager";
|
|
6
6
|
import { difference } from "@/lib/set-operations";
|
|
7
7
|
|
|
8
|
-
export class
|
|
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.
|
|
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.
|
|
43
|
+
: this.data.focusManager.focusedItem.get();
|
|
42
44
|
if (!_item) return;
|
|
43
45
|
|
|
44
46
|
const deselectedKeys = _item.deselect(false);
|
|
@@ -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
|
+
}
|
package/src/components/ui/composite/{composite-component.tsx → components/composite-component.tsx}
RENAMED
|
@@ -1,34 +1,14 @@
|
|
|
1
|
-
import { useCallback, useEffect, useImperativeHandle,
|
|
2
|
-
import type { CompositeItemKey
|
|
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 "
|
|
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.
|
|
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.
|
|
39
|
+
}, [softFocus, data.focusManager.focusedItem, $rootRef]);
|
|
60
40
|
|
|
61
41
|
useImperativeHandle(handleRef, () => {
|
|
62
42
|
return {
|
|
63
|
-
|
|
64
|
-
|
|
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.
|
|
53
|
+
if (initialFocus === "FirstItem") initialFocusItem = data.focusManager.firstFocusableItem;
|
|
79
54
|
else if (initialFocus === "SelectedItem") {
|
|
80
|
-
const selecedItem = data.
|
|
81
|
-
initialFocusItem = selecedItem ?? data.
|
|
55
|
+
const selecedItem = data.selectionManager?.selectedKeys.get().values().next().value;
|
|
56
|
+
initialFocusItem = selecedItem ?? data.focusManager.firstFocusableItem;
|
|
82
57
|
}
|
|
83
58
|
|
|
84
|
-
data.
|
|
59
|
+
data.focusManager.focus(initialFocusItem);
|
|
85
60
|
if (!onlyData) focusElement();
|
|
86
61
|
},
|
|
87
|
-
[data.
|
|
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={
|
|
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={
|
|
115
|
-
groupRole={
|
|
89
|
+
role={itemRole}
|
|
90
|
+
groupRole={groupRole}
|
|
116
91
|
item={item}
|
|
117
92
|
key={item.key}
|
|
118
93
|
render={renderItem}
|
|
@@ -1,15 +1,17 @@
|
|
|
1
1
|
import { CompositeComponent } from "./composite-component";
|
|
2
2
|
import { useCallback } from "react";
|
|
3
|
-
import { CompositeDataItem } from "
|
|
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.
|
|
22
|
+
data.focusManager.focusUp();
|
|
21
23
|
$handleRef.current?.focusElement();
|
|
22
24
|
},
|
|
23
25
|
ArrowDown: () => {
|
|
24
|
-
data.
|
|
26
|
+
data.focusManager.focusDown();
|
|
25
27
|
$handleRef.current?.focusElement();
|
|
26
28
|
},
|
|
27
29
|
Home: () => {
|
|
28
|
-
data.
|
|
30
|
+
data.focusManager.focusToFirst();
|
|
29
31
|
$handleRef.current?.focusElement();
|
|
30
32
|
},
|
|
31
33
|
End: () => {
|
|
32
|
-
data.
|
|
34
|
+
data.focusManager.focusToLast();
|
|
33
35
|
$handleRef.current?.focusElement();
|
|
34
36
|
},
|
|
35
37
|
Space: () => {
|
|
36
|
-
data.
|
|
38
|
+
data.selectionManager?.toggleSelect();
|
|
37
39
|
$handleRef.current?.focusElement();
|
|
38
40
|
},
|
|
39
41
|
},
|
|
40
|
-
[$handleRef, data.
|
|
42
|
+
[$handleRef, data.focusManager],
|
|
41
43
|
);
|
|
42
44
|
|
|
43
45
|
const handleItemMouseEvent = useCallback(
|
|
44
46
|
(item: CompositeDataItem<T>) => {
|
|
45
|
-
data.
|
|
46
|
-
data.
|
|
47
|
+
data.focusManager.focus(item.key);
|
|
48
|
+
data.selectionManager?.toggleSelect(item);
|
|
47
49
|
$handleRef.current?.focusElement();
|
|
48
50
|
},
|
|
49
|
-
[$handleRef, data.
|
|
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
|
-
|
|
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
|
+
}
|