@tpzdsp/next-toolkit 2.5.0 → 3.0.0
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/package.json +22 -13
- package/src/assets/styles/globals.css +75 -95
- package/src/assets/styles/ol.css +57 -16
- package/src/assets/styles/utils.css +80 -0
- package/src/components/Accordion/Accordion.css +85 -0
- package/src/components/Accordion/Accordion.stories.tsx +264 -0
- package/src/components/Accordion/Accordion.tsx +98 -0
- package/src/components/Alert/Alert.css +129 -0
- package/src/components/Alert/Alert.stories.tsx +291 -0
- package/src/components/Alert/Alert.tsx +76 -0
- package/src/components/BackLink/BackLink.css +12 -0
- package/src/components/BackLink/BackLink.stories.tsx +52 -0
- package/src/components/BackLink/BackLink.tsx +44 -0
- package/src/components/BackToTop/BackToTop.css +23 -0
- package/src/components/BackToTop/BackToTop.stories.tsx +41 -0
- package/src/components/BackToTop/BackToTop.tsx +48 -0
- package/src/components/Breadcrumb/Breadcrumb.css +42 -0
- package/src/components/Breadcrumb/Breadcrumb.stories.tsx +94 -0
- package/src/components/Breadcrumb/Breadcrumb.tsx +135 -0
- package/src/components/Button/Button.css +229 -0
- package/src/components/Button/Button.stories.tsx +140 -20
- package/src/components/Button/Button.tsx +53 -28
- package/src/components/Button/ButtonSkeleton.stories.tsx +50 -0
- package/src/components/Button/ButtonSkeleton.tsx +14 -0
- package/src/components/ButtonGroup/ButtonGroup.css +52 -0
- package/src/components/ButtonGroup/ButtonGroup.stories.tsx +156 -0
- package/src/components/ButtonGroup/ButtonGroup.tsx +50 -0
- package/src/components/ButtonLink/ButtonLink.css +114 -0
- package/src/components/ButtonLink/ButtonLink.stories.tsx +38 -51
- package/src/components/ButtonLink/ButtonLink.tsx +54 -23
- package/src/components/Card/Card.css +13 -0
- package/src/components/Card/Card.stories.tsx +98 -57
- package/src/components/Card/Card.tsx +4 -13
- package/src/components/CardGroup/CardGroup.css +10 -0
- package/src/components/CardGroup/CardGroup.stories.tsx +81 -0
- package/src/components/CardGroup/CardGroup.tsx +9 -0
- package/src/components/Checkbox/Checkbox.css +49 -0
- package/src/components/Checkbox/Checkbox.stories.tsx +86 -0
- package/src/components/Checkbox/Checkbox.tsx +32 -0
- package/src/components/Chip/Chip.css +54 -0
- package/src/components/Chip/Chip.stories.tsx +94 -0
- package/src/components/Chip/Chip.tsx +34 -0
- package/src/components/CookieBanner/CookieBanner.css +19 -0
- package/src/components/CookieBanner/CookieBanner.stories.tsx +52 -0
- package/src/components/CookieBanner/CookieBanner.tsx +55 -0
- package/src/components/CopyButton/CopyButton.stories.tsx +28 -0
- package/src/components/CopyButton/CopyButton.tsx +65 -0
- package/src/components/DateInput/DateInput.css +13 -0
- package/src/components/DateInput/DateInput.stories.tsx +131 -0
- package/src/components/DateInput/DateInput.test.tsx +21 -0
- package/src/components/DateInput/DateInput.tsx +98 -0
- package/src/components/Details/Details.css +28 -0
- package/src/components/Details/Details.stories.tsx +48 -0
- package/src/components/Details/Details.tsx +41 -0
- package/src/components/Dropdown/Dropdown.css +81 -0
- package/src/components/Dropdown/Dropdown.stories.tsx +131 -0
- package/src/components/Dropdown/Dropdown.tsx +69 -0
- package/src/components/ErrorBoundary/ErrorBoundary.stories.tsx +4 -39
- package/src/components/ErrorBoundary/ErrorBoundary.tsx +7 -4
- package/src/components/ErrorBoundary/ErrorFallback.tsx +32 -66
- package/src/components/ErrorModal/ErrorModal.css +32 -0
- package/src/components/ErrorModal/ErrorModal.stories.tsx +101 -0
- package/src/components/ErrorModal/ErrorModal.tsx +120 -0
- package/src/components/ErrorText/ErrorText.css +22 -0
- package/src/components/ErrorText/ErrorText.stories.tsx +42 -27
- package/src/components/ErrorText/ErrorText.tsx +10 -8
- package/src/components/ExternalLink/ExternalLink.css +12 -0
- package/src/components/ExternalLink/ExternalLink.stories.tsx +43 -0
- package/src/components/ExternalLink/ExternalLink.tsx +43 -0
- package/src/components/Fieldset/Fieldset.css +103 -0
- package/src/components/Fieldset/Fieldset.stories.tsx +304 -0
- package/src/components/Fieldset/Fieldset.tsx +38 -0
- package/src/components/FileUpload/FileUpload.css +85 -0
- package/src/components/FileUpload/FileUpload.stories.tsx +106 -0
- package/src/components/FileUpload/FileUpload.tsx +99 -0
- package/src/components/{layout/footer → Footer}/Copyright.tsx +4 -2
- package/src/components/Footer/Footer.stories.tsx +24 -0
- package/src/components/Footer/Footer.tsx +41 -0
- package/src/components/Footer/Licence.tsx +18 -0
- package/src/components/{layout/footer → Footer}/MetaLinks.tsx +8 -3
- package/src/components/Footer/Small/SmallFooter.tsx +43 -42
- package/src/components/Form/FormGroup.css +18 -0
- package/src/components/Form/FormGroup.stories.tsx +87 -0
- package/src/components/Form/FormGroup.tsx +16 -0
- package/src/components/{layout/header → Header}/Header.stories.tsx +26 -12
- package/src/components/Header/Header.test.tsx +87 -0
- package/src/components/Header/Header.tsx +303 -0
- package/src/components/Header/HeaderAppContent.tsx +120 -0
- package/src/components/Heading/Heading.css +40 -0
- package/src/components/Heading/Heading.stories.tsx +15 -5
- package/src/components/Heading/Heading.tsx +21 -44
- package/src/components/Heading/HeadingSkeleton.stories.tsx +30 -0
- package/src/components/Heading/HeadingSkeleton.tsx +14 -0
- package/src/components/Hero/Hero.css +19 -0
- package/src/components/Hero/Hero.stories.tsx +41 -0
- package/src/components/Hero/Hero.tsx +14 -0
- package/src/components/HintText/HintText.css +5 -0
- package/src/components/HintText/HintText.stories.tsx +29 -0
- package/src/components/HintText/HintText.tsx +13 -0
- package/src/components/InfoBox/InfoBox.css +38 -0
- package/src/components/InfoBox/InfoBox.stories.tsx +51 -403
- package/src/components/InfoBox/InfoBox.tsx +31 -158
- package/src/components/Input/Input.css +77 -0
- package/src/components/Input/Input.stories.tsx +123 -0
- package/src/components/Input/Input.tsx +74 -0
- package/src/components/InputGroup/InputGroup.css +140 -0
- package/src/components/InputGroup/InputGroup.stories.tsx +125 -0
- package/src/components/InputGroup/InputGroup.tsx +47 -0
- package/src/components/InsetText/InsetText.css +5 -0
- package/src/components/InsetText/InsetText.stories.tsx +30 -0
- package/src/components/InsetText/InsetText.tsx +9 -0
- package/src/components/Keyboard/Keyboard.css +9 -0
- package/src/components/Keyboard/Keyboard.stories.tsx +63 -0
- package/src/components/Keyboard/Keyboard.tsx +30 -0
- package/src/components/Label/Label.css +27 -0
- package/src/components/Label/Label.stories.tsx +40 -0
- package/src/components/Label/Label.tsx +17 -0
- package/src/components/LayerSwitcher/LayerSwitcher.css +59 -0
- package/src/components/LayerSwitcher/LayerSwitcher.stories.tsx +112 -0
- package/src/components/LayerSwitcher/LayerSwitcher.tsx +61 -0
- package/src/components/Layout/401/401.stories.tsx +23 -0
- package/src/components/Layout/401/401.tsx +1 -0
- package/src/components/Layout/403/403.stories.tsx +23 -0
- package/src/components/Layout/403/403.tsx +21 -0
- package/src/components/Layout/404/404.stories.tsx +23 -0
- package/src/components/Layout/404/404.tsx +28 -0
- package/src/components/Layout/500/500.stories.tsx +56 -0
- package/src/components/Layout/500/500.tsx +63 -0
- package/src/components/Layout/ButtonGroup/ButtonGroup.css +52 -0
- package/src/components/Layout/ButtonGroup/ButtonGroup.stories.tsx +156 -0
- package/src/components/Layout/ButtonGroup/ButtonGroup.tsx +50 -0
- package/src/components/Layout/HeadingGroup/HeadingGroup.css +21 -0
- package/src/components/Layout/HeadingGroup/HeadingGroup.stories.tsx +84 -0
- package/src/components/Layout/HeadingGroup/HeadingGroup.tsx +9 -0
- package/src/components/Layout/MainWrapper/MainWrapper.css +12 -0
- package/src/components/Layout/MainWrapper/MainWrapper.stories.tsx +36 -0
- package/src/components/Layout/MainWrapper/MainWrapper.tsx +14 -0
- package/src/components/Layout/PageShell/PageShell.css +42 -0
- package/src/components/Layout/PageShell/PageShell.stories.tsx +108 -0
- package/src/components/Layout/PageShell/PageShell.tsx +31 -0
- package/src/components/Layout/ParagraphGroup/ParagraphGroup.css +5 -0
- package/src/components/Layout/ParagraphGroup/ParagraphGroup.stories.tsx +34 -0
- package/src/components/Layout/ParagraphGroup/ParagraphGroup.tsx +9 -0
- package/src/components/Layout/Stack/Stack.css +29 -0
- package/src/components/Layout/Stack/Stack.stories.tsx +83 -0
- package/src/components/Layout/Stack/Stack.tsx +19 -0
- package/src/components/Layout/WidthContainer/WidthContainer.css +17 -0
- package/src/components/Layout/WidthContainer/WidthContainer.stories.tsx +59 -0
- package/src/components/Layout/WidthContainer/WidthContainer.tsx +13 -0
- package/src/components/Legend/Legend.css +95 -0
- package/src/components/Legend/Legend.stories.tsx +81 -0
- package/src/components/Legend/Legend.tsx +32 -0
- package/src/components/Legend/LegendDot.tsx +15 -0
- package/src/components/Legend/LegendPanel.stories.tsx +101 -0
- package/src/components/Legend/LegendPanel.tsx +68 -0
- package/src/components/Legend/LegendSkeleton.stories.tsx +87 -0
- package/src/components/Legend/LegendSkeleton.tsx +73 -0
- package/src/components/LegendSelect/LegendSelect.css +8 -0
- package/src/components/LegendSelect/LegendSelect.stories.tsx +80 -0
- package/src/components/LegendSelect/LegendSelect.tsx +26 -0
- package/src/components/Link/Link.css +98 -0
- package/src/components/Link/Link.stories.tsx +70 -0
- package/src/components/Link/Link.tsx +52 -0
- package/src/components/LinkButton/LinkButton.css +67 -0
- package/src/components/LinkButton/LinkButton.stories.tsx +90 -42
- package/src/components/LinkButton/LinkButton.tsx +50 -63
- package/src/components/LinkList/LinkList.css +28 -0
- package/src/components/LinkList/LinkList.stories.tsx +46 -0
- package/src/components/LinkList/LinkList.tsx +23 -0
- package/src/components/List/Li.tsx +9 -0
- package/src/components/List/List.css +9 -0
- package/src/components/List/OrderedList.stories.tsx +36 -0
- package/src/components/List/OrderedList.tsx +9 -0
- package/src/components/List/UnorderedList.stories.tsx +36 -0
- package/src/components/List/UnorderedList.tsx +9 -0
- package/src/components/LoadingBox/LoadingBox.css +27 -0
- package/src/components/LoadingBox/LoadingBox.stories.tsx +66 -0
- package/src/components/LoadingBox/LoadingBox.tsx +40 -0
- package/src/components/Modal/Modal.css +114 -0
- package/src/components/Modal/Modal.stories.tsx +41 -160
- package/src/components/Modal/Modal.tsx +132 -75
- package/src/components/NoScriptBanner/NoScriptBanner.css +17 -0
- package/src/components/NoScriptBanner/NoScriptBanner.stories.tsx +46 -0
- package/src/components/NoScriptBanner/NoScriptBanner.tsx +26 -0
- package/src/components/Paragraph/Paragraph.css +22 -0
- package/src/components/Paragraph/Paragraph.stories.tsx +1 -42
- package/src/components/Paragraph/Paragraph.tsx +4 -3
- package/src/components/Paragraph/ParagraphSkeleton.stories.tsx +44 -0
- package/src/components/Paragraph/ParagraphSkeleton.tsx +33 -0
- package/src/components/PasswordInput/PasswordInput.css +3 -0
- package/src/components/PasswordInput/PasswordInput.stories.tsx +47 -0
- package/src/components/PasswordInput/PasswordInput.tsx +45 -0
- package/src/components/PhaseBanner/PhaseBanner.css +12 -0
- package/src/components/PhaseBanner/PhaseBanner.stories.tsx +61 -0
- package/src/components/PhaseBanner/PhaseBanner.tsx +17 -0
- package/src/components/Popover/Popover.css +43 -0
- package/src/components/Popover/Popover.stories.tsx +102 -0
- package/src/components/Popover/Popover.tsx +40 -0
- package/src/components/Radio/Radio.css +69 -0
- package/src/components/Radio/Radio.stories.tsx +169 -0
- package/src/components/Radio/Radio.tsx +56 -0
- package/src/components/Resizable/Resizable.css +86 -0
- package/src/components/Resizable/Resizable.stories.tsx +123 -0
- package/src/components/Resizable/Resizable.tsx +86 -0
- package/src/components/ScrollArea/ScrollArea.css +9 -0
- package/src/components/ScrollArea/ScrollArea.stories.tsx +42 -0
- package/src/components/ScrollArea/ScrollArea.tsx +11 -0
- package/src/components/SearchBox/SearchBox.stories.tsx +88 -0
- package/src/components/SearchBox/SearchBox.tsx +36 -0
- package/src/components/Select/AsyncSelect.stories.tsx +84 -0
- package/src/components/Select/AsyncSelect.tsx +41 -0
- package/src/components/Select/Select.css +194 -0
- package/src/components/Select/Select.stories.tsx +162 -0
- package/src/components/Select/Select.test.tsx +36 -0
- package/src/components/Select/Select.tsx +291 -0
- package/src/components/Select/SelectSkeleton.stories.tsx +51 -0
- package/src/components/Select/SelectSkeleton.tsx +23 -0
- package/src/components/Separator/Separator.css +41 -0
- package/src/components/Separator/Separator.stories.tsx +63 -0
- package/src/components/Separator/Separator.tsx +42 -0
- package/src/components/Sidebar/ResizableSidebar.tsx +118 -0
- package/src/components/Sidebar/Sidebar.css +203 -0
- package/src/components/Sidebar/Sidebar.stories.tsx +438 -0
- package/src/components/Sidebar/Sidebar.tsx +225 -0
- package/src/components/Skeleton/Skeleton.css +16 -0
- package/src/components/Skeleton/Skeleton.stories.tsx +39 -0
- package/src/components/Skeleton/Skeleton.tsx +12 -0
- package/src/components/SkipLink/SkipLink.css +16 -0
- package/src/components/SkipLink/SkipLink.stories.tsx +40 -0
- package/src/components/SkipLink/SkipLink.tsx +15 -0
- package/src/components/Spinner/Spinner.css +30 -0
- package/src/components/Spinner/Spinner.stories.tsx +36 -0
- package/src/components/Spinner/Spinner.tsx +45 -0
- package/src/components/StartButton/StartButton.css +13 -0
- package/src/components/StartButton/StartButton.stories.tsx +68 -0
- package/src/components/StartButton/StartButton.tsx +50 -0
- package/src/components/SubmitButton/SubmitButton.stories.tsx +31 -0
- package/src/components/SubmitButton/SubmitButton.tsx +14 -0
- package/src/components/SummaryList/SummaryList.css +71 -0
- package/src/components/SummaryList/SummaryList.stories.tsx +211 -0
- package/src/components/SummaryList/SummaryList.tsx +41 -0
- package/src/components/SummaryList/SummaryListSkeleton.stories.tsx +67 -0
- package/src/components/SummaryList/SummaryListSkeleton.tsx +40 -0
- package/src/components/Tabs/Tabs.css +84 -0
- package/src/components/Tabs/Tabs.stories.tsx +133 -0
- package/src/components/Tabs/Tabs.tsx +66 -0
- package/src/components/Tag/Tag.css +8 -0
- package/src/components/Tag/Tag.stories.tsx +96 -0
- package/src/components/Tag/Tag.tsx +34 -0
- package/src/components/Textarea/Textarea.css +38 -0
- package/src/components/Textarea/Textarea.stories.tsx +131 -0
- package/src/components/Textarea/Textarea.tsx +50 -0
- package/src/components/TextareaCount/TextareaCount.css +17 -0
- package/src/components/TextareaCount/TextareaCount.stories.tsx +131 -0
- package/src/components/TextareaCount/TextareaCount.tsx +108 -0
- package/src/components/Tooltip/Tooltip.css +43 -0
- package/src/components/Tooltip/Tooltip.stories.tsx +110 -0
- package/src/components/Tooltip/Tooltip.tsx +53 -0
- package/src/components/WarningText/WarningText.css +20 -0
- package/src/components/WarningText/WarningText.stories.tsx +29 -0
- package/src/components/WarningText/WarningText.tsx +17 -0
- package/src/components/index.ts +342 -61
- package/src/errors/errorDisplay.ts +70 -0
- package/src/errors/index.ts +1 -0
- package/src/http/constants.ts +1 -0
- package/src/http/fetch.ts +7 -3
- package/src/map/LayerSwitcherPanel.tsx +2 -173
- package/src/map/LegendShim.tsx +2 -0
- package/src/map/MapComponent.tsx +7 -17
- package/src/map/MapControlsOverlay.tsx +68 -175
- package/src/map/Popup.tsx +71 -24
- package/src/map/geocoder/Geocoder.test.tsx +46 -3
- package/src/map/geocoder/Geocoder.tsx +155 -278
- package/src/map/index.ts +1 -1
- package/src/types/utils.ts +0 -10
- package/src/utils/components.ts +96 -0
- package/src/utils/index.ts +1 -0
- package/src/utils/utils.ts +0 -12
- package/src/assets/images/defra-logo.svg +0 -51
- package/src/assets/images/ea-logo.svg +0 -58
- package/src/assets/images/ogl.svg +0 -1
- package/src/components/Button/Button.test.tsx +0 -53
- package/src/components/ButtonLink/ButtonLink.test.tsx +0 -154
- package/src/components/Card/Card.test.tsx +0 -51
- package/src/components/ErrorBoundary/ErrorBoundary.test.tsx +0 -75
- package/src/components/ErrorBoundary/ErrorFallback.test.tsx +0 -107
- package/src/components/ErrorText/ErrorText.test.tsx +0 -33
- package/src/components/Heading/Heading.test.tsx +0 -22
- package/src/components/Hint/Hint.stories.tsx +0 -58
- package/src/components/Hint/Hint.test.tsx +0 -33
- package/src/components/Hint/Hint.tsx +0 -12
- package/src/components/InfoBox/InfoBox.test.tsx +0 -330
- package/src/components/InfoBox/types.ts +0 -6
- package/src/components/LinkButton/LinkButton.test.tsx +0 -177
- package/src/components/Modal/Modal.test.tsx +0 -246
- package/src/components/NotificationBanner/NotificationBanner.stories.tsx +0 -45
- package/src/components/NotificationBanner/NotificationBanner.test.tsx +0 -58
- package/src/components/NotificationBanner/NotificationBanner.tsx +0 -45
- package/src/components/Paragraph/Paragraph.test.tsx +0 -10
- package/src/components/SlidingPanel/SlidingPanel.stories.tsx +0 -129
- package/src/components/SlidingPanel/SlidingPanel.test.tsx +0 -83
- package/src/components/SlidingPanel/SlidingPanel.tsx +0 -258
- package/src/components/accordion/Accordion.stories.tsx +0 -234
- package/src/components/accordion/Accordion.test.tsx +0 -192
- package/src/components/accordion/Accordion.tsx +0 -73
- package/src/components/backToTop/BackToTop.stories.tsx +0 -407
- package/src/components/backToTop/BackToTop.test.tsx +0 -57
- package/src/components/backToTop/BackToTop.tsx +0 -134
- package/src/components/chip/Chip.stories.tsx +0 -61
- package/src/components/chip/Chip.test.tsx +0 -31
- package/src/components/chip/Chip.tsx +0 -23
- package/src/components/container/Container.tsx +0 -42
- package/src/components/cookieBanner/CookieBanner.stories.tsx +0 -257
- package/src/components/cookieBanner/CookieBanner.test.tsx +0 -68
- package/src/components/cookieBanner/CookieBanner.tsx +0 -73
- package/src/components/divider/RuleDivider.stories.tsx +0 -254
- package/src/components/divider/RuleDivider.test.tsx +0 -164
- package/src/components/divider/RuleDivider.tsx +0 -23
- package/src/components/dropdown/DropdownMenu.test.tsx +0 -211
- package/src/components/dropdown/DropdownMenu.tsx +0 -108
- package/src/components/dropdown/useDropdownMenu.ts +0 -249
- package/src/components/form/Input.stories.tsx +0 -435
- package/src/components/form/Input.test.tsx +0 -206
- package/src/components/form/Input.tsx +0 -23
- package/src/components/form/TextArea.stories.tsx +0 -464
- package/src/components/form/TextArea.test.tsx +0 -232
- package/src/components/form/TextArea.tsx +0 -23
- package/src/components/layout/footer/Footer.tsx +0 -26
- package/src/components/layout/footer/Licence.tsx +0 -19
- package/src/components/layout/header/Header.test.tsx +0 -36
- package/src/components/layout/header/Header.tsx +0 -90
- package/src/components/layout/header/HeaderAuthClient.test.tsx +0 -45
- package/src/components/layout/header/HeaderAuthClient.tsx +0 -40
- package/src/components/layout/header/HeaderNavClient.test.tsx +0 -44
- package/src/components/layout/header/HeaderNavClient.tsx +0 -67
- package/src/components/link/ExternalLink.test.tsx +0 -102
- package/src/components/link/ExternalLink.tsx +0 -29
- package/src/components/link/Link.tsx +0 -26
- package/src/components/select/Select.stories.tsx +0 -336
- package/src/components/select/Select.test.tsx +0 -474
- package/src/components/select/Select.tsx +0 -125
- package/src/components/select/SelectSkeleton.stories.tsx +0 -194
- package/src/components/select/SelectSkeleton.test.tsx +0 -104
- package/src/components/select/SelectSkeleton.tsx +0 -23
- package/src/components/select/common.ts +0 -3
- package/src/components/select/index.ts +0 -18
- package/src/components/skipLink/SkipLink.stories.tsx +0 -346
- package/src/components/skipLink/SkipLink.test.tsx +0 -22
- package/src/components/skipLink/SkipLink.tsx +0 -49
- package/src/map/LegendPanel.tsx +0 -85
- /package/src/{components → assets}/images/DefraLogo.tsx +0 -0
- /package/src/{components → assets}/images/EaLogo.tsx +0 -0
- /package/src/{components → assets}/images/OglLogo.tsx +0 -0
- /package/src/components/{googleAnalytics → GoogleAnalytics}/GlobalVars.tsx +0 -0
- /package/src/components/{googleAnalytics → GoogleAnalytics}/GoogleAnalytics.tsx +0 -0
|
@@ -1,211 +0,0 @@
|
|
|
1
|
-
import { DropdownMenu, type DropdownMenuItem, type DrowndownMenuButton } from './DropdownMenu';
|
|
2
|
-
import { render, screen, userEvent, waitFor } from '../../test/renderers';
|
|
3
|
-
|
|
4
|
-
type Item = { label: string };
|
|
5
|
-
|
|
6
|
-
const ITEMS: Item[] = [{ label: 'A' }, { label: 'B' }, { label: 'C' }, { label: 'D' }];
|
|
7
|
-
|
|
8
|
-
const CustomButton = ({ ...props }: DrowndownMenuButton) => {
|
|
9
|
-
return <button {...props}>Some Custom Text</button>;
|
|
10
|
-
};
|
|
11
|
-
|
|
12
|
-
const CustomItem = ({ label, ...props }: DropdownMenuItem<Item>) => {
|
|
13
|
-
return (
|
|
14
|
-
<a {...props} role="link">
|
|
15
|
-
{label}
|
|
16
|
-
</a>
|
|
17
|
-
);
|
|
18
|
-
};
|
|
19
|
-
|
|
20
|
-
describe('DropdownMenu Component', () => {
|
|
21
|
-
it('should render with no menu when unopened', () => {
|
|
22
|
-
render(<DropdownMenu items={ITEMS}></DropdownMenu>);
|
|
23
|
-
|
|
24
|
-
expect(screen.getByRole('button', { name: /menu/i })).toBeInTheDocument();
|
|
25
|
-
expect(() => screen.getByRole('menu')).toThrow();
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
it('should render with menu when opened', async () => {
|
|
29
|
-
render(<DropdownMenu items={ITEMS}></DropdownMenu>);
|
|
30
|
-
|
|
31
|
-
const user = userEvent.setup();
|
|
32
|
-
|
|
33
|
-
await user.click(screen.getByRole('button', { name: /menu/i }));
|
|
34
|
-
|
|
35
|
-
for (const item of ITEMS) {
|
|
36
|
-
expect(await screen.findByRole('menuitem', { name: item.label })).toBeInTheDocument();
|
|
37
|
-
}
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
it('should render with a custom item component', async () => {
|
|
41
|
-
render(<DropdownMenu items={ITEMS} itemRenderer={CustomItem}></DropdownMenu>);
|
|
42
|
-
|
|
43
|
-
const user = userEvent.setup();
|
|
44
|
-
|
|
45
|
-
await user.click(screen.getByRole('button', { name: /menu/i }));
|
|
46
|
-
|
|
47
|
-
for (const item of ITEMS) {
|
|
48
|
-
expect(await screen.findByRole('link', { name: item.label })).toBeInTheDocument();
|
|
49
|
-
}
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
it('should render with a custom button component', () => {
|
|
53
|
-
render(<DropdownMenu items={ITEMS} buttonRenderer={CustomButton}></DropdownMenu>);
|
|
54
|
-
|
|
55
|
-
expect(screen.getByRole('button', { name: /some custom text/i })).toBeInTheDocument();
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
it('should open the menu when button is clicked', async () => {
|
|
59
|
-
render(<DropdownMenu items={ITEMS} />);
|
|
60
|
-
|
|
61
|
-
const user = userEvent.setup();
|
|
62
|
-
|
|
63
|
-
const button = screen.getByRole('button', { name: /menu/i });
|
|
64
|
-
|
|
65
|
-
await user.click(button);
|
|
66
|
-
|
|
67
|
-
expect(await screen.findByRole('menu')).toBeInTheDocument();
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
it('should open the menu when Space or Enter is pressed', async () => {
|
|
71
|
-
render(<DropdownMenu items={ITEMS} />);
|
|
72
|
-
|
|
73
|
-
const user = userEvent.setup();
|
|
74
|
-
|
|
75
|
-
const button = screen.getByRole('button', { name: /menu/i });
|
|
76
|
-
|
|
77
|
-
button.focus();
|
|
78
|
-
|
|
79
|
-
await user.keyboard(' ');
|
|
80
|
-
|
|
81
|
-
expect(await screen.findByRole('menu')).toBeInTheDocument();
|
|
82
|
-
|
|
83
|
-
// pressing another key will close the menu but assuming the menu wasnt open already this would open it anyway
|
|
84
|
-
await user.keyboard('{Enter}');
|
|
85
|
-
|
|
86
|
-
await waitFor(() => expect(screen.queryByRole('menu')).not.toBeInTheDocument());
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
it('should close when Escape is pressed', async () => {
|
|
90
|
-
render(<DropdownMenu items={ITEMS} />);
|
|
91
|
-
|
|
92
|
-
const user = userEvent.setup();
|
|
93
|
-
|
|
94
|
-
const button = screen.getByRole('button', { name: /menu/i });
|
|
95
|
-
|
|
96
|
-
await user.click(button);
|
|
97
|
-
|
|
98
|
-
expect(await screen.findByRole('menu')).toBeInTheDocument();
|
|
99
|
-
|
|
100
|
-
await user.keyboard('{Escape}');
|
|
101
|
-
|
|
102
|
-
await waitFor(() => expect(screen.queryByRole('menu')).not.toBeInTheDocument());
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
it('should close when clicking outside', async () => {
|
|
106
|
-
render(
|
|
107
|
-
<>
|
|
108
|
-
<DropdownMenu items={ITEMS} />
|
|
109
|
-
|
|
110
|
-
<button>Outside</button>
|
|
111
|
-
</>,
|
|
112
|
-
);
|
|
113
|
-
|
|
114
|
-
const user = userEvent.setup();
|
|
115
|
-
|
|
116
|
-
const button = screen.getByRole('button', { name: /menu/i });
|
|
117
|
-
|
|
118
|
-
await user.click(button);
|
|
119
|
-
|
|
120
|
-
expect(await screen.findByRole('menu')).toBeInTheDocument();
|
|
121
|
-
|
|
122
|
-
// click away to another element, which should close the menu
|
|
123
|
-
await user.click(screen.getByRole('button', { name: /outside/i }));
|
|
124
|
-
|
|
125
|
-
await waitFor(() => expect(screen.queryByRole('menu')).not.toBeInTheDocument());
|
|
126
|
-
});
|
|
127
|
-
|
|
128
|
-
it('should close when focus moves outside (blur)', async () => {
|
|
129
|
-
render(
|
|
130
|
-
<>
|
|
131
|
-
<DropdownMenu items={ITEMS} />
|
|
132
|
-
|
|
133
|
-
<button>Outside</button>
|
|
134
|
-
</>,
|
|
135
|
-
);
|
|
136
|
-
|
|
137
|
-
const user = userEvent.setup();
|
|
138
|
-
|
|
139
|
-
const button = screen.getByRole('button', { name: /menu/i });
|
|
140
|
-
|
|
141
|
-
await user.click(button);
|
|
142
|
-
|
|
143
|
-
expect(await screen.findByRole('menu')).toBeInTheDocument();
|
|
144
|
-
|
|
145
|
-
// move focus away from the menu, which should close it
|
|
146
|
-
await user.tab();
|
|
147
|
-
|
|
148
|
-
await waitFor(() => expect(screen.queryByRole('menu')).not.toBeInTheDocument());
|
|
149
|
-
});
|
|
150
|
-
|
|
151
|
-
it('should navigate items with ArrowUp/ArrowDown', async () => {
|
|
152
|
-
render(<DropdownMenu items={ITEMS} />);
|
|
153
|
-
|
|
154
|
-
const user = userEvent.setup();
|
|
155
|
-
|
|
156
|
-
const button = screen.getByRole('button', { name: /menu/i });
|
|
157
|
-
|
|
158
|
-
await user.click(button);
|
|
159
|
-
|
|
160
|
-
const items = screen.getAllByRole('menuitem');
|
|
161
|
-
|
|
162
|
-
// first element is focussed when the menu is open
|
|
163
|
-
expect(items[0]).toHaveFocus();
|
|
164
|
-
|
|
165
|
-
await user.keyboard('{ArrowDown}');
|
|
166
|
-
expect(items[1]).toHaveFocus();
|
|
167
|
-
|
|
168
|
-
await user.keyboard('{ArrowUp}');
|
|
169
|
-
expect(items[0]).toHaveFocus();
|
|
170
|
-
});
|
|
171
|
-
|
|
172
|
-
it('should select an item with Enter or Space', async () => {
|
|
173
|
-
render(<DropdownMenu items={ITEMS} />);
|
|
174
|
-
|
|
175
|
-
const user = userEvent.setup();
|
|
176
|
-
|
|
177
|
-
const button = screen.getByRole('button', { name: /menu/i });
|
|
178
|
-
|
|
179
|
-
await user.click(button);
|
|
180
|
-
const items = screen.getAllByRole('menuitem');
|
|
181
|
-
|
|
182
|
-
await user.keyboard('{ArrowDown}');
|
|
183
|
-
await user.keyboard('{Enter}');
|
|
184
|
-
|
|
185
|
-
await waitFor(() => expect(screen.queryByRole('menu')).not.toBeInTheDocument());
|
|
186
|
-
|
|
187
|
-
await user.click(button);
|
|
188
|
-
|
|
189
|
-
expect(items[0]).toHaveFocus();
|
|
190
|
-
});
|
|
191
|
-
|
|
192
|
-
it('should move focus away when pressing Tab', async () => {
|
|
193
|
-
render(
|
|
194
|
-
<>
|
|
195
|
-
<DropdownMenu items={ITEMS} />
|
|
196
|
-
|
|
197
|
-
<button>Other Button</button>
|
|
198
|
-
</>,
|
|
199
|
-
);
|
|
200
|
-
|
|
201
|
-
const user = userEvent.setup();
|
|
202
|
-
|
|
203
|
-
const button = screen.getByRole('button', { name: /menu/i });
|
|
204
|
-
|
|
205
|
-
await user.click(button);
|
|
206
|
-
|
|
207
|
-
await user.tab();
|
|
208
|
-
|
|
209
|
-
await waitFor(() => expect(screen.queryByRole('menu')).not.toBeInTheDocument());
|
|
210
|
-
});
|
|
211
|
-
});
|
|
@@ -1,108 +0,0 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
|
|
3
|
-
import type { ComponentType, HTMLAttributes } from 'react';
|
|
4
|
-
|
|
5
|
-
import { LuChevronDown } from 'react-icons/lu';
|
|
6
|
-
|
|
7
|
-
import {
|
|
8
|
-
type ButtonRendererProps,
|
|
9
|
-
type ItemRendererProps,
|
|
10
|
-
useDropdownMenu,
|
|
11
|
-
} from './useDropdownMenu';
|
|
12
|
-
import { cn } from '../../utils';
|
|
13
|
-
|
|
14
|
-
export type DropdownMenuItem<ItemProps extends object> = ItemRendererProps & {
|
|
15
|
-
label: string;
|
|
16
|
-
} & ItemProps;
|
|
17
|
-
|
|
18
|
-
export type DrowndownMenuButton = ButtonRendererProps;
|
|
19
|
-
|
|
20
|
-
type DropdownMenuProps<ItemProps extends object> = HTMLAttributes<HTMLDivElement> & {
|
|
21
|
-
items: DropdownMenuItem<ItemProps>[];
|
|
22
|
-
containerClassName?: string;
|
|
23
|
-
menuContainerClassName?: string;
|
|
24
|
-
buttonRenderer?: ComponentType<ButtonRendererProps>;
|
|
25
|
-
buttonClassName?: string;
|
|
26
|
-
itemRenderer?: ComponentType<DropdownMenuItem<ItemProps>>;
|
|
27
|
-
itemClassName?: string;
|
|
28
|
-
};
|
|
29
|
-
|
|
30
|
-
const DefaultButton = ({ state: { isOpen }, ...props }: ButtonRendererProps) => {
|
|
31
|
-
return (
|
|
32
|
-
<button
|
|
33
|
-
{...props}
|
|
34
|
-
aria-label="Open Menu"
|
|
35
|
-
className={cn(
|
|
36
|
-
`text-black flex gap-2 items-center justify-center border rounded-md border-gray-300
|
|
37
|
-
bg-white px-2 py-1 text-sm shadow-sm focus:border-brand focus:outline-none focus:ring-1
|
|
38
|
-
focus:ring-brand`,
|
|
39
|
-
props.className,
|
|
40
|
-
)}
|
|
41
|
-
>
|
|
42
|
-
<span className="text-base pointer-events-none">Menu</span>
|
|
43
|
-
|
|
44
|
-
<LuChevronDown className={`pointer-events-none ${isOpen ? 'rotate-180' : ''}`} />
|
|
45
|
-
</button>
|
|
46
|
-
);
|
|
47
|
-
};
|
|
48
|
-
|
|
49
|
-
const DefaultItem = <Item extends object>({ label, ...props }: DropdownMenuItem<Item>) => {
|
|
50
|
-
return (
|
|
51
|
-
<div
|
|
52
|
-
{...props}
|
|
53
|
-
aria-label={label}
|
|
54
|
-
className={cn(
|
|
55
|
-
`text-black cursor-pointer hover:bg-slate-200 focus:bg-slate-200 active:bg-slate-300 px-2
|
|
56
|
-
py-1`,
|
|
57
|
-
props.className,
|
|
58
|
-
)}
|
|
59
|
-
>
|
|
60
|
-
{label}
|
|
61
|
-
</div>
|
|
62
|
-
);
|
|
63
|
-
};
|
|
64
|
-
|
|
65
|
-
export const DropdownMenu = <Item extends object>({
|
|
66
|
-
items,
|
|
67
|
-
containerClassName,
|
|
68
|
-
menuContainerClassName,
|
|
69
|
-
buttonRenderer = DefaultButton,
|
|
70
|
-
buttonClassName,
|
|
71
|
-
itemRenderer = DefaultItem,
|
|
72
|
-
itemClassName,
|
|
73
|
-
className,
|
|
74
|
-
...props
|
|
75
|
-
}: DropdownMenuProps<Item>) => {
|
|
76
|
-
// rebind to avoid linting errors
|
|
77
|
-
const ButtonRenderer = buttonRenderer;
|
|
78
|
-
const ItemRenderer = itemRenderer;
|
|
79
|
-
|
|
80
|
-
const { isOpen, buttonProps, itemProps } = useDropdownMenu(items.length);
|
|
81
|
-
|
|
82
|
-
return (
|
|
83
|
-
<div className={cn('relative', containerClassName, className)} {...props}>
|
|
84
|
-
<ButtonRenderer {...buttonProps} className={buttonClassName} />
|
|
85
|
-
|
|
86
|
-
<div
|
|
87
|
-
style={{ display: isOpen ? 'flex' : 'none' }}
|
|
88
|
-
aria-hidden={!isOpen}
|
|
89
|
-
className={cn(
|
|
90
|
-
`absolute right-0 mt-1 bg-white border shadow-md rounded-md min-w-full flex-col gap-0
|
|
91
|
-
z-[999] divide-y divide-slate-400`,
|
|
92
|
-
menuContainerClassName,
|
|
93
|
-
)}
|
|
94
|
-
role="menu"
|
|
95
|
-
>
|
|
96
|
-
{items.map((item, i) => (
|
|
97
|
-
<ItemRenderer
|
|
98
|
-
{...item}
|
|
99
|
-
{...itemProps[i]}
|
|
100
|
-
key={i}
|
|
101
|
-
label={item.label}
|
|
102
|
-
className={itemClassName}
|
|
103
|
-
/>
|
|
104
|
-
))}
|
|
105
|
-
</div>
|
|
106
|
-
</div>
|
|
107
|
-
);
|
|
108
|
-
};
|
|
@@ -1,249 +0,0 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
|
|
3
|
-
import {
|
|
4
|
-
useState,
|
|
5
|
-
useRef,
|
|
6
|
-
useId,
|
|
7
|
-
useEffect,
|
|
8
|
-
useMemo,
|
|
9
|
-
type HTMLAttributes,
|
|
10
|
-
useCallback,
|
|
11
|
-
} from 'react';
|
|
12
|
-
|
|
13
|
-
import type { ExtendProps } from '../../types/utils';
|
|
14
|
-
import { KeyboardKeys } from '../../utils';
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* The current state of the menu
|
|
18
|
-
*/
|
|
19
|
-
export type DropdownMenuState = { state: { isOpen: boolean } };
|
|
20
|
-
|
|
21
|
-
// use a data attribute with a unique value to tell us which elements belong to the current menu
|
|
22
|
-
type DropdownMenuAttribute = { 'data-menu': string };
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* The props that are passed into the button that is used to open/close the menu.
|
|
26
|
-
*/
|
|
27
|
-
export type ButtonRendererProps = ExtendProps<'button', DropdownMenuState>;
|
|
28
|
-
|
|
29
|
-
// can't use `ExtendProps` here since we just want the most base level of HTML attributes.
|
|
30
|
-
// a number of elements use only these props, like `abbr`, but that implies the menu
|
|
31
|
-
// item is an `<abbr>` which isn't strictly true, and so qualifying it ourselves is better
|
|
32
|
-
/**
|
|
33
|
-
* The props passed into each child item of the menu.
|
|
34
|
-
*/
|
|
35
|
-
export type ItemRendererProps = HTMLAttributes<HTMLElement>;
|
|
36
|
-
|
|
37
|
-
export type DropdownMenuHook = {
|
|
38
|
-
isOpen: boolean;
|
|
39
|
-
buttonProps: ButtonRendererProps & DropdownMenuAttribute;
|
|
40
|
-
itemProps: (ItemRendererProps & DropdownMenuAttribute)[];
|
|
41
|
-
selectedItemIndex: number;
|
|
42
|
-
};
|
|
43
|
-
|
|
44
|
-
/**
|
|
45
|
-
* This hook handles the behaviour and accessibility concerns when creating a dropdown menu (*not* a select menu).
|
|
46
|
-
* while leaving all the styling and design up to the user.
|
|
47
|
-
*
|
|
48
|
-
* The hook returns an object with 4 keys:
|
|
49
|
-
* - `isOpen`: Whether the dropdown is open or not.
|
|
50
|
-
* - `buttonProps`: These props are generated by the hook and need to be passed down (via spreading) into the `button` element
|
|
51
|
-
* that the user can click to open/close the menu. Other attributes can be applied to the button along side these, but at minimum
|
|
52
|
-
* these MUST be added.
|
|
53
|
-
* - `itemProps`: These props are also generated by the hook, but they should be passed down to each item displayed in the menu.
|
|
54
|
-
* Since the style and design is up to the user, when you create the menu children from some data, you can use the index to get
|
|
55
|
-
* the props for each item and spread them in. For example:
|
|
56
|
-
* ```tsx
|
|
57
|
-
* data.map((_, i) => <div {...itemProps[i]}></div>)
|
|
58
|
-
* ```
|
|
59
|
-
* - `selectedItemIndex`: This represents which item was last selected by the user.
|
|
60
|
-
*
|
|
61
|
-
* ### NOTES:
|
|
62
|
-
* This hook makes use of `refs`. For that reason, the menu items **must not** be unmounted from the DOM via a conditional statement with `isOpen`.
|
|
63
|
-
* Instead, you can show/hide the menu item container using its `display` or `visibility` style and set it to a different value depending on
|
|
64
|
-
* if the menu is open or not. This does not affect accessibility.
|
|
65
|
-
*
|
|
66
|
-
* Keyboard interactivity sources:
|
|
67
|
-
* https://www.w3.org/WAI/ARIA/apg/patterns/menubar/examples/menubar-navigation/
|
|
68
|
-
*
|
|
69
|
-
* @param itemCount The number of items to show in the menu.
|
|
70
|
-
* @returns
|
|
71
|
-
*/
|
|
72
|
-
export const useDropdownMenu = (itemCount: number): DropdownMenuHook => {
|
|
73
|
-
const [isOpen, setIsOpen] = useState(false);
|
|
74
|
-
const [selectedItem, setSelectedItem] = useState(0);
|
|
75
|
-
|
|
76
|
-
const currentFocusIndex = useRef<number | null>(null);
|
|
77
|
-
|
|
78
|
-
const menuId = useId();
|
|
79
|
-
|
|
80
|
-
// track the refs of each child so they can be focussed for accessibility
|
|
81
|
-
// this is memoified so it only updates when the number of items changes
|
|
82
|
-
const itemRefs: { current: Record<number, HTMLElement | null> } = useMemo(
|
|
83
|
-
() => ({ current: {} }),
|
|
84
|
-
[],
|
|
85
|
-
);
|
|
86
|
-
|
|
87
|
-
// use the unique id to determive if a given element is part of the menu
|
|
88
|
-
const isMenuItem = useCallback(
|
|
89
|
-
(element: Element | null): boolean => {
|
|
90
|
-
return (
|
|
91
|
-
element?.getAttribute('data-menu') === menuId ||
|
|
92
|
-
// get the closest element with that id, essentially checking if we are a child somewhere down the line
|
|
93
|
-
element?.closest(`[data-menu="${menuId}"]`) !== null
|
|
94
|
-
);
|
|
95
|
-
},
|
|
96
|
-
[menuId],
|
|
97
|
-
);
|
|
98
|
-
|
|
99
|
-
const focusItem = useCallback(
|
|
100
|
-
(index: number) => {
|
|
101
|
-
currentFocusIndex.current = index;
|
|
102
|
-
itemRefs.current[index]?.focus();
|
|
103
|
-
},
|
|
104
|
-
[itemRefs],
|
|
105
|
-
);
|
|
106
|
-
|
|
107
|
-
const selectItem = (index: number) => {
|
|
108
|
-
setSelectedItem(index);
|
|
109
|
-
setIsOpen(false);
|
|
110
|
-
};
|
|
111
|
-
|
|
112
|
-
useEffect(() => {
|
|
113
|
-
if (isOpen) {
|
|
114
|
-
focusItem(0);
|
|
115
|
-
}
|
|
116
|
-
}, [focusItem, isOpen]);
|
|
117
|
-
|
|
118
|
-
useEffect(() => {
|
|
119
|
-
// handles exiting the menu when the `Escape` key is pressed
|
|
120
|
-
const escapeExitMenuHandler = (event: KeyboardEvent) => {
|
|
121
|
-
if (
|
|
122
|
-
event.key === KeyboardKeys.Escape &&
|
|
123
|
-
document.activeElement &&
|
|
124
|
-
isMenuItem(document.activeElement)
|
|
125
|
-
) {
|
|
126
|
-
event.preventDefault();
|
|
127
|
-
|
|
128
|
-
setIsOpen(false);
|
|
129
|
-
}
|
|
130
|
-
};
|
|
131
|
-
|
|
132
|
-
// handles exiting the menu when focus moved to another element
|
|
133
|
-
const blurExitMenuHandler = () => {
|
|
134
|
-
if (!isMenuItem(document.activeElement)) {
|
|
135
|
-
setIsOpen(false);
|
|
136
|
-
}
|
|
137
|
-
};
|
|
138
|
-
|
|
139
|
-
// handles exiting the menu when the user clicks/touches outside the menu
|
|
140
|
-
const clickExitMenuHandler = (event: PointerEvent) => {
|
|
141
|
-
if (event.button === 0 && !isMenuItem(event.target as Element)) {
|
|
142
|
-
setIsOpen(false);
|
|
143
|
-
}
|
|
144
|
-
};
|
|
145
|
-
|
|
146
|
-
window.addEventListener('keydown', escapeExitMenuHandler);
|
|
147
|
-
// `passive` events just mean we dont call `preventDefault` and it allows the browser to be a bit smarter about the events
|
|
148
|
-
// `capture` is a bit more complicated but from what I understand it essentially makes it so this event handler fires BEFORE
|
|
149
|
-
// the element gets the event, whereas without it it would trigger AFTER the element recieved the event
|
|
150
|
-
document.addEventListener('focus', blurExitMenuHandler, { capture: true, passive: true });
|
|
151
|
-
// make use of pointer events instead of click events to support touch devices.
|
|
152
|
-
document.addEventListener('pointerup', clickExitMenuHandler, { passive: true });
|
|
153
|
-
|
|
154
|
-
return () => {
|
|
155
|
-
window.removeEventListener('keydown', escapeExitMenuHandler);
|
|
156
|
-
document.removeEventListener('focus', blurExitMenuHandler);
|
|
157
|
-
document.removeEventListener('pointerup', clickExitMenuHandler);
|
|
158
|
-
};
|
|
159
|
-
}, [isMenuItem]);
|
|
160
|
-
|
|
161
|
-
return {
|
|
162
|
-
isOpen,
|
|
163
|
-
selectedItemIndex: selectedItem,
|
|
164
|
-
|
|
165
|
-
buttonProps: {
|
|
166
|
-
state: { isOpen },
|
|
167
|
-
'data-menu': menuId,
|
|
168
|
-
'aria-haspopup': 'menu',
|
|
169
|
-
'aria-expanded': isOpen,
|
|
170
|
-
|
|
171
|
-
onPointerUp: () => setIsOpen(!isOpen),
|
|
172
|
-
onKeyDown: (event) => {
|
|
173
|
-
// space and enter act more like clicking and can toggle the menu open/closed
|
|
174
|
-
if (event.key === KeyboardKeys.Space || event.key === KeyboardKeys.Enter) {
|
|
175
|
-
event.preventDefault();
|
|
176
|
-
|
|
177
|
-
setIsOpen(!isOpen);
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
// the down arrow should open the menu
|
|
181
|
-
if (event.key === KeyboardKeys.ArrowDown) {
|
|
182
|
-
event.preventDefault();
|
|
183
|
-
|
|
184
|
-
if (!isOpen) {
|
|
185
|
-
setIsOpen(true);
|
|
186
|
-
} else {
|
|
187
|
-
focusItem(0);
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
},
|
|
191
|
-
},
|
|
192
|
-
|
|
193
|
-
itemProps: Array.from({ length: itemCount }, (_, itemIndex) => ({
|
|
194
|
-
key: itemIndex,
|
|
195
|
-
|
|
196
|
-
role: 'menuitem',
|
|
197
|
-
// the items in the menu should not be focussable. using `Tab` should move the focus away from the menu entirely.
|
|
198
|
-
// only arrow keys should navigate each item
|
|
199
|
-
tabIndex: -1,
|
|
200
|
-
'data-menu': menuId,
|
|
201
|
-
|
|
202
|
-
ref: (el: HTMLElement) => {
|
|
203
|
-
itemRefs.current[itemIndex] = el;
|
|
204
|
-
},
|
|
205
|
-
|
|
206
|
-
// by using a 0 second `setTimeout` the event handler is moved to the bottom of the stack
|
|
207
|
-
// this is important because it means our handler is fired AFTER the default behaviour
|
|
208
|
-
// without the setTimeout things like links might not work properly because the menu
|
|
209
|
-
// would hide BEFORE the browsers own default click handler could redirect you
|
|
210
|
-
onPointerUp: () => setTimeout(() => selectItem(itemIndex), 0),
|
|
211
|
-
onKeyDown: (event) => {
|
|
212
|
-
let focusIndex = currentFocusIndex.current;
|
|
213
|
-
|
|
214
|
-
// close the menu if you "select" an item
|
|
215
|
-
if (event.key === KeyboardKeys.Space || event.key === KeyboardKeys.Enter) {
|
|
216
|
-
event.preventDefault();
|
|
217
|
-
|
|
218
|
-
// we just called `preventDefault` so we need to manually click it otherwise nothing will happen
|
|
219
|
-
// the reason we must call `preventDefault` is to avoid other behaviour like spacebar scrolling the page
|
|
220
|
-
itemRefs.current[itemIndex]?.click();
|
|
221
|
-
|
|
222
|
-
selectItem(itemIndex);
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
if (focusIndex !== null) {
|
|
226
|
-
// arrow keys should cycle through each item in the menu
|
|
227
|
-
if (event.key === KeyboardKeys.ArrowDown) {
|
|
228
|
-
event.preventDefault();
|
|
229
|
-
|
|
230
|
-
focusIndex += 1;
|
|
231
|
-
} else if (event.key === KeyboardKeys.ArrowUp) {
|
|
232
|
-
event.preventDefault();
|
|
233
|
-
|
|
234
|
-
focusIndex -= 1;
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
// allow wrapping of focus
|
|
238
|
-
if (focusIndex > itemCount - 1) {
|
|
239
|
-
focusIndex = 0;
|
|
240
|
-
} else if (focusIndex < 0) {
|
|
241
|
-
focusIndex = itemCount - 1;
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
focusItem(focusIndex);
|
|
245
|
-
}
|
|
246
|
-
},
|
|
247
|
-
})),
|
|
248
|
-
};
|
|
249
|
-
};
|