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