@basic-ui/core 0.0.31 → 0.0.34

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 (128) hide show
  1. package/build/cjs/index.js +113 -71
  2. package/build/cjs/index.js.map +1 -1
  3. package/build/esm/FocusLock/useFocusLock.js +21 -7
  4. package/build/esm/FocusLock/useFocusLock.js.map +1 -1
  5. package/build/esm/Tooltip/stateMachine.d.ts +17 -19
  6. package/build/esm/Tooltip/stateMachine.js +45 -49
  7. package/build/esm/Tooltip/stateMachine.js.map +1 -1
  8. package/build/esm/Tooltip/useTooltip.js +9 -9
  9. package/build/esm/Tooltip/useTooltip.js.map +1 -1
  10. package/build/esm/hooks/useGestureHandlers.d.ts +2 -0
  11. package/build/esm/hooks/useGestureHandlers.js +39 -7
  12. package/build/esm/hooks/useGestureHandlers.js.map +1 -1
  13. package/build/tsconfig.tsbuildinfo +383 -88
  14. package/package.json +6 -6
  15. package/src/Accordion/Accordion.story.tsx +72 -0
  16. package/src/Accordion/Accordion.tsx +51 -0
  17. package/src/Accordion/AccordionBody.tsx +53 -0
  18. package/src/Accordion/AccordionHeader.tsx +165 -0
  19. package/src/Accordion/AccordionItem.tsx +43 -0
  20. package/src/Accordion/context.ts +35 -0
  21. package/src/Accordion/index.ts +4 -0
  22. package/src/Accordion/scopeQuery.ts +7 -0
  23. package/src/Accordion/styles.css +21 -0
  24. package/src/CheckBox/CheckBox.tsx +41 -0
  25. package/src/CheckBox/index.ts +1 -0
  26. package/src/ComboBox/ComboBox.story.tsx +118 -0
  27. package/src/ComboBox/Combobox.tsx +153 -0
  28. package/src/ComboBox/ComboboxButton.tsx +60 -0
  29. package/src/ComboBox/ComboboxInput.tsx +178 -0
  30. package/src/ComboBox/ComboboxLabel.tsx +32 -0
  31. package/src/ComboBox/ComboboxList.tsx +47 -0
  32. package/src/ComboBox/ComboboxOption.tsx +107 -0
  33. package/src/ComboBox/ComboboxPopover.tsx +58 -0
  34. package/src/ComboBox/cities.ts +23194 -0
  35. package/src/ComboBox/context.ts +33 -0
  36. package/src/ComboBox/hooks.tsx +428 -0
  37. package/src/ComboBox/index.ts +8 -0
  38. package/src/ComboBox/makeHash.ts +19 -0
  39. package/src/ComboBox/scopeQuery.ts +6 -0
  40. package/src/ComboBox/styles.css +30 -0
  41. package/src/FocusLock/FocusLock.tsx +59 -0
  42. package/src/FocusLock/index.ts +1 -0
  43. package/src/FocusLock/tabUtils.ts +28 -0
  44. package/src/FocusLock/useFocusLock.ts +61 -0
  45. package/src/List/List.story.tsx +17 -0
  46. package/src/List/List.tsx +17 -0
  47. package/src/List/ListItem.tsx +23 -0
  48. package/src/List/context.ts +19 -0
  49. package/src/List/index.ts +2 -0
  50. package/src/Menu/.gitkeep +0 -0
  51. package/src/Menu/Menu.story.tsx +158 -0
  52. package/src/Menu/Menu.tsx +60 -0
  53. package/src/Menu/MenuButton.tsx +83 -0
  54. package/src/Menu/MenuItem.tsx +83 -0
  55. package/src/Menu/MenuList.tsx +201 -0
  56. package/src/Menu/MenuPopover.tsx +25 -0
  57. package/src/Menu/context.ts +32 -0
  58. package/src/Menu/index.ts +5 -0
  59. package/src/Menu/scope.ts +7 -0
  60. package/src/Menu/styles.css +42 -0
  61. package/src/Modal/Modal.story.tsx +242 -0
  62. package/src/Modal/Modal.tsx +42 -0
  63. package/src/Modal/ModalBackdrop.tsx +72 -0
  64. package/src/Modal/NavDrawer.story.tsx +157 -0
  65. package/src/Modal/index.ts +2 -0
  66. package/src/Modal/styles.css +46 -0
  67. package/src/Popover/.gitkeep +0 -0
  68. package/src/Popper/Popper.story.tsx +267 -0
  69. package/src/Popper/Popper.tsx +149 -0
  70. package/src/Popper/PopperArrow.tsx +36 -0
  71. package/src/Popper/context.ts +9 -0
  72. package/src/Popper/index.ts +3 -0
  73. package/src/Popper/styles.css +60 -0
  74. package/src/Portal/Portal.tsx +20 -0
  75. package/src/Portal/index.ts +1 -0
  76. package/src/RadioButton/RadioButton.story.tsx +73 -0
  77. package/src/RadioButton/RadioButton.tsx +48 -0
  78. package/src/RadioButton/RadioGroup.tsx +56 -0
  79. package/src/RadioButton/context.ts +19 -0
  80. package/src/RadioButton/index.ts +2 -0
  81. package/src/SkipNav/SkipNav.tsx +16 -0
  82. package/src/SkipNav/index.tsx +1 -0
  83. package/src/Spinner/Spinner.story.tsx +30 -0
  84. package/src/Spinner/Spinner.tsx +112 -0
  85. package/src/Spinner/SpinnerButton.tsx +48 -0
  86. package/src/Spinner/context.ts +21 -0
  87. package/src/Spinner/index.ts +2 -0
  88. package/src/Spinner/styles.css +23 -0
  89. package/src/Tabs/Tab.story.tsx +78 -0
  90. package/src/Tabs/Tab.tsx +131 -0
  91. package/src/Tabs/TabList.tsx +63 -0
  92. package/src/Tabs/TabPanel.tsx +52 -0
  93. package/src/Tabs/TabPanels.tsx +30 -0
  94. package/src/Tabs/Tabs.tsx +47 -0
  95. package/src/Tabs/context.ts +30 -0
  96. package/src/Tabs/index.tsx +5 -0
  97. package/src/Tabs/scopeQuery.ts +6 -0
  98. package/src/Tabs/styles.css +0 -0
  99. package/src/Tooltip/.gitkeep +0 -0
  100. package/src/Tooltip/Tooltip.story.tsx +59 -0
  101. package/src/Tooltip/Tooltip.tsx +48 -0
  102. package/src/Tooltip/index.ts +1 -0
  103. package/src/Tooltip/stateMachine.ts +196 -0
  104. package/src/Tooltip/styles.css +17 -0
  105. package/src/Tooltip/useTooltip.ts +128 -0
  106. package/src/hooks/index.ts +14 -0
  107. package/src/hooks/useAutoFocus.ts +13 -0
  108. package/src/hooks/useChildrenCounter.ts +50 -0
  109. package/src/hooks/useControlledState.ts +37 -0
  110. package/src/hooks/useFocusReturn.ts +23 -0
  111. package/src/hooks/useFocusState.ts +28 -0
  112. package/src/hooks/useGestureHandlers.ts +253 -0
  113. package/src/hooks/useId.ts +18 -0
  114. package/src/hooks/useMeasure.ts +33 -0
  115. package/src/hooks/useOnClickOutside.ts +32 -0
  116. package/src/hooks/useOnKeyDown.ts +18 -0
  117. package/src/hooks/useReducerMachine.ts +59 -0
  118. package/src/hooks/useRemoveBodyScroll.ts +37 -0
  119. package/src/hooks/useScope.ts +51 -0
  120. package/src/hooks/useThrottle.ts +19 -0
  121. package/src/index.ts +19 -0
  122. package/src/utils/assignRef.ts +27 -0
  123. package/src/utils/clamp.ts +3 -0
  124. package/src/utils/createSubscription.ts +16 -0
  125. package/src/utils/getCircularIndex.ts +7 -0
  126. package/src/utils/index.ts +4 -0
  127. package/src/utils/rubberBandClamp.ts +25 -0
  128. package/src/utils/wrapEvent.ts +20 -0
