@bccampus/ui-components 0.4.1 → 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.
Files changed (29) hide show
  1. package/dist/AbstractFocusProvider-CxvlcEki.js +29 -0
  2. package/dist/AbstractSelectionProvider-BtaROstC.js +30 -0
  3. package/dist/CompositeDataItem-DuHOHCWy.js +158 -0
  4. package/dist/ListboxFocusProvider.d.ts +149 -0
  5. package/dist/ListboxFocusProvider.js +53 -0
  6. package/dist/MultipleSelectionProvider.d.ts +141 -0
  7. package/dist/MultipleSelectionProvider.js +19 -0
  8. package/dist/SingleSelectionProvider.d.ts +141 -0
  9. package/dist/SingleSelectionProvider.js +23 -0
  10. package/dist/composite-component-DSUbd1XS.js +122 -0
  11. package/dist/composite.d.ts +108 -51
  12. package/dist/composite.js +57 -447
  13. package/dist/listbox.d.ts +171 -0
  14. package/dist/listbox.js +76 -0
  15. package/package.json +5 -1
  16. package/src/components/ui/composite/CompositeData.ts +22 -114
  17. package/src/components/ui/composite/FocusProvider/AbstractFocusProvider.ts +83 -0
  18. package/src/components/ui/composite/FocusProvider/ListboxFocusProvider.ts +74 -0
  19. package/src/components/ui/composite/SelectionProvider/AbstractSelectionProvider.ts +45 -0
  20. package/src/components/ui/composite/SelectionProvider/MultipleSelectionProvider.ts +28 -0
  21. package/src/components/ui/composite/SelectionProvider/SingleSelectionProvider.ts +37 -0
  22. package/src/components/ui/composite/composite-component-item.tsx +12 -10
  23. package/src/components/ui/composite/composite-component.tsx +39 -68
  24. package/src/components/ui/composite/index.ts +4 -1
  25. package/src/components/ui/composite/listbox.tsx +61 -0
  26. package/src/components/ui/composite/types.ts +51 -30
  27. package/src/hooks/use-keyboard-event.ts +31 -42
  28. package/vite.config.ts +4 -0
  29. package/src/components/ui/composite/composite-data-context.tsx +0 -31
