@tpzdsp/next-toolkit 2.5.1 → 3.1.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 +21 -12
- 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 +34 -0
- package/src/components/ErrorModal/ErrorModal.stories.tsx +100 -0
- package/src/components/ErrorModal/ErrorModal.tsx +123 -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 +3 -2
- package/src/components/Footer/Footer.stories.tsx +27 -0
- package/src/components/Footer/Footer.tsx +46 -0
- package/src/components/Footer/Licence.tsx +18 -0
- package/src/components/Footer/MetaLinks.tsx +47 -0
- package/src/components/Footer/Small/SmallFooter.stories.tsx +3 -0
- package/src/components/Footer/Small/SmallFooter.tsx +53 -41
- 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 +304 -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 +111 -0
- package/src/components/Modal/Modal.stories.tsx +94 -155
- package/src/components/Modal/Modal.tsx +141 -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 +202 -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 +296 -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 +343 -61
- package/src/errors/errorDisplay.ts +70 -0
- package/src/errors/index.ts +1 -0
- package/src/http/constants.ts +2 -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/footer/MetaLinks.tsx +0 -36
- 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
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react-vite';
|
|
2
|
+
|
|
3
|
+
import { SearchBox } from './SearchBox';
|
|
4
|
+
import { ErrorText } from '../ErrorText/ErrorText';
|
|
5
|
+
import { FormGroup } from '../Form/FormGroup';
|
|
6
|
+
import { HintText } from '../HintText/HintText';
|
|
7
|
+
import { Label } from '../Label/Label';
|
|
8
|
+
|
|
9
|
+
const meta: Meta<typeof SearchBox> = {
|
|
10
|
+
title: 'Components/SearchBox',
|
|
11
|
+
component: SearchBox,
|
|
12
|
+
tags: ['autodocs'],
|
|
13
|
+
parameters: {
|
|
14
|
+
docs: {
|
|
15
|
+
description: {
|
|
16
|
+
component:
|
|
17
|
+
'A search input with a primary submit button. Wrap in a `<form role="search">` to wire up submission. The button is `type="submit"` so it works with standard form behaviour. Accepts all `Input` props except `type`.',
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export default meta;
|
|
24
|
+
|
|
25
|
+
type Story = StoryObj<typeof SearchBox>;
|
|
26
|
+
|
|
27
|
+
export const Default: Story = {
|
|
28
|
+
render: (args) => (
|
|
29
|
+
<form role="search" onSubmit={(event) => event.preventDefault()}>
|
|
30
|
+
<SearchBox placeholder="Search" {...args} />
|
|
31
|
+
</form>
|
|
32
|
+
),
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export const WithLabel: Story = {
|
|
36
|
+
render: (args) => (
|
|
37
|
+
<form role="search" onSubmit={(event) => event.preventDefault()}>
|
|
38
|
+
<div className="flex flex-col gap-gds-1">
|
|
39
|
+
<Label htmlFor="site-search">Search this site</Label>
|
|
40
|
+
|
|
41
|
+
<HintText id="site-search-hint">Enter a keyword or phrase.</HintText>
|
|
42
|
+
|
|
43
|
+
<SearchBox
|
|
44
|
+
id="site-search"
|
|
45
|
+
placeholder="Search"
|
|
46
|
+
aria-describedby="site-search-hint"
|
|
47
|
+
{...args}
|
|
48
|
+
/>
|
|
49
|
+
</div>
|
|
50
|
+
</form>
|
|
51
|
+
),
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
export const Invalid: Story = {
|
|
55
|
+
render: (args) => (
|
|
56
|
+
<form role="search" onSubmit={(event) => event.preventDefault()}>
|
|
57
|
+
<FormGroup invalid>
|
|
58
|
+
<Label htmlFor="search-invalid">Search this site</Label>
|
|
59
|
+
|
|
60
|
+
<ErrorText id="search-invalid-error">Enter a search term</ErrorText>
|
|
61
|
+
|
|
62
|
+
<SearchBox
|
|
63
|
+
id="search-invalid"
|
|
64
|
+
placeholder="Search"
|
|
65
|
+
aria-invalid
|
|
66
|
+
aria-describedby="search-invalid-error"
|
|
67
|
+
{...args}
|
|
68
|
+
/>
|
|
69
|
+
</FormGroup>
|
|
70
|
+
</form>
|
|
71
|
+
),
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
export const Disabled: Story = {
|
|
75
|
+
render: (args) => (
|
|
76
|
+
<form role="search">
|
|
77
|
+
<SearchBox placeholder="Search" disabled {...args} />
|
|
78
|
+
</form>
|
|
79
|
+
),
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
export const CustomLabel: Story = {
|
|
83
|
+
render: (args) => (
|
|
84
|
+
<form role="search" onSubmit={(event) => event.preventDefault()}>
|
|
85
|
+
<SearchBox placeholder="Enter location" buttonLabel="Find" {...args} />
|
|
86
|
+
</form>
|
|
87
|
+
),
|
|
88
|
+
};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useId, type ComponentProps } from 'react';
|
|
4
|
+
|
|
5
|
+
import { LuSearch } from 'react-icons/lu';
|
|
6
|
+
|
|
7
|
+
import { Button } from '../Button/Button';
|
|
8
|
+
import { Input } from '../Input/Input';
|
|
9
|
+
import { InputGroup, InputSuffix } from '../InputGroup/InputGroup';
|
|
10
|
+
|
|
11
|
+
export type SearchBoxProps = Omit<ComponentProps<typeof Input>, 'type'> & {
|
|
12
|
+
buttonLabel?: string;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export const SearchBox = ({ id, buttonLabel = 'Search', disabled, ...props }: SearchBoxProps) => {
|
|
16
|
+
const generatedId = useId();
|
|
17
|
+
const inputId = id ?? generatedId;
|
|
18
|
+
|
|
19
|
+
return (
|
|
20
|
+
<InputGroup aria-disabled={disabled}>
|
|
21
|
+
<Input id={inputId} type="search" disabled={disabled} {...props} />
|
|
22
|
+
|
|
23
|
+
<InputSuffix>
|
|
24
|
+
<Button
|
|
25
|
+
type="submit"
|
|
26
|
+
appearance="primary"
|
|
27
|
+
variant="icon"
|
|
28
|
+
aria-label={buttonLabel}
|
|
29
|
+
disabled={disabled}
|
|
30
|
+
>
|
|
31
|
+
<LuSearch aria-hidden />
|
|
32
|
+
</Button>
|
|
33
|
+
</InputSuffix>
|
|
34
|
+
</InputGroup>
|
|
35
|
+
);
|
|
36
|
+
};
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { useCallback, useRef } from 'react';
|
|
2
|
+
|
|
3
|
+
import type { OptionsOrGroups } from 'react-select';
|
|
4
|
+
|
|
5
|
+
import type { Meta, StoryObj } from '@storybook/react-vite';
|
|
6
|
+
|
|
7
|
+
import { AsyncSelect } from './AsyncSelect';
|
|
8
|
+
import { Label } from '../Label/Label';
|
|
9
|
+
|
|
10
|
+
const meta: Meta<typeof AsyncSelect> = {
|
|
11
|
+
title: 'Components/Select/AsyncSelect',
|
|
12
|
+
component: AsyncSelect,
|
|
13
|
+
tags: ['autodocs'],
|
|
14
|
+
parameters: {
|
|
15
|
+
docs: {
|
|
16
|
+
description: {
|
|
17
|
+
component:
|
|
18
|
+
"Select with remote, `loadOptions`-driven options instead of a fixed local list, for search-as-you-type against an API. Composes react-select's own `useAsync` and `useStateManager` directly onto this project's Select, so it shares the exact same styling and indicator overrides (including the loading spinner). `loadOptions` isn't debounced by react-select itself, it fires on every keystroke, wrap it in your own debounce as shown here.",
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export default meta;
|
|
25
|
+
|
|
26
|
+
type Story = StoryObj<typeof AsyncSelect>;
|
|
27
|
+
|
|
28
|
+
type Country = { id: string; label: string };
|
|
29
|
+
|
|
30
|
+
const COUNTRIES: Country[] = [
|
|
31
|
+
{ id: 'eng', label: 'England' },
|
|
32
|
+
{ id: 'sco', label: 'Scotland' },
|
|
33
|
+
{ id: 'wal', label: 'Wales' },
|
|
34
|
+
{ id: 'nir', label: 'Northern Ireland' },
|
|
35
|
+
];
|
|
36
|
+
|
|
37
|
+
// Stands in for a real API call, filters the local list after a short delay
|
|
38
|
+
// so the loading indicator is visible.
|
|
39
|
+
const fakeSearch = (query: string): Promise<Country[]> =>
|
|
40
|
+
new Promise((resolve) => {
|
|
41
|
+
setTimeout(() => {
|
|
42
|
+
resolve(
|
|
43
|
+
COUNTRIES.filter((country) => country.label.toLowerCase().includes(query.toLowerCase())),
|
|
44
|
+
);
|
|
45
|
+
}, 600);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
const DebouncedExample = () => {
|
|
49
|
+
const debounceRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
50
|
+
|
|
51
|
+
const loadOptions = useCallback(
|
|
52
|
+
(inputValue: string, callback: (options: OptionsOrGroups<Country, never>) => void) => {
|
|
53
|
+
if (debounceRef.current) {
|
|
54
|
+
clearTimeout(debounceRef.current);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
debounceRef.current = setTimeout(async () => {
|
|
58
|
+
callback(await fakeSearch(inputValue));
|
|
59
|
+
}, 300);
|
|
60
|
+
},
|
|
61
|
+
[],
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
return (
|
|
65
|
+
<div>
|
|
66
|
+
<Label htmlFor="async-country">Country</Label>
|
|
67
|
+
|
|
68
|
+
<AsyncSelect<Country>
|
|
69
|
+
inputId="async-country"
|
|
70
|
+
loadOptions={loadOptions}
|
|
71
|
+
getOptionLabel={(option) => option.label}
|
|
72
|
+
getOptionValue={(option) => option.id}
|
|
73
|
+
placeholder="Search for a country"
|
|
74
|
+
noOptionsMessage={({ inputValue }) =>
|
|
75
|
+
inputValue ? 'No countries found' : 'Start typing to search'
|
|
76
|
+
}
|
|
77
|
+
/>
|
|
78
|
+
</div>
|
|
79
|
+
);
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
export const Default: Story = {
|
|
83
|
+
render: () => <DebouncedExample />,
|
|
84
|
+
};
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import type { GroupBase } from 'react-select';
|
|
4
|
+
import { useStateManager } from 'react-select';
|
|
5
|
+
import { useAsync } from 'react-select/async';
|
|
6
|
+
import type { AsyncProps } from 'react-select/async';
|
|
7
|
+
|
|
8
|
+
import { Select } from './Select';
|
|
9
|
+
import type { SelectProps } from './Select';
|
|
10
|
+
|
|
11
|
+
export type AsyncSelectProps<
|
|
12
|
+
Option = unknown,
|
|
13
|
+
IsMulti extends boolean = false,
|
|
14
|
+
Group extends GroupBase<Option> = GroupBase<Option>,
|
|
15
|
+
> = Omit<SelectProps<Option, IsMulti, Group>, keyof AsyncProps<Option, IsMulti, Group>> &
|
|
16
|
+
AsyncProps<Option, IsMulti, Group>;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Select with remote, `loadOptions`-driven options instead of a fixed local
|
|
20
|
+
* list. Composes react-select's own `useAsync` (manages the loading state
|
|
21
|
+
* and current options) and `useStateManager` (controlled/uncontrolled
|
|
22
|
+
* value/inputValue bridging) directly onto our own styled Select, the same
|
|
23
|
+
* way react-select's own AsyncSelect composes them onto its unstyled one, so
|
|
24
|
+
* this gets identical behaviour with our styling and indicator overrides.
|
|
25
|
+
*
|
|
26
|
+
* `loadOptions` is not debounced by react-select itself, it fires on every
|
|
27
|
+
* keystroke, wrap it in your own debounce (e.g. a timeout cleared on each
|
|
28
|
+
* call) if that's not the desired behaviour.
|
|
29
|
+
*/
|
|
30
|
+
export const AsyncSelect = <
|
|
31
|
+
Option = unknown,
|
|
32
|
+
IsMulti extends boolean = false,
|
|
33
|
+
Group extends GroupBase<Option> = GroupBase<Option>,
|
|
34
|
+
>(
|
|
35
|
+
props: AsyncSelectProps<Option, IsMulti, Group>,
|
|
36
|
+
) => {
|
|
37
|
+
const asyncProps = useAsync(props);
|
|
38
|
+
const statefulProps = useStateManager(asyncProps);
|
|
39
|
+
|
|
40
|
+
return <Select {...statefulProps} />;
|
|
41
|
+
};
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
@layer components {
|
|
2
|
+
/* MARK: Select
|
|
3
|
+
Styled to match the GDS text input: same height, border, padding, and
|
|
4
|
+
focus ring. The dropdown menu uses panel (white bg, form-weight border)
|
|
5
|
+
with no border-radius, matching GDS panel conventions. */
|
|
6
|
+
|
|
7
|
+
/* MARK: Container
|
|
8
|
+
w-full mirrors .cn-input: harmless in normal block flow (already full
|
|
9
|
+
width there), but needed as a flex item (e.g. the main field in an
|
|
10
|
+
InputGroup row alongside InputPrefix/InputSuffix), where it would
|
|
11
|
+
otherwise shrink to the width of its content instead of filling the
|
|
12
|
+
remaining row space. InputGroup.css overrides this back to w-auto when
|
|
13
|
+
a Select is used as a narrow addon nested inside a prefix/suffix. */
|
|
14
|
+
.cn-select-container {
|
|
15
|
+
@apply relative w-full;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/* MARK: Control
|
|
19
|
+
Mirrors .cn-input: 40px height, 2px border, 5px horizontal padding. */
|
|
20
|
+
.cn-select-control {
|
|
21
|
+
@apply cn-form-border h-10 px-gds-1 bg-white flex items-center cursor-default text-base
|
|
22
|
+
box-border;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
.cn-select-control-focused {
|
|
26
|
+
@apply outline-focus outline-offset-0;
|
|
27
|
+
box-shadow: inset 0 0 0 var(--border-width-form) var(--color-divider-input);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
.cn-select-control-invalid {
|
|
31
|
+
@apply border-error;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
.cn-select-control-disabled {
|
|
35
|
+
@apply opacity-50 cursor-not-allowed;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/* MARK: Value area */
|
|
39
|
+
|
|
40
|
+
.cn-select-value-container {
|
|
41
|
+
@apply flex flex-1 items-center flex-wrap h-full overflow-hidden;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
.cn-select-value-container-multi {
|
|
45
|
+
/* The visible gap between chips equals the gap property value: the 3px white
|
|
46
|
+
borders on each side of each chip are part of the perceived separation, not
|
|
47
|
+
subtracted from it (border-to-border = gap - 6, but visible = gap - 6 + 3 + 3 = gap).
|
|
48
|
+
Edge gap = px-gds-1 (5px) + chip border (3px) = 8px, so gap-2 (8px) matches. */
|
|
49
|
+
@apply gap-2;
|
|
50
|
+
|
|
51
|
+
&:has(.cn-select-multi-value) {
|
|
52
|
+
/* Horizontal padding only, vertical padding reduces the centering area
|
|
53
|
+
from 40px to 34px and shifts chips off-centre. The chip's focus border
|
|
54
|
+
(3px) sits 5.5px from the top/bottom container edges, so no vertical
|
|
55
|
+
padding is needed to protect it from overflow: hidden. */
|
|
56
|
+
padding-right: var(--focus-ring-width);
|
|
57
|
+
padding-left: var(--focus-ring-width);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
.cn-select-single-value {
|
|
62
|
+
@apply text-body truncate;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
.cn-select-placeholder {
|
|
66
|
+
@apply text-body-secondary;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/* The hidden search input react-select renders inside the control */
|
|
70
|
+
.cn-select-input {
|
|
71
|
+
@apply text-base text-body;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/* MARK: Indicators */
|
|
75
|
+
|
|
76
|
+
.cn-select-indicators {
|
|
77
|
+
@apply flex items-center gap-1 flex-none;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
.cn-select-dropdown-indicator {
|
|
81
|
+
@apply w-5 h-5 flex items-center justify-center text-body;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
.cn-select-dropdown-indicator-open {
|
|
85
|
+
@apply rotate-180;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
.cn-select-clear-indicator {
|
|
89
|
+
@apply w-5 h-5 flex items-center justify-center cursor-pointer text-body-secondary
|
|
90
|
+
hover:text-body;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
.cn-select-loading-indicator {
|
|
94
|
+
@apply w-5 h-5 flex items-center justify-center text-body-secondary;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/* MARK: Menu */
|
|
98
|
+
|
|
99
|
+
.cn-select-menu {
|
|
100
|
+
@apply panel absolute w-full mt-1 overflow-hidden cn-z-overlay;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/* react-select flips the menu above the control when there isn't room
|
|
104
|
+
below. mt-1 pushes it further down rather than opening a gap in that
|
|
105
|
+
direction, mirror it as mb-1 so there's still a visible gap between
|
|
106
|
+
control and menu regardless of which side it opens on. */
|
|
107
|
+
.cn-select-menu-top {
|
|
108
|
+
@apply mt-0 mb-1;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
.cn-select-menu-list {
|
|
112
|
+
@apply flex flex-col overflow-y-auto max-h-60;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/* MARK: Options */
|
|
116
|
+
|
|
117
|
+
.cn-select-option {
|
|
118
|
+
@apply px-gds-1 py-1 text-base text-body cursor-pointer;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
.cn-select-option-focused {
|
|
122
|
+
@apply bg-wash;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
.cn-select-option-selected {
|
|
126
|
+
@apply bg-accent text-on-accent;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
.cn-select-option-disabled {
|
|
130
|
+
@apply opacity-50 cursor-not-allowed;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
.cn-select-no-options {
|
|
134
|
+
@apply px-gds-1 py-gds-1 text-base text-body select-none;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/* MARK: Groups */
|
|
138
|
+
|
|
139
|
+
.cn-select-group:first-child {
|
|
140
|
+
@apply pt-2;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/* MARK: Multi-value chips
|
|
144
|
+
The entire chip is a <button> that removes the value on click, giving a
|
|
145
|
+
larger, GDS-compliant touch target rather than a tiny X icon alone. */
|
|
146
|
+
|
|
147
|
+
.cn-select-multi-value {
|
|
148
|
+
/* The margin cancels the space the 3px border takes up in the flex layout so
|
|
149
|
+
chips look the same size as they would without a border. */
|
|
150
|
+
@apply cn-chip-size-sm cn-chip-appearance-neutral flex items-center gap-1 cursor-pointer
|
|
151
|
+
border-solid border-panel outline-none;
|
|
152
|
+
border-width: var(--focus-ring-width);
|
|
153
|
+
margin: 0 calc(var(--focus-ring-width) * -1) 0 calc(var(--focus-ring-width) * -1);
|
|
154
|
+
|
|
155
|
+
&:focus {
|
|
156
|
+
@apply border-focus;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
.cn-select-multi-value-remove {
|
|
161
|
+
@apply w-3.5 h-3.5 flex-none;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/* MARK: Tags below
|
|
165
|
+
Used when `tagsPlacement="below"`: selected values render in a row under
|
|
166
|
+
the control instead of as inline chips, so the control keeps a fixed
|
|
167
|
+
40px height regardless of how many values are selected. */
|
|
168
|
+
|
|
169
|
+
.cn-select-tags {
|
|
170
|
+
@apply flex flex-wrap gap-2 mt-2;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
.cn-select-tag {
|
|
174
|
+
/* Same margin-cancels-border trick as .cn-select-multi-value, so the gap
|
|
175
|
+
between tags matches the gap between inline chips exactly. */
|
|
176
|
+
@apply cn-chip-size-sm cn-chip-appearance-neutral flex items-center gap-1 cursor-pointer
|
|
177
|
+
border-solid border-panel outline-none;
|
|
178
|
+
border-width: var(--focus-ring-width);
|
|
179
|
+
margin: 0 calc(var(--focus-ring-width) * -1) 0 calc(var(--focus-ring-width) * -1);
|
|
180
|
+
|
|
181
|
+
&:focus {
|
|
182
|
+
@apply border-focus;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
&:disabled {
|
|
186
|
+
@apply opacity-50 cursor-not-allowed;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/* MARK: Skeleton
|
|
191
|
+
SelectSkeleton reuses cn-select-container/control/value-container/
|
|
192
|
+
indicators directly, so its dimensions (40px height, border, padding,
|
|
193
|
+
the flex-1 vs flex-none split) always match the real control, these
|
|
194
|
+
two are just the decorative placeholder shapes inside it. */
|
|
195
|
+
.cn-select-skeleton-value {
|
|
196
|
+
@apply h-4 w-24;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
.cn-select-skeleton-indicator {
|
|
200
|
+
@apply size-4;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react-vite';
|
|
2
|
+
|
|
3
|
+
import { Select } from './Select';
|
|
4
|
+
import { ErrorText } from '../ErrorText/ErrorText';
|
|
5
|
+
import { FormGroup } from '../Form/FormGroup';
|
|
6
|
+
import { Label } from '../Label/Label';
|
|
7
|
+
|
|
8
|
+
const meta: Meta<typeof Select> = {
|
|
9
|
+
title: 'Components/Select/Select',
|
|
10
|
+
component: Select,
|
|
11
|
+
tags: ['autodocs'],
|
|
12
|
+
parameters: {
|
|
13
|
+
docs: {
|
|
14
|
+
description: {
|
|
15
|
+
component:
|
|
16
|
+
'Searchable select built on react-select. Styled to match the GDS text input, same height, border, and focus ring. The dropdown uses the standard GDS panel (white background, black border, no border-radius). Supports single and multi-select, option groups, async loading, and creatable options via react-select props.',
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export default meta;
|
|
23
|
+
|
|
24
|
+
type Story = StoryObj<typeof Select>;
|
|
25
|
+
|
|
26
|
+
const SELECT_A_COUNTRY = 'Select a country';
|
|
27
|
+
|
|
28
|
+
const COUNTRIES = [
|
|
29
|
+
{ value: 'eng', label: 'England' },
|
|
30
|
+
{ value: 'sco', label: 'Scotland' },
|
|
31
|
+
{ value: 'wal', label: 'Wales' },
|
|
32
|
+
{ value: 'nir', label: 'Northern Ireland' },
|
|
33
|
+
];
|
|
34
|
+
|
|
35
|
+
const SPECIES = [
|
|
36
|
+
{
|
|
37
|
+
label: 'Birds',
|
|
38
|
+
options: [
|
|
39
|
+
{ value: 'lapwing', label: 'Lapwing' },
|
|
40
|
+
{ value: 'curlew', label: 'Curlew' },
|
|
41
|
+
{ value: 'redshank', label: 'Redshank' },
|
|
42
|
+
],
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
label: 'Mammals',
|
|
46
|
+
options: [
|
|
47
|
+
{ value: 'otter', label: 'Otter' },
|
|
48
|
+
{ value: 'water-vole', label: 'Water vole' },
|
|
49
|
+
{ value: 'hedgehog', label: 'Hedgehog' },
|
|
50
|
+
],
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
label: 'Plants',
|
|
54
|
+
options: [
|
|
55
|
+
{ value: 'bluebell', label: 'Bluebell' },
|
|
56
|
+
{ value: 'meadow-buttercup', label: 'Meadow buttercup' },
|
|
57
|
+
],
|
|
58
|
+
},
|
|
59
|
+
];
|
|
60
|
+
|
|
61
|
+
export const Default: Story = {
|
|
62
|
+
args: {
|
|
63
|
+
options: COUNTRIES,
|
|
64
|
+
placeholder: SELECT_A_COUNTRY,
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
export const Clearable: Story = {
|
|
69
|
+
args: {
|
|
70
|
+
options: COUNTRIES,
|
|
71
|
+
placeholder: SELECT_A_COUNTRY,
|
|
72
|
+
isClearable: true,
|
|
73
|
+
defaultValue: COUNTRIES[0],
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
export const MultiSelect: Story = {
|
|
78
|
+
args: {
|
|
79
|
+
options: COUNTRIES,
|
|
80
|
+
placeholder: 'Select countries',
|
|
81
|
+
isMulti: true,
|
|
82
|
+
isClearable: true,
|
|
83
|
+
defaultValue: [COUNTRIES[0], COUNTRIES[2]],
|
|
84
|
+
},
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
export const MultiSelectTagsBelow: Story = {
|
|
88
|
+
parameters: {
|
|
89
|
+
docs: {
|
|
90
|
+
description: {
|
|
91
|
+
story:
|
|
92
|
+
'Set `tagsPlacement="below"` to list selected values under the control instead of as inline chips. The control keeps a fixed height regardless of how many values are selected.',
|
|
93
|
+
},
|
|
94
|
+
},
|
|
95
|
+
},
|
|
96
|
+
args: {
|
|
97
|
+
options: COUNTRIES,
|
|
98
|
+
placeholder: 'Select countries',
|
|
99
|
+
isMulti: true,
|
|
100
|
+
isClearable: true,
|
|
101
|
+
tagsPlacement: 'below',
|
|
102
|
+
defaultValue: [COUNTRIES[0], COUNTRIES[2]],
|
|
103
|
+
},
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
export const WithGroups: Story = {
|
|
107
|
+
args: {
|
|
108
|
+
options: SPECIES,
|
|
109
|
+
placeholder: 'Select a species',
|
|
110
|
+
isClearable: true,
|
|
111
|
+
},
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
export const Disabled: Story = {
|
|
115
|
+
args: {
|
|
116
|
+
options: COUNTRIES,
|
|
117
|
+
placeholder: SELECT_A_COUNTRY,
|
|
118
|
+
isDisabled: true,
|
|
119
|
+
defaultValue: COUNTRIES[1],
|
|
120
|
+
},
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
export const Invalid: Story = {
|
|
124
|
+
parameters: {
|
|
125
|
+
docs: {
|
|
126
|
+
description: {
|
|
127
|
+
story: 'Pass `invalid` to show the error border. Clears when the control receives focus.',
|
|
128
|
+
},
|
|
129
|
+
},
|
|
130
|
+
},
|
|
131
|
+
render: () => (
|
|
132
|
+
<FormGroup invalid>
|
|
133
|
+
<Label htmlFor="select-invalid">Country</Label>
|
|
134
|
+
|
|
135
|
+
<ErrorText id="select-invalid-error">Select a country</ErrorText>
|
|
136
|
+
|
|
137
|
+
<Select
|
|
138
|
+
inputId="select-invalid"
|
|
139
|
+
aria-describedby="select-invalid-error"
|
|
140
|
+
options={COUNTRIES}
|
|
141
|
+
placeholder={SELECT_A_COUNTRY}
|
|
142
|
+
invalid
|
|
143
|
+
/>
|
|
144
|
+
</FormGroup>
|
|
145
|
+
),
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
export const NonSearchable: Story = {
|
|
149
|
+
parameters: {
|
|
150
|
+
docs: {
|
|
151
|
+
description: {
|
|
152
|
+
story:
|
|
153
|
+
'Set `isSearchable={false}` for short lists where filtering adds no value. The cursor becomes a pointer to reinforce non-editable behaviour.',
|
|
154
|
+
},
|
|
155
|
+
},
|
|
156
|
+
},
|
|
157
|
+
args: {
|
|
158
|
+
options: COUNTRIES,
|
|
159
|
+
placeholder: 'Select a country',
|
|
160
|
+
isSearchable: false,
|
|
161
|
+
},
|
|
162
|
+
};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { Select } from './Select';
|
|
2
|
+
import { render, screen } from '../../test/renderers';
|
|
3
|
+
|
|
4
|
+
const COUNTRIES = [
|
|
5
|
+
{ value: 'eng', label: 'England' },
|
|
6
|
+
{ value: 'sco', label: 'Scotland' },
|
|
7
|
+
];
|
|
8
|
+
|
|
9
|
+
const ERROR_ID = 'country-error';
|
|
10
|
+
|
|
11
|
+
describe('Select', () => {
|
|
12
|
+
it('merges aria-describedby alongside react-select’s own value, rather than replacing it', () => {
|
|
13
|
+
render(
|
|
14
|
+
<Select
|
|
15
|
+
inputId="country"
|
|
16
|
+
options={COUNTRIES}
|
|
17
|
+
aria-describedby={ERROR_ID}
|
|
18
|
+
aria-label="Country"
|
|
19
|
+
/>,
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
const describedBy = screen.getByRole('combobox').getAttribute('aria-describedby');
|
|
23
|
+
|
|
24
|
+
expect(describedBy).toContain(ERROR_ID);
|
|
25
|
+
// react-select's own placeholder/live-region id is still present, not replaced.
|
|
26
|
+
expect(describedBy?.split(' ').length).toBeGreaterThan(1);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('omits the extra id when aria-describedby is not provided', () => {
|
|
30
|
+
render(<Select inputId="country" options={COUNTRIES} aria-label="Country" />);
|
|
31
|
+
|
|
32
|
+
const describedBy = screen.getByRole('combobox').getAttribute('aria-describedby');
|
|
33
|
+
|
|
34
|
+
expect(describedBy).not.toContain(ERROR_ID);
|
|
35
|
+
});
|
|
36
|
+
});
|