@@ -0,0 +1,17 @@
1
+ import { forwardRef } from 'react';
2
+ import type * as React from 'react';
3
+
4
+ export interface ListProps extends React.HTMLAttributes<HTMLUListElement> {
5
+ as?: React.ElementType<any>;
6
+ innerAs?: React.ElementType<any>;
7
+ children?: React.ReactNode;
8
+ }
9
+
10
+ export const List = forwardRef<HTMLUListElement, ListProps>(function List(
11
+ props,
12
+ forwardedRef
13
+ ) {
14
+ const { as: Comp = 'ul', ...otherProps } = props;
15
+
16
+ return <Comp ref={forwardedRef} {...otherProps} />;
17
+ });
@@ -0,0 +1,23 @@
1
+ import { forwardRef } from 'react';
2
+ import type * as React from 'react';
3
+
4
+ export interface ListItemProps extends React.HTMLAttributes<HTMLLIElement> {
5
+ as?: React.ElementType<any>;
6
+ innerAs?: React.ElementType<any>;
7
+ children?: React.ReactNode;
8
+ }
9
+
10
+ export const ListItem = forwardRef<HTMLLIElement, ListItemProps>(
11
+ function ListItem(props, forwardedRef) {
12
+ const { as: Comp = 'li', ...otherProps } = props;
13
+
14
+ return (
15
+ <Comp
16
+ tabIndex={-1}
17
+ data-list-item=""
18
+ ref={forwardedRef}
19
+ {...otherProps}
20
+ />
21
+ );
22
+ }
23
+ );
@@ -0,0 +1,19 @@
1
+ import { createContext, useContext } from 'react';
2
+
3
+ // List Component
4
+ export interface ListContextProps {
5
+ id: string | undefined;
6
+ }
7
+
8
+ const ListContext = createContext<ListContextProps | null>(null);
9
+ ListContext.displayName = 'ListContext';
10
+
11
+ export const useListContext = (): ListContextProps => {
12
+ const ctx = useContext(ListContext);
13
+ if (!ctx) {
14
+ throw new Error(
15
+ 'useListContext must be used within a ListContext.Provider'
16
+ );
17
+ }
18
+ return ctx;
19
+ };
@@ -0,0 +1,2 @@
1
+ export * from './List';
2
+ export * from './ListItem';
File without changes
@@ -0,0 +1,158 @@
1
+ import { useState } from 'react';
2
+ import type * as React from 'react';
3
+ import { storiesOf } from '@storybook/react';
4
+ import { Menu, MenuButton, MenuItem, MenuList, MenuPopover } from './';
5
+ import { animated, useSpring } from 'react-spring';
6
+ import './styles.css';
7
+
8
+ const stories = storiesOf('Components/Menu', module);
9
+
10
+ const noop = () => {
11
+ // noop function to be used on onRest, as a cleanup
12
+ };
13
+
14
+ const Wrapper = ({ children }) => {
15
+ const handleLinkClick = (e: React.MouseEvent<HTMLAnchorElement>) => {
16
+ console.log('Clicked ' + e.currentTarget.innerText);
17
+ e.preventDefault();
18
+ };
19
+
20
+ return (
21
+ <div
22
+ style={{
23
+ boxSizing: 'border-box',
24
+ display: 'flex',
25
+ alignItems: 'flex-start',
26
+ padding: '85% 48px',
27
+ height: '150vh',
28
+ justifyContent: 'space-around',
29
+ width: '100%',
30
+ position: 'relative',
31
+ }}
32
+ >
33
+ <a href="#" onClick={handleLinkClick}>
34
+ Link 1
35
+ </a>
36
+ <div style={{ minHeight: 120, width: 100 }}>{children}</div>
37
+ <a href="#" onClick={handleLinkClick}>
38
+ Link 2
39
+ </a>
40
+ </div>
41
+ );
42
+ };
43
+
44
+ const MenuControlled = () => {
45
+ return (
46
+ <Menu>
47
+ <MenuButton>Click me</MenuButton>
48
+ <MenuPopover>
49
+ <MenuList>
50
+ <MenuItem>Dog</MenuItem>
51
+ <MenuItem>Lion</MenuItem>
52
+ <MenuItem>Tiger</MenuItem>
53
+ <MenuItem disabled>Cat</MenuItem>
54
+ <hr />
55
+ <MenuItem>Bear</MenuItem>
56
+ </MenuList>
57
+ </MenuPopover>
58
+ </Menu>
59
+ );
60
+ };
61
+
62
+ const MenuControlledWithAnimation = () => {
63
+ const [open, setOpen] = useState(false);
64
+
65
+ const [{ scale, opacity }, set] = useSpring(() => ({
66
+ scale: 0.75,
67
+ opacity: 0,
68
+ config: { mass: 1, tension: 750, friction: 50, clamp: true },
69
+ }));
70
+
71
+ const handleChange = (e: any, isOpen: boolean) => {
72
+ if (isOpen) {
73
+ set({ scale: 1, opacity: 1, onRest: noop });
74
+ setOpen(true);
75
+ } else {
76
+ set({
77
+ scale: 0.75,
78
+ opacity: 0,
79
+ onRest: () => {
80
+ setOpen(false);
81
+ },
82
+ });
83
+ }
84
+ };
85
+
86
+ return (
87
+ <Menu onChange={handleChange} open={open}>
88
+ <MenuButton>Click me</MenuButton>
89
+ <MenuPopover>
90
+ <MenuList
91
+ as={animated.ul}
92
+ style={{
93
+ opacity: (opacity as unknown) as number,
94
+ transform: (scale.to(
95
+ (x) => `scale(${x}, ${x * x})`
96
+ ) as unknown) as string,
97
+ }}
98
+ >
99
+ <MenuItem>Dog</MenuItem>
100
+ <MenuItem>Lion</MenuItem>
101
+ <MenuItem>Tiger</MenuItem>
102
+ <MenuItem disabled>Cat</MenuItem>
103
+ <hr />
104
+ <MenuItem>Bear</MenuItem>
105
+ </MenuList>
106
+ </MenuPopover>
107
+ </Menu>
108
+ );
109
+ };
110
+
111
+ const MenuWithMultipleButtons = () => {
112
+ return (
113
+ <Menu>
114
+ <div style={{ display: 'flex', gap: 32 }}>
115
+ <MenuButton>
116
+ <span>Click me</span>
117
+ </MenuButton>
118
+ <MenuButton>
119
+ <span>Click me</span>
120
+ </MenuButton>
121
+ <MenuButton>
122
+ <span>Click me</span>
123
+ </MenuButton>
124
+ <MenuButton>
125
+ <span>Click me</span>
126
+ </MenuButton>
127
+ </div>
128
+ <MenuPopover>
129
+ <MenuList>
130
+ <MenuItem>Dog</MenuItem>
131
+ <MenuItem>Lion</MenuItem>
132
+ <MenuItem>Tiger</MenuItem>
133
+ <MenuItem disabled>Cat</MenuItem>
134
+ <hr />
135
+ <MenuItem>Bear</MenuItem>
136
+ </MenuList>
137
+ </MenuPopover>
138
+ </Menu>
139
+ );
140
+ };
141
+
142
+ stories.add('Controlled', () => (
143
+ <Wrapper>
144
+ <MenuControlled />
145
+ </Wrapper>
146
+ ));
147
+
148
+ stories.add('With animation, controlled', () => (
149
+ <Wrapper>
150
+ <MenuControlledWithAnimation />
151
+ </Wrapper>
152
+ ));
153
+
154
+ stories.add('With multiple buttons', () => (
155
+ <Wrapper>
156
+ <MenuWithMultipleButtons />
157
+ </Wrapper>
158
+ ));
@@ -0,0 +1,60 @@
1
+ import { forwardRef, useRef, Fragment } from 'react';
2
+ import type * as React from 'react';
3
+ import { MenuProvider, MenuContextProps } from './context';
4
+ import { useId } from '../hooks/useId';
5
+ import { useControlledState } from '../hooks';
6
+
7
+ export interface MenuProps {
8
+ as?: React.ElementType<any>;
9
+ innerAs?: React.ElementType<any>;
10
+ children?: React.ReactNode;
11
+ onChange?: (
12
+ e:
13
+ | React.KeyboardEvent<HTMLElement>
14
+ | React.MouseEvent<HTMLElement>
15
+ | React.PointerEvent<HTMLElement>,
16
+ isOpen: boolean
17
+ ) => void;
18
+ open?: boolean;
19
+ defaultOpen?: boolean;
20
+ }
21
+
22
+ export const Menu = forwardRef<HTMLDivElement, MenuProps>(function Menu(
23
+ props,
24
+ forwardedRef
25
+ ) {
26
+ const {
27
+ as: Comp = Fragment,
28
+ open: openProp,
29
+ defaultOpen = false,
30
+ onChange: onChangeProp,
31
+ ...otherProps
32
+ } = props;
33
+ const menuListIdRef = useRef<string>();
34
+ const openWithArrowKeyRef = useRef<'ArrowUp' | 'ArrowDown' | null>(null);
35
+ const buttonRef = useRef<HTMLButtonElement>(null);
36
+ const [open, onChange] = useControlledState(
37
+ openProp,
38
+ onChangeProp,
39
+ defaultOpen,
40
+ (setState) => (e, isOpen) => {
41
+ setState(isOpen);
42
+ }
43
+ );
44
+
45
+ menuListIdRef.current = useId();
46
+
47
+ const value: MenuContextProps = {
48
+ menuListIdRef,
49
+ openWithArrowKeyRef,
50
+ open,
51
+ onChange,
52
+ buttonRef,
53
+ };
54
+
55
+ return (
56
+ <MenuProvider value={value}>
57
+ <Comp ref={forwardedRef} {...otherProps} />
58
+ </MenuProvider>
59
+ );
60
+ });
@@ -0,0 +1,83 @@
1
+ import { forwardRef } from 'react';
2
+ import type * as React from 'react';
3
+ import { useMenuContext } from './context';
4
+ import { wrapEvent } from '../utils/wrapEvent';
5
+ import { useId } from '../hooks';
6
+
7
+ export type MenuButtonProps = React.ButtonHTMLAttributes<HTMLButtonElement> & {
8
+ as?: React.ElementType<any>;
9
+ innerAs?: React.ElementType<any>;
10
+ onClick?: (
11
+ e:
12
+ | React.MouseEvent<HTMLButtonElement>
13
+ | React.KeyboardEvent<HTMLButtonElement>
14
+ ) => void;
15
+ };
16
+
17
+ export const MenuButton = forwardRef<HTMLButtonElement, MenuButtonProps>(
18
+ function MenuButton(props, forwardedRef) {
19
+ const {
20
+ as: Comp = 'button',
21
+ id: preferredId,
22
+ onClick,
23
+ onKeyDown,
24
+ disabled,
25
+ ...otherProps
26
+ } = props;
27
+ const {
28
+ menuListIdRef,
29
+ openWithArrowKeyRef,
30
+ open,
31
+ buttonRef,
32
+ onChange,
33
+ } = useMenuContext();
34
+
35
+ const buttonId = useId(preferredId);
36
+
37
+ const handleKeyDown = (e: React.KeyboardEvent<HTMLButtonElement>) => {
38
+ if (disabled) {
39
+ return;
40
+ }
41
+ buttonRef.current = e.currentTarget;
42
+
43
+ const isArrowKey = () => ['ArrowUp', 'ArrowDown'].includes(e.key);
44
+ const isEnterKey = () => [' ', 'Enter'].includes(e.key);
45
+
46
+ const openedWithArrow = isArrowKey();
47
+ if (openedWithArrow || isEnterKey()) {
48
+ if (openedWithArrow) {
49
+ // Used to make it open at the end or begining of the list
50
+ openWithArrowKeyRef.current = e.key;
51
+ }
52
+ onChange && onChange(e, true);
53
+ e.preventDefault();
54
+ }
55
+ };
56
+
57
+ const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {
58
+ if (disabled) {
59
+ return;
60
+ }
61
+ buttonRef.current = e.currentTarget;
62
+
63
+ onChange && onChange(e, !open);
64
+ };
65
+
66
+ return (
67
+ <Comp
68
+ ref={forwardedRef}
69
+ id={buttonId}
70
+ role="button"
71
+ type="button"
72
+ aria-haspopup={true}
73
+ aria-controls={menuListIdRef.current}
74
+ aria-expanded={open ? true : undefined}
75
+ data-menu-button=""
76
+ onClick={wrapEvent(onClick, handleClick)}
77
+ onKeyDown={wrapEvent(onKeyDown, handleKeyDown)}
78
+ disabled={disabled}
79
+ {...otherProps}
80
+ />
81
+ );
82
+ }
83
+ );
@@ -0,0 +1,83 @@
1
+ import { forwardRef, useRef } from 'react';
2
+ import type * as React from 'react';
3
+ import { useMenuContext, useMenuListContext } from './context';
4
+ import { assignMultipleRefs } from '../utils/assignRef';
5
+ import { wrapEvent } from '../utils';
6
+
7
+ export interface MenuItemProps extends React.LiHTMLAttributes<HTMLLIElement> {
8
+ as?: React.ElementType<any>;
9
+ innerAs?: React.ElementType<any>;
10
+ onSelect?: (
11
+ e: React.MouseEvent<HTMLLIElement> | React.KeyboardEvent<HTMLLIElement>
12
+ ) => void;
13
+ disabled?: boolean;
14
+ }
15
+
16
+ export const MenuItem = forwardRef<any, MenuItemProps>(function MenuItem(
17
+ props,
18
+ forwardedRef
19
+ ) {
20
+ const {
21
+ as: Comp = 'li',
22
+ disabled,
23
+ onSelect,
24
+ onClick,
25
+ onKeyDown,
26
+ ...otherProps
27
+ } = props;
28
+ const { menuListIdRef, onChange, buttonRef } = useMenuContext();
29
+ const { navigationItem, onNavigate } = useMenuListContext();
30
+ const ref = useRef<HTMLLIElement>();
31
+ const itemIndex = useRef(-1);
32
+
33
+ const isActive = ref.current && ref.current === navigationItem;
34
+
35
+ const handleSelect = wrapEvent(
36
+ onSelect,
37
+ (
38
+ e: React.KeyboardEvent<HTMLLIElement> | React.MouseEvent<HTMLLIElement>
39
+ ) => {
40
+ onChange && onChange(e, false);
41
+ buttonRef.current?.focus();
42
+ e.preventDefault();
43
+ }
44
+ );
45
+
46
+ const handleClick = (e: React.MouseEvent<HTMLLIElement>) => {
47
+ if (!disabled) {
48
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
49
+ onNavigate && onNavigate(ref.current!);
50
+ handleSelect(e);
51
+ }
52
+ };
53
+
54
+ const handleKeyDown: React.KeyboardEventHandler<HTMLLIElement> = (e) => {
55
+ switch (e.key) {
56
+ case 'Enter':
57
+ case ' ':
58
+ if (!disabled) {
59
+ handleSelect(e);
60
+ }
61
+ break;
62
+ }
63
+ };
64
+
65
+ return (
66
+ <Comp
67
+ ref={assignMultipleRefs(ref, forwardedRef)}
68
+ id={
69
+ disabled ? undefined : `${menuListIdRef.current}_${itemIndex.current}`
70
+ }
71
+ data-menu-item=""
72
+ data-highlighted={isActive ? '' : undefined}
73
+ role="menuitem"
74
+ onClick={wrapEvent(onClick, handleClick)}
75
+ tabIndex={disabled ? -1 : 0}
76
+ onKeyDown={wrapEvent(onKeyDown, handleKeyDown)}
77
+ data-disabled={disabled ? '' : undefined}
78
+ aria-disabled={disabled ? '' : undefined}
79
+ disabled={disabled}
80
+ {...otherProps}
81
+ />
82
+ );
83
+ });
@@ -0,0 +1,201 @@
1
+ import {
2
+ forwardRef,
3
+ useEffect,
4
+ useRef,
5
+ useState,
6
+ useLayoutEffect,
7
+ } from 'react';
8
+ import type * as React from 'react';
9
+ import { useMenuContext, MenuListProvider } from './context';
10
+ import { assignMultipleRefs } from '../utils/assignRef';
11
+ import { useOnClickOutside } from '../hooks/useOnClickOutside';
12
+ import { useScope } from '../hooks';
13
+ import { queryScope } from './scope';
14
+ import { getCircularIndex, wrapEvent } from '../utils';
15
+
16
+ const useEnhancedEffect =
17
+ typeof window !== 'undefined' ? useLayoutEffect : useEffect;
18
+
19
+ export interface MenuListProps extends React.HTMLAttributes<HTMLUListElement> {
20
+ as?: React.ElementType<any>;
21
+ innerAs?: React.ElementType<any>;
22
+ defaultActiveItemValue?: string;
23
+ }
24
+
25
+ export const MenuList = forwardRef<HTMLUListElement, MenuListProps>(
26
+ function MenuList(props, forwardedRef) {
27
+ const {
28
+ as: Comp = 'ul',
29
+ onKeyDown,
30
+ id: preferredId,
31
+ defaultActiveItemValue,
32
+ ...otherProps
33
+ } = props;
34
+
35
+ const {
36
+ menuListIdRef,
37
+ buttonRef,
38
+ onChange,
39
+ openWithArrowKeyRef,
40
+ open,
41
+ } = useMenuContext();
42
+
43
+ const [navigationItem, setNavigationItem] = useState<
44
+ HTMLElement | undefined
45
+ >();
46
+
47
+ const [mounted, setMounted] = useState(false);
48
+
49
+ const menuListRef = useRef<HTMLUListElement>();
50
+
51
+ const scope = useScope<HTMLLIElement, HTMLUListElement>(menuListRef);
52
+
53
+ const onNavigate = (el: HTMLElement) => {
54
+ el.focus();
55
+ setNavigationItem(el);
56
+ };
57
+
58
+ menuListIdRef.current = preferredId || menuListIdRef.current;
59
+
60
+ useEnhancedEffect(() => {
61
+ if (!mounted) {
62
+ const allItems = scope.current.queryAllNodes(queryScope);
63
+ let index = getCircularIndex(
64
+ openWithArrowKeyRef.current === 'ArrowUp' ? -1 : 0,
65
+ allItems.length
66
+ );
67
+
68
+ if (defaultActiveItemValue) {
69
+ for (let i = 0; i < allItems.length; i++) {
70
+ if (allItems[i].dataset.value === defaultActiveItemValue) {
71
+ index = i;
72
+ break;
73
+ }
74
+ }
75
+ }
76
+
77
+ if (index !== null) {
78
+ onNavigate && onNavigate(allItems[index]);
79
+ }
80
+ }
81
+
82
+ openWithArrowKeyRef.current = null;
83
+ setMounted(true);
84
+ }, [
85
+ mounted,
86
+ navigationItem,
87
+ onNavigate,
88
+ openWithArrowKeyRef,
89
+ scope,
90
+ defaultActiveItemValue,
91
+ ]);
92
+
93
+ useOnClickOutside(
94
+ menuListRef,
95
+ (e) => {
96
+ if (
97
+ e.target instanceof HTMLElement &&
98
+ e.target !== buttonRef.current &&
99
+ !buttonRef.current?.contains(e.target)
100
+ ) {
101
+ onChange && onChange(e as any, false);
102
+ }
103
+ e.preventDefault();
104
+ },
105
+ true
106
+ );
107
+
108
+ function handleKeyDown(e: React.KeyboardEvent<HTMLUListElement>) {
109
+ switch (e.key) {
110
+ case 'Escape':
111
+ case 'Tab': {
112
+ onChange && onChange(e, false);
113
+ e.preventDefault(); // prevents focusing on next element, because we will be handling it
114
+ buttonRef.current?.focus();
115
+ break;
116
+ }
117
+ case 'Home':
118
+ case 'End':
119
+ case 'ArrowDown':
120
+ case 'ArrowUp':
121
+ e.preventDefault();
122
+ const allItems = scope ? scope.current.queryAllNodes(queryScope) : [];
123
+ const currentIndex = allItems.findIndex((e) => e === navigationItem);
124
+ if (allItems.length === 0) {
125
+ return;
126
+ }
127
+ let nextIndex: number = currentIndex;
128
+ switch (e.key) {
129
+ case 'ArrowUp':
130
+ nextIndex += -1;
131
+ break;
132
+ case 'ArrowDown':
133
+ nextIndex += 1;
134
+ break;
135
+ case 'Home':
136
+ nextIndex = 0;
137
+ break;
138
+ case 'End':
139
+ nextIndex = -1;
140
+ break;
141
+ }
142
+ // We already checked if allItems.length = 0 above
143
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
144
+ nextIndex = getCircularIndex(nextIndex, allItems.length)!;
145
+ onNavigate && onNavigate(allItems[nextIndex]);
146
+ break;
147
+ default: {
148
+ if (e.key.length === 1) {
149
+ // A-Z
150
+ e.preventDefault();
151
+ const allItems = scope
152
+ ? scope.current.queryAllNodes(queryScope)
153
+ : [];
154
+ const currentIndex = allItems.findIndex(
155
+ (e) => e === navigationItem
156
+ );
157
+ const firstLetter = e.key.toLowerCase();
158
+ let nextIndex = -1;
159
+ for (let i = 1; i < allItems.length; i++) {
160
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
161
+ const idx = getCircularIndex(currentIndex + i, allItems.length)!;
162
+ const dom = allItems[idx];
163
+ const domText = dom.innerText.toLowerCase();
164
+ if (domText.length > 0 && domText.charAt(0) === firstLetter) {
165
+ nextIndex = idx;
166
+ break;
167
+ }
168
+ }
169
+ if (nextIndex >= 0 && nextIndex < allItems.length) {
170
+ onNavigate && onNavigate(allItems[nextIndex]);
171
+ }
172
+ }
173
+ }
174
+ }
175
+ }
176
+
177
+ if (!open) {
178
+ return null;
179
+ }
180
+
181
+ return (
182
+ <MenuListProvider
183
+ value={{
184
+ navigationItem,
185
+ onNavigate,
186
+ }}
187
+ >
188
+ <Comp
189
+ ref={assignMultipleRefs(forwardedRef, menuListRef)}
190
+ id={menuListIdRef.current}
191
+ role="menu"
192
+ aria-labelledby={buttonRef.current?.id}
193
+ data-menu-list=""
194
+ tabIndex="-1"
195
+ onKeyDown={wrapEvent(onKeyDown, handleKeyDown)}
196
+ {...otherProps}
197
+ />
198
+ </MenuListProvider>
199
+ );
200
+ }
201
+ );
@@ -0,0 +1,25 @@
1
+ import { forwardRef } from 'react';
2
+ import type * as React from 'react';
3
+ import { Popper, PopperProps } from '../Popper';
4
+ import { useMenuContext } from './context';
5
+
6
+ export interface MenuPopoverProps extends Omit<PopperProps, 'anchorEl'> {
7
+ as?: React.ElementType<any>;
8
+ innerAs?: React.ElementType<any>;
9
+ children?: React.ReactNode;
10
+ }
11
+
12
+ export const MenuPopover = forwardRef<HTMLDivElement, MenuPopoverProps>(
13
+ function MenuPopover(props, forwardedRef) {
14
+ const { as = 'div', ...otherProps } = props;
15
+ const { buttonRef, open } = useMenuContext();
16
+
17
+ if (!open) {
18
+ return null;
19
+ }
20
+
21
+ return (
22
+ <Popper as={as} ref={forwardedRef} anchorEl={buttonRef} {...otherProps} />
23
+ );
24
+ }
25
+ );