@bccampus/ui-components 0.4.0 → 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/icon-generator-tuhuqdpL.js +76 -0
- package/dist/icon-generator.d.ts +11 -4
- package/dist/icon-generator.js +4 -74
- package/dist/listbox.d.ts +171 -0
- package/dist/listbox.js +76 -0
- package/dist/masked-image-generator.js +7 -7
- package/dist/ui-components.js +5 -5
- 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/components/ui/icon-generator/index.ts +2 -0
- package/src/hooks/use-keyboard-event.ts +31 -42
- package/vite.config.ts +6 -2
- package/src/components/ui/composite/composite-data-context.tsx +0 -31
- /package/dist/{igenerate-tiles.d.ts → generate-tiles.d.ts} +0 -0
- /package/dist/{igenerate-tiles.js → generate-tiles.js} +0 -0
|
@@ -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>
|
|
@@ -1,98 +1,69 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import type { CompositeItemKey, CompositeProps } from "./types";
|
|
4
|
-
import { useKeyboardEvent } from "@/hooks/use-keyboard-event";
|
|
5
|
-
import { useCompositeContext } from "./composite-data-context";
|
|
1
|
+
import { useImperativeHandle, useMemo } from "react";
|
|
2
|
+
import type { CompositeProps } from "./types";
|
|
6
3
|
import { CompositeComponentItem } from "./composite-component-item";
|
|
7
4
|
import { useId } from "@/hooks/use-id";
|
|
8
5
|
|
|
6
|
+
const defaultRoles = {
|
|
7
|
+
listbox: {
|
|
8
|
+
rootRole: "listbox",
|
|
9
|
+
groupRole: "group",
|
|
10
|
+
itemRole: "option",
|
|
11
|
+
},
|
|
12
|
+
grid: {
|
|
13
|
+
rootRole: "grid",
|
|
14
|
+
groupRole: "rowgroup",
|
|
15
|
+
itemRole: "row",
|
|
16
|
+
},
|
|
17
|
+
custom: undefined,
|
|
18
|
+
};
|
|
19
|
+
|
|
9
20
|
export function CompositeComponent<T extends object>({
|
|
21
|
+
data,
|
|
22
|
+
variant,
|
|
23
|
+
rootRole,
|
|
24
|
+
itemRole,
|
|
25
|
+
groupRole,
|
|
10
26
|
renderItem,
|
|
11
27
|
className,
|
|
12
28
|
itemClassName,
|
|
13
29
|
ref,
|
|
30
|
+
handleRef,
|
|
14
31
|
id,
|
|
32
|
+
itemMouseEventHandler,
|
|
33
|
+
itemKeyboardEventHandler,
|
|
15
34
|
...props
|
|
16
35
|
}: CompositeProps<T>) {
|
|
17
|
-
const compositeData = useCompositeContext<T>();
|
|
18
|
-
const compositeRef = useRef<HTMLDivElement>(null);
|
|
19
36
|
const compositeId = useId(id);
|
|
20
37
|
|
|
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
38
|
useImperativeHandle(
|
|
61
|
-
|
|
39
|
+
handleRef,
|
|
62
40
|
() => {
|
|
63
41
|
return {
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
},
|
|
67
|
-
focusUp() {
|
|
68
|
-
focusUp();
|
|
69
|
-
},
|
|
70
|
-
select() {
|
|
71
|
-
if (compositeData.focusedItem.get()) compositeData.toggleSelect(compositeData.focusedItem.get()!);
|
|
72
|
-
},
|
|
42
|
+
focusProvider: data.focusProvider,
|
|
43
|
+
selectionProvider: data.selectionProvider,
|
|
73
44
|
};
|
|
74
45
|
},
|
|
75
|
-
[
|
|
46
|
+
[data]
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
const roles = useMemo(
|
|
50
|
+
() => defaultRoles[variant] ?? { rootRole, groupRole, itemRole },
|
|
51
|
+
[groupRole, itemRole, rootRole, variant]
|
|
76
52
|
);
|
|
77
53
|
|
|
78
54
|
return (
|
|
79
|
-
<div
|
|
80
|
-
|
|
81
|
-
id={compositeId}
|
|
82
|
-
className={className}
|
|
83
|
-
tabIndex={-1}
|
|
84
|
-
role="listbox"
|
|
85
|
-
onKeyDown={handleKeyDown}
|
|
86
|
-
{...props}
|
|
87
|
-
>
|
|
88
|
-
{[...compositeData].map((item) => (
|
|
55
|
+
<div ref={ref} id={compositeId} className={className} tabIndex={-1} role={roles.rootRole} {...props}>
|
|
56
|
+
{[...data].map((item) => (
|
|
89
57
|
<CompositeComponentItem
|
|
90
58
|
className={itemClassName}
|
|
91
59
|
id={`${compositeId}-${item.key}`}
|
|
60
|
+
role={roles.itemRole}
|
|
61
|
+
groupRole={roles.groupRole}
|
|
92
62
|
item={item}
|
|
93
63
|
key={item.key}
|
|
94
64
|
render={renderItem}
|
|
95
|
-
|
|
65
|
+
itemMouseEventHandler={itemMouseEventHandler}
|
|
66
|
+
itemKeyboardEventHandler={itemKeyboardEventHandler}
|
|
96
67
|
/>
|
|
97
68
|
))}
|
|
98
69
|
</div>
|
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
export * from './composite-component.tsx'
|
|
2
2
|
export * from './composite-component-item.tsx'
|
|
3
|
-
export * from './
|
|
3
|
+
export * from './CompositeData.ts'
|
|
4
|
+
export * from './CompositeDataItem.ts'
|
|
5
|
+
export * from './FocusProvider/AbstractFocusProvider.ts'
|
|
6
|
+
export * from './SelectionProvider/AbstractSelectionProvider.ts'
|
|
4
7
|
export * from './types.ts'
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { CompositeComponent } from "./composite-component";
|
|
2
|
+
import { useCallback, useRef } from "react";
|
|
3
|
+
import { CompositeDataItem } from "./CompositeDataItem";
|
|
4
|
+
import type { BaseCompositeProps } from "./types";
|
|
5
|
+
import { useKeyboardEvent } from "@/hooks/use-keyboard-event";
|
|
6
|
+
|
|
7
|
+
export function Listbox<T extends object>({ data, ...props }: BaseCompositeProps<T>) {
|
|
8
|
+
const compositeRef = useRef<HTMLDivElement>(null);
|
|
9
|
+
|
|
10
|
+
const focusElement = useCallback(() => {
|
|
11
|
+
const itemKey = data.focusProvider.focusedItem.get()?.key;
|
|
12
|
+
|
|
13
|
+
if (itemKey && compositeRef.current) {
|
|
14
|
+
const focusedItemEl = compositeRef.current.querySelector<HTMLDivElement>(`[data-key="${itemKey}"]`);
|
|
15
|
+
if (focusedItemEl) focusedItemEl.focus();
|
|
16
|
+
}
|
|
17
|
+
}, [data]);
|
|
18
|
+
|
|
19
|
+
const handleKeyboardEvent = useKeyboardEvent({
|
|
20
|
+
ArrowUp: () => {
|
|
21
|
+
data.focusProvider.focusUp();
|
|
22
|
+
focusElement();
|
|
23
|
+
},
|
|
24
|
+
ArrowDown: () => {
|
|
25
|
+
data.focusProvider.focusDown();
|
|
26
|
+
focusElement();
|
|
27
|
+
},
|
|
28
|
+
Home: () => {
|
|
29
|
+
data.focusProvider.focusToFirst();
|
|
30
|
+
focusElement();
|
|
31
|
+
},
|
|
32
|
+
End: () => {
|
|
33
|
+
data.focusProvider.focusToLast();
|
|
34
|
+
focusElement();
|
|
35
|
+
},
|
|
36
|
+
Space: () => {
|
|
37
|
+
data.selectionProvider?.toggleSelect();
|
|
38
|
+
focusElement();
|
|
39
|
+
},
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
const handleItemMouseEvent = useCallback(
|
|
43
|
+
(item: CompositeDataItem<T>) => {
|
|
44
|
+
data.focusProvider.focus(item.key);
|
|
45
|
+
data.selectionProvider?.toggleSelect(item);
|
|
46
|
+
focusElement();
|
|
47
|
+
},
|
|
48
|
+
[data.focusProvider, data.selectionProvider, focusElement]
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
return (
|
|
52
|
+
<CompositeComponent
|
|
53
|
+
ref={compositeRef}
|
|
54
|
+
variant="listbox"
|
|
55
|
+
data={data}
|
|
56
|
+
onKeyDown={handleKeyboardEvent}
|
|
57
|
+
itemMouseEventHandler={handleItemMouseEvent}
|
|
58
|
+
{...props}
|
|
59
|
+
/>
|
|
60
|
+
);
|
|
61
|
+
}
|
|
@@ -1,8 +1,29 @@
|
|
|
1
|
-
import type { KeyboardEventHandler, MouseEventHandler, ReactNode } from "react";
|
|
1
|
+
import type { AriaRole, KeyboardEventHandler, MouseEventHandler, ReactNode } from "react";
|
|
2
2
|
import type { CompositeDataItem } from "./CompositeDataItem";
|
|
3
|
+
import type { AbstractFocusProvider, FocusProvider } from "./FocusProvider/AbstractFocusProvider";
|
|
4
|
+
import type { AbstractSelectionProvider, SelectionProvider } from "./SelectionProvider/AbstractSelectionProvider";
|
|
5
|
+
import type { CompositeData } from "./CompositeData";
|
|
3
6
|
|
|
4
7
|
export type CompositeItemKey = string | number;
|
|
5
8
|
|
|
9
|
+
;
|
|
10
|
+
export type CompositeRoles = {
|
|
11
|
+
variant: 'listbox'
|
|
12
|
+
rootRole?: never
|
|
13
|
+
itemRole?: never
|
|
14
|
+
groupRole?: never
|
|
15
|
+
} | {
|
|
16
|
+
variant: 'grid'
|
|
17
|
+
rootRole?: never
|
|
18
|
+
itemRole?: never
|
|
19
|
+
groupRole?: never
|
|
20
|
+
} | {
|
|
21
|
+
variant: 'custom'
|
|
22
|
+
rootRole: AriaRole
|
|
23
|
+
itemRole: AriaRole
|
|
24
|
+
groupRole: AriaRole
|
|
25
|
+
};
|
|
26
|
+
|
|
6
27
|
export interface CompositeOptions {
|
|
7
28
|
disabledKeys?: CompositeItemKey[];
|
|
8
29
|
selectedKeys?: CompositeItemKey[];
|
|
@@ -16,6 +37,11 @@ interface CompositeDataPropGetters<T> {
|
|
|
16
37
|
getItemChildren: (item: T) => T[] | undefined;
|
|
17
38
|
}
|
|
18
39
|
|
|
40
|
+
export interface CompositeProviderOptions<T extends object> {
|
|
41
|
+
focusProvider: AbstractFocusProvider<T>;
|
|
42
|
+
selectionProvider?: AbstractSelectionProvider<T>;
|
|
43
|
+
}
|
|
44
|
+
|
|
19
45
|
export type CompositeDataOptions<T> = Required<CompositeOptions> & CompositeDataPropGetters<T>;
|
|
20
46
|
|
|
21
47
|
export type CompositeDataItemOptions<T> = CompositeDataPropGetters<T> & {
|
|
@@ -29,53 +55,48 @@ export interface CompositeDataItemState {
|
|
|
29
55
|
disabled: boolean;
|
|
30
56
|
}
|
|
31
57
|
|
|
32
|
-
export interface
|
|
33
|
-
|
|
58
|
+
export interface CompositeEventHandlers {
|
|
59
|
+
mouseEventHandler?: MouseEventHandler<HTMLElement>;
|
|
60
|
+
keyboardEventHandler?: KeyboardEventHandler<HTMLElement>;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export interface CompositeItemEventHandlerFunctions<T extends object> {
|
|
64
|
+
itemMouseEventHandler?: (itemAtom: CompositeDataItem<T>) => void;
|
|
65
|
+
itemKeyboardEventHandler?: (itemAtom: CompositeDataItem<T>) => void;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export interface BaseCompositeProps<T extends object> extends React.ComponentPropsWithoutRef<"div"> {
|
|
69
|
+
data: CompositeData<T>;
|
|
34
70
|
className?: string;
|
|
71
|
+
ref?: React.Ref<HTMLDivElement>
|
|
72
|
+
handleRef?: React.Ref<CompositeHandle>
|
|
73
|
+
|
|
74
|
+
renderItem: CompositeItemRenderFn<T>;
|
|
35
75
|
itemClassName?: string;
|
|
36
|
-
|
|
76
|
+
|
|
37
77
|
}
|
|
38
78
|
|
|
79
|
+
export type CompositeProps<T extends object> = BaseCompositeProps<T> & CompositeItemEventHandlerFunctions<T> & CompositeRoles;
|
|
39
80
|
|
|
40
81
|
export interface CompositeHandle {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
select: () => void;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
export interface CompositeItemEventHandlers {
|
|
47
|
-
mouseEventHandler?: MouseEventHandler<HTMLElement>;
|
|
48
|
-
keyboardEventHandler?: KeyboardEventHandler<HTMLElement>;
|
|
82
|
+
focusProvider: FocusProvider;
|
|
83
|
+
selectionProvider?: SelectionProvider;
|
|
49
84
|
}
|
|
50
85
|
|
|
51
86
|
export type CompositeItemRenderFn<T extends object> = (
|
|
52
87
|
item: { data: T; level: number; key: CompositeItemKey },
|
|
53
88
|
state: CompositeDataItemState,
|
|
54
|
-
eventHandlers:
|
|
89
|
+
eventHandlers: CompositeEventHandlers
|
|
55
90
|
) => ReactNode;
|
|
56
91
|
|
|
57
|
-
export interface CompositeItemProps<T extends object> {
|
|
92
|
+
export interface CompositeItemProps<T extends object> extends CompositeItemEventHandlerFunctions<T> {
|
|
58
93
|
id: string;
|
|
59
94
|
className?: string;
|
|
95
|
+
role?: AriaRole;
|
|
96
|
+
groupRole?: AriaRole;
|
|
60
97
|
|
|
61
98
|
item: CompositeDataItem<T>;
|
|
62
99
|
|
|
63
|
-
mouseEventHandler?: (itemAtom: CompositeDataItem<T>) => void;
|
|
64
|
-
keyboardEventHandler?: (itemAtom: CompositeDataItem<T>) => void;
|
|
65
|
-
|
|
66
100
|
remove?: () => void;
|
|
67
101
|
render: CompositeItemRenderFn<T>;
|
|
68
102
|
}
|
|
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
|
-
}
|