@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.
Files changed (31) hide show
  1. package/dist/components/ui/button.d.ts +2 -1
  2. package/dist/components/ui/button.js +11 -2
  3. package/dist/components/ui/composite/CompositeDataItem.js +7 -12
  4. package/dist/components/ui/composite/FocusProvider/AbstractFocusProvider.d.ts +9 -2
  5. package/dist/components/ui/composite/FocusProvider/AbstractFocusProvider.js +9 -4
  6. package/dist/components/ui/composite/FocusProvider/ListboxFocusProvider.js +8 -16
  7. package/dist/components/ui/composite/SelectionProvider/AbstractSelectionProvider.d.ts +10 -1
  8. package/dist/components/ui/composite/SelectionProvider/AbstractSelectionProvider.js +3 -0
  9. package/dist/components/ui/composite/composite-component-item.d.ts +1 -1
  10. package/dist/components/ui/composite/composite-component-item.js +5 -3
  11. package/dist/components/ui/composite/composite-component.d.ts +1 -1
  12. package/dist/components/ui/composite/composite-component.js +73 -25
  13. package/dist/components/ui/composite/listbox.d.ts +1 -1
  14. package/dist/components/ui/composite/listbox.js +39 -34
  15. package/dist/components/ui/composite/types.d.ts +16 -8
  16. package/dist/hooks/use-keyboard-event.d.ts +3 -3
  17. package/dist/hooks/use-keyboard-event.js +2 -2
  18. package/dist/hooks/use-required-ref.d.ts +1 -0
  19. package/dist/hooks/use-required-ref.js +8 -0
  20. package/package.json +1 -1
  21. package/src/components/ui/button.tsx +14 -3
  22. package/src/components/ui/composite/CompositeDataItem.ts +105 -110
  23. package/src/components/ui/composite/FocusProvider/AbstractFocusProvider.ts +25 -16
  24. package/src/components/ui/composite/FocusProvider/ListboxFocusProvider.ts +15 -29
  25. package/src/components/ui/composite/SelectionProvider/AbstractSelectionProvider.ts +16 -1
  26. package/src/components/ui/composite/composite-component-item.tsx +3 -1
  27. package/src/components/ui/composite/composite-component.tsx +70 -15
  28. package/src/components/ui/composite/listbox.tsx +39 -36
  29. package/src/components/ui/composite/types.ts +70 -56
  30. package/src/hooks/use-keyboard-event.ts +13 -13
  31. package/src/hooks/use-required-ref.ts +6 -0
@@ -1,58 +1,61 @@
1
1
  import { CompositeComponent } from "./composite-component";
2
- import { useCallback, useRef } from "react";
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>({ data, ...props }: BaseCompositeProps<T>) {
8
- const compositeRef = useRef<HTMLDivElement>(null);
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 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();
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
- End: () => {
33
- data.focusProvider.focusToLast();
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, focusElement]
49
+ [$handleRef, data.focusProvider, data.selectionProvider],
49
50
  );
50
51
 
51
52
  return (
52
53
  <CompositeComponent
53
- ref={compositeRef}
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
- 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
- };
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
- disabledKeys?: CompositeItemKey[];
29
- selectedKeys?: CompositeItemKey[];
32
+ disabledKeys?: CompositeItemKey[];
33
+ selectedKeys?: CompositeItemKey[];
30
34
 
31
- itemKeyProp?: string;
32
- itemChildrenProp?: string;
35
+ itemKeyProp?: string;
36
+ itemChildrenProp?: string;
33
37
  }
34
38
 
35
39
  interface CompositeDataPropGetters<T> {
36
- getItemKey: (item: T) => CompositeItemKey;
37
- getItemChildren: (item: T) => T[] | undefined;
40
+ getItemKey: (item: T) => CompositeItemKey;
41
+ getItemChildren: (item: T) => T[] | undefined;
38
42
  }
39
43
 
40
44
  export interface CompositeProviderOptions<T extends object> {
41
- focusProvider: AbstractFocusProvider<T>;
42
- selectionProvider?: AbstractSelectionProvider<T>;
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
- initialState?: CompositeDataItemState;
49
- itemChildrenProp: string;
52
+ initialState?: CompositeDataItemState;
53
+ itemChildrenProp: string;
50
54
  };
51
55
 
52
56
  export interface CompositeDataItemState {
53
- focused: boolean;
54
- selected: boolean;
55
- disabled: boolean;
57
+ focused: boolean;
58
+ selected: boolean;
59
+ disabled: boolean;
56
60
  }
57
61
 
58
62
  export interface CompositeEventHandlers {
59
- mouseEventHandler?: MouseEventHandler<HTMLElement>;
60
- keyboardEventHandler?: KeyboardEventHandler<HTMLElement>;
63
+ mouseEventHandler?: MouseEventHandler<HTMLElement>;
64
+ keyboardEventHandler?: KeyboardEventHandler<HTMLElement>;
61
65
  }
62
66
 
63
67
  export interface CompositeItemEventHandlerFunctions<T extends object> {
64
- itemMouseEventHandler?: (itemAtom: CompositeDataItem<T>) => void;
65
- itemKeyboardEventHandler?: (itemAtom: CompositeDataItem<T>) => void;
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
- data: CompositeData<T>;
70
- className?: string;
71
- ref?: React.Ref<HTMLDivElement>
72
- handleRef?: React.Ref<CompositeHandle>
73
-
74
- renderItem: CompositeItemRenderFn<T>;
75
- itemClassName?: string;
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> & CompositeItemEventHandlerFunctions<T> & CompositeRoles;
88
+ export type CompositeProps<T extends object> = BaseCompositeProps<T> &
89
+ CompositeItemEventHandlerFunctions<T> &
90
+ CompositeRoles;
80
91
 
81
- export interface CompositeHandle {
82
- focusProvider: FocusProvider;
83
- selectionProvider?: SelectionProvider;
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
- item: { data: T; level: number; key: CompositeItemKey },
88
- state: CompositeDataItemState,
89
- eventHandlers: CompositeEventHandlers
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
- id: string;
94
- className?: string;
95
- role?: AriaRole;
96
- groupRole?: AriaRole;
105
+ id: string;
106
+ className?: string;
107
+ role?: AriaRole;
108
+ groupRole?: AriaRole;
109
+
110
+ item: CompositeDataItem<T>;
97
111
 
98
- item: CompositeDataItem<T>;
112
+ remove?: () => void;
113
+ render: CompositeItemRenderFn<T>;
99
114
 
100
- remove?: () => void;
101
- render: CompositeItemRenderFn<T>;
115
+ softFocus?: boolean;
102
116
  }
@@ -1,5 +1,5 @@
1
- import type { KeyboardEvent, KeyboardEventHandler } from 'react';
2
- import { useMemo } from 'react';
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: 'key' | 'code';
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: 'key',
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
- return useMemo(() => keyboardEventHandler(bindings, options), [bindings, options]);
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
  }
@@ -0,0 +1,6 @@
1
+ import { useRef } from "react";
2
+
3
+ export function useRequiredRef<T>(ref?: React.RefObject<T | null>) {
4
+ const emptyRef = useRef<T>(null);
5
+ return ref ?? emptyRef;
6
+ }