@bccampus/ui-components 0.4.1 → 0.4.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.
- package/dist/AbstractFocusProvider-CxvlcEki.js +29 -0
- package/dist/AbstractSelectionProvider-BtaROstC.js +30 -0
- package/dist/CompositeDataItem-DuHOHCWy.js +158 -0
- package/dist/ListboxFocusProvider.d.ts +149 -0
- package/dist/ListboxFocusProvider.js +53 -0
- package/dist/MultipleSelectionProvider.d.ts +141 -0
- package/dist/MultipleSelectionProvider.js +19 -0
- package/dist/SingleSelectionProvider.d.ts +141 -0
- package/dist/SingleSelectionProvider.js +23 -0
- package/dist/composite-component-DSUbd1XS.js +122 -0
- package/dist/composite.d.ts +108 -51
- package/dist/composite.js +57 -447
- package/dist/listbox.d.ts +171 -0
- package/dist/listbox.js +76 -0
- package/package.json +5 -1
- package/src/components/ui/composite/CompositeData.ts +22 -114
- package/src/components/ui/composite/FocusProvider/AbstractFocusProvider.ts +83 -0
- package/src/components/ui/composite/FocusProvider/ListboxFocusProvider.ts +74 -0
- package/src/components/ui/composite/SelectionProvider/AbstractSelectionProvider.ts +45 -0
- package/src/components/ui/composite/SelectionProvider/MultipleSelectionProvider.ts +28 -0
- package/src/components/ui/composite/SelectionProvider/SingleSelectionProvider.ts +37 -0
- package/src/components/ui/composite/composite-component-item.tsx +12 -10
- package/src/components/ui/composite/composite-component.tsx +39 -68
- package/src/components/ui/composite/index.ts +4 -1
- package/src/components/ui/composite/listbox.tsx +61 -0
- package/src/components/ui/composite/types.ts +51 -30
- package/src/hooks/use-keyboard-event.ts +31 -42
- package/vite.config.ts +4 -0
- package/src/components/ui/composite/composite-data-context.tsx +0 -31
package/dist/listbox.js
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { jsx as y } from "react/jsx-runtime";
|
|
2
|
+
import { C as d } from "./composite-component-DSUbd1XS.js";
|
|
3
|
+
import { useMemo as m, useRef as a, useCallback as u } from "react";
|
|
4
|
+
const h = /* @__PURE__ */ new Set(["ctrl", "shift", "alt", "meta"]), f = {
|
|
5
|
+
" ": "space"
|
|
6
|
+
};
|
|
7
|
+
function K(e) {
|
|
8
|
+
const r = [];
|
|
9
|
+
for (const [t, n] of Object.entries(e)) {
|
|
10
|
+
const o = t.toLowerCase().trim().split(/\s*\+\s*/g);
|
|
11
|
+
o.length === 1 && h.has(o[0]) ? console.error(`[useKeyboardEvent] '${t}': A key sequence cannot be only a modifier key.`) : o.includes("") ? console.error(`[useKeyboardEvent] '${t}': Invalid key sequence defined in the sequence.`) : r.push({
|
|
12
|
+
sequence: new RegExp("^" + o.join("\\+") + "$"),
|
|
13
|
+
handler: n
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
return r;
|
|
17
|
+
}
|
|
18
|
+
const l = {
|
|
19
|
+
eventKeyProp: "key"
|
|
20
|
+
};
|
|
21
|
+
function v(e, r = l) {
|
|
22
|
+
const t = { ...r, ...l }, n = K(e);
|
|
23
|
+
return (o) => {
|
|
24
|
+
const c = o[t.eventKeyProp], s = [];
|
|
25
|
+
o.ctrlKey && s.push("ctrl"), o.shiftKey && s.push("shift"), o.altKey && s.push("alt"), o.metaKey && s.push("meta"), f[c] ? s.push(f[c]) : s.push(c.toLowerCase());
|
|
26
|
+
const i = n.find((p) => p.sequence.test(s.join("+")));
|
|
27
|
+
i && (o.preventDefault(), o.stopPropagation(), i.handler(o));
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
function P(e, r) {
|
|
31
|
+
return m(() => v(e, r), [e, r]);
|
|
32
|
+
}
|
|
33
|
+
function k({ data: e, ...r }) {
|
|
34
|
+
const t = a(null), n = u(() => {
|
|
35
|
+
const s = e.focusProvider.focusedItem.get()?.key;
|
|
36
|
+
if (s && t.current) {
|
|
37
|
+
const i = t.current.querySelector(`[data-key="${s}"]`);
|
|
38
|
+
i && i.focus();
|
|
39
|
+
}
|
|
40
|
+
}, [e]), o = P({
|
|
41
|
+
ArrowUp: () => {
|
|
42
|
+
e.focusProvider.focusUp(), n();
|
|
43
|
+
},
|
|
44
|
+
ArrowDown: () => {
|
|
45
|
+
e.focusProvider.focusDown(), n();
|
|
46
|
+
},
|
|
47
|
+
Home: () => {
|
|
48
|
+
e.focusProvider.focusToFirst(), n();
|
|
49
|
+
},
|
|
50
|
+
End: () => {
|
|
51
|
+
e.focusProvider.focusToLast(), n();
|
|
52
|
+
},
|
|
53
|
+
Space: () => {
|
|
54
|
+
e.selectionProvider?.toggleSelect(), n();
|
|
55
|
+
}
|
|
56
|
+
}), c = u(
|
|
57
|
+
(s) => {
|
|
58
|
+
e.focusProvider.focus(s.key), e.selectionProvider?.toggleSelect(s), n();
|
|
59
|
+
},
|
|
60
|
+
[e.focusProvider, e.selectionProvider, n]
|
|
61
|
+
);
|
|
62
|
+
return /* @__PURE__ */ y(
|
|
63
|
+
d,
|
|
64
|
+
{
|
|
65
|
+
ref: t,
|
|
66
|
+
variant: "listbox",
|
|
67
|
+
data: e,
|
|
68
|
+
onKeyDown: o,
|
|
69
|
+
itemMouseEventHandler: c,
|
|
70
|
+
...r
|
|
71
|
+
}
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
export {
|
|
75
|
+
k as Listbox
|
|
76
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bccampus/ui-components",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"packageManager": "yarn@4.10.3",
|
|
6
6
|
"exports": {
|
|
@@ -12,6 +12,10 @@
|
|
|
12
12
|
"types": "./dist/composite.d.ts",
|
|
13
13
|
"import": "./dist/composite.js"
|
|
14
14
|
},
|
|
15
|
+
"./listbox": {
|
|
16
|
+
"types": "./dist/listbox.d.ts",
|
|
17
|
+
"import": "./dist/listbox.js"
|
|
18
|
+
},
|
|
15
19
|
"./icon-generator": {
|
|
16
20
|
"types": "./dist/icon-generator.d.ts",
|
|
17
21
|
"import": "./dist/icon-generator.js"
|
|
@@ -1,21 +1,22 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { atom, type PreinitializedWritableAtom } from "nanostores";
|
|
3
|
-
import type { CompositeDataOptions, CompositeItemKey, CompositeOptions } from "./types";
|
|
1
|
+
import type { CompositeDataOptions, CompositeItemKey, CompositeOptions, CompositeProviderOptions } from "./types";
|
|
4
2
|
import { CompositeDataItem } from "./CompositeDataItem";
|
|
3
|
+
import type { AbstractFocusProvider } from "./FocusProvider/AbstractFocusProvider";
|
|
4
|
+
import type { AbstractSelectionProvider } from "./SelectionProvider/AbstractSelectionProvider";
|
|
5
|
+
import { union } from "@/lib/set-operations";
|
|
5
6
|
|
|
6
7
|
export class CompositeData<T extends object> implements Iterable<CompositeDataItem<T>> {
|
|
7
8
|
#options: CompositeDataOptions<T>;
|
|
8
9
|
|
|
9
10
|
lookup: Map<CompositeItemKey, CompositeDataItem<T>> = new Map();
|
|
10
11
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
12
|
+
focusProvider: AbstractFocusProvider<T>;
|
|
13
|
+
selectionProvider?: AbstractSelectionProvider<T>;
|
|
14
|
+
|
|
15
|
+
disabledKeys: Set<CompositeItemKey> = new Set();
|
|
15
16
|
|
|
16
17
|
root!: CompositeDataItem<T>;
|
|
17
18
|
|
|
18
|
-
constructor(items: T[], options?: CompositeOptions) {
|
|
19
|
+
constructor(items: T[], providerOptions: CompositeProviderOptions<T>, options?: CompositeOptions) {
|
|
19
20
|
this.#options = {
|
|
20
21
|
itemKeyProp: "key",
|
|
21
22
|
itemChildrenProp: "children",
|
|
@@ -29,6 +30,9 @@ export class CompositeData<T extends object> implements Iterable<CompositeDataIt
|
|
|
29
30
|
...options
|
|
30
31
|
};
|
|
31
32
|
|
|
33
|
+
this.focusProvider = providerOptions.focusProvider;
|
|
34
|
+
this.selectionProvider = providerOptions.selectionProvider;
|
|
35
|
+
|
|
32
36
|
this.init(items);
|
|
33
37
|
}
|
|
34
38
|
|
|
@@ -49,7 +53,6 @@ export class CompositeData<T extends object> implements Iterable<CompositeDataIt
|
|
|
49
53
|
|
|
50
54
|
|
|
51
55
|
init(items: T[]) {
|
|
52
|
-
// this.itemAtoms = this.#setItemMap(items);
|
|
53
56
|
this.root = new CompositeDataItem(
|
|
54
57
|
{ [this.#options.itemKeyProp]: "ALL", [this.#options.itemChildrenProp]: items } as T,
|
|
55
58
|
this.#options,
|
|
@@ -60,50 +63,12 @@ export class CompositeData<T extends object> implements Iterable<CompositeDataIt
|
|
|
60
63
|
this.lookup = new Map([...this.root].map(item => [item.key, item]));
|
|
61
64
|
|
|
62
65
|
// Set initially disabled items
|
|
63
|
-
this.#options.disabledKeys.forEach(disabledKey => this
|
|
64
|
-
|
|
65
|
-
// Set initially selected items : SelectionProvider
|
|
66
|
-
this.#options.selectedKeys.forEach(selectedKey => this.select(selectedKey));
|
|
67
|
-
|
|
68
|
-
// Set initially focused items : FocusProvider
|
|
69
|
-
this.firstFocusableItem = this.getFirstFocusableItem() ?? null;
|
|
70
|
-
if (this.firstFocusableItem) {
|
|
71
|
-
this.firstFocusableItem.state.setKey("focused", true);
|
|
72
|
-
this.focusedItem.set(this.firstFocusableItem);
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
// Set focus pointers: FocusProvider
|
|
76
|
-
const lookupData = [...this.lookup];
|
|
77
|
-
for (let index = 0; index < lookupData.length; index++) {
|
|
78
|
-
const [key, item] = lookupData[index];
|
|
79
|
-
|
|
80
|
-
if (!this.isFocusable(item)) continue;
|
|
81
|
-
|
|
82
|
-
if (index < lookupData.length - 1) {
|
|
83
|
-
let newIndex = index === lookupData.length - 1 ? 0 : index + 1;
|
|
84
|
-
while (newIndex < lookupData.length && !this.isFocusable(lookupData[newIndex][1])) {
|
|
85
|
-
newIndex = (newIndex + 1) % lookupData.length;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
item.pointers.down = lookupData[newIndex][0];
|
|
89
|
-
lookupData[newIndex][1].pointers.up = key;
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
}
|
|
66
|
+
this.#options.disabledKeys.forEach(disabledKey => this.#disable(disabledKey));
|
|
93
67
|
|
|
68
|
+
if (this.selectionProvider)
|
|
69
|
+
this.selectionProvider.init(this, this.#options);
|
|
94
70
|
|
|
95
|
-
|
|
96
|
-
return !itemAtom.state.get().disabled;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
getFirstFocusableItem() {
|
|
100
|
-
let iterItem = this.lookup.values().next();
|
|
101
|
-
|
|
102
|
-
while (iterItem.value && !this.isFocusable(iterItem.value)) {
|
|
103
|
-
iterItem = this.lookup.values().next();
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
return iterItem.value;
|
|
71
|
+
this.focusProvider.init(this, this.#options);
|
|
107
72
|
}
|
|
108
73
|
|
|
109
74
|
item(key: CompositeItemKey) { return this.lookup.get(key) };
|
|
@@ -122,71 +87,14 @@ export class CompositeData<T extends object> implements Iterable<CompositeDataIt
|
|
|
122
87
|
return item.ancestors();
|
|
123
88
|
}
|
|
124
89
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
const _item = item instanceof CompositeDataItem ? item : this.lookup.get(item);
|
|
129
|
-
if (!_item) return;
|
|
130
|
-
|
|
131
|
-
if (this.focusedItem.get()) {
|
|
132
|
-
if (_item.key === this.focusedItem.get()?.key) return;
|
|
133
|
-
|
|
134
|
-
this.focusedItem.get()!.state.setKey("focused", false);
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
_item.state.setKey("focused", true);
|
|
138
|
-
this.focusedItem.set(_item);
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
toggleSelect(itemKey: CompositeItemKey, recursive?: boolean): void;
|
|
142
|
-
toggleSelect(item: CompositeDataItem<T>, recursive?: boolean): void;
|
|
143
|
-
toggleSelect(item: CompositeDataItem<T> | CompositeItemKey, recursive: boolean = false) {
|
|
144
|
-
const _item = item instanceof CompositeDataItem ? item : this.lookup.get(item);
|
|
145
|
-
if (!_item) return;
|
|
146
|
-
|
|
147
|
-
if (_item.state.get().selected) this.deselect(_item, recursive);
|
|
148
|
-
else this.select(_item, recursive);
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
select(itemKey: CompositeItemKey, recursive?: boolean): void;
|
|
152
|
-
select(item: CompositeDataItem<T>, recursive?: boolean): void;
|
|
153
|
-
select(item: CompositeDataItem<T> | CompositeItemKey, recursive: boolean = false) {
|
|
154
|
-
const _item = item instanceof CompositeDataItem ? item : this.lookup.get(item);
|
|
155
|
-
if (!_item) return;
|
|
156
|
-
|
|
157
|
-
const selectedKeys = _item.select(recursive);
|
|
158
|
-
this.selectedKeys.set(union(this.selectedKeys.get(), selectedKeys));
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
deselect(itemKey: CompositeItemKey, recursive?: boolean): void;
|
|
162
|
-
deselect(item: CompositeDataItem<T>, recursive?: boolean): void;
|
|
163
|
-
deselect(item: CompositeDataItem<T> | CompositeItemKey, recursive: boolean = false) {
|
|
164
|
-
const _item = item instanceof CompositeDataItem ? item : this.lookup.get(item);
|
|
165
|
-
if (!_item) return;
|
|
166
|
-
|
|
167
|
-
const deselectedKeys = _item.deselect(recursive);
|
|
168
|
-
this.selectedKeys.set(difference(this.selectedKeys.get(), deselectedKeys));
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
disable(itemKey: CompositeItemKey, recursive?: boolean): void;
|
|
172
|
-
disable(item: CompositeDataItem<T>, recursive?: boolean): void;
|
|
173
|
-
disable(item: CompositeDataItem<T> | CompositeItemKey, recursive: boolean = false) {
|
|
90
|
+
#disable(itemKey: CompositeItemKey, recursive?: boolean): void;
|
|
91
|
+
#disable(item: CompositeDataItem<T>, recursive?: boolean): void;
|
|
92
|
+
#disable(item: CompositeDataItem<T> | CompositeItemKey, recursive: boolean = false) {
|
|
174
93
|
const _item = item instanceof CompositeDataItem ? item : this.lookup.get(item);
|
|
175
94
|
if (!_item) return;
|
|
176
95
|
|
|
177
96
|
const disabledKeys = _item.disable(recursive);
|
|
178
|
-
this.disabledKeys
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
enable(itemKey: CompositeItemKey, recursive?: boolean): void;
|
|
183
|
-
enable(item: CompositeDataItem<T>, recursive?: boolean): void;
|
|
184
|
-
enable(item: CompositeDataItem<T> | CompositeItemKey, recursive: boolean = false) {
|
|
185
|
-
const _item = item instanceof CompositeDataItem ? item : this.lookup.get(item);
|
|
186
|
-
if (!_item) return;
|
|
187
|
-
|
|
188
|
-
const enabledKeys = _item.enable(recursive);
|
|
189
|
-
this.disabledKeys.set(union(this.selectedKeys.get(), enabledKeys));
|
|
97
|
+
this.disabledKeys = union(this.disabledKeys, disabledKeys);
|
|
190
98
|
}
|
|
191
99
|
|
|
192
100
|
|
|
@@ -208,8 +116,8 @@ export class CompositeData<T extends object> implements Iterable<CompositeDataIt
|
|
|
208
116
|
}
|
|
209
117
|
|
|
210
118
|
#updateIfSelectedItem(key: CompositeItemKey) {
|
|
211
|
-
if (this.selectedKeys.get().has(key)) {
|
|
212
|
-
this.selectedKeys.set(new Set([...this.selectedKeys.get()]));
|
|
119
|
+
if (this.selectionProvider && this.selectionProvider.selectedKeys.get().has(key)) {
|
|
120
|
+
this.selectionProvider.selectedKeys.set(new Set([...this.selectionProvider.selectedKeys.get()]));
|
|
213
121
|
}
|
|
214
122
|
}
|
|
215
123
|
}
|
|
@@ -0,0 +1,83 @@
|
|
|
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 {
|
|
7
|
+
focus(itemKey: CompositeItemKey): void;
|
|
8
|
+
|
|
9
|
+
focusUp(): void;
|
|
10
|
+
focusDown(): void;
|
|
11
|
+
focusLeft(): void;
|
|
12
|
+
focusRight(): void;
|
|
13
|
+
focusPageUp(): void;
|
|
14
|
+
focesPageDown(): void;
|
|
15
|
+
focusToFirst(): void;
|
|
16
|
+
focusToLast(): void;
|
|
17
|
+
focusToFirstInRow(): void;
|
|
18
|
+
focusToLastInRow(): void;
|
|
19
|
+
focusToTypeAheadMatch(): void;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export abstract class AbstractFocusProvider<T extends object> implements FocusProvider {
|
|
23
|
+
protected data!: CompositeData<T>;
|
|
24
|
+
protected dataOptions!: CompositeDataOptions<T>;
|
|
25
|
+
|
|
26
|
+
firstFocusableItem!: CompositeDataItem<T> | null;
|
|
27
|
+
lastFocusableItem!: CompositeDataItem<T> | null;
|
|
28
|
+
focusedItem: PreinitializedWritableAtom<CompositeDataItem<T> | null> = atom(null);
|
|
29
|
+
|
|
30
|
+
init(data: CompositeData<T>, dataOptions: CompositeDataOptions<T>) {
|
|
31
|
+
this.data = data;
|
|
32
|
+
this.dataOptions = dataOptions;
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
// Set focus pointers
|
|
36
|
+
const [first, last] = this.setFocusPointers();
|
|
37
|
+
|
|
38
|
+
// Set initially focused items
|
|
39
|
+
this.firstFocusableItem = first;
|
|
40
|
+
if (this.firstFocusableItem) {
|
|
41
|
+
this.firstFocusableItem.state.setKey("focused", true);
|
|
42
|
+
this.focusedItem.set(this.firstFocusableItem);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
this.lastFocusableItem = last;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
isFocusable(itemAtom: CompositeDataItem<T>) {
|
|
50
|
+
return !itemAtom.state.get().disabled;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
focus(itemKey: CompositeItemKey): void;
|
|
54
|
+
focus(item: CompositeDataItem<T>): void;
|
|
55
|
+
focus(item: CompositeDataItem<T> | CompositeItemKey) {
|
|
56
|
+
const _item = item instanceof CompositeDataItem ? item : this.data.lookup.get(item);
|
|
57
|
+
if (!_item) return;
|
|
58
|
+
|
|
59
|
+
if (this.focusedItem.get()) {
|
|
60
|
+
if (_item.key === this.focusedItem.get()?.key) return;
|
|
61
|
+
|
|
62
|
+
this.focusedItem.get()!.state.setKey("focused", false);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
_item.state.setKey("focused", true);
|
|
66
|
+
this.focusedItem.set(_item);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
abstract setFocusPointers(): readonly [first: CompositeDataItem<T> | null, last: CompositeDataItem<T> | null];
|
|
70
|
+
|
|
71
|
+
abstract focusUp(): void;
|
|
72
|
+
abstract focusDown(): void;
|
|
73
|
+
abstract focusLeft(): void;
|
|
74
|
+
abstract focusRight(): void;
|
|
75
|
+
abstract focusPageUp(): void;
|
|
76
|
+
abstract focesPageDown(): void;
|
|
77
|
+
abstract focusToFirst(): void;
|
|
78
|
+
abstract focusToLast(): void;
|
|
79
|
+
abstract focusToFirstInRow(): void;
|
|
80
|
+
abstract focusToLastInRow(): void;
|
|
81
|
+
abstract focusToTypeAheadMatch(): void;
|
|
82
|
+
|
|
83
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
|
|
2
|
+
import type { CompositeDataItem } from "../CompositeDataItem";
|
|
3
|
+
import { AbstractFocusProvider } from "./AbstractFocusProvider";
|
|
4
|
+
|
|
5
|
+
export class ListboxFocusProvider<T extends object> extends AbstractFocusProvider<T> {
|
|
6
|
+
setFocusPointers() {
|
|
7
|
+
let first: CompositeDataItem<T> | null = null;
|
|
8
|
+
let last: CompositeDataItem<T> | null = null
|
|
9
|
+
|
|
10
|
+
const lookupData = [...this.data.lookup];
|
|
11
|
+
for (let index = 0; index < lookupData.length; index++) {
|
|
12
|
+
const [key, item] = lookupData[index];
|
|
13
|
+
|
|
14
|
+
if (!this.isFocusable(item)) continue;
|
|
15
|
+
if (!first) first = item;
|
|
16
|
+
last = item;
|
|
17
|
+
|
|
18
|
+
if (index < lookupData.length - 1) {
|
|
19
|
+
let newIndex = index === lookupData.length - 1 ? 0 : index + 1;
|
|
20
|
+
while (newIndex < lookupData.length && !this.isFocusable(lookupData[newIndex][1])) {
|
|
21
|
+
newIndex = (newIndex + 1) % lookupData.length;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
item.pointers.down = lookupData[newIndex][0];
|
|
25
|
+
lookupData[newIndex][1].pointers.up = key;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return [first, last] as const;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
focusUp(): void {
|
|
33
|
+
if (this.focusedItem.get()?.pointers.up) {
|
|
34
|
+
this.focus(this.focusedItem.get()!.pointers.up!);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
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
|
+
}
|
|
54
|
+
focusToFirst(): void {
|
|
55
|
+
if (this.firstFocusableItem) {
|
|
56
|
+
this.focus(this.firstFocusableItem);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
focusToLast(): void {
|
|
60
|
+
if (this.lastFocusableItem) {
|
|
61
|
+
this.focus(this.lastFocusableItem);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
focusToFirstInRow(): void {
|
|
65
|
+
throw new Error("Method not implemented.");
|
|
66
|
+
}
|
|
67
|
+
focusToLastInRow(): void {
|
|
68
|
+
throw new Error("Method not implemented.");
|
|
69
|
+
}
|
|
70
|
+
focusToTypeAheadMatch(): void {
|
|
71
|
+
throw new Error("Method not implemented.");
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
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 SelectionProvider {
|
|
7
|
+
select(itemKey?: CompositeItemKey, recursive?: boolean): void;
|
|
8
|
+
deselect(itemKey?: CompositeItemKey, recursive?: boolean): void;
|
|
9
|
+
toggleSelect(itemKey?: CompositeItemKey, recursive?: boolean): void;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export abstract class AbstractSelectionProvider<T extends object> {
|
|
13
|
+
protected data!: CompositeData<T>;
|
|
14
|
+
protected dataOptions!: CompositeDataOptions<T>;
|
|
15
|
+
|
|
16
|
+
selectedKeys: PreinitializedWritableAtom<Set<CompositeItemKey>> = atom(new Set());
|
|
17
|
+
|
|
18
|
+
init(data: CompositeData<T>, dataOptions: CompositeDataOptions<T>) {
|
|
19
|
+
this.data = data;
|
|
20
|
+
this.dataOptions = dataOptions;
|
|
21
|
+
|
|
22
|
+
// Set initially selected items : SelectionProvider
|
|
23
|
+
this.dataOptions.selectedKeys.forEach(selectedKey => this.select(selectedKey));
|
|
24
|
+
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
abstract select(itemKey?: CompositeItemKey, recursive?: boolean): void;
|
|
28
|
+
abstract select(item: CompositeDataItem<T>, recursive?: boolean): void;
|
|
29
|
+
abstract select(item?: CompositeDataItem<T> | CompositeItemKey, recursive?: boolean): void;
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
abstract deselect(itemKey?: CompositeItemKey, recursive?: boolean): void;
|
|
33
|
+
abstract deselect(item: CompositeDataItem<T>, recursive?: boolean): void;
|
|
34
|
+
abstract deselect(item?: CompositeDataItem<T> | CompositeItemKey, recursive?: boolean): void;
|
|
35
|
+
|
|
36
|
+
toggleSelect(itemKey?: CompositeItemKey, recursive?: boolean): void;
|
|
37
|
+
toggleSelect(item: CompositeDataItem<T>, recursive?: boolean): void;
|
|
38
|
+
toggleSelect(item?: CompositeDataItem<T> | CompositeItemKey, recursive: boolean = false) {
|
|
39
|
+
const _item = item ? item instanceof CompositeDataItem ? item : this.data.lookup.get(item) : this.data.focusProvider.focusedItem.get();
|
|
40
|
+
if (!_item) return;
|
|
41
|
+
|
|
42
|
+
if (_item.state.get().selected) this.deselect(_item, recursive);
|
|
43
|
+
else this.select(_item, recursive);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { CompositeDataItem } from '../CompositeDataItem';
|
|
2
|
+
import type { CompositeItemKey } from '../types';
|
|
3
|
+
import { AbstractSelectionProvider } from './AbstractSelectionProvider';
|
|
4
|
+
import { difference, union } from "@/lib/set-operations";
|
|
5
|
+
|
|
6
|
+
export class SingleSelectionProvider<T extends object> extends AbstractSelectionProvider<T> {
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
select(itemKey: CompositeItemKey, recursive?: boolean): void;
|
|
10
|
+
select(item: CompositeDataItem<T>, recursive?: boolean): void;
|
|
11
|
+
select(item: CompositeDataItem<T> | CompositeItemKey, recursive: boolean = false) {
|
|
12
|
+
const _item = item instanceof CompositeDataItem ? item : this.data.lookup.get(item);
|
|
13
|
+
if (!_item) return;
|
|
14
|
+
|
|
15
|
+
const selectedKeys = _item.select(recursive);
|
|
16
|
+
this.selectedKeys.set(union(this.selectedKeys.get(), selectedKeys));
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
deselect(itemKey: CompositeItemKey, recursive?: boolean): void;
|
|
20
|
+
deselect(item: CompositeDataItem<T>, recursive?: boolean): void;
|
|
21
|
+
deselect(item: CompositeDataItem<T> | CompositeItemKey, recursive: boolean = false) {
|
|
22
|
+
const _item = item instanceof CompositeDataItem ? item : this.data.lookup.get(item);
|
|
23
|
+
if (!_item) return;
|
|
24
|
+
|
|
25
|
+
const deselectedKeys = _item.deselect(recursive);
|
|
26
|
+
this.selectedKeys.set(difference(this.selectedKeys.get(), deselectedKeys));
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-unused-vars */
|
|
2
|
+
|
|
3
|
+
import { CompositeDataItem } from '../CompositeDataItem';
|
|
4
|
+
import type { CompositeItemKey } from '../types';
|
|
5
|
+
import { AbstractSelectionProvider } from './AbstractSelectionProvider';
|
|
6
|
+
import { difference } from "@/lib/set-operations";
|
|
7
|
+
|
|
8
|
+
export class SingleSelectionProvider<T extends object> extends AbstractSelectionProvider<T> {
|
|
9
|
+
|
|
10
|
+
select(itemKey?: CompositeItemKey, recursive?: boolean): void;
|
|
11
|
+
select(item: CompositeDataItem<T>, recursive?: boolean): void;
|
|
12
|
+
select(item?: CompositeDataItem<T> | CompositeItemKey, _recursive: boolean = false) {
|
|
13
|
+
const _item = item ? item instanceof CompositeDataItem ? item : this.data.lookup.get(item) : this.data.focusProvider.focusedItem.get();
|
|
14
|
+
if (!_item) return;
|
|
15
|
+
|
|
16
|
+
this.selectedKeys.get().forEach(selectedKey => {
|
|
17
|
+
const _item = this.data.lookup.get(selectedKey);
|
|
18
|
+
if (!_item) return;
|
|
19
|
+
|
|
20
|
+
_item.deselect(false);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
const selectedKeys = _item.select(false);
|
|
24
|
+
|
|
25
|
+
this.selectedKeys.set(new Set(selectedKeys));
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
deselect(itemKey?: CompositeItemKey, recursive?: boolean): void;
|
|
29
|
+
deselect(item: CompositeDataItem<T>, recursive?: boolean): void;
|
|
30
|
+
deselect(item?: CompositeDataItem<T> | CompositeItemKey, _recursive: boolean = false) {
|
|
31
|
+
const _item = item ? item instanceof CompositeDataItem ? item : this.data.lookup.get(item) : this.data.focusProvider.focusedItem.get();
|
|
32
|
+
if (!_item) return;
|
|
33
|
+
|
|
34
|
+
const deselectedKeys = _item.deselect(false);
|
|
35
|
+
this.selectedKeys.set(difference(this.selectedKeys.get(), deselectedKeys));
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -1,31 +1,32 @@
|
|
|
1
1
|
import { useMemo } from "react";
|
|
2
|
-
import type {
|
|
2
|
+
import type { CompositeEventHandlers, CompositeItemProps } from "./types";
|
|
3
3
|
import { useStore } from "@nanostores/react";
|
|
4
4
|
|
|
5
5
|
export function CompositeComponentItem<T extends object>({
|
|
6
6
|
id,
|
|
7
7
|
className,
|
|
8
8
|
item,
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
role,
|
|
10
|
+
itemMouseEventHandler,
|
|
11
|
+
itemKeyboardEventHandler,
|
|
11
12
|
render,
|
|
12
13
|
}: CompositeItemProps<T>) {
|
|
13
14
|
const data = useStore(item.data);
|
|
14
15
|
const state = useStore(item.state);
|
|
15
16
|
|
|
16
|
-
const handlers:
|
|
17
|
+
const handlers: CompositeEventHandlers = useMemo(() => {
|
|
17
18
|
if (state.disabled) return {};
|
|
18
19
|
|
|
19
20
|
return {
|
|
20
|
-
mouseEventHandler: () =>
|
|
21
|
-
keyboardEventHandler: () =>
|
|
21
|
+
mouseEventHandler: () => itemMouseEventHandler?.(item),
|
|
22
|
+
keyboardEventHandler: () => itemKeyboardEventHandler?.(item),
|
|
22
23
|
};
|
|
23
|
-
}, [state.disabled, item,
|
|
24
|
+
}, [state.disabled, item, itemKeyboardEventHandler, itemMouseEventHandler]);
|
|
24
25
|
|
|
25
26
|
return (
|
|
26
27
|
<div
|
|
27
28
|
id={id}
|
|
28
|
-
role=
|
|
29
|
+
role={role}
|
|
29
30
|
aria-disabled={state.disabled}
|
|
30
31
|
data-key={item.key}
|
|
31
32
|
tabIndex={!state.disabled ? (state.focused ? 0 : -1) : undefined}
|
|
@@ -37,12 +38,13 @@ export function CompositeComponentItem<T extends object>({
|
|
|
37
38
|
[...item.children].map((child) => (
|
|
38
39
|
<CompositeComponentItem
|
|
39
40
|
id={`${id}-${item.key}`}
|
|
41
|
+
role={role}
|
|
40
42
|
item={child}
|
|
41
43
|
key={child.key}
|
|
42
44
|
render={render}
|
|
43
45
|
className={className}
|
|
44
|
-
|
|
45
|
-
|
|
46
|
+
itemMouseEventHandler={itemMouseEventHandler}
|
|
47
|
+
itemKeyboardEventHandler={itemKeyboardEventHandler}
|
|
46
48
|
/>
|
|
47
49
|
))}
|
|
48
50
|
</div>
|