@@ -1,98 +1,69 @@
1
- import { useCallback, useImperativeHandle, useRef } from "react";
2
- import { CompositeDataItem } from "./CompositeDataItem";
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
- ref,
39
+ handleRef,
62
40
  () => {
63
41
  return {
64
- focusDown() {
65
- focusDown();
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
- [compositeData, focusDown, focusUp]
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
- ref={compositeRef}
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
- mouseEventHandler={handleItemMouseEvent}
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 './composite-data-context.tsx'
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 CompositeProps<T extends object> extends React.ComponentPropsWithoutRef<"div"> {
33
- renderItem: CompositeItemRenderFn<T>;
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
- ref?: React.Ref<CompositeHandle>
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
- focusUp: () => void;
42
- focusDown: () => void;
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: CompositeItemEventHandlers
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
- }
@@ -1,52 +1,38 @@
1
1
  import type { KeyboardEvent, KeyboardEventHandler } from 'react';
2
2
  import { useMemo } from 'react';
3
3
 
4
- const MODIFIER_KEYS = new Set(['ctrl', 'shift', 'alt', 'meta']);
5
-
6
- const KEY_MAPPINGS: Record<string, string> = {
7
- ' ': 'space',
8
- };
9
-
10
- export interface KeyBindings {
4
+ interface KeyBindings {
11
5
  [sequence: string]: (event: KeyboardEvent) => void;
12
6
  }
13
-
14
7
  interface KeybindingLookupItem {
15
- sequence: Set<string>;
8
+ sequence: RegExp;
16
9
  handler: (event: KeyboardEvent) => void;
17
10
  }
11
+ const MODIFIER_KEYS = new Set(["ctrl", "shift", "alt", "meta"]);
12
+
13
+ const KEY_MAPPINGS: Record<string, string> = {
14
+ " ": "space",
15
+ };
18
16
 
19
17
  interface UseKeyboardEventOptions {
20
18
  eventKeyProp: 'key' | 'code';
21
19
  }
22
20
 
23
- function isSequenceEqual<T>(sequenceA: Set<T>, sequenceB: Set<T>) {
24
- if (sequenceA.size !== sequenceB.size) return false;
25
-
26
- for (const element of sequenceB) {
27
- if (!sequenceA.has(element)) return false;
28
- }
29
-
30
- return true;
31
- }
32
-
33
21
  function parseKeybindings(bindings: KeyBindings) {
34
22
  const parsedKeybindings: KeybindingLookupItem[] = [];
35
23
  for (const [sequence, handler] of Object.entries(bindings)) {
36
24
  const parsedSequence = sequence
37
25
  .toLowerCase()
38
26
  .trim()
39
- .split(/\s*\+\s*/);
27
+ .split(/\s*\+\s*/g);
40
28
 
41
29
  if (parsedSequence.length === 1 && MODIFIER_KEYS.has(parsedSequence[0])) {
42
- console.error(`[useKeyboardEvent] \`${sequence}\`: A key sequence cannot be only a modifier key.`);
43
- }
44
- else if (parsedSequence.includes('')) {
45
- console.error(`[useKeyboardEvent] \`${sequence}\`: Unknown key defined in the sequence.`);
46
- }
47
- else {
30
+ console.error(`[useKeyboardEvent] '${sequence}': A key sequence cannot be only a modifier key.`);
31
+ } else if (parsedSequence.includes("")) {
32
+ console.error(`[useKeyboardEvent] '${sequence}': Invalid key sequence defined in the sequence.`);
33
+ } else {
48
34
  parsedKeybindings.push({
49
- sequence: new Set(parsedSequence),
35
+ sequence: new RegExp("^" + parsedSequence.join("\\+") + "$"),
50
36
  handler,
51
37
  });
52
38
  }
@@ -68,21 +54,22 @@ export function keyboardEventHandler(
68
54
  const keyBindings = parseKeybindings(bindings);
69
55
 
70
56
  return (event: KeyboardEvent) => {
71
- const keySequence = new Set<string>();
72
57
  const eventKey = event[_options.eventKeyProp];
58
+ const keySequence: string[] = [];
73
59
 
74
- if (event.ctrlKey) keySequence.add('ctrl');
60
+ if (event.ctrlKey) keySequence.push("ctrl");
75
61
 
76
- if (event.shiftKey) keySequence.add('shift');
62
+ if (event.shiftKey) keySequence.push("shift");
77
63
 
78
- if (event.altKey) keySequence.add('alt');
64
+ if (event.altKey) keySequence.push("alt");
79
65
 
80
- if (event.metaKey) keySequence.add('meta');
66
+ if (event.metaKey) keySequence.push("meta");
81
67
 
82
- if (!KEY_MAPPINGS[eventKey]) keySequence.add(eventKey.toLowerCase());
83
- else keySequence.add(KEY_MAPPINGS[eventKey]);
68
+ if (!KEY_MAPPINGS[eventKey]) keySequence.push(eventKey.toLowerCase());
69
+ else keySequence.push(KEY_MAPPINGS[eventKey]);
84
70
 
85
- const matchedSequence = keyBindings.find(keyBinding => isSequenceEqual(keySequence, keyBinding.sequence));
71
+ const matchedSequence = keyBindings.find((keyBinding) => keyBinding.sequence.test(keySequence.join("+")));
72
+
86
73
  if (matchedSequence) {
87
74
  event.preventDefault();
88
75
  event.stopPropagation();
@@ -91,16 +78,18 @@ export function keyboardEventHandler(
91
78
  };
92
79
  }
93
80
 
81
+
94
82
  /**
95
83
  * Returns a `KeyboardEventHandler`
96
- * that checks the defined key binding sequences against a keyboard event
84
+ * that checks the defined key sequences against a keyboard event
97
85
  * and executes the handler of the first matched key binding.
98
- *
99
- * Limitations:
100
- * - Space character (` `) cannot be used in the key sequences.
101
- * Use the `space` keyword instead.
102
- * - Plus character (`+`) cannot be used in the key sequences.
103
- * Use `shit + =` instead.
86
+ *
87
+ * Key Sequence Rules:
88
+ * - Multiple key must be seperated by `+`
89
+ * - Only the following modifier key values as allowed: ctrl, shift, alt, meta
90
+ * - Modifier key must followed by a key
91
+ * - Space character (` `) cannot be used in the key sequences. Use the `space` keyword instead.
92
+ * - Plus character (`+`) cannot be used in the key sequences. Use the `shit + =` sequence instead.
104
93
  *
105
94
  * @example
106
95
  * ```
package/vite.config.ts CHANGED
@@ -21,6 +21,10 @@ export default defineConfig({
21
21
  entry: {
22
22
  'ui-components': path.resolve(__dirname, 'src/components/ui/index.ts'),
23
23
  'composite': path.resolve(__dirname, 'src/components/ui/composite/index.ts'),
24
+ 'listbox': path.resolve(__dirname, 'src/components/ui/composite/listbox.tsx'),
25
+ 'SingleSelectionProvider': path.resolve(__dirname, 'src/components/ui/composite/SelectionProvider/SingleSelectionProvider.ts'),
26
+ 'MultipleSelectionProvider': path.resolve(__dirname, 'src/components/ui/composite/SelectionProvider/MultipleSelectionProvider.ts'),
27
+ 'ListboxFocusProvider': path.resolve(__dirname, 'src/components/ui/composite/FocusProvider/ListboxFocusProvider.ts'),
24
28
  'icon-generator': path.resolve(__dirname, 'src/components/ui/icon-generator/index.ts'),
25
29
  'generate-tiles': path.resolve(__dirname, 'src/components/ui/icon-generator/generate-tiles.tsx'),
26
30
  'masked-image-generator': path.resolve(__dirname, 'src/components/ui/icon-generator/masked-image-generator.tsx'),
@@ -1,31 +0,0 @@
1
- import { createContext, useContext, useMemo } from "react";
2
- import { CompositeData } from "./CompositeData";
3
- import type { CompositeOptions } from "./types";
4
-
5
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
6
- const CompositeDataContext = createContext<CompositeData<any> | undefined>(undefined);
7
-
8
- export function useCompositeContext<T extends object>() {
9
- const context = useContext(CompositeDataContext);
10
-
11
- if (!context) {
12
- throw new Error("No CompositeDataContext has been set, use CompositeDataContext to provide a context");
13
- }
14
-
15
- return context as CompositeData<T>;
16
- }
17
-
18
- interface CompositeComponentContextProps<T extends object> extends CompositeOptions {
19
- data: T[];
20
- children: React.ReactNode;
21
- }
22
-
23
- export function CompositeComponentContext<T extends object>({
24
- data,
25
- children,
26
- ...compositeOptions
27
- }: CompositeComponentContextProps<T>) {
28
- const composite = useMemo(() => new CompositeData(data, compositeOptions), [data, compositeOptions]);
29
-
30
- return <CompositeDataContext value={composite}>{children}</CompositeDataContext>;
31
- }