@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
|
@@ -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}
|
|
@@ -6,97 +6,111 @@ import type { CompositeData } from "./CompositeData";
|
|
|
6
6
|
|
|
7
7
|
export type CompositeItemKey = string | number;
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
9
|
+
export type CompositeRoles =
|
|
10
|
+
| {
|
|
11
|
+
variant: "listbox";
|
|
12
|
+
rootRole?: never;
|
|
13
|
+
itemRole?: never;
|
|
14
|
+
groupRole?: never;
|
|
15
|
+
}
|
|
16
|
+
| {
|
|
17
|
+
variant: "grid";
|
|
18
|
+
rootRole?: never;
|
|
19
|
+
itemRole?: never;
|
|
20
|
+
groupRole?: never;
|
|
21
|
+
}
|
|
22
|
+
| {
|
|
23
|
+
variant: "custom";
|
|
24
|
+
rootRole: AriaRole;
|
|
25
|
+
itemRole: AriaRole;
|
|
26
|
+
groupRole: AriaRole;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export type InitialFocusTarget = "None" | "LastFocusedItem" | "SelectedItem" | "FirstItem";
|
|
26
30
|
|
|
27
31
|
export interface CompositeOptions {
|
|
28
|
-
|
|
29
|
-
|
|
32
|
+
disabledKeys?: CompositeItemKey[];
|
|
33
|
+
selectedKeys?: CompositeItemKey[];
|
|
30
34
|
|
|
31
|
-
|
|
32
|
-
|
|
35
|
+
itemKeyProp?: string;
|
|
36
|
+
itemChildrenProp?: string;
|
|
33
37
|
}
|
|
34
38
|
|
|
35
39
|
interface CompositeDataPropGetters<T> {
|
|
36
|
-
|
|
37
|
-
|
|
40
|
+
getItemKey: (item: T) => CompositeItemKey;
|
|
41
|
+
getItemChildren: (item: T) => T[] | undefined;
|
|
38
42
|
}
|
|
39
43
|
|
|
40
44
|
export interface CompositeProviderOptions<T extends object> {
|
|
41
|
-
|
|
42
|
-
|
|
45
|
+
focusProvider: AbstractFocusProvider<T>;
|
|
46
|
+
selectionProvider?: AbstractSelectionProvider<T>;
|
|
43
47
|
}
|
|
44
48
|
|
|
45
49
|
export type CompositeDataOptions<T> = Required<CompositeOptions> & CompositeDataPropGetters<T>;
|
|
46
50
|
|
|
47
51
|
export type CompositeDataItemOptions<T> = CompositeDataPropGetters<T> & {
|
|
48
|
-
|
|
49
|
-
|
|
52
|
+
initialState?: CompositeDataItemState;
|
|
53
|
+
itemChildrenProp: string;
|
|
50
54
|
};
|
|
51
55
|
|
|
52
56
|
export interface CompositeDataItemState {
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
57
|
+
focused: boolean;
|
|
58
|
+
selected: boolean;
|
|
59
|
+
disabled: boolean;
|
|
56
60
|
}
|
|
57
61
|
|
|
58
62
|
export interface CompositeEventHandlers {
|
|
59
|
-
|
|
60
|
-
|
|
63
|
+
mouseEventHandler?: MouseEventHandler<HTMLElement>;
|
|
64
|
+
keyboardEventHandler?: KeyboardEventHandler<HTMLElement>;
|
|
61
65
|
}
|
|
62
66
|
|
|
63
67
|
export interface CompositeItemEventHandlerFunctions<T extends object> {
|
|
64
|
-
|
|
65
|
-
|
|
68
|
+
itemMouseEventHandler?: (itemAtom: CompositeDataItem<T>) => void;
|
|
69
|
+
itemKeyboardEventHandler?: (itemAtom: CompositeDataItem<T>) => void;
|
|
66
70
|
}
|
|
67
71
|
|
|
68
72
|
export interface BaseCompositeProps<T extends object> extends React.ComponentPropsWithoutRef<"div"> {
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
73
|
+
data: CompositeData<T>;
|
|
74
|
+
className?: string;
|
|
75
|
+
rootRef?: React.RefObject<HTMLDivElement | null>;
|
|
76
|
+
handleRef?: React.RefObject<CompositeHandle<T> | null>;
|
|
77
|
+
|
|
78
|
+
renderItem: CompositeItemRenderFn<T>;
|
|
79
|
+
itemClassName?: string;
|
|
80
|
+
|
|
81
|
+
initialFocus?: InitialFocusTarget;
|
|
82
|
+
/**
|
|
83
|
+
* Set `item.focused = true`, but not focus on the HTMLElement
|
|
84
|
+
*/
|
|
85
|
+
softFocus?: boolean;
|
|
77
86
|
}
|
|
78
87
|
|
|
79
|
-
export type CompositeProps<T extends object> = BaseCompositeProps<T> &
|
|
88
|
+
export type CompositeProps<T extends object> = BaseCompositeProps<T> &
|
|
89
|
+
CompositeItemEventHandlerFunctions<T> &
|
|
90
|
+
CompositeRoles;
|
|
80
91
|
|
|
81
|
-
export interface CompositeHandle {
|
|
82
|
-
|
|
83
|
-
|
|
92
|
+
export interface CompositeHandle<T extends object> {
|
|
93
|
+
focusProvider: FocusProvider<T>;
|
|
94
|
+
focusElement: () => void;
|
|
95
|
+
selectionProvider?: SelectionProvider<T>;
|
|
84
96
|
}
|
|
85
97
|
|
|
86
98
|
export type CompositeItemRenderFn<T extends object> = (
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
99
|
+
item: { data: T; level: number; key: CompositeItemKey },
|
|
100
|
+
state: CompositeDataItemState,
|
|
101
|
+
eventHandlers: CompositeEventHandlers,
|
|
90
102
|
) => ReactNode;
|
|
91
103
|
|
|
92
104
|
export interface CompositeItemProps<T extends object> extends CompositeItemEventHandlerFunctions<T> {
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
105
|
+
id: string;
|
|
106
|
+
className?: string;
|
|
107
|
+
role?: AriaRole;
|
|
108
|
+
groupRole?: AriaRole;
|
|
109
|
+
|
|
110
|
+
item: CompositeDataItem<T>;
|
|
97
111
|
|
|
98
|
-
|
|
112
|
+
remove?: () => void;
|
|
113
|
+
render: CompositeItemRenderFn<T>;
|
|
99
114
|
|
|
100
|
-
|
|
101
|
-
render: CompositeItemRenderFn<T>;
|
|
115
|
+
softFocus?: boolean;
|
|
102
116
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type { KeyboardEvent, KeyboardEventHandler } from
|
|
2
|
-
import { useMemo } from
|
|
1
|
+
import type { KeyboardEvent, KeyboardEventHandler } from "react";
|
|
2
|
+
import { useMemo } from "react";
|
|
3
3
|
|
|
4
4
|
interface KeyBindings {
|
|
5
5
|
[sequence: string]: (event: KeyboardEvent) => void;
|
|
@@ -15,7 +15,7 @@ const KEY_MAPPINGS: Record<string, string> = {
|
|
|
15
15
|
};
|
|
16
16
|
|
|
17
17
|
interface UseKeyboardEventOptions {
|
|
18
|
-
eventKeyProp:
|
|
18
|
+
eventKeyProp: "key" | "code";
|
|
19
19
|
}
|
|
20
20
|
|
|
21
21
|
function parseKeybindings(bindings: KeyBindings) {
|
|
@@ -42,14 +42,14 @@ function parseKeybindings(bindings: KeyBindings) {
|
|
|
42
42
|
}
|
|
43
43
|
|
|
44
44
|
const defaultOptions: UseKeyboardEventOptions = {
|
|
45
|
-
eventKeyProp:
|
|
45
|
+
eventKeyProp: "key",
|
|
46
46
|
};
|
|
47
47
|
|
|
48
48
|
export function keyboardEventHandler(
|
|
49
49
|
bindings: KeyBindings,
|
|
50
|
-
options: UseKeyboardEventOptions = defaultOptions
|
|
50
|
+
options: UseKeyboardEventOptions = defaultOptions,
|
|
51
51
|
): KeyboardEventHandler {
|
|
52
|
-
const _options = { ...options, ...defaultOptions }
|
|
52
|
+
const _options = { ...options, ...defaultOptions };
|
|
53
53
|
|
|
54
54
|
const keyBindings = parseKeybindings(bindings);
|
|
55
55
|
|
|
@@ -69,7 +69,7 @@ export function keyboardEventHandler(
|
|
|
69
69
|
else keySequence.push(KEY_MAPPINGS[eventKey]);
|
|
70
70
|
|
|
71
71
|
const matchedSequence = keyBindings.find((keyBinding) => keyBinding.sequence.test(keySequence.join("+")));
|
|
72
|
-
|
|
72
|
+
|
|
73
73
|
if (matchedSequence) {
|
|
74
74
|
event.preventDefault();
|
|
75
75
|
event.stopPropagation();
|
|
@@ -78,16 +78,15 @@ export function keyboardEventHandler(
|
|
|
78
78
|
};
|
|
79
79
|
}
|
|
80
80
|
|
|
81
|
-
|
|
82
81
|
/**
|
|
83
82
|
* Returns a `KeyboardEventHandler`
|
|
84
83
|
* that checks the defined key sequences against a keyboard event
|
|
85
84
|
* and executes the handler of the first matched key binding.
|
|
86
|
-
*
|
|
85
|
+
*
|
|
87
86
|
* Key Sequence Rules:
|
|
88
87
|
* - Multiple key must be seperated by `+`
|
|
89
88
|
* - Only the following modifier key values as allowed: ctrl, shift, alt, meta
|
|
90
|
-
* - Modifier key must followed by a key
|
|
89
|
+
* - Modifier key must followed by a key
|
|
91
90
|
* - Space character (` `) cannot be used in the key sequences. Use the `space` keyword instead.
|
|
92
91
|
* - Plus character (`+`) cannot be used in the key sequences. Use the `shit + =` sequence instead.
|
|
93
92
|
*
|
|
@@ -117,7 +116,7 @@ export function keyboardEventHandler(
|
|
|
117
116
|
* 'escape': clearInput,
|
|
118
117
|
* 'ctrl+c': clearInput,
|
|
119
118
|
* 'ctrl + shift + c': deleteAll,
|
|
120
|
-
* });
|
|
119
|
+
* },[]);
|
|
121
120
|
*
|
|
122
121
|
* return (
|
|
123
122
|
* <input
|
|
@@ -128,6 +127,7 @@ export function keyboardEventHandler(
|
|
|
128
127
|
* }
|
|
129
128
|
* ```
|
|
130
129
|
*/
|
|
131
|
-
export function useKeyboardEvent(bindings: KeyBindings, options?: UseKeyboardEventOptions) {
|
|
132
|
-
|
|
130
|
+
export function useKeyboardEvent(bindings: KeyBindings, deps: React.DependencyList, options?: UseKeyboardEventOptions) {
|
|
131
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
132
|
+
return useMemo(() => keyboardEventHandler(bindings, options), [deps]);
|
|
133
133
|
}
|