@bccampus/ui-components 0.7.0 → 0.7.1
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 +1 -0
- package/dist/components/ui/composite/FocusProvider/AbstractFocusProvider.js +1 -4
- package/dist/components/ui/composite/FocusProvider/ListboxFocusProvider.js +8 -16
- package/dist/components/ui/composite/SelectionProvider/AbstractSelectionProvider.d.ts +1 -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 +13 -5
- 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 +8 -14
- package/src/components/ui/composite/FocusProvider/ListboxFocusProvider.ts +15 -29
- package/src/components/ui/composite/SelectionProvider/AbstractSelectionProvider.ts +1 -0
- 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 +69 -55
- package/src/hooks/use-keyboard-event.ts +13 -13
- package/src/hooks/use-required-ref.ts +6 -0
|
@@ -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,7 +1,7 @@
|
|
|
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
5
|
|
|
6
6
|
export interface FocusProvider {
|
|
7
7
|
focus(itemKey: CompositeItemKey): void;
|
|
@@ -31,28 +31,23 @@ export abstract class AbstractFocusProvider<T extends object> implements FocusPr
|
|
|
31
31
|
this.data = data;
|
|
32
32
|
this.dataOptions = dataOptions;
|
|
33
33
|
|
|
34
|
-
|
|
35
34
|
// Set focus pointers
|
|
36
35
|
const [first, last] = this.setFocusPointers();
|
|
37
36
|
|
|
38
|
-
// Set initially focused items
|
|
39
37
|
this.firstFocusableItem = first;
|
|
40
|
-
if (this.firstFocusableItem) {
|
|
41
|
-
this.firstFocusableItem.state.setKey("focused", true);
|
|
42
|
-
this.focusedItem.set(this.firstFocusableItem);
|
|
43
|
-
}
|
|
44
|
-
|
|
45
38
|
this.lastFocusableItem = last;
|
|
46
39
|
}
|
|
47
40
|
|
|
48
|
-
|
|
49
41
|
isFocusable(itemAtom: CompositeDataItem<T>) {
|
|
50
42
|
return !itemAtom.state.get().disabled;
|
|
51
43
|
}
|
|
52
44
|
|
|
53
45
|
focus(itemKey: CompositeItemKey): void;
|
|
54
46
|
focus(item: CompositeDataItem<T>): void;
|
|
55
|
-
focus(item: CompositeDataItem<T> | CompositeItemKey)
|
|
47
|
+
focus(item: CompositeDataItem<T> | CompositeItemKey | null): void;
|
|
48
|
+
focus(item: CompositeDataItem<T> | CompositeItemKey | null) {
|
|
49
|
+
if (!item) return;
|
|
50
|
+
|
|
56
51
|
const _item = item instanceof CompositeDataItem ? item : this.data.lookup.get(item);
|
|
57
52
|
if (!_item) return;
|
|
58
53
|
|
|
@@ -79,5 +74,4 @@ export abstract class AbstractFocusProvider<T extends object> implements FocusPr
|
|
|
79
74
|
abstract focusToFirstInRow(): void;
|
|
80
75
|
abstract focusToLastInRow(): void;
|
|
81
76
|
abstract focusToTypeAheadMatch(): void;
|
|
82
|
-
|
|
83
77
|
}
|
|
@@ -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
|
+
}
|
|
@@ -44,6 +44,7 @@ export abstract class AbstractSelectionProvider<T extends object> {
|
|
|
44
44
|
|
|
45
45
|
toggleSelect(itemKey?: CompositeItemKey, recursive?: boolean): void;
|
|
46
46
|
toggleSelect(item: CompositeDataItem<T>, recursive?: boolean): void;
|
|
47
|
+
toggleSelect(item: CompositeDataItem<T> | CompositeItemKey, recursive?: boolean): void;
|
|
47
48
|
toggleSelect(item?: CompositeDataItem<T> | CompositeItemKey, recursive: boolean = false) {
|
|
48
49
|
const _item = item
|
|
49
50
|
? 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>
|
|
@@ -1,58 +1,61 @@
|
|
|
1
1
|
import { CompositeComponent } from "./composite-component";
|
|
2
|
-
import { useCallback
|
|
2
|
+
import { useCallback } from "react";
|
|
3
3
|
import { CompositeDataItem } from "./CompositeDataItem";
|
|
4
4
|
import type { BaseCompositeProps } from "./types";
|
|
5
5
|
import { useKeyboardEvent } from "@/hooks/use-keyboard-event";
|
|
6
|
+
import { useRequiredRef } from "@/hooks/use-required-ref";
|
|
6
7
|
|
|
7
|
-
export function Listbox<T extends object>({
|
|
8
|
-
|
|
8
|
+
export function Listbox<T extends object>({
|
|
9
|
+
data,
|
|
10
|
+
rootRef,
|
|
11
|
+
handleRef,
|
|
12
|
+
initialFocus = "SelectedItem",
|
|
13
|
+
...props
|
|
14
|
+
}: BaseCompositeProps<T>) {
|
|
15
|
+
const $handleRef = useRequiredRef(handleRef);
|
|
9
16
|
|
|
10
|
-
const
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
17
|
+
const handleKeyboardEvent = useKeyboardEvent(
|
|
18
|
+
{
|
|
19
|
+
ArrowUp: () => {
|
|
20
|
+
data.focusProvider.focusUp();
|
|
21
|
+
$handleRef.current?.focusElement();
|
|
22
|
+
},
|
|
23
|
+
ArrowDown: () => {
|
|
24
|
+
data.focusProvider.focusDown();
|
|
25
|
+
$handleRef.current?.focusElement();
|
|
26
|
+
},
|
|
27
|
+
Home: () => {
|
|
28
|
+
data.focusProvider.focusToFirst();
|
|
29
|
+
$handleRef.current?.focusElement();
|
|
30
|
+
},
|
|
31
|
+
End: () => {
|
|
32
|
+
data.focusProvider.focusToLast();
|
|
33
|
+
$handleRef.current?.focusElement();
|
|
34
|
+
},
|
|
35
|
+
Space: () => {
|
|
36
|
+
data.selectionProvider?.toggleSelect();
|
|
37
|
+
$handleRef.current?.focusElement();
|
|
38
|
+
},
|
|
31
39
|
},
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
focusElement();
|
|
35
|
-
},
|
|
36
|
-
Space: () => {
|
|
37
|
-
data.selectionProvider?.toggleSelect();
|
|
38
|
-
focusElement();
|
|
39
|
-
},
|
|
40
|
-
});
|
|
40
|
+
[$handleRef, data.focusProvider],
|
|
41
|
+
);
|
|
41
42
|
|
|
42
43
|
const handleItemMouseEvent = useCallback(
|
|
43
44
|
(item: CompositeDataItem<T>) => {
|
|
44
45
|
data.focusProvider.focus(item.key);
|
|
45
46
|
data.selectionProvider?.toggleSelect(item);
|
|
46
|
-
focusElement();
|
|
47
|
+
$handleRef.current?.focusElement();
|
|
47
48
|
},
|
|
48
|
-
[data.focusProvider, data.selectionProvider,
|
|
49
|
+
[$handleRef, data.focusProvider, data.selectionProvider],
|
|
49
50
|
);
|
|
50
51
|
|
|
51
52
|
return (
|
|
52
53
|
<CompositeComponent
|
|
53
|
-
|
|
54
|
+
rootRef={rootRef}
|
|
55
|
+
handleRef={$handleRef}
|
|
54
56
|
variant="listbox"
|
|
55
57
|
data={data}
|
|
58
|
+
initialFocus={initialFocus}
|
|
56
59
|
onKeyDown={handleKeyboardEvent}
|
|
57
60
|
itemMouseEventHandler={handleItemMouseEvent}
|
|
58
61
|
{...props}
|