@bccampus/ui-components 0.2.0 → 0.4.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/dist/banner.js +14 -14
- package/dist/button.d.ts +1 -1
- package/dist/button.js +17 -17
- package/dist/caption.js +7 -8
- package/dist/card.js +34 -34
- package/dist/composite.d.ts +151 -0
- package/dist/composite.js +472 -0
- package/dist/generate-tiles-DuagGD1d.js +244 -0
- package/dist/horizontal-list.js +32 -32
- package/dist/icon-generator.js +43 -270
- package/dist/igenerate-tiles.d.ts +43 -0
- package/dist/igenerate-tiles.js +7 -0
- package/dist/index-U7DVCmS_.js +76 -0
- package/dist/input.js +4 -4
- package/dist/masked-image-generator.js +18 -18
- package/dist/navigation-menu.d.ts +27 -0
- package/dist/navigation-menu.js +1139 -0
- package/dist/overlay.js +2 -2
- package/dist/page-header.d.ts +5 -1
- package/dist/page-header.js +10 -9
- package/dist/page-section.js +14 -14
- package/dist/page.js +4 -4
- package/dist/search-input.js +11 -11
- package/dist/tag.js +8 -9
- package/dist/ui-components.d.ts +30 -2
- package/dist/ui-components.js +47 -37
- package/package.json +17 -1
- package/src/components/ui/banner.tsx +2 -2
- package/src/components/ui/button.tsx +8 -7
- package/src/components/ui/card.tsx +5 -5
- package/src/components/ui/composite/CompositeData.ts +215 -0
- package/src/components/ui/composite/CompositeDataItem.ts +144 -0
- package/src/components/ui/composite/composite-component-item.tsx +50 -0
- package/src/components/ui/composite/composite-component.tsx +100 -0
- package/src/components/ui/composite/composite-data-context.tsx +31 -0
- package/src/components/ui/composite/index.ts +4 -0
- package/src/components/ui/composite/types.ts +81 -0
- package/src/components/ui/horizontal-list.tsx +2 -2
- package/src/components/ui/index.ts +1 -0
- package/src/components/ui/navigation-menu.tsx +165 -0
- package/src/components/ui/page-header.tsx +13 -5
- package/src/components/ui/page-section.tsx +8 -8
- package/src/components/ui/page.tsx +3 -1
- package/src/components/ui/popover.tsx +46 -0
- package/src/hooks/use-effect-after-mount.ts +27 -0
- package/src/hooks/use-id.ts +5 -0
- package/src/hooks/use-keyboard-event.ts +144 -0
- package/src/lib/object.ts +48 -0
- package/src/lib/set-operations.ts +52 -0
- package/src/styles/theme.css +7 -7
- package/src/styles/typography.css +334 -341
- package/tsconfig.node.json +25 -25
- package/vite.config.ts +7 -3
- package/vite.ladle.config.ts +17 -0
- package/dist/index-DlfV3JTY.js +0 -70
- package/dist/jsx-runtime-BzflLqGi.js +0 -282
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
import { difference, union } from "@/lib/set-operations";
|
|
2
|
+
import { atom, type PreinitializedWritableAtom } from "nanostores";
|
|
3
|
+
import type { CompositeDataOptions, CompositeItemKey, CompositeOptions } from "./types";
|
|
4
|
+
import { CompositeDataItem } from "./CompositeDataItem";
|
|
5
|
+
|
|
6
|
+
export class CompositeData<T extends object> implements Iterable<CompositeDataItem<T>> {
|
|
7
|
+
#options: CompositeDataOptions<T>;
|
|
8
|
+
|
|
9
|
+
lookup: Map<CompositeItemKey, CompositeDataItem<T>> = new Map();
|
|
10
|
+
|
|
11
|
+
firstFocusableItem!: CompositeDataItem<T> | null;
|
|
12
|
+
focusedItem: PreinitializedWritableAtom<CompositeDataItem<T> | null> = atom(null);
|
|
13
|
+
disabledKeys: PreinitializedWritableAtom<Set<CompositeItemKey>> = atom(new Set());
|
|
14
|
+
selectedKeys: PreinitializedWritableAtom<Set<CompositeItemKey>> = atom(new Set());
|
|
15
|
+
|
|
16
|
+
root!: CompositeDataItem<T>;
|
|
17
|
+
|
|
18
|
+
constructor(items: T[], options?: CompositeOptions) {
|
|
19
|
+
this.#options = {
|
|
20
|
+
itemKeyProp: "key",
|
|
21
|
+
itemChildrenProp: "children",
|
|
22
|
+
disabledKeys: [],
|
|
23
|
+
selectedKeys: [],
|
|
24
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
25
|
+
getItemKey: (item: any) => item[this.#options.itemKeyProp] as CompositeItemKey,
|
|
26
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
27
|
+
getItemChildren: (item: any) => item[this.#options.itemChildrenProp] ? item[this.#options.itemChildrenProp] as T[] : undefined,
|
|
28
|
+
|
|
29
|
+
...options
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
this.init(items);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
*[Symbol.iterator](): Iterator<CompositeDataItem<T>> {
|
|
36
|
+
if (this.root.children) {
|
|
37
|
+
for (const node of this.root.children) {
|
|
38
|
+
yield node;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
toJSON() {
|
|
44
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
45
|
+
const json: any = this.root.toJSON()
|
|
46
|
+
|
|
47
|
+
return json[this.#options.itemChildrenProp] as T[];
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
init(items: T[]) {
|
|
52
|
+
// this.itemAtoms = this.#setItemMap(items);
|
|
53
|
+
this.root = new CompositeDataItem(
|
|
54
|
+
{ [this.#options.itemKeyProp]: "ALL", [this.#options.itemChildrenProp]: items } as T,
|
|
55
|
+
this.#options,
|
|
56
|
+
null
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
// Build item lookup table
|
|
60
|
+
this.lookup = new Map([...this.root].map(item => [item.key, item]));
|
|
61
|
+
|
|
62
|
+
// Set initially disabled items
|
|
63
|
+
this.#options.disabledKeys.forEach(disabledKey => this.disable(disabledKey));
|
|
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
|
+
}
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
isFocusable(itemAtom: CompositeDataItem<T>) {
|
|
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;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
item(key: CompositeItemKey) { return this.lookup.get(key) };
|
|
110
|
+
|
|
111
|
+
descendants(itemKey: CompositeItemKey) {
|
|
112
|
+
const item = this.lookup.get(itemKey);
|
|
113
|
+
if (!item) return undefined;
|
|
114
|
+
|
|
115
|
+
return item.descendants();
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
ancestors(itemKey: CompositeItemKey) {
|
|
119
|
+
const item = this.lookup.get(itemKey);
|
|
120
|
+
if (!item) return undefined;
|
|
121
|
+
|
|
122
|
+
return item.ancestors();
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
focus(itemKey: CompositeItemKey): void;
|
|
126
|
+
focus(item: CompositeDataItem<T>): void;
|
|
127
|
+
focus(item: CompositeDataItem<T> | CompositeItemKey) {
|
|
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) {
|
|
174
|
+
const _item = item instanceof CompositeDataItem ? item : this.lookup.get(item);
|
|
175
|
+
if (!_item) return;
|
|
176
|
+
|
|
177
|
+
const disabledKeys = _item.disable(recursive);
|
|
178
|
+
this.disabledKeys.set(union(this.selectedKeys.get(), 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));
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
updateItem(key: CompositeItemKey, data: Partial<T> | ((item: T) => Partial<T>)) {
|
|
194
|
+
const item = this.lookup.get(key);
|
|
195
|
+
if (item) {
|
|
196
|
+
const newData = {
|
|
197
|
+
...item.data.get(), ...(
|
|
198
|
+
(typeof data === "function") ? data(item.data.get()) : data
|
|
199
|
+
)
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
const newKey = this.#options.getItemKey(newData);
|
|
203
|
+
if (item.key !== newKey) throw "Item key cannot be updated!";
|
|
204
|
+
|
|
205
|
+
this.#updateIfSelectedItem(key);
|
|
206
|
+
item.data.set(newData);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
#updateIfSelectedItem(key: CompositeItemKey) {
|
|
211
|
+
if (this.selectedKeys.get().has(key)) {
|
|
212
|
+
this.selectedKeys.set(new Set([...this.selectedKeys.get()]));
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { omit } from "@/lib/object";
|
|
2
|
+
import { map, type PreinitializedMapStore, } from "nanostores";
|
|
3
|
+
import type { CompositeDataItemState, CompositeItemKey, CompositeDataItemOptions } from "./types";
|
|
4
|
+
|
|
5
|
+
export class CompositeDataItem<T extends object> implements Iterable<CompositeDataItem<T>> {
|
|
6
|
+
#key: CompositeItemKey;
|
|
7
|
+
parent: CompositeDataItem<T> | null = null;
|
|
8
|
+
children?: CompositeDataItem<T>[];
|
|
9
|
+
level: number;
|
|
10
|
+
|
|
11
|
+
data: PreinitializedMapStore<T>;
|
|
12
|
+
state: PreinitializedMapStore<CompositeDataItemState>;
|
|
13
|
+
|
|
14
|
+
pointers: {
|
|
15
|
+
left?: CompositeItemKey;
|
|
16
|
+
right?: CompositeItemKey;
|
|
17
|
+
up?: CompositeItemKey;
|
|
18
|
+
down?: CompositeItemKey;
|
|
19
|
+
};
|
|
20
|
+
childrenProp: string
|
|
21
|
+
|
|
22
|
+
constructor(item: T, options: CompositeDataItemOptions<T>, parent: CompositeDataItem<T> | null) {
|
|
23
|
+
this.#key = options.getItemKey(item);
|
|
24
|
+
this.data = map(omit(item, [options.itemChildrenProp]));
|
|
25
|
+
this.state = map({
|
|
26
|
+
focused: false,
|
|
27
|
+
selected: false,
|
|
28
|
+
disabled: false,
|
|
29
|
+
...options.initialState
|
|
30
|
+
})
|
|
31
|
+
this.pointers = {};
|
|
32
|
+
this.parent = parent;
|
|
33
|
+
this.level = parent ? parent.level + 1 : 0;
|
|
34
|
+
|
|
35
|
+
this.childrenProp = options.itemChildrenProp;
|
|
36
|
+
|
|
37
|
+
const children = options.getItemChildren(item);
|
|
38
|
+
if (children) {
|
|
39
|
+
this.children = children.map(child => {
|
|
40
|
+
const childItem = new CompositeDataItem(child, options, this);
|
|
41
|
+
return childItem;
|
|
42
|
+
})
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
get key() {
|
|
47
|
+
return this.#key;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
*[Symbol.iterator](): Iterator<CompositeDataItem<T>> {
|
|
51
|
+
if (this.key !== "ALL") yield this;
|
|
52
|
+
if (this.children)
|
|
53
|
+
for (const child of this.children)
|
|
54
|
+
for (const item of child) yield item;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
toJSON() {
|
|
58
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
59
|
+
const json: any = { ...this.data.get() };
|
|
60
|
+
|
|
61
|
+
if (this.children) {
|
|
62
|
+
json[this.childrenProp] = [];
|
|
63
|
+
for (const child of this.children)
|
|
64
|
+
json[this.childrenProp].push(child.toJSON());
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return json as T;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
descendants() {
|
|
71
|
+
if (!this.children) return [];
|
|
72
|
+
|
|
73
|
+
const descendants: CompositeDataItem<T>[] = [];
|
|
74
|
+
this.children.forEach((child) => {
|
|
75
|
+
descendants.push(child);
|
|
76
|
+
const childDescendants = child.descendants();
|
|
77
|
+
if (childDescendants) descendants.push(...childDescendants);
|
|
78
|
+
}
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
return descendants;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
ancestors() {
|
|
85
|
+
const ancestors: CompositeDataItem<T>[] = [];
|
|
86
|
+
let parent = this.parent;
|
|
87
|
+
while (parent) {
|
|
88
|
+
ancestors.push(parent);
|
|
89
|
+
parent = parent.parent;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return ancestors;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
toggleSelect(recursive: boolean = false) {
|
|
97
|
+
if (this.state.get().selected) this.deselect(recursive);
|
|
98
|
+
else this.select(recursive);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
select(recursive: boolean = false): CompositeItemKey[] {
|
|
102
|
+
this.state.setKey("selected", true);
|
|
103
|
+
|
|
104
|
+
if (recursive && this.children) {
|
|
105
|
+
const selectedChildKeys = this.children.map(child => child.select(true)).flat();
|
|
106
|
+
return [this.key, ...selectedChildKeys];
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return [this.key]
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
deselect(recursive: boolean = false): CompositeItemKey[] {
|
|
113
|
+
this.state.setKey("selected", false);
|
|
114
|
+
|
|
115
|
+
if (recursive && this.children) {
|
|
116
|
+
const deselectedChildKeys = this.children.map(child => child.deselect(true)).flat();
|
|
117
|
+
return [this.key, ...deselectedChildKeys];
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return [this.key];
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
disable(recursive: boolean = false): CompositeItemKey[] {
|
|
124
|
+
this.state.setKey("disabled", true);
|
|
125
|
+
|
|
126
|
+
if (recursive && this.children) {
|
|
127
|
+
const disabledChildKeys = this.children.map(child => child.disable(true)).flat();
|
|
128
|
+
return [this.key, ...disabledChildKeys];
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return [this.key]
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
enable(recursive: boolean = false): CompositeItemKey[] {
|
|
135
|
+
this.state.setKey("disabled", false);
|
|
136
|
+
|
|
137
|
+
if (recursive && this.children) {
|
|
138
|
+
const enabledChildKeys = this.children.map(child => child.enable(true)).flat();
|
|
139
|
+
return [this.key, ...enabledChildKeys];
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return [this.key];
|
|
143
|
+
}
|
|
144
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { useMemo } from "react";
|
|
2
|
+
import type { CompositeItemEventHandlers, CompositeItemProps } from "./types";
|
|
3
|
+
import { useStore } from "@nanostores/react";
|
|
4
|
+
|
|
5
|
+
export function CompositeComponentItem<T extends object>({
|
|
6
|
+
id,
|
|
7
|
+
className,
|
|
8
|
+
item,
|
|
9
|
+
mouseEventHandler,
|
|
10
|
+
keyboardEventHandler,
|
|
11
|
+
render,
|
|
12
|
+
}: CompositeItemProps<T>) {
|
|
13
|
+
const data = useStore(item.data);
|
|
14
|
+
const state = useStore(item.state);
|
|
15
|
+
|
|
16
|
+
const handlers: CompositeItemEventHandlers = useMemo(() => {
|
|
17
|
+
if (state.disabled) return {};
|
|
18
|
+
|
|
19
|
+
return {
|
|
20
|
+
mouseEventHandler: () => mouseEventHandler?.(item),
|
|
21
|
+
keyboardEventHandler: () => keyboardEventHandler?.(item),
|
|
22
|
+
};
|
|
23
|
+
}, [state.disabled, item, keyboardEventHandler, mouseEventHandler]);
|
|
24
|
+
|
|
25
|
+
return (
|
|
26
|
+
<div
|
|
27
|
+
id={id}
|
|
28
|
+
role="option"
|
|
29
|
+
aria-disabled={state.disabled}
|
|
30
|
+
data-key={item.key}
|
|
31
|
+
tabIndex={!state.disabled ? (state.focused ? 0 : -1) : undefined}
|
|
32
|
+
className={className}
|
|
33
|
+
>
|
|
34
|
+
{render({ data, key: item.key, level: item.level }, state, handlers)}
|
|
35
|
+
{item.children &&
|
|
36
|
+
item.children.length > 0 &&
|
|
37
|
+
[...item.children].map((child) => (
|
|
38
|
+
<CompositeComponentItem
|
|
39
|
+
id={`${id}-${item.key}`}
|
|
40
|
+
item={child}
|
|
41
|
+
key={child.key}
|
|
42
|
+
render={render}
|
|
43
|
+
className={className}
|
|
44
|
+
mouseEventHandler={mouseEventHandler}
|
|
45
|
+
keyboardEventHandler={keyboardEventHandler}
|
|
46
|
+
/>
|
|
47
|
+
))}
|
|
48
|
+
</div>
|
|
49
|
+
);
|
|
50
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { useCallback, useImperativeHandle, useRef } from "react";
|
|
2
|
+
import { CompositeDataItem } from "./CompositeDataItem";
|
|
3
|
+
import type { CompositeItemKey, CompositeProps } from "./types";
|
|
4
|
+
import { useKeyboardEvent } from "@/hooks/use-keyboard-event";
|
|
5
|
+
import { useCompositeContext } from "./composite-data-context";
|
|
6
|
+
import { CompositeComponentItem } from "./composite-component-item";
|
|
7
|
+
import { useId } from "@/hooks/use-id";
|
|
8
|
+
|
|
9
|
+
export function CompositeComponent<T extends object>({
|
|
10
|
+
renderItem,
|
|
11
|
+
className,
|
|
12
|
+
itemClassName,
|
|
13
|
+
ref,
|
|
14
|
+
id,
|
|
15
|
+
...props
|
|
16
|
+
}: CompositeProps<T>) {
|
|
17
|
+
const compositeData = useCompositeContext<T>();
|
|
18
|
+
const compositeRef = useRef<HTMLDivElement>(null);
|
|
19
|
+
const compositeId = useId(id);
|
|
20
|
+
|
|
21
|
+
const focus = useCallback(
|
|
22
|
+
(itemKey: CompositeItemKey) => {
|
|
23
|
+
compositeData.focus(itemKey);
|
|
24
|
+
// const focusedItemEl = compositeRef.current?.querySelector<HTMLDivElement>(`[data-key="${itemKey}"]`);
|
|
25
|
+
// if (focusedItemEl) focusedItemEl.focus();
|
|
26
|
+
},
|
|
27
|
+
[compositeData]
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
const focusDown = useCallback(() => {
|
|
31
|
+
if (compositeData.focusedItem.get()?.pointers.down) {
|
|
32
|
+
focus(compositeData.focusedItem.get()!.pointers.down!);
|
|
33
|
+
}
|
|
34
|
+
}, [compositeData, focus]);
|
|
35
|
+
|
|
36
|
+
const focusUp = useCallback(() => {
|
|
37
|
+
if (compositeData.focusedItem.get()?.pointers.up) {
|
|
38
|
+
focus(compositeData.focusedItem.get()!.pointers.up!);
|
|
39
|
+
}
|
|
40
|
+
}, [compositeData, focus]);
|
|
41
|
+
|
|
42
|
+
const handleKeyDown = useKeyboardEvent({
|
|
43
|
+
ArrowUp: focusUp,
|
|
44
|
+
ArrowLeft: focusUp,
|
|
45
|
+
ArrowDown: focusDown,
|
|
46
|
+
ArrowRight: focusDown,
|
|
47
|
+
Space: () => {
|
|
48
|
+
if (compositeData.focusedItem.get()) compositeData.toggleSelect(compositeData.focusedItem.get()!);
|
|
49
|
+
},
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
const handleItemMouseEvent = useCallback(
|
|
53
|
+
(item: CompositeDataItem<T>) => {
|
|
54
|
+
focus(item.key);
|
|
55
|
+
compositeData.toggleSelect(item);
|
|
56
|
+
},
|
|
57
|
+
[compositeData, focus]
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
useImperativeHandle(
|
|
61
|
+
ref,
|
|
62
|
+
() => {
|
|
63
|
+
return {
|
|
64
|
+
focusDown() {
|
|
65
|
+
focusDown();
|
|
66
|
+
},
|
|
67
|
+
focusUp() {
|
|
68
|
+
focusUp();
|
|
69
|
+
},
|
|
70
|
+
select() {
|
|
71
|
+
if (compositeData.focusedItem.get()) compositeData.toggleSelect(compositeData.focusedItem.get()!);
|
|
72
|
+
},
|
|
73
|
+
};
|
|
74
|
+
},
|
|
75
|
+
[compositeData, focusDown, focusUp]
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
return (
|
|
79
|
+
<div
|
|
80
|
+
ref={compositeRef}
|
|
81
|
+
id={compositeId}
|
|
82
|
+
className={className}
|
|
83
|
+
tabIndex={-1}
|
|
84
|
+
role="listbox"
|
|
85
|
+
onKeyDown={handleKeyDown}
|
|
86
|
+
{...props}
|
|
87
|
+
>
|
|
88
|
+
{[...compositeData].map((item) => (
|
|
89
|
+
<CompositeComponentItem
|
|
90
|
+
className={itemClassName}
|
|
91
|
+
id={`${compositeId}-${item.key}`}
|
|
92
|
+
item={item}
|
|
93
|
+
key={item.key}
|
|
94
|
+
render={renderItem}
|
|
95
|
+
mouseEventHandler={handleItemMouseEvent}
|
|
96
|
+
/>
|
|
97
|
+
))}
|
|
98
|
+
</div>
|
|
99
|
+
);
|
|
100
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { createContext, useContext, useMemo } from "react";
|
|
2
|
+
import { CompositeData } from "./CompositeData";
|
|
3
|
+
import type { CompositeOptions } from "./types";
|
|
4
|
+
|
|
5
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
6
|
+
const CompositeDataContext = createContext<CompositeData<any> | undefined>(undefined);
|
|
7
|
+
|
|
8
|
+
export function useCompositeContext<T extends object>() {
|
|
9
|
+
const context = useContext(CompositeDataContext);
|
|
10
|
+
|
|
11
|
+
if (!context) {
|
|
12
|
+
throw new Error("No CompositeDataContext has been set, use CompositeDataContext to provide a context");
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
return context as CompositeData<T>;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
interface CompositeComponentContextProps<T extends object> extends CompositeOptions {
|
|
19
|
+
data: T[];
|
|
20
|
+
children: React.ReactNode;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function CompositeComponentContext<T extends object>({
|
|
24
|
+
data,
|
|
25
|
+
children,
|
|
26
|
+
...compositeOptions
|
|
27
|
+
}: CompositeComponentContextProps<T>) {
|
|
28
|
+
const composite = useMemo(() => new CompositeData(data, compositeOptions), [data, compositeOptions]);
|
|
29
|
+
|
|
30
|
+
return <CompositeDataContext value={composite}>{children}</CompositeDataContext>;
|
|
31
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import type { KeyboardEventHandler, MouseEventHandler, ReactNode } from "react";
|
|
2
|
+
import type { CompositeDataItem } from "./CompositeDataItem";
|
|
3
|
+
|
|
4
|
+
export type CompositeItemKey = string | number;
|
|
5
|
+
|
|
6
|
+
export interface CompositeOptions {
|
|
7
|
+
disabledKeys?: CompositeItemKey[];
|
|
8
|
+
selectedKeys?: CompositeItemKey[];
|
|
9
|
+
|
|
10
|
+
itemKeyProp?: string;
|
|
11
|
+
itemChildrenProp?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
interface CompositeDataPropGetters<T> {
|
|
15
|
+
getItemKey: (item: T) => CompositeItemKey;
|
|
16
|
+
getItemChildren: (item: T) => T[] | undefined;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export type CompositeDataOptions<T> = Required<CompositeOptions> & CompositeDataPropGetters<T>;
|
|
20
|
+
|
|
21
|
+
export type CompositeDataItemOptions<T> = CompositeDataPropGetters<T> & {
|
|
22
|
+
initialState?: CompositeDataItemState;
|
|
23
|
+
itemChildrenProp: string;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export interface CompositeDataItemState {
|
|
27
|
+
focused: boolean;
|
|
28
|
+
selected: boolean;
|
|
29
|
+
disabled: boolean;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface CompositeProps<T extends object> extends React.ComponentPropsWithoutRef<"div"> {
|
|
33
|
+
renderItem: CompositeItemRenderFn<T>;
|
|
34
|
+
className?: string;
|
|
35
|
+
itemClassName?: string;
|
|
36
|
+
ref?: React.Ref<CompositeHandle>
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
export interface CompositeHandle {
|
|
41
|
+
focusUp: () => void;
|
|
42
|
+
focusDown: () => void;
|
|
43
|
+
select: () => void;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface CompositeItemEventHandlers {
|
|
47
|
+
mouseEventHandler?: MouseEventHandler<HTMLElement>;
|
|
48
|
+
keyboardEventHandler?: KeyboardEventHandler<HTMLElement>;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export type CompositeItemRenderFn<T extends object> = (
|
|
52
|
+
item: { data: T; level: number; key: CompositeItemKey },
|
|
53
|
+
state: CompositeDataItemState,
|
|
54
|
+
eventHandlers: CompositeItemEventHandlers
|
|
55
|
+
) => ReactNode;
|
|
56
|
+
|
|
57
|
+
export interface CompositeItemProps<T extends object> {
|
|
58
|
+
id: string;
|
|
59
|
+
className?: string;
|
|
60
|
+
|
|
61
|
+
item: CompositeDataItem<T>;
|
|
62
|
+
|
|
63
|
+
mouseEventHandler?: (itemAtom: CompositeDataItem<T>) => void;
|
|
64
|
+
keyboardEventHandler?: (itemAtom: CompositeDataItem<T>) => void;
|
|
65
|
+
|
|
66
|
+
remove?: () => void;
|
|
67
|
+
render: CompositeItemRenderFn<T>;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export interface CompositeFocusProvider {
|
|
71
|
+
setPointers: () => void;
|
|
72
|
+
|
|
73
|
+
goFirst: () => void;
|
|
74
|
+
goLast: () => void;
|
|
75
|
+
|
|
76
|
+
goUp: () => void;
|
|
77
|
+
goDown: () => void;
|
|
78
|
+
goLeft: () => void;
|
|
79
|
+
goRight: () => void;
|
|
80
|
+
|
|
81
|
+
}
|
|
@@ -5,12 +5,12 @@ import { useCallback, useRef } from "react";
|
|
|
5
5
|
import { cva, type VariantProps } from "class-variance-authority";
|
|
6
6
|
|
|
7
7
|
const horizontalListVariants = cva(
|
|
8
|
-
"scrollbar-hidden overscroll-x-contain flex flex-row flex-nowrap py-1 gap-card overflow-x-auto snap-x snap-mandatory touch-pan-x scroll-smooth *:shrink-0 *:grow-0 *:snap-center *:select-none",
|
|
8
|
+
"scrollbar-hidden overscroll-x-contain flex flex-row flex-nowrap py-1 gap-(--gap-card) overflow-x-auto snap-x snap-mandatory touch-pan-x scroll-smooth *:shrink-0 *:grow-0 *:snap-center *:select-none",
|
|
9
9
|
{
|
|
10
10
|
variants: {
|
|
11
11
|
variant: {
|
|
12
12
|
contain: "w-full ",
|
|
13
|
-
overflow: "w-screen px-section -ms-section scroll-px-(--section
|
|
13
|
+
overflow: "w-screen px-(--spacing-section) -ms-(--spacing-section) scroll-px-(--spacing-section)",
|
|
14
14
|
},
|
|
15
15
|
},
|
|
16
16
|
defaultVariants: {
|
|
@@ -6,6 +6,7 @@ export * from './button.tsx'
|
|
|
6
6
|
export * from './card.tsx'
|
|
7
7
|
export * from './horizontal-list.tsx'
|
|
8
8
|
export * from './input.tsx'
|
|
9
|
+
export * from './navigation-menu.tsx'
|
|
9
10
|
export * from './overlay.tsx'
|
|
10
11
|
export * from './page-header.tsx'
|
|
11
12
|
export * from './page-section.tsx'
|