@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,296 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useId } from 'react';
|
|
4
|
+
|
|
5
|
+
import { LuChevronDown, LuLoaderCircle, LuX } from 'react-icons/lu';
|
|
6
|
+
import type {
|
|
7
|
+
ClassNamesConfig,
|
|
8
|
+
ClearIndicatorProps,
|
|
9
|
+
ContainerProps,
|
|
10
|
+
DropdownIndicatorProps,
|
|
11
|
+
GroupBase,
|
|
12
|
+
GroupHeadingProps,
|
|
13
|
+
InputProps,
|
|
14
|
+
LoadingIndicatorProps,
|
|
15
|
+
MultiValueProps,
|
|
16
|
+
OnChangeValue,
|
|
17
|
+
Props as ReactSelectProps,
|
|
18
|
+
} from 'react-select';
|
|
19
|
+
import { components, default as ReactSelect } from 'react-select';
|
|
20
|
+
|
|
21
|
+
import { cn } from '../../utils/components';
|
|
22
|
+
import { Separator } from '../Separator/Separator';
|
|
23
|
+
|
|
24
|
+
export type SelectTagsPlacement = 'inline' | 'below';
|
|
25
|
+
|
|
26
|
+
export type SelectProps<
|
|
27
|
+
Option = unknown,
|
|
28
|
+
IsMulti extends boolean = false,
|
|
29
|
+
Group extends GroupBase<Option> = GroupBase<Option>,
|
|
30
|
+
> = Omit<ReactSelectProps<Option, IsMulti, Group>, 'className'> & {
|
|
31
|
+
invalid?: boolean;
|
|
32
|
+
/** Where selected values render for a multi-select. `below` keeps the control a fixed
|
|
33
|
+
* height and lists selected values underneath instead of as inline chips. */
|
|
34
|
+
tagsPlacement?: SelectTagsPlacement;
|
|
35
|
+
/** Suppresses the built-in loading spinner in the indicators area, for callers that
|
|
36
|
+
* render their own loading state elsewhere (e.g. a fixed-width InputSuffix) instead
|
|
37
|
+
* of having the indicators container grow/shrink as it mounts and unmounts. */
|
|
38
|
+
hideLoadingIndicator?: boolean;
|
|
39
|
+
/** react-select's own Props type doesn't declare aria-describedby at all (only
|
|
40
|
+
* aria-errormessage/invalid/label/labelledby/live), and renderInput() computes its own
|
|
41
|
+
* hardcoded value for the real input (pointing at its internal live-region/placeholder),
|
|
42
|
+
* overwriting anything already there. Declaring it explicitly here is what makes this
|
|
43
|
+
* actually work: the Input override below reads it back out of selectProps (react-select's
|
|
44
|
+
* untouched copy of everything passed to Select) and merges it alongside react-select's
|
|
45
|
+
* own separately-computed value, rather than it being silently dropped. */
|
|
46
|
+
'aria-describedby'?: string;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const buildClassNames = <Option, IsMulti extends boolean, Group extends GroupBase<Option>>(
|
|
50
|
+
invalid: boolean,
|
|
51
|
+
userClassNames?: ClassNamesConfig<Option, IsMulti, Group>,
|
|
52
|
+
): ClassNamesConfig<Option, IsMulti, Group> => ({
|
|
53
|
+
container: (props) => cn('cn-select-container', userClassNames?.container?.(props)),
|
|
54
|
+
control: (props) =>
|
|
55
|
+
cn(
|
|
56
|
+
'cn-select-control',
|
|
57
|
+
props.isFocused && 'cn-select-control-focused',
|
|
58
|
+
invalid && !props.isFocused && 'cn-select-control-invalid',
|
|
59
|
+
props.isDisabled && 'cn-select-control-disabled',
|
|
60
|
+
userClassNames?.control?.(props),
|
|
61
|
+
),
|
|
62
|
+
valueContainer: (props) =>
|
|
63
|
+
cn(
|
|
64
|
+
'cn-select-value-container',
|
|
65
|
+
props.isMulti && 'cn-select-value-container-multi',
|
|
66
|
+
userClassNames?.valueContainer?.(props),
|
|
67
|
+
),
|
|
68
|
+
singleValue: (props) => cn('cn-select-single-value', userClassNames?.singleValue?.(props)),
|
|
69
|
+
placeholder: (props) => cn('cn-select-placeholder', userClassNames?.placeholder?.(props)),
|
|
70
|
+
input: (props) => cn('cn-select-input', userClassNames?.input?.(props)),
|
|
71
|
+
indicatorsContainer: (props) =>
|
|
72
|
+
cn('cn-select-indicators', userClassNames?.indicatorsContainer?.(props)),
|
|
73
|
+
indicatorSeparator: () => 'hidden',
|
|
74
|
+
dropdownIndicator: (props) =>
|
|
75
|
+
cn(
|
|
76
|
+
'cn-select-dropdown-indicator',
|
|
77
|
+
props.selectProps.menuIsOpen && 'cn-select-dropdown-indicator-open',
|
|
78
|
+
userClassNames?.dropdownIndicator?.(props),
|
|
79
|
+
),
|
|
80
|
+
clearIndicator: (props) =>
|
|
81
|
+
cn('cn-select-clear-indicator', userClassNames?.clearIndicator?.(props)),
|
|
82
|
+
loadingIndicator: (props) =>
|
|
83
|
+
cn('cn-select-loading-indicator', userClassNames?.loadingIndicator?.(props)),
|
|
84
|
+
menu: (props) =>
|
|
85
|
+
cn(
|
|
86
|
+
'cn-select-menu',
|
|
87
|
+
props.placement === 'top' && 'cn-select-menu-top',
|
|
88
|
+
userClassNames?.menu?.(props),
|
|
89
|
+
),
|
|
90
|
+
menuList: (props) => cn('cn-select-menu-list', userClassNames?.menuList?.(props)),
|
|
91
|
+
option: (props) =>
|
|
92
|
+
cn(
|
|
93
|
+
'cn-select-option',
|
|
94
|
+
props.isFocused && !props.isSelected && 'cn-select-option-focused',
|
|
95
|
+
props.isSelected && 'cn-select-option-selected',
|
|
96
|
+
props.isDisabled && 'cn-select-option-disabled',
|
|
97
|
+
userClassNames?.option?.(props),
|
|
98
|
+
),
|
|
99
|
+
noOptionsMessage: (props) =>
|
|
100
|
+
cn('cn-select-no-options', userClassNames?.noOptionsMessage?.(props)),
|
|
101
|
+
multiValue: (props) => cn('cn-select-multi-value', userClassNames?.multiValue?.(props)),
|
|
102
|
+
multiValueLabel: (props) =>
|
|
103
|
+
cn('cn-select-multi-value-label', userClassNames?.multiValueLabel?.(props)),
|
|
104
|
+
multiValueRemove: (props) =>
|
|
105
|
+
cn('cn-select-multi-value-remove', userClassNames?.multiValueRemove?.(props)),
|
|
106
|
+
group: (props) => cn('cn-select-group', userClassNames?.group?.(props)),
|
|
107
|
+
groupHeading: (props) => cn('cn-select-group-heading', userClassNames?.groupHeading?.(props)),
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
const DropdownIndicator = <Option, IsMulti extends boolean, Group extends GroupBase<Option>>(
|
|
111
|
+
props: DropdownIndicatorProps<Option, IsMulti, Group>,
|
|
112
|
+
) => (
|
|
113
|
+
<LuChevronDown
|
|
114
|
+
className={props.getClassNames('dropdownIndicator', props)}
|
|
115
|
+
strokeWidth={2.5}
|
|
116
|
+
aria-hidden
|
|
117
|
+
/>
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
const ClearIndicator = <Option, IsMulti extends boolean, Group extends GroupBase<Option>>(
|
|
121
|
+
props: ClearIndicatorProps<Option, IsMulti, Group>,
|
|
122
|
+
) => (
|
|
123
|
+
<components.ClearIndicator {...props}>
|
|
124
|
+
<LuX className="w-full h-full" strokeWidth={2.5} aria-hidden />
|
|
125
|
+
</components.ClearIndicator>
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
const LoadingIndicator = <Option, IsMulti extends boolean, Group extends GroupBase<Option>>(
|
|
129
|
+
props: LoadingIndicatorProps<Option, IsMulti, Group>,
|
|
130
|
+
) => (
|
|
131
|
+
<LuLoaderCircle
|
|
132
|
+
className={cn(props.getClassNames('loadingIndicator', props), 'animate-spin')}
|
|
133
|
+
aria-hidden
|
|
134
|
+
/>
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
// react-select's own renderInput() computes its own aria-describedby (pointing
|
|
138
|
+
// at its internal live-region or placeholder element) and spreads it onto the
|
|
139
|
+
// real <input> last, after any of its own props, so it always wins over
|
|
140
|
+
// whatever Select was originally given, see SelectProps['aria-describedby'].
|
|
141
|
+
// selectProps is react-select's own full, unfiltered instance props object
|
|
142
|
+
// though, still holding that original untouched value, so it survives there
|
|
143
|
+
// and can be merged back in, just needs a cast, react-select's own Props type
|
|
144
|
+
// doesn't declare aria-describedby at all (unlike its aria-invalid/label/etc,
|
|
145
|
+
// which it does forward as-is, no merging needed for those).
|
|
146
|
+
const Input = <Option, IsMulti extends boolean, Group extends GroupBase<Option>>(
|
|
147
|
+
props: InputProps<Option, IsMulti, Group>,
|
|
148
|
+
) => {
|
|
149
|
+
const external = (props.selectProps as { 'aria-describedby'?: string })['aria-describedby'];
|
|
150
|
+
const ariaDescribedBy = [props['aria-describedby'], external].filter(Boolean).join(' ');
|
|
151
|
+
|
|
152
|
+
return <components.Input {...props} aria-describedby={ariaDescribedBy || undefined} />;
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
const GroupHeading = <Option, IsMulti extends boolean, Group extends GroupBase<Option>>(
|
|
156
|
+
props: GroupHeadingProps<Option, IsMulti, Group>,
|
|
157
|
+
) => <Separator className={props.getClassNames('groupHeading', props)}>{props.children}</Separator>;
|
|
158
|
+
|
|
159
|
+
const MultiValue = <Option, IsMulti extends boolean, Group extends GroupBase<Option>>({
|
|
160
|
+
removeProps,
|
|
161
|
+
isDisabled,
|
|
162
|
+
selectProps,
|
|
163
|
+
data,
|
|
164
|
+
}: MultiValueProps<Option, IsMulti, Group>) => {
|
|
165
|
+
const label = selectProps.getOptionLabel(data);
|
|
166
|
+
|
|
167
|
+
return (
|
|
168
|
+
<button
|
|
169
|
+
type="button"
|
|
170
|
+
className="cn-select-multi-value"
|
|
171
|
+
aria-label={`Remove ${label}`}
|
|
172
|
+
disabled={isDisabled}
|
|
173
|
+
onClick={removeProps.onClick as unknown as React.MouseEventHandler<HTMLButtonElement>}
|
|
174
|
+
onMouseDown={removeProps.onMouseDown as unknown as React.MouseEventHandler<HTMLButtonElement>}
|
|
175
|
+
onTouchEnd={removeProps.onTouchEnd as unknown as React.TouchEventHandler<HTMLButtonElement>}
|
|
176
|
+
>
|
|
177
|
+
{label}
|
|
178
|
+
<LuX aria-hidden className="cn-select-multi-value-remove" strokeWidth={2.5} />
|
|
179
|
+
</button>
|
|
180
|
+
);
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
const HiddenMultiValue = () => null;
|
|
184
|
+
|
|
185
|
+
const HiddenLoadingIndicator = () => null;
|
|
186
|
+
|
|
187
|
+
const SelectContainerWithTagsBelow = <
|
|
188
|
+
Option,
|
|
189
|
+
IsMulti extends boolean,
|
|
190
|
+
Group extends GroupBase<Option>,
|
|
191
|
+
>(
|
|
192
|
+
props: ContainerProps<Option, IsMulti, Group>,
|
|
193
|
+
) => {
|
|
194
|
+
const { children, selectProps, getValue, hasValue } = props;
|
|
195
|
+
const { getOptionLabel, getOptionValue, onChange, name, isDisabled } = selectProps;
|
|
196
|
+
|
|
197
|
+
const handleRemove = (option: Option) => {
|
|
198
|
+
const candidate = getOptionValue(option);
|
|
199
|
+
const newValue = getValue().filter((item) => getOptionValue(item) !== candidate);
|
|
200
|
+
|
|
201
|
+
onChange(newValue as OnChangeValue<Option, IsMulti>, {
|
|
202
|
+
action: 'remove-value',
|
|
203
|
+
removedValue: option,
|
|
204
|
+
name,
|
|
205
|
+
});
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
return (
|
|
209
|
+
<components.SelectContainer {...props}>
|
|
210
|
+
{children}
|
|
211
|
+
|
|
212
|
+
{hasValue ? (
|
|
213
|
+
<div className="cn-select-tags">
|
|
214
|
+
{getValue().map((option) => (
|
|
215
|
+
<button
|
|
216
|
+
key={getOptionValue(option)}
|
|
217
|
+
type="button"
|
|
218
|
+
className="cn-select-tag"
|
|
219
|
+
aria-label={`Remove ${getOptionLabel(option)}`}
|
|
220
|
+
disabled={isDisabled}
|
|
221
|
+
onClick={() => handleRemove(option)}
|
|
222
|
+
>
|
|
223
|
+
{getOptionLabel(option)}
|
|
224
|
+
<LuX aria-hidden className="cn-select-multi-value-remove" strokeWidth={2.5} />
|
|
225
|
+
</button>
|
|
226
|
+
))}
|
|
227
|
+
</div>
|
|
228
|
+
) : null}
|
|
229
|
+
</components.SelectContainer>
|
|
230
|
+
);
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
export const Select = <
|
|
234
|
+
Option = unknown,
|
|
235
|
+
IsMulti extends boolean = false,
|
|
236
|
+
Group extends GroupBase<Option> = GroupBase<Option>,
|
|
237
|
+
>({
|
|
238
|
+
classNames: userClassNames,
|
|
239
|
+
styles: userStyles,
|
|
240
|
+
invalid = false,
|
|
241
|
+
isMulti,
|
|
242
|
+
tagsPlacement = 'inline',
|
|
243
|
+
hideLoadingIndicator = false,
|
|
244
|
+
...props
|
|
245
|
+
}: SelectProps<Option, IsMulti, Group>) => {
|
|
246
|
+
const instanceId = useId();
|
|
247
|
+
const showTagsBelow = isMulti && tagsPlacement === 'below';
|
|
248
|
+
|
|
249
|
+
return (
|
|
250
|
+
<ReactSelect
|
|
251
|
+
menuPortalTarget={typeof document !== 'undefined' ? document.body : null}
|
|
252
|
+
menuPosition="fixed"
|
|
253
|
+
tabSelectsValue={false}
|
|
254
|
+
{...props}
|
|
255
|
+
isMulti={isMulti}
|
|
256
|
+
instanceId={instanceId}
|
|
257
|
+
unstyled
|
|
258
|
+
aria-invalid={invalid ? 'true' : undefined}
|
|
259
|
+
styles={{
|
|
260
|
+
...userStyles,
|
|
261
|
+
// react-select injects outline: 0 !important and transition via Emotion
|
|
262
|
+
// on the control div even in unstyled mode. By returning an empty object
|
|
263
|
+
// (not spreading base) we prevent that class from being generated, so
|
|
264
|
+
// our CSS classes can set outline freely without needing !important.
|
|
265
|
+
control: (_, controlProps) => ({
|
|
266
|
+
...userStyles?.control?.({}, controlProps),
|
|
267
|
+
}),
|
|
268
|
+
// Removes the `pointer-events: none` style.
|
|
269
|
+
container: (_, containerProps) => ({
|
|
270
|
+
...userStyles?.container?.({}, containerProps),
|
|
271
|
+
}),
|
|
272
|
+
// react-select renders the portal wrapper itself with an inline
|
|
273
|
+
// style, a separate element from .cn-select-menu (which already
|
|
274
|
+
// has cn-z-overlay), so it isn't reachable via our own CSS classes
|
|
275
|
+
// and has to independently match that same value (999999) here, or
|
|
276
|
+
// this outer wrapper's z-index is what actually determines stacking
|
|
277
|
+
// against unrelated high z-index content elsewhere on the page.
|
|
278
|
+
menuPortal: (base, portalProps) => ({
|
|
279
|
+
...base,
|
|
280
|
+
zIndex: 999999,
|
|
281
|
+
...userStyles?.menuPortal?.(base, portalProps),
|
|
282
|
+
}),
|
|
283
|
+
}}
|
|
284
|
+
classNames={buildClassNames<Option, IsMulti, Group>(invalid, userClassNames)}
|
|
285
|
+
components={{
|
|
286
|
+
Input,
|
|
287
|
+
DropdownIndicator,
|
|
288
|
+
ClearIndicator,
|
|
289
|
+
LoadingIndicator: hideLoadingIndicator ? HiddenLoadingIndicator : LoadingIndicator,
|
|
290
|
+
MultiValue: showTagsBelow ? HiddenMultiValue : MultiValue,
|
|
291
|
+
GroupHeading,
|
|
292
|
+
...(showTagsBelow ? { SelectContainer: SelectContainerWithTagsBelow } : {}),
|
|
293
|
+
}}
|
|
294
|
+
/>
|
|
295
|
+
);
|
|
296
|
+
};
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react-vite';
|
|
2
|
+
|
|
3
|
+
import { Select } from './Select';
|
|
4
|
+
import { SelectSkeleton } from './SelectSkeleton';
|
|
5
|
+
|
|
6
|
+
const meta: Meta<typeof SelectSkeleton> = {
|
|
7
|
+
title: 'Components/Select/Skeleton',
|
|
8
|
+
component: SelectSkeleton,
|
|
9
|
+
tags: ['autodocs'],
|
|
10
|
+
parameters: {
|
|
11
|
+
docs: {
|
|
12
|
+
description: {
|
|
13
|
+
component:
|
|
14
|
+
'Placeholder for a Select while its options are loading. Reuses cn-select-container/control/value-container/indicators directly, so its dimensions always match the real control exactly, no layout shift when it swaps in.',
|
|
15
|
+
},
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export default meta;
|
|
21
|
+
|
|
22
|
+
type Story = StoryObj<typeof SelectSkeleton>;
|
|
23
|
+
|
|
24
|
+
export const Default: Story = {
|
|
25
|
+
render: () => (
|
|
26
|
+
<div className="w-64">
|
|
27
|
+
<SelectSkeleton />
|
|
28
|
+
</div>
|
|
29
|
+
),
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export const SideBySide: Story = {
|
|
33
|
+
parameters: {
|
|
34
|
+
docs: {
|
|
35
|
+
description: {
|
|
36
|
+
story: 'The skeleton and the real control line up exactly, same height, border, padding.',
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
render: () => (
|
|
41
|
+
<div className="flex gap-gds-4">
|
|
42
|
+
<div className="w-64">
|
|
43
|
+
<SelectSkeleton />
|
|
44
|
+
</div>
|
|
45
|
+
|
|
46
|
+
<div className="w-64">
|
|
47
|
+
<Select options={[{ value: 'a', label: 'Option A' }]} placeholder="Select an option" />
|
|
48
|
+
</div>
|
|
49
|
+
</div>
|
|
50
|
+
),
|
|
51
|
+
};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { ComponentProps } from 'react';
|
|
2
|
+
|
|
3
|
+
import { cn } from '../../utils/components';
|
|
4
|
+
import { Skeleton } from '../Skeleton/Skeleton';
|
|
5
|
+
|
|
6
|
+
export type SelectSkeletonProps = ComponentProps<'div'> & {
|
|
7
|
+
/** Explicitly disallow children, a skeleton is a placeholder shape, not a content wrapper. */
|
|
8
|
+
children?: never;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export const SelectSkeleton = ({ className, ...props }: SelectSkeletonProps) => (
|
|
12
|
+
<div aria-hidden className={cn('cn-select-container', className)} {...props}>
|
|
13
|
+
<div className="cn-select-control">
|
|
14
|
+
<div className="cn-select-value-container">
|
|
15
|
+
<Skeleton className="cn-select-skeleton-value" />
|
|
16
|
+
</div>
|
|
17
|
+
|
|
18
|
+
<div className="cn-select-indicators">
|
|
19
|
+
<Skeleton className="cn-select-skeleton-indicator" />
|
|
20
|
+
</div>
|
|
21
|
+
</div>
|
|
22
|
+
</div>
|
|
23
|
+
);
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
@layer components {
|
|
2
|
+
/*
|
|
3
|
+
MARK: Separator
|
|
4
|
+
GDS section break using the divider colour.
|
|
5
|
+
No external margin. Spacing belongs to the surrounding layout.
|
|
6
|
+
|
|
7
|
+
Vertical separators require a flex parent to get natural height via
|
|
8
|
+
align-self: stretch. Without one, set an explicit height on the separator.
|
|
9
|
+
*/
|
|
10
|
+
.cn-separator {
|
|
11
|
+
@apply relative flex items-center justify-center;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
.cn-separator[data-orientation='horizontal'] {
|
|
15
|
+
@apply w-full max-h-px;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
.cn-separator[data-orientation='vertical'] {
|
|
19
|
+
@apply self-stretch flex-col max-w-px;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/* MARK: Line */
|
|
23
|
+
|
|
24
|
+
.cn-separator-line {
|
|
25
|
+
@apply absolute shrink-0 bg-divider border-none;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
.cn-separator-line[data-orientation='horizontal'] {
|
|
29
|
+
@apply w-full top-1/2 -translate-y-1/2 h-px;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
.cn-separator-line[data-orientation='vertical'] {
|
|
33
|
+
@apply h-full left-1/2 -translate-x-1/2 w-px;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/* MARK: Content */
|
|
37
|
+
|
|
38
|
+
.cn-separator-content {
|
|
39
|
+
@apply relative z-10;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react-vite';
|
|
2
|
+
|
|
3
|
+
import { Separator } from './Separator';
|
|
4
|
+
|
|
5
|
+
const meta: Meta<typeof Separator> = {
|
|
6
|
+
title: 'Components/Separator',
|
|
7
|
+
component: Separator,
|
|
8
|
+
tags: ['autodocs'],
|
|
9
|
+
parameters: {
|
|
10
|
+
docs: {
|
|
11
|
+
description: {
|
|
12
|
+
component: 'Use Separator to visually divide content sections in layouts and forms.',
|
|
13
|
+
},
|
|
14
|
+
},
|
|
15
|
+
},
|
|
16
|
+
argTypes: {
|
|
17
|
+
orientation: {
|
|
18
|
+
control: { type: 'radio' },
|
|
19
|
+
options: ['horizontal', 'vertical'],
|
|
20
|
+
},
|
|
21
|
+
decorative: { control: 'boolean' },
|
|
22
|
+
},
|
|
23
|
+
args: {
|
|
24
|
+
orientation: 'horizontal',
|
|
25
|
+
decorative: true,
|
|
26
|
+
},
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export default meta;
|
|
30
|
+
|
|
31
|
+
type Story = StoryObj<typeof Separator>;
|
|
32
|
+
|
|
33
|
+
export const Horizontal: Story = {};
|
|
34
|
+
|
|
35
|
+
export const Vertical: Story = {
|
|
36
|
+
args: {
|
|
37
|
+
orientation: 'vertical',
|
|
38
|
+
decorative: false,
|
|
39
|
+
// hardcode height for example otherwise it will collapse to 0px
|
|
40
|
+
style: { height: '50px' },
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export const WithText: Story = {
|
|
45
|
+
args: {
|
|
46
|
+
orientation: 'horizontal',
|
|
47
|
+
children: 'Section Title',
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
export const InlineExample: Story = {
|
|
52
|
+
// Vertical separators can't have children.
|
|
53
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
54
|
+
render: ({ children, ...args }) => (
|
|
55
|
+
<div className="flex h-24 items-center gap-3">
|
|
56
|
+
<span>Left content</span>
|
|
57
|
+
|
|
58
|
+
<Separator {...args} orientation="vertical" decorative={false} />
|
|
59
|
+
|
|
60
|
+
<span>Right content</span>
|
|
61
|
+
</div>
|
|
62
|
+
),
|
|
63
|
+
};
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import type { ComponentProps, ReactNode } from 'react';
|
|
4
|
+
|
|
5
|
+
import { Separator as SeparatorPrimitive } from 'radix-ui';
|
|
6
|
+
|
|
7
|
+
import { cn } from '../../utils/components';
|
|
8
|
+
import { Chip } from '../Chip/Chip';
|
|
9
|
+
|
|
10
|
+
type HorizontalSeparatorProps = ComponentProps<typeof SeparatorPrimitive.Root> & {
|
|
11
|
+
orientation?: 'horizontal';
|
|
12
|
+
children?: ReactNode;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
type VerticalSeparatorProps = ComponentProps<typeof SeparatorPrimitive.Root> & {
|
|
16
|
+
orientation: 'vertical';
|
|
17
|
+
children?: never;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export type SeparatorProps = HorizontalSeparatorProps | VerticalSeparatorProps;
|
|
21
|
+
|
|
22
|
+
export const Separator = ({
|
|
23
|
+
className,
|
|
24
|
+
orientation,
|
|
25
|
+
decorative = true,
|
|
26
|
+
children,
|
|
27
|
+
...props
|
|
28
|
+
}: SeparatorProps) => (
|
|
29
|
+
<div {...props} className={cn('cn-separator', className)} data-orientation={orientation}>
|
|
30
|
+
<SeparatorPrimitive.Root
|
|
31
|
+
data-slot="separator"
|
|
32
|
+
decorative={decorative}
|
|
33
|
+
orientation={orientation}
|
|
34
|
+
className="cn-separator-line"
|
|
35
|
+
/>
|
|
36
|
+
{children && (
|
|
37
|
+
<Chip className="cn-separator-content" appearance="neutral" size="sm">
|
|
38
|
+
{children}
|
|
39
|
+
</Chip>
|
|
40
|
+
)}
|
|
41
|
+
</div>
|
|
42
|
+
);
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useLayoutEffect, useRef, useState, type ComponentProps } from 'react';
|
|
4
|
+
|
|
5
|
+
import { usePanelRef } from 'react-resizable-panels';
|
|
6
|
+
|
|
7
|
+
import { useSidebar } from './Sidebar';
|
|
8
|
+
import { cn } from '../../utils/components';
|
|
9
|
+
import { ResizableHandle, ResizablePanel } from '../Resizable/Resizable';
|
|
10
|
+
|
|
11
|
+
export type ResizableSidebarProps = Omit<
|
|
12
|
+
ComponentProps<typeof ResizablePanel>,
|
|
13
|
+
'panelRef' | 'onResize' | 'collapsible'
|
|
14
|
+
>;
|
|
15
|
+
|
|
16
|
+
export const ResizableSidebar = ({
|
|
17
|
+
className,
|
|
18
|
+
defaultSize = '16rem',
|
|
19
|
+
minSize = '12rem',
|
|
20
|
+
collapsedSize = 0,
|
|
21
|
+
...props
|
|
22
|
+
}: ResizableSidebarProps) => {
|
|
23
|
+
const { open, setOpen } = useSidebar();
|
|
24
|
+
const panelRef = usePanelRef();
|
|
25
|
+
const isFirstRender = useRef(true);
|
|
26
|
+
|
|
27
|
+
// Captured once and never recomputed. Panel's own registration (and the
|
|
28
|
+
// "most recent size" expand() restores) depends on this prop, so passing
|
|
29
|
+
// a value that changes every time `open` toggles corrupts that memory,
|
|
30
|
+
// reopening lands back on this value instead of wherever the user last
|
|
31
|
+
// dragged to before closing.
|
|
32
|
+
const [initialSize] = useState(() => (open ? defaultSize : collapsedSize));
|
|
33
|
+
|
|
34
|
+
// Skip the very first run: ResizableSidebar commits before its parent
|
|
35
|
+
// ResizablePanelGroup finishes registering itself, so calling expand() or
|
|
36
|
+
// collapse() here on mount throws ("Group not found"). defaultSize below
|
|
37
|
+
// already renders the correct initial state, so this effect only needs to
|
|
38
|
+
// handle `open` changing after mount, by which point the group is stable.
|
|
39
|
+
//
|
|
40
|
+
// The only drag-driven `open` change left is closing (handleResize below
|
|
41
|
+
// sets it): ResizableSidebarHandle disables itself while closed, so a drag
|
|
42
|
+
// can never produce the reverse (open) transition. collapse() here is a
|
|
43
|
+
// harmless no-op for that case, the panel's already exactly at
|
|
44
|
+
// collapsedSize by the time the drag triggers it.
|
|
45
|
+
useLayoutEffect(() => {
|
|
46
|
+
if (isFirstRender.current) {
|
|
47
|
+
isFirstRender.current = false;
|
|
48
|
+
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// expand()/collapse() throw if this panel's ResizablePanelGroup isn't
|
|
53
|
+
// currently registered (react-resizable-panels keeps a module-level
|
|
54
|
+
// registry, looked up by id on every call, not a stable reference). The
|
|
55
|
+
// isFirstRender guard above only covers this component's own first
|
|
56
|
+
// commit; a Next.js route transition can still unmount/remount the whole
|
|
57
|
+
// group+panel tree, or briefly leave it unregistered, independently of
|
|
58
|
+
// that. There's nothing to sync in that instant, the panel already
|
|
59
|
+
// renders at the correct size once mounted, so swallow it rather than
|
|
60
|
+
// crashing the page.
|
|
61
|
+
try {
|
|
62
|
+
if (open) {
|
|
63
|
+
panelRef.current?.expand();
|
|
64
|
+
} else {
|
|
65
|
+
panelRef.current?.collapse();
|
|
66
|
+
}
|
|
67
|
+
} catch {
|
|
68
|
+
// no-op, see comment above
|
|
69
|
+
}
|
|
70
|
+
}, [open, panelRef]);
|
|
71
|
+
|
|
72
|
+
const handleResize = () => {
|
|
73
|
+
const collapsed = panelRef.current?.isCollapsed() ?? false;
|
|
74
|
+
|
|
75
|
+
// Dragging the handle closed doesn't go through the trigger, so sync
|
|
76
|
+
// `open` back here to keep it (and the trigger button) accurate.
|
|
77
|
+
if (collapsed === open) {
|
|
78
|
+
setOpen(!collapsed);
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
return (
|
|
83
|
+
<ResizablePanel
|
|
84
|
+
panelRef={panelRef}
|
|
85
|
+
collapsible
|
|
86
|
+
defaultSize={initialSize}
|
|
87
|
+
minSize={minSize}
|
|
88
|
+
collapsedSize={collapsedSize}
|
|
89
|
+
onResize={handleResize}
|
|
90
|
+
// Matches Sidebar's own <aside inert={...}>: at 0 width the panel is
|
|
91
|
+
// still reachable by Tab unless explicitly made inert.
|
|
92
|
+
inert={open ? undefined : true}
|
|
93
|
+
className={cn('cn-resizable-sidebar', className)}
|
|
94
|
+
{...props}
|
|
95
|
+
/>
|
|
96
|
+
);
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
export type ResizableSidebarHandleProps = ComponentProps<typeof ResizableHandle>;
|
|
100
|
+
|
|
101
|
+
// react-resizable-panels won't drag a collapsible panel open again from a
|
|
102
|
+
// fully collapsed (0) size unless the gesture crosses roughly the halfway
|
|
103
|
+
// point to minSize, and arrow keys move in increments far too small to ever
|
|
104
|
+
// cross it, so a collapsed ResizableSidebar's handle would look broken rather
|
|
105
|
+
// than draggable if it stayed visible. Disabling it hides it entirely (see
|
|
106
|
+
// [data-separator='disabled'] in Resizable.css) and stops it responding to
|
|
107
|
+
// input, while still rendering the same mounted Separator: actually removing
|
|
108
|
+
// it from the tree while closed (an earlier version of this returned null)
|
|
109
|
+
// broke react-resizable-panels' own Group, unmounting/remounting a Separator
|
|
110
|
+
// confuses its internal pointer-listener bookkeeping, throwing
|
|
111
|
+
// "object that is not, or is no longer, usable" the next time you tried to
|
|
112
|
+
// drag anything in the group. `disabled` still lets a consumer force-disable
|
|
113
|
+
// it while open, for reasons unrelated to the collapsed state.
|
|
114
|
+
export const ResizableSidebarHandle = ({ disabled, ...props }: ResizableSidebarHandleProps) => {
|
|
115
|
+
const { open } = useSidebar();
|
|
116
|
+
|
|
117
|
+
return <ResizableHandle disabled={disabled ?? !open} {...props} />;
|
|
118
|
+
};
|