@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,66 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import type { ComponentProps } from 'react';
|
|
4
|
+
|
|
5
|
+
import { Tabs as TabsPrimitive } from 'radix-ui';
|
|
6
|
+
|
|
7
|
+
import { cn } from '../../utils/components';
|
|
8
|
+
|
|
9
|
+
export type TabsProps<T extends string> = Omit<
|
|
10
|
+
ComponentProps<typeof TabsPrimitive.Root>,
|
|
11
|
+
'defaultValue' | 'value' | 'onValueChange'
|
|
12
|
+
> & {
|
|
13
|
+
defaultValue?: T;
|
|
14
|
+
value?: T;
|
|
15
|
+
onValueChange?: (value: T) => void;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export const Tabs = <T extends string = string>({
|
|
19
|
+
className,
|
|
20
|
+
defaultValue,
|
|
21
|
+
value,
|
|
22
|
+
onValueChange,
|
|
23
|
+
...props
|
|
24
|
+
}: TabsProps<T>) => (
|
|
25
|
+
<TabsPrimitive.Root
|
|
26
|
+
data-slot="tabs"
|
|
27
|
+
className={cn('cn-tabs', className)}
|
|
28
|
+
defaultValue={defaultValue as string}
|
|
29
|
+
value={value as string}
|
|
30
|
+
onValueChange={(value) => onValueChange?.(value as T)}
|
|
31
|
+
{...props}
|
|
32
|
+
/>
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
export type TabsListProps = ComponentProps<typeof TabsPrimitive.List>;
|
|
36
|
+
|
|
37
|
+
export const TabsList = ({ className, ...props }: TabsListProps) => (
|
|
38
|
+
<TabsPrimitive.List data-slot="tabs-list" className={cn('cn-tabs-list', className)} {...props} />
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
export type TabsTriggerProps = ComponentProps<typeof TabsPrimitive.Trigger>;
|
|
42
|
+
|
|
43
|
+
export const TabsTrigger = ({ className, children, ...props }: TabsTriggerProps) => (
|
|
44
|
+
<TabsPrimitive.Trigger
|
|
45
|
+
data-slot="tabs-trigger"
|
|
46
|
+
className={cn('cn-tabs-trigger', className)}
|
|
47
|
+
{...props}
|
|
48
|
+
>
|
|
49
|
+
{/* Bold copy reserves the active-state width so siblings never shift. */}
|
|
50
|
+
<span aria-hidden className="cn-tabs-trigger-sizer">
|
|
51
|
+
{children}
|
|
52
|
+
</span>
|
|
53
|
+
|
|
54
|
+
<span className="cn-tabs-trigger-text">{children}</span>
|
|
55
|
+
</TabsPrimitive.Trigger>
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
export type TabsContentProps = ComponentProps<typeof TabsPrimitive.Content>;
|
|
59
|
+
|
|
60
|
+
export const TabsContent = ({ className, ...props }: TabsContentProps) => (
|
|
61
|
+
<TabsPrimitive.Content
|
|
62
|
+
data-slot="tabs-content"
|
|
63
|
+
className={cn('cn-tabs-content', className)}
|
|
64
|
+
{...props}
|
|
65
|
+
/>
|
|
66
|
+
);
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react-vite';
|
|
2
|
+
|
|
3
|
+
import { Tag } from './Tag';
|
|
4
|
+
|
|
5
|
+
const meta: Meta<typeof Tag> = {
|
|
6
|
+
title: 'Components/Tag',
|
|
7
|
+
component: Tag,
|
|
8
|
+
tags: ['autodocs'],
|
|
9
|
+
parameters: {
|
|
10
|
+
docs: {
|
|
11
|
+
description: {
|
|
12
|
+
component:
|
|
13
|
+
'GDS-style status label for communicating phase, status, or category. Rendered as `<strong>` for semantic importance. Uses the same colour palette as Chip but always bold.',
|
|
14
|
+
},
|
|
15
|
+
},
|
|
16
|
+
},
|
|
17
|
+
argTypes: {
|
|
18
|
+
appearance: {
|
|
19
|
+
control: { type: 'radio' },
|
|
20
|
+
options: ['neutral', 'success', 'warning', 'error', 'info'],
|
|
21
|
+
},
|
|
22
|
+
size: {
|
|
23
|
+
control: { type: 'radio' },
|
|
24
|
+
options: ['sm', 'base', 'lg'],
|
|
25
|
+
},
|
|
26
|
+
children: { control: 'text' },
|
|
27
|
+
},
|
|
28
|
+
args: {
|
|
29
|
+
appearance: 'info',
|
|
30
|
+
children: 'Alpha',
|
|
31
|
+
size: 'base',
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export default meta;
|
|
36
|
+
|
|
37
|
+
type Story = StoryObj<typeof Tag>;
|
|
38
|
+
|
|
39
|
+
export const Default: Story = {};
|
|
40
|
+
|
|
41
|
+
export const Appearances: Story = {
|
|
42
|
+
parameters: {
|
|
43
|
+
docs: {
|
|
44
|
+
description: {
|
|
45
|
+
story: 'All appearances share the same subtle tinted palette as Chip.',
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
render: (args) => (
|
|
50
|
+
<div className="flex flex-wrap gap-gds-2">
|
|
51
|
+
<Tag {...args} appearance="neutral">
|
|
52
|
+
Inactive
|
|
53
|
+
</Tag>
|
|
54
|
+
|
|
55
|
+
<Tag {...args} appearance="success">
|
|
56
|
+
Active
|
|
57
|
+
</Tag>
|
|
58
|
+
|
|
59
|
+
<Tag {...args} appearance="warning">
|
|
60
|
+
Pending
|
|
61
|
+
</Tag>
|
|
62
|
+
|
|
63
|
+
<Tag {...args} appearance="error">
|
|
64
|
+
Rejected
|
|
65
|
+
</Tag>
|
|
66
|
+
|
|
67
|
+
<Tag {...args} appearance="info">
|
|
68
|
+
Alpha
|
|
69
|
+
</Tag>
|
|
70
|
+
</div>
|
|
71
|
+
),
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
export const Small: Story = {
|
|
75
|
+
args: { size: 'sm', children: 'Beta' },
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
export const Large: Story = {
|
|
79
|
+
args: { size: 'lg', children: 'New' },
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
export const Inline: Story = {
|
|
83
|
+
parameters: {
|
|
84
|
+
docs: {
|
|
85
|
+
description: {
|
|
86
|
+
story:
|
|
87
|
+
'Tags use negative margins so they sit inline within body text without inflating line height.',
|
|
88
|
+
},
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
render: (args) => (
|
|
92
|
+
<p>
|
|
93
|
+
This service is <Tag {...args} /> and actively being developed.
|
|
94
|
+
</p>
|
|
95
|
+
),
|
|
96
|
+
};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { ComponentProps } from 'react';
|
|
2
|
+
|
|
3
|
+
import { cn } from '../../utils/components';
|
|
4
|
+
|
|
5
|
+
const TAG_APPEARANCES = {
|
|
6
|
+
neutral: 'cn-chip-appearance-neutral',
|
|
7
|
+
success: 'cn-chip-appearance-success',
|
|
8
|
+
warning: 'cn-chip-appearance-warning',
|
|
9
|
+
error: 'cn-chip-appearance-error',
|
|
10
|
+
info: 'cn-chip-appearance-info',
|
|
11
|
+
} as const;
|
|
12
|
+
|
|
13
|
+
const TAG_SIZES = {
|
|
14
|
+
sm: 'cn-chip-size-sm',
|
|
15
|
+
base: 'cn-chip-size-base',
|
|
16
|
+
lg: 'cn-chip-size-lg',
|
|
17
|
+
} as const;
|
|
18
|
+
|
|
19
|
+
export type TagAppearance = keyof typeof TAG_APPEARANCES;
|
|
20
|
+
export type TagSize = keyof typeof TAG_SIZES;
|
|
21
|
+
|
|
22
|
+
export type TagProps = ComponentProps<'strong'> & {
|
|
23
|
+
appearance: TagAppearance;
|
|
24
|
+
size?: TagSize;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export const Tag = ({ className, appearance, size = 'base', ...props }: TagProps) => (
|
|
28
|
+
<strong
|
|
29
|
+
data-slot="tag"
|
|
30
|
+
data-size={size}
|
|
31
|
+
className={cn('cn-tag', TAG_APPEARANCES[appearance], TAG_SIZES[size], className)}
|
|
32
|
+
{...props}
|
|
33
|
+
/>
|
|
34
|
+
);
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
@layer components {
|
|
2
|
+
/* MARK: Textarea
|
|
3
|
+
GDS textarea: same border and focus as Input, vertical resize only.
|
|
4
|
+
Height is content-driven via rows, min-height matches the input baseline. */
|
|
5
|
+
.cn-textarea {
|
|
6
|
+
@apply cn-form-border w-full p-gds-1 text-base box-border appearance-none resize-y min-h-32;
|
|
7
|
+
|
|
8
|
+
&:focus {
|
|
9
|
+
@apply outline-focus;
|
|
10
|
+
box-shadow: inset 0 0 0 var(--border-width-form) var(--color-divider-input);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
&[aria-invalid='true'],
|
|
14
|
+
&:user-invalid {
|
|
15
|
+
@apply border-error;
|
|
16
|
+
|
|
17
|
+
&:focus {
|
|
18
|
+
box-shadow: inset 0 0 0 var(--border-width-form) var(--color-error);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
&[readonly] {
|
|
23
|
+
@apply bg-wash resize-none;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
&:disabled,
|
|
27
|
+
&[aria-disabled='true'] {
|
|
28
|
+
@apply opacity-50 cursor-not-allowed select-none;
|
|
29
|
+
color: inherit;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/* MARK: Appearances */
|
|
34
|
+
.cn-textarea-appearance-code {
|
|
35
|
+
font-variant-numeric: tabular-nums;
|
|
36
|
+
letter-spacing: 0.05em;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import type { Meta, StoryObj } from '@storybook/react-vite';
|
|
4
|
+
|
|
5
|
+
import { Textarea } from './Textarea';
|
|
6
|
+
import { CopyButton } from '../CopyButton/CopyButton';
|
|
7
|
+
import { ErrorText } from '../ErrorText/ErrorText';
|
|
8
|
+
import { FormGroup } from '../Form/FormGroup';
|
|
9
|
+
import { HintText } from '../HintText/HintText';
|
|
10
|
+
import { InputGroup, InputToolbar } from '../InputGroup/InputGroup';
|
|
11
|
+
import { Label } from '../Label/Label';
|
|
12
|
+
import { TextareaCount, TextareaCountMessage } from '../TextareaCount/TextareaCount';
|
|
13
|
+
|
|
14
|
+
const meta: Meta<typeof Textarea> = {
|
|
15
|
+
title: 'Components/Textarea',
|
|
16
|
+
component: Textarea,
|
|
17
|
+
tags: ['autodocs'],
|
|
18
|
+
parameters: {
|
|
19
|
+
docs: {
|
|
20
|
+
description: {
|
|
21
|
+
component:
|
|
22
|
+
'GDS-style multi-line text input. Resizes vertically. Compose with `TextareaCount` and `TextareaCountMessage` to add character or word count feedback. Use a vertical `InputGroup` with `InputToolbar` to add action buttons above the field.',
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
argTypes: {
|
|
27
|
+
appearance: {
|
|
28
|
+
control: { type: 'radio' },
|
|
29
|
+
options: ['default', 'code'],
|
|
30
|
+
},
|
|
31
|
+
rows: { control: { type: 'number', min: 1, max: 20 } },
|
|
32
|
+
disabled: { control: 'boolean' },
|
|
33
|
+
},
|
|
34
|
+
args: {
|
|
35
|
+
rows: 5,
|
|
36
|
+
appearance: 'default',
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export default meta;
|
|
41
|
+
|
|
42
|
+
type Story = StoryObj<typeof Textarea>;
|
|
43
|
+
|
|
44
|
+
const SHARE_URL = 'https://environment.data.gov.uk/share/flood-risk-assessment/abc123xyz';
|
|
45
|
+
|
|
46
|
+
export const Default: Story = {};
|
|
47
|
+
|
|
48
|
+
export const Invalid: Story = {
|
|
49
|
+
render: (args) => (
|
|
50
|
+
<FormGroup invalid>
|
|
51
|
+
<Label htmlFor="textarea-invalid">Description</Label>
|
|
52
|
+
|
|
53
|
+
<ErrorText id="textarea-invalid-error">Enter a description</ErrorText>
|
|
54
|
+
|
|
55
|
+
<Textarea
|
|
56
|
+
id="textarea-invalid"
|
|
57
|
+
aria-invalid
|
|
58
|
+
aria-describedby="textarea-invalid-error"
|
|
59
|
+
{...args}
|
|
60
|
+
/>
|
|
61
|
+
</FormGroup>
|
|
62
|
+
),
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
export const ReadOnly: Story = {
|
|
66
|
+
args: {
|
|
67
|
+
readOnly: true,
|
|
68
|
+
defaultValue: 'This text can be selected and copied but not edited.',
|
|
69
|
+
rows: 3,
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
export const Disabled: Story = {
|
|
74
|
+
args: { disabled: true, value: 'This content cannot be edited' },
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
export const Code: Story = {
|
|
78
|
+
args: {
|
|
79
|
+
appearance: 'code',
|
|
80
|
+
rows: 8,
|
|
81
|
+
defaultValue: 'SELECT *\nFROM users\nWHERE active = true;',
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
export const WithToolbar: Story = {
|
|
86
|
+
render: () => {
|
|
87
|
+
return (
|
|
88
|
+
<div className="flex flex-col gap-gds-1">
|
|
89
|
+
<Label htmlFor="share-url">Share link</Label>
|
|
90
|
+
|
|
91
|
+
<HintText id="share-url-hint">Copy this link to share your flood risk assessment.</HintText>
|
|
92
|
+
|
|
93
|
+
<InputGroup direction="vertical">
|
|
94
|
+
<InputToolbar>
|
|
95
|
+
<CopyButton appearance="ghost" size="sm" content={SHARE_URL} />
|
|
96
|
+
</InputToolbar>
|
|
97
|
+
|
|
98
|
+
<Textarea
|
|
99
|
+
id="share-url"
|
|
100
|
+
readOnly
|
|
101
|
+
rows={3}
|
|
102
|
+
value={SHARE_URL}
|
|
103
|
+
aria-describedby="share-url-hint"
|
|
104
|
+
/>
|
|
105
|
+
</InputGroup>
|
|
106
|
+
</div>
|
|
107
|
+
);
|
|
108
|
+
},
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
export const WithToolbarAndCount: Story = {
|
|
112
|
+
render: () => (
|
|
113
|
+
<div className="flex flex-col gap-gds-1">
|
|
114
|
+
<Label htmlFor="notes-count">Notes</Label>
|
|
115
|
+
|
|
116
|
+
<HintText id="notes-count-hint">Do not enter more than 200 characters.</HintText>
|
|
117
|
+
|
|
118
|
+
<TextareaCount maxLength={200}>
|
|
119
|
+
<InputGroup direction="vertical">
|
|
120
|
+
<InputToolbar>
|
|
121
|
+
<CopyButton appearance="ghost" size="sm" content="Some content" />
|
|
122
|
+
</InputToolbar>
|
|
123
|
+
|
|
124
|
+
<Textarea id="notes-count" rows={5} aria-describedby="notes-count-hint" />
|
|
125
|
+
</InputGroup>
|
|
126
|
+
|
|
127
|
+
<TextareaCountMessage />
|
|
128
|
+
</TextareaCount>
|
|
129
|
+
</div>
|
|
130
|
+
),
|
|
131
|
+
};
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useContext, type ChangeEvent, type ComponentProps } from 'react';
|
|
4
|
+
|
|
5
|
+
import { cn } from '../../utils/components';
|
|
6
|
+
import { TextareaCountContext } from '../TextareaCount/TextareaCount';
|
|
7
|
+
|
|
8
|
+
const TEXTAREA_APPEARANCES = {
|
|
9
|
+
default: '',
|
|
10
|
+
code: 'cn-textarea-appearance-code',
|
|
11
|
+
} as const;
|
|
12
|
+
|
|
13
|
+
export type TextareaAppearance = keyof typeof TEXTAREA_APPEARANCES;
|
|
14
|
+
|
|
15
|
+
export type TextareaProps = ComponentProps<'textarea'> & {
|
|
16
|
+
appearance?: TextareaAppearance;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export const Textarea = ({
|
|
20
|
+
className,
|
|
21
|
+
appearance = 'default',
|
|
22
|
+
rows = 5,
|
|
23
|
+
onChange,
|
|
24
|
+
'aria-describedby': ariaDescribedby,
|
|
25
|
+
'aria-invalid': ariaInvalid,
|
|
26
|
+
...props
|
|
27
|
+
}: TextareaProps) => {
|
|
28
|
+
const countContext = useContext(TextareaCountContext);
|
|
29
|
+
const isCountOver = countContext !== null && countContext.currentCount > countContext.maxCount;
|
|
30
|
+
|
|
31
|
+
const handleChange = (event: ChangeEvent<HTMLTextAreaElement>) => {
|
|
32
|
+
countContext?.reportValue(event.target.value);
|
|
33
|
+
onChange?.(event);
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const describedBy =
|
|
37
|
+
[ariaDescribedby, countContext?.messageId].filter(Boolean).join(' ') || undefined;
|
|
38
|
+
|
|
39
|
+
return (
|
|
40
|
+
<textarea
|
|
41
|
+
rows={rows}
|
|
42
|
+
data-slot="textarea"
|
|
43
|
+
aria-describedby={describedBy}
|
|
44
|
+
aria-invalid={ariaInvalid ?? (isCountOver ? 'true' : undefined)}
|
|
45
|
+
className={cn('cn-textarea', TEXTAREA_APPEARANCES[appearance], className)}
|
|
46
|
+
onChange={handleChange}
|
|
47
|
+
{...props}
|
|
48
|
+
/>
|
|
49
|
+
);
|
|
50
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
@layer components {
|
|
2
|
+
/* MARK: Count message
|
|
3
|
+
Mirrors hint text styling in the normal state.
|
|
4
|
+
Hidden (but still in DOM for live region) when below threshold.
|
|
5
|
+
Over-limit state uses error styling. */
|
|
6
|
+
.cn-textarea-count-message {
|
|
7
|
+
@apply text-sm text-body-secondary mt-gds-1;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
.cn-textarea-count-message[data-hidden] {
|
|
11
|
+
visibility: hidden;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
.cn-textarea-count-message[data-over] {
|
|
15
|
+
@apply text-error font-bold;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState } from 'react';
|
|
4
|
+
|
|
5
|
+
import type { Meta, StoryObj } from '@storybook/react-vite';
|
|
6
|
+
|
|
7
|
+
import { TextareaCount, TextareaCountMessage } from './TextareaCount';
|
|
8
|
+
import { FormGroup } from '../Form/FormGroup';
|
|
9
|
+
import { Label } from '../Label/Label';
|
|
10
|
+
import { Textarea } from '../Textarea/Textarea';
|
|
11
|
+
|
|
12
|
+
const meta: Meta<typeof TextareaCount> = {
|
|
13
|
+
title: 'Components/TextareaCount',
|
|
14
|
+
component: TextareaCount,
|
|
15
|
+
tags: ['autodocs'],
|
|
16
|
+
parameters: {
|
|
17
|
+
docs: {
|
|
18
|
+
description: {
|
|
19
|
+
component:
|
|
20
|
+
'Wrapper that adds live character or word count feedback to a `Textarea`. Renders into context. Place `TextareaCountMessage` anywhere below the textarea to show the count hint.',
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export default meta;
|
|
27
|
+
|
|
28
|
+
type Story = StoryObj<typeof TextareaCount>;
|
|
29
|
+
|
|
30
|
+
export const CharacterCount: Story = {
|
|
31
|
+
render: () => (
|
|
32
|
+
<div>
|
|
33
|
+
<Label htmlFor="char-count-textarea">Description</Label>
|
|
34
|
+
|
|
35
|
+
<TextareaCount maxLength={200}>
|
|
36
|
+
<Textarea id="char-count-textarea" />
|
|
37
|
+
|
|
38
|
+
<TextareaCountMessage />
|
|
39
|
+
</TextareaCount>
|
|
40
|
+
</div>
|
|
41
|
+
),
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export const WordCount: Story = {
|
|
45
|
+
render: () => (
|
|
46
|
+
<div>
|
|
47
|
+
<Label htmlFor="word-count-textarea">Summary</Label>
|
|
48
|
+
|
|
49
|
+
<TextareaCount maxWords={50}>
|
|
50
|
+
<Textarea id="word-count-textarea" />
|
|
51
|
+
|
|
52
|
+
<TextareaCountMessage />
|
|
53
|
+
</TextareaCount>
|
|
54
|
+
</div>
|
|
55
|
+
),
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
export const WithThreshold: Story = {
|
|
59
|
+
parameters: {
|
|
60
|
+
docs: {
|
|
61
|
+
description: {
|
|
62
|
+
story:
|
|
63
|
+
'The count message is hidden until the user reaches 75% of the limit, then appears. Type more than 150 characters to see it.',
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
render: () => (
|
|
68
|
+
<div>
|
|
69
|
+
<Label htmlFor="threshold-textarea">Description</Label>
|
|
70
|
+
|
|
71
|
+
<TextareaCount maxLength={200} threshold={0.75}>
|
|
72
|
+
<Textarea id="threshold-textarea" />
|
|
73
|
+
|
|
74
|
+
<TextareaCountMessage />
|
|
75
|
+
</TextareaCount>
|
|
76
|
+
</div>
|
|
77
|
+
),
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
export const PreFilled: Story = {
|
|
81
|
+
parameters: {
|
|
82
|
+
docs: {
|
|
83
|
+
description: {
|
|
84
|
+
story:
|
|
85
|
+
'Pass `initialValue` to `TextareaCount` to seed the count when the textarea has a pre-filled value.',
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
render: () => {
|
|
90
|
+
const value = 'This textarea already has some content in it.';
|
|
91
|
+
|
|
92
|
+
return (
|
|
93
|
+
<div>
|
|
94
|
+
<Label htmlFor="prefilled-textarea">Description</Label>
|
|
95
|
+
|
|
96
|
+
<TextareaCount maxLength={200} initialValue={value}>
|
|
97
|
+
<Textarea id="prefilled-textarea" defaultValue={value} />
|
|
98
|
+
|
|
99
|
+
<TextareaCountMessage />
|
|
100
|
+
</TextareaCount>
|
|
101
|
+
</div>
|
|
102
|
+
);
|
|
103
|
+
},
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
export const OverLimit: Story = {
|
|
107
|
+
parameters: {
|
|
108
|
+
docs: {
|
|
109
|
+
description: {
|
|
110
|
+
story:
|
|
111
|
+
'The count message switches to error styling when the user exceeds the limit. `onValidityChange` fires whenever the over-limit boundary is crossed, letting a parent `FormGroup` react to the state.',
|
|
112
|
+
},
|
|
113
|
+
},
|
|
114
|
+
},
|
|
115
|
+
render: () => {
|
|
116
|
+
const value = 'a'.repeat(220);
|
|
117
|
+
const [isOver, setIsOver] = useState(false);
|
|
118
|
+
|
|
119
|
+
return (
|
|
120
|
+
<FormGroup invalid={isOver}>
|
|
121
|
+
<Label htmlFor="over-limit-textarea">Description</Label>
|
|
122
|
+
|
|
123
|
+
<TextareaCount maxLength={200} initialValue={value} onValidityChange={setIsOver}>
|
|
124
|
+
<Textarea id="over-limit-textarea" defaultValue={value} />
|
|
125
|
+
|
|
126
|
+
<TextareaCountMessage />
|
|
127
|
+
</TextareaCount>
|
|
128
|
+
</FormGroup>
|
|
129
|
+
);
|
|
130
|
+
},
|
|
131
|
+
};
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
createContext,
|
|
5
|
+
useCallback,
|
|
6
|
+
useContext,
|
|
7
|
+
useEffect,
|
|
8
|
+
useId,
|
|
9
|
+
useMemo,
|
|
10
|
+
useState,
|
|
11
|
+
type ComponentProps,
|
|
12
|
+
type ReactNode,
|
|
13
|
+
} from 'react';
|
|
14
|
+
|
|
15
|
+
import { cn } from '../../utils/components';
|
|
16
|
+
|
|
17
|
+
type TextareaCountMode = 'characters' | 'words';
|
|
18
|
+
|
|
19
|
+
export type TextareaCountContextValue = {
|
|
20
|
+
mode: TextareaCountMode;
|
|
21
|
+
maxCount: number;
|
|
22
|
+
threshold: number;
|
|
23
|
+
currentCount: number;
|
|
24
|
+
messageId: string;
|
|
25
|
+
reportValue: (value: string) => void;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export const TextareaCountContext = createContext<TextareaCountContextValue | null>(null);
|
|
29
|
+
|
|
30
|
+
export type TextareaCountProps = {
|
|
31
|
+
children: ReactNode;
|
|
32
|
+
threshold?: number;
|
|
33
|
+
initialValue?: string;
|
|
34
|
+
onValidityChange?: (isOver: boolean) => void;
|
|
35
|
+
} & ({ maxLength: number; maxWords?: never } | { maxWords: number; maxLength?: never });
|
|
36
|
+
|
|
37
|
+
const countWords = (value: string) => (value.trim() === '' ? 0 : value.trim().split(/\s+/).length);
|
|
38
|
+
|
|
39
|
+
export const TextareaCount = ({
|
|
40
|
+
children,
|
|
41
|
+
maxLength,
|
|
42
|
+
maxWords,
|
|
43
|
+
threshold = 0,
|
|
44
|
+
initialValue = '',
|
|
45
|
+
onValidityChange,
|
|
46
|
+
}: TextareaCountProps) => {
|
|
47
|
+
const mode: TextareaCountMode = maxWords !== undefined ? 'words' : 'characters';
|
|
48
|
+
const maxCount = (mode === 'words' ? maxWords : maxLength) ?? 0;
|
|
49
|
+
const messageId = useId();
|
|
50
|
+
|
|
51
|
+
const [currentCount, setCurrentCount] = useState(() =>
|
|
52
|
+
mode === 'words' ? countWords(initialValue) : initialValue.length,
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
const isCountOver = currentCount > maxCount;
|
|
56
|
+
|
|
57
|
+
useEffect(() => {
|
|
58
|
+
onValidityChange?.(isCountOver);
|
|
59
|
+
}, [isCountOver, onValidityChange]);
|
|
60
|
+
|
|
61
|
+
const reportValue = useCallback(
|
|
62
|
+
(value: string) => {
|
|
63
|
+
setCurrentCount(mode === 'words' ? countWords(value) : value.length);
|
|
64
|
+
},
|
|
65
|
+
[mode],
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
const contextValue = useMemo(
|
|
69
|
+
() => ({ mode, maxCount, threshold, currentCount, messageId, reportValue }),
|
|
70
|
+
[mode, maxCount, threshold, currentCount, messageId, reportValue],
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
return (
|
|
74
|
+
<TextareaCountContext.Provider value={contextValue}>{children}</TextareaCountContext.Provider>
|
|
75
|
+
);
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
export type TextareaCountMessageProps = ComponentProps<'p'>;
|
|
79
|
+
|
|
80
|
+
export const TextareaCountMessage = ({ className, ...props }: TextareaCountMessageProps) => {
|
|
81
|
+
const context = useContext(TextareaCountContext);
|
|
82
|
+
|
|
83
|
+
if (!context) {
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const { mode, maxCount, threshold, currentCount, messageId } = context;
|
|
88
|
+
const remaining = maxCount - currentCount;
|
|
89
|
+
const isOver = remaining < 0;
|
|
90
|
+
const isHidden = threshold > 0 && currentCount < maxCount * threshold;
|
|
91
|
+
const count = Math.abs(remaining);
|
|
92
|
+
const unit = mode === 'words' ? 'word' : 'character';
|
|
93
|
+
const label = count === 1 ? unit : `${unit}s`;
|
|
94
|
+
|
|
95
|
+
return (
|
|
96
|
+
<p
|
|
97
|
+
id={messageId}
|
|
98
|
+
aria-live="polite"
|
|
99
|
+
data-slot="textarea-count-message"
|
|
100
|
+
data-over={isOver ? true : undefined}
|
|
101
|
+
data-hidden={isHidden ? true : undefined}
|
|
102
|
+
className={cn('cn-textarea-count-message', className)}
|
|
103
|
+
{...props}
|
|
104
|
+
>
|
|
105
|
+
{isOver ? `You have ${count} ${label} too many` : `You have ${count} ${label} remaining`}
|
|
106
|
+
</p>
|
|
107
|
+
);
|
|
108
|
+
};
|