@tpzdsp/next-toolkit 2.5.0 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +22 -13
- package/src/assets/styles/globals.css +75 -95
- package/src/assets/styles/ol.css +57 -16
- package/src/assets/styles/utils.css +80 -0
- package/src/components/Accordion/Accordion.css +85 -0
- package/src/components/Accordion/Accordion.stories.tsx +264 -0
- package/src/components/Accordion/Accordion.tsx +98 -0
- package/src/components/Alert/Alert.css +129 -0
- package/src/components/Alert/Alert.stories.tsx +291 -0
- package/src/components/Alert/Alert.tsx +76 -0
- package/src/components/BackLink/BackLink.css +12 -0
- package/src/components/BackLink/BackLink.stories.tsx +52 -0
- package/src/components/BackLink/BackLink.tsx +44 -0
- package/src/components/BackToTop/BackToTop.css +23 -0
- package/src/components/BackToTop/BackToTop.stories.tsx +41 -0
- package/src/components/BackToTop/BackToTop.tsx +48 -0
- package/src/components/Breadcrumb/Breadcrumb.css +42 -0
- package/src/components/Breadcrumb/Breadcrumb.stories.tsx +94 -0
- package/src/components/Breadcrumb/Breadcrumb.tsx +135 -0
- package/src/components/Button/Button.css +229 -0
- package/src/components/Button/Button.stories.tsx +140 -20
- package/src/components/Button/Button.tsx +53 -28
- package/src/components/Button/ButtonSkeleton.stories.tsx +50 -0
- package/src/components/Button/ButtonSkeleton.tsx +14 -0
- package/src/components/ButtonGroup/ButtonGroup.css +52 -0
- package/src/components/ButtonGroup/ButtonGroup.stories.tsx +156 -0
- package/src/components/ButtonGroup/ButtonGroup.tsx +50 -0
- package/src/components/ButtonLink/ButtonLink.css +114 -0
- package/src/components/ButtonLink/ButtonLink.stories.tsx +38 -51
- package/src/components/ButtonLink/ButtonLink.tsx +54 -23
- package/src/components/Card/Card.css +13 -0
- package/src/components/Card/Card.stories.tsx +98 -57
- package/src/components/Card/Card.tsx +4 -13
- package/src/components/CardGroup/CardGroup.css +10 -0
- package/src/components/CardGroup/CardGroup.stories.tsx +81 -0
- package/src/components/CardGroup/CardGroup.tsx +9 -0
- package/src/components/Checkbox/Checkbox.css +49 -0
- package/src/components/Checkbox/Checkbox.stories.tsx +86 -0
- package/src/components/Checkbox/Checkbox.tsx +32 -0
- package/src/components/Chip/Chip.css +54 -0
- package/src/components/Chip/Chip.stories.tsx +94 -0
- package/src/components/Chip/Chip.tsx +34 -0
- package/src/components/CookieBanner/CookieBanner.css +19 -0
- package/src/components/CookieBanner/CookieBanner.stories.tsx +52 -0
- package/src/components/CookieBanner/CookieBanner.tsx +55 -0
- package/src/components/CopyButton/CopyButton.stories.tsx +28 -0
- package/src/components/CopyButton/CopyButton.tsx +65 -0
- package/src/components/DateInput/DateInput.css +13 -0
- package/src/components/DateInput/DateInput.stories.tsx +131 -0
- package/src/components/DateInput/DateInput.test.tsx +21 -0
- package/src/components/DateInput/DateInput.tsx +98 -0
- package/src/components/Details/Details.css +28 -0
- package/src/components/Details/Details.stories.tsx +48 -0
- package/src/components/Details/Details.tsx +41 -0
- package/src/components/Dropdown/Dropdown.css +81 -0
- package/src/components/Dropdown/Dropdown.stories.tsx +131 -0
- package/src/components/Dropdown/Dropdown.tsx +69 -0
- package/src/components/ErrorBoundary/ErrorBoundary.stories.tsx +4 -39
- package/src/components/ErrorBoundary/ErrorBoundary.tsx +7 -4
- package/src/components/ErrorBoundary/ErrorFallback.tsx +32 -66
- package/src/components/ErrorModal/ErrorModal.css +32 -0
- package/src/components/ErrorModal/ErrorModal.stories.tsx +101 -0
- package/src/components/ErrorModal/ErrorModal.tsx +120 -0
- package/src/components/ErrorText/ErrorText.css +22 -0
- package/src/components/ErrorText/ErrorText.stories.tsx +42 -27
- package/src/components/ErrorText/ErrorText.tsx +10 -8
- package/src/components/ExternalLink/ExternalLink.css +12 -0
- package/src/components/ExternalLink/ExternalLink.stories.tsx +43 -0
- package/src/components/ExternalLink/ExternalLink.tsx +43 -0
- package/src/components/Fieldset/Fieldset.css +103 -0
- package/src/components/Fieldset/Fieldset.stories.tsx +304 -0
- package/src/components/Fieldset/Fieldset.tsx +38 -0
- package/src/components/FileUpload/FileUpload.css +85 -0
- package/src/components/FileUpload/FileUpload.stories.tsx +106 -0
- package/src/components/FileUpload/FileUpload.tsx +99 -0
- package/src/components/{layout/footer → Footer}/Copyright.tsx +4 -2
- package/src/components/Footer/Footer.stories.tsx +24 -0
- package/src/components/Footer/Footer.tsx +41 -0
- package/src/components/Footer/Licence.tsx +18 -0
- package/src/components/{layout/footer → Footer}/MetaLinks.tsx +8 -3
- package/src/components/Footer/Small/SmallFooter.tsx +43 -42
- package/src/components/Form/FormGroup.css +18 -0
- package/src/components/Form/FormGroup.stories.tsx +87 -0
- package/src/components/Form/FormGroup.tsx +16 -0
- package/src/components/{layout/header → Header}/Header.stories.tsx +26 -12
- package/src/components/Header/Header.test.tsx +87 -0
- package/src/components/Header/Header.tsx +303 -0
- package/src/components/Header/HeaderAppContent.tsx +120 -0
- package/src/components/Heading/Heading.css +40 -0
- package/src/components/Heading/Heading.stories.tsx +15 -5
- package/src/components/Heading/Heading.tsx +21 -44
- package/src/components/Heading/HeadingSkeleton.stories.tsx +30 -0
- package/src/components/Heading/HeadingSkeleton.tsx +14 -0
- package/src/components/Hero/Hero.css +19 -0
- package/src/components/Hero/Hero.stories.tsx +41 -0
- package/src/components/Hero/Hero.tsx +14 -0
- package/src/components/HintText/HintText.css +5 -0
- package/src/components/HintText/HintText.stories.tsx +29 -0
- package/src/components/HintText/HintText.tsx +13 -0
- package/src/components/InfoBox/InfoBox.css +38 -0
- package/src/components/InfoBox/InfoBox.stories.tsx +51 -403
- package/src/components/InfoBox/InfoBox.tsx +31 -158
- package/src/components/Input/Input.css +77 -0
- package/src/components/Input/Input.stories.tsx +123 -0
- package/src/components/Input/Input.tsx +74 -0
- package/src/components/InputGroup/InputGroup.css +140 -0
- package/src/components/InputGroup/InputGroup.stories.tsx +125 -0
- package/src/components/InputGroup/InputGroup.tsx +47 -0
- package/src/components/InsetText/InsetText.css +5 -0
- package/src/components/InsetText/InsetText.stories.tsx +30 -0
- package/src/components/InsetText/InsetText.tsx +9 -0
- package/src/components/Keyboard/Keyboard.css +9 -0
- package/src/components/Keyboard/Keyboard.stories.tsx +63 -0
- package/src/components/Keyboard/Keyboard.tsx +30 -0
- package/src/components/Label/Label.css +27 -0
- package/src/components/Label/Label.stories.tsx +40 -0
- package/src/components/Label/Label.tsx +17 -0
- package/src/components/LayerSwitcher/LayerSwitcher.css +59 -0
- package/src/components/LayerSwitcher/LayerSwitcher.stories.tsx +112 -0
- package/src/components/LayerSwitcher/LayerSwitcher.tsx +61 -0
- package/src/components/Layout/401/401.stories.tsx +23 -0
- package/src/components/Layout/401/401.tsx +1 -0
- package/src/components/Layout/403/403.stories.tsx +23 -0
- package/src/components/Layout/403/403.tsx +21 -0
- package/src/components/Layout/404/404.stories.tsx +23 -0
- package/src/components/Layout/404/404.tsx +28 -0
- package/src/components/Layout/500/500.stories.tsx +56 -0
- package/src/components/Layout/500/500.tsx +63 -0
- package/src/components/Layout/ButtonGroup/ButtonGroup.css +52 -0
- package/src/components/Layout/ButtonGroup/ButtonGroup.stories.tsx +156 -0
- package/src/components/Layout/ButtonGroup/ButtonGroup.tsx +50 -0
- package/src/components/Layout/HeadingGroup/HeadingGroup.css +21 -0
- package/src/components/Layout/HeadingGroup/HeadingGroup.stories.tsx +84 -0
- package/src/components/Layout/HeadingGroup/HeadingGroup.tsx +9 -0
- package/src/components/Layout/MainWrapper/MainWrapper.css +12 -0
- package/src/components/Layout/MainWrapper/MainWrapper.stories.tsx +36 -0
- package/src/components/Layout/MainWrapper/MainWrapper.tsx +14 -0
- package/src/components/Layout/PageShell/PageShell.css +42 -0
- package/src/components/Layout/PageShell/PageShell.stories.tsx +108 -0
- package/src/components/Layout/PageShell/PageShell.tsx +31 -0
- package/src/components/Layout/ParagraphGroup/ParagraphGroup.css +5 -0
- package/src/components/Layout/ParagraphGroup/ParagraphGroup.stories.tsx +34 -0
- package/src/components/Layout/ParagraphGroup/ParagraphGroup.tsx +9 -0
- package/src/components/Layout/Stack/Stack.css +29 -0
- package/src/components/Layout/Stack/Stack.stories.tsx +83 -0
- package/src/components/Layout/Stack/Stack.tsx +19 -0
- package/src/components/Layout/WidthContainer/WidthContainer.css +17 -0
- package/src/components/Layout/WidthContainer/WidthContainer.stories.tsx +59 -0
- package/src/components/Layout/WidthContainer/WidthContainer.tsx +13 -0
- package/src/components/Legend/Legend.css +95 -0
- package/src/components/Legend/Legend.stories.tsx +81 -0
- package/src/components/Legend/Legend.tsx +32 -0
- package/src/components/Legend/LegendDot.tsx +15 -0
- package/src/components/Legend/LegendPanel.stories.tsx +101 -0
- package/src/components/Legend/LegendPanel.tsx +68 -0
- package/src/components/Legend/LegendSkeleton.stories.tsx +87 -0
- package/src/components/Legend/LegendSkeleton.tsx +73 -0
- package/src/components/LegendSelect/LegendSelect.css +8 -0
- package/src/components/LegendSelect/LegendSelect.stories.tsx +80 -0
- package/src/components/LegendSelect/LegendSelect.tsx +26 -0
- package/src/components/Link/Link.css +98 -0
- package/src/components/Link/Link.stories.tsx +70 -0
- package/src/components/Link/Link.tsx +52 -0
- package/src/components/LinkButton/LinkButton.css +67 -0
- package/src/components/LinkButton/LinkButton.stories.tsx +90 -42
- package/src/components/LinkButton/LinkButton.tsx +50 -63
- package/src/components/LinkList/LinkList.css +28 -0
- package/src/components/LinkList/LinkList.stories.tsx +46 -0
- package/src/components/LinkList/LinkList.tsx +23 -0
- package/src/components/List/Li.tsx +9 -0
- package/src/components/List/List.css +9 -0
- package/src/components/List/OrderedList.stories.tsx +36 -0
- package/src/components/List/OrderedList.tsx +9 -0
- package/src/components/List/UnorderedList.stories.tsx +36 -0
- package/src/components/List/UnorderedList.tsx +9 -0
- package/src/components/LoadingBox/LoadingBox.css +27 -0
- package/src/components/LoadingBox/LoadingBox.stories.tsx +66 -0
- package/src/components/LoadingBox/LoadingBox.tsx +40 -0
- package/src/components/Modal/Modal.css +114 -0
- package/src/components/Modal/Modal.stories.tsx +41 -160
- package/src/components/Modal/Modal.tsx +132 -75
- package/src/components/NoScriptBanner/NoScriptBanner.css +17 -0
- package/src/components/NoScriptBanner/NoScriptBanner.stories.tsx +46 -0
- package/src/components/NoScriptBanner/NoScriptBanner.tsx +26 -0
- package/src/components/Paragraph/Paragraph.css +22 -0
- package/src/components/Paragraph/Paragraph.stories.tsx +1 -42
- package/src/components/Paragraph/Paragraph.tsx +4 -3
- package/src/components/Paragraph/ParagraphSkeleton.stories.tsx +44 -0
- package/src/components/Paragraph/ParagraphSkeleton.tsx +33 -0
- package/src/components/PasswordInput/PasswordInput.css +3 -0
- package/src/components/PasswordInput/PasswordInput.stories.tsx +47 -0
- package/src/components/PasswordInput/PasswordInput.tsx +45 -0
- package/src/components/PhaseBanner/PhaseBanner.css +12 -0
- package/src/components/PhaseBanner/PhaseBanner.stories.tsx +61 -0
- package/src/components/PhaseBanner/PhaseBanner.tsx +17 -0
- package/src/components/Popover/Popover.css +43 -0
- package/src/components/Popover/Popover.stories.tsx +102 -0
- package/src/components/Popover/Popover.tsx +40 -0
- package/src/components/Radio/Radio.css +69 -0
- package/src/components/Radio/Radio.stories.tsx +169 -0
- package/src/components/Radio/Radio.tsx +56 -0
- package/src/components/Resizable/Resizable.css +86 -0
- package/src/components/Resizable/Resizable.stories.tsx +123 -0
- package/src/components/Resizable/Resizable.tsx +86 -0
- package/src/components/ScrollArea/ScrollArea.css +9 -0
- package/src/components/ScrollArea/ScrollArea.stories.tsx +42 -0
- package/src/components/ScrollArea/ScrollArea.tsx +11 -0
- package/src/components/SearchBox/SearchBox.stories.tsx +88 -0
- package/src/components/SearchBox/SearchBox.tsx +36 -0
- package/src/components/Select/AsyncSelect.stories.tsx +84 -0
- package/src/components/Select/AsyncSelect.tsx +41 -0
- package/src/components/Select/Select.css +194 -0
- package/src/components/Select/Select.stories.tsx +162 -0
- package/src/components/Select/Select.test.tsx +36 -0
- package/src/components/Select/Select.tsx +291 -0
- package/src/components/Select/SelectSkeleton.stories.tsx +51 -0
- package/src/components/Select/SelectSkeleton.tsx +23 -0
- package/src/components/Separator/Separator.css +41 -0
- package/src/components/Separator/Separator.stories.tsx +63 -0
- package/src/components/Separator/Separator.tsx +42 -0
- package/src/components/Sidebar/ResizableSidebar.tsx +118 -0
- package/src/components/Sidebar/Sidebar.css +203 -0
- package/src/components/Sidebar/Sidebar.stories.tsx +438 -0
- package/src/components/Sidebar/Sidebar.tsx +225 -0
- package/src/components/Skeleton/Skeleton.css +16 -0
- package/src/components/Skeleton/Skeleton.stories.tsx +39 -0
- package/src/components/Skeleton/Skeleton.tsx +12 -0
- package/src/components/SkipLink/SkipLink.css +16 -0
- package/src/components/SkipLink/SkipLink.stories.tsx +40 -0
- package/src/components/SkipLink/SkipLink.tsx +15 -0
- package/src/components/Spinner/Spinner.css +30 -0
- package/src/components/Spinner/Spinner.stories.tsx +36 -0
- package/src/components/Spinner/Spinner.tsx +45 -0
- package/src/components/StartButton/StartButton.css +13 -0
- package/src/components/StartButton/StartButton.stories.tsx +68 -0
- package/src/components/StartButton/StartButton.tsx +50 -0
- package/src/components/SubmitButton/SubmitButton.stories.tsx +31 -0
- package/src/components/SubmitButton/SubmitButton.tsx +14 -0
- package/src/components/SummaryList/SummaryList.css +71 -0
- package/src/components/SummaryList/SummaryList.stories.tsx +211 -0
- package/src/components/SummaryList/SummaryList.tsx +41 -0
- package/src/components/SummaryList/SummaryListSkeleton.stories.tsx +67 -0
- package/src/components/SummaryList/SummaryListSkeleton.tsx +40 -0
- package/src/components/Tabs/Tabs.css +84 -0
- package/src/components/Tabs/Tabs.stories.tsx +133 -0
- package/src/components/Tabs/Tabs.tsx +66 -0
- package/src/components/Tag/Tag.css +8 -0
- package/src/components/Tag/Tag.stories.tsx +96 -0
- package/src/components/Tag/Tag.tsx +34 -0
- package/src/components/Textarea/Textarea.css +38 -0
- package/src/components/Textarea/Textarea.stories.tsx +131 -0
- package/src/components/Textarea/Textarea.tsx +50 -0
- package/src/components/TextareaCount/TextareaCount.css +17 -0
- package/src/components/TextareaCount/TextareaCount.stories.tsx +131 -0
- package/src/components/TextareaCount/TextareaCount.tsx +108 -0
- package/src/components/Tooltip/Tooltip.css +43 -0
- package/src/components/Tooltip/Tooltip.stories.tsx +110 -0
- package/src/components/Tooltip/Tooltip.tsx +53 -0
- package/src/components/WarningText/WarningText.css +20 -0
- package/src/components/WarningText/WarningText.stories.tsx +29 -0
- package/src/components/WarningText/WarningText.tsx +17 -0
- package/src/components/index.ts +342 -61
- package/src/errors/errorDisplay.ts +70 -0
- package/src/errors/index.ts +1 -0
- package/src/http/constants.ts +1 -0
- package/src/http/fetch.ts +7 -3
- package/src/map/LayerSwitcherPanel.tsx +2 -173
- package/src/map/LegendShim.tsx +2 -0
- package/src/map/MapComponent.tsx +7 -17
- package/src/map/MapControlsOverlay.tsx +68 -175
- package/src/map/Popup.tsx +71 -24
- package/src/map/geocoder/Geocoder.test.tsx +46 -3
- package/src/map/geocoder/Geocoder.tsx +155 -278
- package/src/map/index.ts +1 -1
- package/src/types/utils.ts +0 -10
- package/src/utils/components.ts +96 -0
- package/src/utils/index.ts +1 -0
- package/src/utils/utils.ts +0 -12
- package/src/assets/images/defra-logo.svg +0 -51
- package/src/assets/images/ea-logo.svg +0 -58
- package/src/assets/images/ogl.svg +0 -1
- package/src/components/Button/Button.test.tsx +0 -53
- package/src/components/ButtonLink/ButtonLink.test.tsx +0 -154
- package/src/components/Card/Card.test.tsx +0 -51
- package/src/components/ErrorBoundary/ErrorBoundary.test.tsx +0 -75
- package/src/components/ErrorBoundary/ErrorFallback.test.tsx +0 -107
- package/src/components/ErrorText/ErrorText.test.tsx +0 -33
- package/src/components/Heading/Heading.test.tsx +0 -22
- package/src/components/Hint/Hint.stories.tsx +0 -58
- package/src/components/Hint/Hint.test.tsx +0 -33
- package/src/components/Hint/Hint.tsx +0 -12
- package/src/components/InfoBox/InfoBox.test.tsx +0 -330
- package/src/components/InfoBox/types.ts +0 -6
- package/src/components/LinkButton/LinkButton.test.tsx +0 -177
- package/src/components/Modal/Modal.test.tsx +0 -246
- package/src/components/NotificationBanner/NotificationBanner.stories.tsx +0 -45
- package/src/components/NotificationBanner/NotificationBanner.test.tsx +0 -58
- package/src/components/NotificationBanner/NotificationBanner.tsx +0 -45
- package/src/components/Paragraph/Paragraph.test.tsx +0 -10
- package/src/components/SlidingPanel/SlidingPanel.stories.tsx +0 -129
- package/src/components/SlidingPanel/SlidingPanel.test.tsx +0 -83
- package/src/components/SlidingPanel/SlidingPanel.tsx +0 -258
- package/src/components/accordion/Accordion.stories.tsx +0 -234
- package/src/components/accordion/Accordion.test.tsx +0 -192
- package/src/components/accordion/Accordion.tsx +0 -73
- package/src/components/backToTop/BackToTop.stories.tsx +0 -407
- package/src/components/backToTop/BackToTop.test.tsx +0 -57
- package/src/components/backToTop/BackToTop.tsx +0 -134
- package/src/components/chip/Chip.stories.tsx +0 -61
- package/src/components/chip/Chip.test.tsx +0 -31
- package/src/components/chip/Chip.tsx +0 -23
- package/src/components/container/Container.tsx +0 -42
- package/src/components/cookieBanner/CookieBanner.stories.tsx +0 -257
- package/src/components/cookieBanner/CookieBanner.test.tsx +0 -68
- package/src/components/cookieBanner/CookieBanner.tsx +0 -73
- package/src/components/divider/RuleDivider.stories.tsx +0 -254
- package/src/components/divider/RuleDivider.test.tsx +0 -164
- package/src/components/divider/RuleDivider.tsx +0 -23
- package/src/components/dropdown/DropdownMenu.test.tsx +0 -211
- package/src/components/dropdown/DropdownMenu.tsx +0 -108
- package/src/components/dropdown/useDropdownMenu.ts +0 -249
- package/src/components/form/Input.stories.tsx +0 -435
- package/src/components/form/Input.test.tsx +0 -206
- package/src/components/form/Input.tsx +0 -23
- package/src/components/form/TextArea.stories.tsx +0 -464
- package/src/components/form/TextArea.test.tsx +0 -232
- package/src/components/form/TextArea.tsx +0 -23
- package/src/components/layout/footer/Footer.tsx +0 -26
- package/src/components/layout/footer/Licence.tsx +0 -19
- package/src/components/layout/header/Header.test.tsx +0 -36
- package/src/components/layout/header/Header.tsx +0 -90
- package/src/components/layout/header/HeaderAuthClient.test.tsx +0 -45
- package/src/components/layout/header/HeaderAuthClient.tsx +0 -40
- package/src/components/layout/header/HeaderNavClient.test.tsx +0 -44
- package/src/components/layout/header/HeaderNavClient.tsx +0 -67
- package/src/components/link/ExternalLink.test.tsx +0 -102
- package/src/components/link/ExternalLink.tsx +0 -29
- package/src/components/link/Link.tsx +0 -26
- package/src/components/select/Select.stories.tsx +0 -336
- package/src/components/select/Select.test.tsx +0 -474
- package/src/components/select/Select.tsx +0 -125
- package/src/components/select/SelectSkeleton.stories.tsx +0 -194
- package/src/components/select/SelectSkeleton.test.tsx +0 -104
- package/src/components/select/SelectSkeleton.tsx +0 -23
- package/src/components/select/common.ts +0 -3
- package/src/components/select/index.ts +0 -18
- package/src/components/skipLink/SkipLink.stories.tsx +0 -346
- package/src/components/skipLink/SkipLink.test.tsx +0 -22
- package/src/components/skipLink/SkipLink.tsx +0 -49
- package/src/map/LegendPanel.tsx +0 -85
- /package/src/{components → assets}/images/DefraLogo.tsx +0 -0
- /package/src/{components → assets}/images/EaLogo.tsx +0 -0
- /package/src/{components → assets}/images/OglLogo.tsx +0 -0
- /package/src/components/{googleAnalytics → GoogleAnalytics}/GlobalVars.tsx +0 -0
- /package/src/components/{googleAnalytics → GoogleAnalytics}/GoogleAnalytics.tsx +0 -0
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react-vite';
|
|
2
|
+
|
|
3
|
+
import { FieldDivider, FieldItem, FieldLegend, FieldSet } from './Fieldset';
|
|
4
|
+
import { Checkbox } from '../Checkbox/Checkbox';
|
|
5
|
+
import { ErrorText } from '../ErrorText/ErrorText';
|
|
6
|
+
import { FormGroup } from '../Form/FormGroup';
|
|
7
|
+
import { Heading } from '../Heading/Heading';
|
|
8
|
+
import { HintText } from '../HintText/HintText';
|
|
9
|
+
import { Label } from '../Label/Label';
|
|
10
|
+
import { Radio, RadioGroup } from '../Radio/Radio';
|
|
11
|
+
|
|
12
|
+
const meta: Meta<typeof FieldSet> = {
|
|
13
|
+
title: 'Components/Fieldset',
|
|
14
|
+
component: FieldSet,
|
|
15
|
+
tags: ['autodocs'],
|
|
16
|
+
parameters: {
|
|
17
|
+
docs: {
|
|
18
|
+
description: {
|
|
19
|
+
component:
|
|
20
|
+
"Semantic wrapper for groups of related controls such as checkboxes and radio buttons. `FieldLegend` acts as the group label, screen readers announce it alongside each control's own label. `<fieldset disabled>` propagates disabled to every descendant without individual `disabled` props. `FieldItem` wraps each control, its `Label`, and an optional `HintText`, mirroring GDS's own govuk-radios__item/govuk-checkboxes__item markup (a plain div with separately-associated control/label/hint, not a wrapping label, so a hint is announced as its own accessible description). Wire `id`/`htmlFor`/`aria-describedby` by hand, same as `FormGroup`. Use `FieldDivider` to separate groups within the same fieldset.",
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export default meta;
|
|
27
|
+
|
|
28
|
+
type Story = StoryObj<typeof FieldSet>;
|
|
29
|
+
|
|
30
|
+
export const CheckboxGroup: Story = {
|
|
31
|
+
parameters: {
|
|
32
|
+
docs: {
|
|
33
|
+
description: {
|
|
34
|
+
story: '`FieldLegend` is plain body text by default, matching GDS.',
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
render: () => (
|
|
39
|
+
<FieldSet>
|
|
40
|
+
<FieldLegend>Which countries have you visited?</FieldLegend>
|
|
41
|
+
|
|
42
|
+
<div className="flex flex-col gap-gds-2">
|
|
43
|
+
<FieldItem>
|
|
44
|
+
<Checkbox id="country-france" defaultChecked />
|
|
45
|
+
|
|
46
|
+
<Label htmlFor="country-france">France</Label>
|
|
47
|
+
</FieldItem>
|
|
48
|
+
|
|
49
|
+
<FieldItem>
|
|
50
|
+
<Checkbox id="country-germany" />
|
|
51
|
+
|
|
52
|
+
<Label htmlFor="country-germany">Germany</Label>
|
|
53
|
+
</FieldItem>
|
|
54
|
+
|
|
55
|
+
<FieldItem>
|
|
56
|
+
<Checkbox id="country-spain" />
|
|
57
|
+
|
|
58
|
+
<Label htmlFor="country-spain">Spain</Label>
|
|
59
|
+
</FieldItem>
|
|
60
|
+
</div>
|
|
61
|
+
</FieldSet>
|
|
62
|
+
),
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
export const WithRadios: Story = {
|
|
66
|
+
render: () => (
|
|
67
|
+
<FieldSet>
|
|
68
|
+
<FieldLegend>Have you changed your name?</FieldLegend>
|
|
69
|
+
|
|
70
|
+
<RadioGroup defaultValue="no">
|
|
71
|
+
<FieldItem>
|
|
72
|
+
<Radio id="name-changed-yes" value="yes" />
|
|
73
|
+
|
|
74
|
+
<Label htmlFor="name-changed-yes">Yes</Label>
|
|
75
|
+
</FieldItem>
|
|
76
|
+
|
|
77
|
+
<FieldItem>
|
|
78
|
+
<Radio id="name-changed-no" value="no" />
|
|
79
|
+
|
|
80
|
+
<Label htmlFor="name-changed-no">No</Label>
|
|
81
|
+
</FieldItem>
|
|
82
|
+
</RadioGroup>
|
|
83
|
+
</FieldSet>
|
|
84
|
+
),
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
export const WithHints: Story = {
|
|
88
|
+
parameters: {
|
|
89
|
+
docs: {
|
|
90
|
+
description: {
|
|
91
|
+
story:
|
|
92
|
+
'Add a HintText inside FieldItem for a per-option hint, matching GDS\'s "How do you want to sign in?" pattern. id/htmlFor/aria-describedby are wired by hand here, same as everywhere else in the library.',
|
|
93
|
+
},
|
|
94
|
+
},
|
|
95
|
+
},
|
|
96
|
+
render: () => (
|
|
97
|
+
<FormGroup>
|
|
98
|
+
<FieldSet aria-describedby="sign-in-hint">
|
|
99
|
+
<FieldLegend>
|
|
100
|
+
<Heading level="h1">How do you want to sign in?</Heading>
|
|
101
|
+
</FieldLegend>
|
|
102
|
+
|
|
103
|
+
<HintText id="sign-in-hint">
|
|
104
|
+
You'll need an account to prove your identity and complete your Self Assessment
|
|
105
|
+
</HintText>
|
|
106
|
+
|
|
107
|
+
<RadioGroup defaultValue="government-gateway">
|
|
108
|
+
<FieldItem>
|
|
109
|
+
<Radio id="sign-in" value="government-gateway" aria-describedby="sign-in-item-hint" />
|
|
110
|
+
|
|
111
|
+
<Label htmlFor="sign-in">Sign in with Government Gateway</Label>
|
|
112
|
+
|
|
113
|
+
<HintText id="sign-in-item-hint">
|
|
114
|
+
You'll have a user ID if you've registered for Self Assessment or filed a
|
|
115
|
+
tax return online before
|
|
116
|
+
</HintText>
|
|
117
|
+
</FieldItem>
|
|
118
|
+
|
|
119
|
+
<FieldItem>
|
|
120
|
+
<Radio id="sign-in-2" value="one-login" aria-describedby="sign-in-2-item-hint" />
|
|
121
|
+
|
|
122
|
+
<Label htmlFor="sign-in-2">Sign in with GOV.UK One Login</Label>
|
|
123
|
+
|
|
124
|
+
<HintText id="sign-in-2-item-hint">
|
|
125
|
+
If you don't have a GOV.UK One Login, you can create one
|
|
126
|
+
</HintText>
|
|
127
|
+
</FieldItem>
|
|
128
|
+
</RadioGroup>
|
|
129
|
+
</FieldSet>
|
|
130
|
+
</FormGroup>
|
|
131
|
+
),
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
export const WithDivider: Story = {
|
|
135
|
+
parameters: {
|
|
136
|
+
docs: {
|
|
137
|
+
description: {
|
|
138
|
+
story:
|
|
139
|
+
'`FieldDivider` separates two groups of options within the same fieldset. The default text is "or" but it can be overridden. Matches the GDS pattern for "select one of these, or this other option".',
|
|
140
|
+
},
|
|
141
|
+
},
|
|
142
|
+
},
|
|
143
|
+
render: () => (
|
|
144
|
+
<FieldSet>
|
|
145
|
+
<FieldLegend>What is your nationality?</FieldLegend>
|
|
146
|
+
|
|
147
|
+
<div className="flex flex-col gap-gds-2">
|
|
148
|
+
<FieldItem>
|
|
149
|
+
<Checkbox id="nationality-british" />
|
|
150
|
+
|
|
151
|
+
<Label htmlFor="nationality-british">British</Label>
|
|
152
|
+
</FieldItem>
|
|
153
|
+
|
|
154
|
+
<FieldItem>
|
|
155
|
+
<Checkbox id="nationality-irish" />
|
|
156
|
+
|
|
157
|
+
<Label htmlFor="nationality-irish">Irish</Label>
|
|
158
|
+
</FieldItem>
|
|
159
|
+
|
|
160
|
+
<FieldDivider />
|
|
161
|
+
|
|
162
|
+
<FieldItem>
|
|
163
|
+
<Checkbox id="nationality-other" />
|
|
164
|
+
|
|
165
|
+
<Label htmlFor="nationality-other">Citizen of another country</Label>
|
|
166
|
+
</FieldItem>
|
|
167
|
+
</div>
|
|
168
|
+
</FieldSet>
|
|
169
|
+
),
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
export const WithDividerRadios: Story = {
|
|
173
|
+
render: () => (
|
|
174
|
+
<FieldSet>
|
|
175
|
+
<FieldLegend>Do you want to be contacted about this?</FieldLegend>
|
|
176
|
+
|
|
177
|
+
<RadioGroup>
|
|
178
|
+
<FieldItem>
|
|
179
|
+
<Radio id="contact-yes" value="yes" />
|
|
180
|
+
|
|
181
|
+
<Label htmlFor="contact-yes">Yes</Label>
|
|
182
|
+
</FieldItem>
|
|
183
|
+
|
|
184
|
+
<FieldItem>
|
|
185
|
+
<Radio id="contact-no" value="no" />
|
|
186
|
+
|
|
187
|
+
<Label htmlFor="contact-no">No</Label>
|
|
188
|
+
</FieldItem>
|
|
189
|
+
|
|
190
|
+
<FieldDivider />
|
|
191
|
+
|
|
192
|
+
<FieldItem>
|
|
193
|
+
<Radio id="contact-unsure" value="unsure" />
|
|
194
|
+
|
|
195
|
+
<Label htmlFor="contact-unsure">I'm not sure</Label>
|
|
196
|
+
</FieldItem>
|
|
197
|
+
</RadioGroup>
|
|
198
|
+
</FieldSet>
|
|
199
|
+
),
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
export const Invalid: Story = {
|
|
203
|
+
render: () => (
|
|
204
|
+
<FormGroup invalid>
|
|
205
|
+
<FieldSet>
|
|
206
|
+
<FieldLegend>Which countries have you visited?</FieldLegend>
|
|
207
|
+
|
|
208
|
+
<ErrorText id="countries-error">Select at least one country</ErrorText>
|
|
209
|
+
|
|
210
|
+
<div className="flex flex-col gap-gds-2">
|
|
211
|
+
<FieldItem>
|
|
212
|
+
<Checkbox id="invalid-country-france" aria-invalid aria-describedby="countries-error" />
|
|
213
|
+
|
|
214
|
+
<Label htmlFor="invalid-country-france">France</Label>
|
|
215
|
+
</FieldItem>
|
|
216
|
+
|
|
217
|
+
<FieldItem>
|
|
218
|
+
<Checkbox
|
|
219
|
+
id="invalid-country-germany"
|
|
220
|
+
aria-invalid
|
|
221
|
+
aria-describedby="countries-error"
|
|
222
|
+
/>
|
|
223
|
+
|
|
224
|
+
<Label htmlFor="invalid-country-germany">Germany</Label>
|
|
225
|
+
</FieldItem>
|
|
226
|
+
|
|
227
|
+
<FieldItem>
|
|
228
|
+
<Checkbox id="invalid-country-spain" aria-invalid aria-describedby="countries-error" />
|
|
229
|
+
|
|
230
|
+
<Label htmlFor="invalid-country-spain">Spain</Label>
|
|
231
|
+
</FieldItem>
|
|
232
|
+
</div>
|
|
233
|
+
</FieldSet>
|
|
234
|
+
</FormGroup>
|
|
235
|
+
),
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
export const Disabled: Story = {
|
|
239
|
+
parameters: {
|
|
240
|
+
docs: {
|
|
241
|
+
description: {
|
|
242
|
+
story:
|
|
243
|
+
'`<fieldset disabled>` disables every descendant form control at once. Labels are also dimmed via the `fieldset:disabled` CSS rule.',
|
|
244
|
+
},
|
|
245
|
+
},
|
|
246
|
+
},
|
|
247
|
+
render: () => (
|
|
248
|
+
<FieldSet disabled>
|
|
249
|
+
<FieldLegend>Which countries have you visited?</FieldLegend>
|
|
250
|
+
|
|
251
|
+
<div className="flex flex-col gap-gds-2">
|
|
252
|
+
<FieldItem>
|
|
253
|
+
<Checkbox id="disabled-country-france" defaultChecked />
|
|
254
|
+
|
|
255
|
+
<Label htmlFor="disabled-country-france">France</Label>
|
|
256
|
+
</FieldItem>
|
|
257
|
+
|
|
258
|
+
<FieldItem>
|
|
259
|
+
<Checkbox id="disabled-country-germany" />
|
|
260
|
+
|
|
261
|
+
<Label htmlFor="disabled-country-germany">Germany</Label>
|
|
262
|
+
</FieldItem>
|
|
263
|
+
|
|
264
|
+
<FieldItem>
|
|
265
|
+
<Checkbox id="disabled-country-spain" />
|
|
266
|
+
|
|
267
|
+
<Label htmlFor="disabled-country-spain">Spain</Label>
|
|
268
|
+
</FieldItem>
|
|
269
|
+
</div>
|
|
270
|
+
</FieldSet>
|
|
271
|
+
),
|
|
272
|
+
};
|
|
273
|
+
|
|
274
|
+
export const WithHeading: Story = {
|
|
275
|
+
parameters: {
|
|
276
|
+
docs: {
|
|
277
|
+
description: {
|
|
278
|
+
story:
|
|
279
|
+
"FieldLegend has no size variants, it's plain body text, matching GDS. For a heading-like look (e.g. when the fieldset's question is the only content on the page), nest a real Heading inside it instead, the same pattern GDS itself documents: an <h1> inside <legend>.",
|
|
280
|
+
},
|
|
281
|
+
},
|
|
282
|
+
},
|
|
283
|
+
render: () => (
|
|
284
|
+
<FieldSet>
|
|
285
|
+
<FieldLegend>
|
|
286
|
+
<Heading level="h1">Consent preferences</Heading>
|
|
287
|
+
</FieldLegend>
|
|
288
|
+
|
|
289
|
+
<div className="flex flex-col gap-gds-2">
|
|
290
|
+
<FieldItem>
|
|
291
|
+
<Checkbox id="consent-name" defaultChecked />
|
|
292
|
+
|
|
293
|
+
<Label htmlFor="consent-name">Share my name</Label>
|
|
294
|
+
</FieldItem>
|
|
295
|
+
|
|
296
|
+
<FieldItem>
|
|
297
|
+
<Checkbox id="consent-email" />
|
|
298
|
+
|
|
299
|
+
<Label htmlFor="consent-email">Share my email address</Label>
|
|
300
|
+
</FieldItem>
|
|
301
|
+
</div>
|
|
302
|
+
</FieldSet>
|
|
303
|
+
),
|
|
304
|
+
};
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import type { ComponentProps } from 'react';
|
|
4
|
+
|
|
5
|
+
import { cn } from '../../utils/components';
|
|
6
|
+
|
|
7
|
+
export type FieldSetProps = ComponentProps<'fieldset'>;
|
|
8
|
+
|
|
9
|
+
export const FieldSet = ({ className, ...props }: FieldSetProps) => (
|
|
10
|
+
<fieldset data-slot="field-set" className={cn('cn-field-set', className)} {...props} />
|
|
11
|
+
);
|
|
12
|
+
|
|
13
|
+
export type FieldLegendProps = ComponentProps<'legend'>;
|
|
14
|
+
|
|
15
|
+
export const FieldLegend = ({ className, ...props }: FieldLegendProps) => (
|
|
16
|
+
<legend data-slot="field-legend" className={cn('cn-field-legend', className)} {...props} />
|
|
17
|
+
);
|
|
18
|
+
|
|
19
|
+
// MARK: FieldItem
|
|
20
|
+
// Mirrors GDS's own govuk-radios__item/govuk-checkboxes__item: a plain div
|
|
21
|
+
// around explicit, separately-associated Radio/Checkbox + Label + optional
|
|
22
|
+
// HintText siblings (real GDS, not a wrapping <label>), so a hint is
|
|
23
|
+
// announced as its own accessible description rather than folded into the
|
|
24
|
+
// control's name. Wire id/htmlFor/aria-describedby by hand, same as
|
|
25
|
+
// FormGroup, no context here, see the WithHints story for the pattern.
|
|
26
|
+
export type FieldItemProps = ComponentProps<'div'>;
|
|
27
|
+
|
|
28
|
+
export const FieldItem = ({ className, ...props }: FieldItemProps) => (
|
|
29
|
+
<div data-slot="field-item" className={cn('cn-field-item', className)} {...props} />
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
export type FieldDividerProps = ComponentProps<'div'>;
|
|
33
|
+
|
|
34
|
+
export const FieldDivider = ({ className, children = 'or', ...props }: FieldDividerProps) => (
|
|
35
|
+
<div data-slot="field-divider" className={cn('cn-field-divider', className)} {...props}>
|
|
36
|
+
{children}
|
|
37
|
+
</div>
|
|
38
|
+
);
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
@layer components {
|
|
2
|
+
/* MARK: File upload
|
|
3
|
+
Two-bordered-section layout: trigger has its own cn-form-border (right edge
|
|
4
|
+
acts as the internal divider), field wrapper has cn-form-border with no left
|
|
5
|
+
border so it merges flush. Error border and focus ring apply only to the
|
|
6
|
+
field section, matching the InputGroup prefix/swatch pattern. */
|
|
7
|
+
.cn-file-upload {
|
|
8
|
+
@apply flex w-full;
|
|
9
|
+
|
|
10
|
+
&:has(:disabled) {
|
|
11
|
+
@apply opacity-50;
|
|
12
|
+
|
|
13
|
+
.cn-file-upload-trigger {
|
|
14
|
+
@apply cursor-not-allowed;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/* MARK: Trigger
|
|
20
|
+
<label> styled as a secondary-button swatch. Its right border doubles as
|
|
21
|
+
the visual divider between the trigger and the filename area. */
|
|
22
|
+
.cn-file-upload-trigger {
|
|
23
|
+
@apply cn-form-swatch shrink-0 px-gds-2 text-body cursor-pointer select-none border-r-0;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
.cn-file-upload:not(:has(:disabled)) .cn-file-upload-trigger {
|
|
27
|
+
&:hover {
|
|
28
|
+
@apply bg-muted;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/* MARK: Field
|
|
33
|
+
Wraps the hidden input and filename display. No left border, butts flush
|
|
34
|
+
against the trigger. Focus ring and error border are scoped here so the
|
|
35
|
+
trigger is unaffected when the field is invalid. bg-panel is explicit
|
|
36
|
+
since, unlike a real <input> (opaque by default via the browser's own
|
|
37
|
+
UA styles), the filename display is a plain <span> with no background
|
|
38
|
+
of its own, it would otherwise show through to whatever's behind the
|
|
39
|
+
component (e.g. blending into an Accordion's own background) instead of
|
|
40
|
+
reading as a distinct field. */
|
|
41
|
+
.cn-file-upload-field {
|
|
42
|
+
@apply cn-form-border relative flex items-center flex-1 min-w-0 h-10 bg-panel;
|
|
43
|
+
|
|
44
|
+
&:focus-within {
|
|
45
|
+
@apply outline-focus;
|
|
46
|
+
box-shadow: inset 0 0 0 var(--border-width-form) var(--color-divider-input);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
&:has([aria-invalid='true']),
|
|
50
|
+
&:has(:user-invalid) {
|
|
51
|
+
@apply border-error;
|
|
52
|
+
|
|
53
|
+
&:focus-within {
|
|
54
|
+
box-shadow: inset 0 0 0 var(--border-width-form) var(--color-error);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/* MARK: Hidden input */
|
|
60
|
+
.cn-file-upload-input {
|
|
61
|
+
@apply sr-only;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/* MARK: Filename display
|
|
65
|
+
min-w-0 overrides the flex item default of min-width: auto, which
|
|
66
|
+
otherwise refuses to shrink below the filename's own unbroken width
|
|
67
|
+
(truncate implies white-space: nowrap) and overflows the container
|
|
68
|
+
instead of actually truncating. Same fix applied to the parent
|
|
69
|
+
cn-file-upload-field above, a flex item itself, for the same reason. */
|
|
70
|
+
.cn-file-upload-filename {
|
|
71
|
+
@apply flex-1 min-w-0 px-gds-2 text-base text-body truncate;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
.cn-file-upload[data-state='empty'] .cn-file-upload-filename {
|
|
75
|
+
@apply text-body-secondary;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/* MARK: Clear button
|
|
79
|
+
Sits inside the same bordered field as the filename, not a separate
|
|
80
|
+
element outside it. mr-gds-1 keeps it off the field's own right border,
|
|
81
|
+
matching the breathing room the filename already gets on its own side. */
|
|
82
|
+
.cn-file-upload-clear {
|
|
83
|
+
@apply mr-gds-1;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react-vite';
|
|
2
|
+
|
|
3
|
+
import { FileUpload } from './FileUpload';
|
|
4
|
+
import { ErrorText } from '../ErrorText/ErrorText';
|
|
5
|
+
import { FormGroup } from '../Form/FormGroup';
|
|
6
|
+
import { HintText } from '../HintText/HintText';
|
|
7
|
+
import { Label } from '../Label/Label';
|
|
8
|
+
|
|
9
|
+
const meta: Meta<typeof FileUpload> = {
|
|
10
|
+
title: 'Components/FileUpload',
|
|
11
|
+
component: FileUpload,
|
|
12
|
+
tags: ['autodocs'],
|
|
13
|
+
parameters: {
|
|
14
|
+
docs: {
|
|
15
|
+
description: {
|
|
16
|
+
component:
|
|
17
|
+
'A styled file picker that matches the height and border treatment of the Input component. The "Select file" trigger is a `<label>` element associated with a visually hidden but keyboard-accessible `<input type="file">`, so no JavaScript is needed to open the file dialog. It works with click, touch, and keyboard. Pair it with a `<Label>` above for the field name, the same as any other form field. Once a file is selected, a clear button appears to remove it, pick a real file in the Default story below to see it.',
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export default meta;
|
|
24
|
+
|
|
25
|
+
type Story = StoryObj<typeof FileUpload>;
|
|
26
|
+
|
|
27
|
+
export const Default: Story = {
|
|
28
|
+
render: (args) => (
|
|
29
|
+
<FormGroup>
|
|
30
|
+
<Label htmlFor="id-a">Upload boundary file</Label>
|
|
31
|
+
|
|
32
|
+
<FileUpload {...args} id="id-a" />
|
|
33
|
+
</FormGroup>
|
|
34
|
+
),
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export const WithAcceptHint: Story = {
|
|
38
|
+
parameters: {
|
|
39
|
+
docs: {
|
|
40
|
+
description: {
|
|
41
|
+
story:
|
|
42
|
+
'Pass `accept` to restrict the file picker to specific types. Hint text below the field describes the constraint to users, the component itself does not render hint text.',
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
render: (args) => (
|
|
47
|
+
<FormGroup>
|
|
48
|
+
<Label htmlFor="id-b">Upload boundary file</Label>
|
|
49
|
+
|
|
50
|
+
<HintText>Supports .geojson, .kml.</HintText>
|
|
51
|
+
|
|
52
|
+
<FileUpload {...args} id="id-b" accept=".geojson,.kml" />
|
|
53
|
+
</FormGroup>
|
|
54
|
+
),
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
export const WithError: Story = {
|
|
58
|
+
parameters: {
|
|
59
|
+
docs: {
|
|
60
|
+
description: {
|
|
61
|
+
story: 'Set `aria-invalid="true"` to show the error border on the filename area.',
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
render: (args) => (
|
|
66
|
+
<FormGroup invalid>
|
|
67
|
+
<Label htmlFor="id-c">Upload boundary file</Label>
|
|
68
|
+
|
|
69
|
+
<ErrorText id="file-upload-error">Select a file to continue</ErrorText>
|
|
70
|
+
|
|
71
|
+
<FileUpload {...args} aria-invalid="true" aria-describedby="file-upload-error" id="id-c" />
|
|
72
|
+
</FormGroup>
|
|
73
|
+
),
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
export const LongFilename: Story = {
|
|
77
|
+
parameters: {
|
|
78
|
+
docs: {
|
|
79
|
+
description: {
|
|
80
|
+
story:
|
|
81
|
+
'A long filename truncates with an ellipsis rather than overflowing the field, the component stays no wider than its container. Uses `placeholder` here to demonstrate this without needing to actually pick a file, real selected filenames go through the same truncation.',
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
},
|
|
85
|
+
render: (args) => (
|
|
86
|
+
<FormGroup className="max-w-xs">
|
|
87
|
+
<Label htmlFor="id-e">Upload boundary file</Label>
|
|
88
|
+
|
|
89
|
+
<FileUpload
|
|
90
|
+
{...args}
|
|
91
|
+
id="id-e"
|
|
92
|
+
placeholder="environment-agency-flood-risk-boundary-data-2024-final-v3.geojson"
|
|
93
|
+
/>
|
|
94
|
+
</FormGroup>
|
|
95
|
+
),
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
export const Disabled: Story = {
|
|
99
|
+
render: (args) => (
|
|
100
|
+
<FormGroup>
|
|
101
|
+
<Label htmlFor="id-d">Upload boundary file</Label>
|
|
102
|
+
|
|
103
|
+
<FileUpload {...args} id="id-d" disabled />
|
|
104
|
+
</FormGroup>
|
|
105
|
+
),
|
|
106
|
+
};
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useRef, useState, type ChangeEvent, type ComponentProps, type ReactNode } from 'react';
|
|
4
|
+
|
|
5
|
+
import { LuX } from 'react-icons/lu';
|
|
6
|
+
|
|
7
|
+
import { cn } from '../../utils/components';
|
|
8
|
+
import { Button } from '../Button/Button';
|
|
9
|
+
|
|
10
|
+
export type FileUploadProps = Omit<ComponentProps<'input'>, 'type' | 'id'> & {
|
|
11
|
+
id: string;
|
|
12
|
+
triggerLabel?: ReactNode;
|
|
13
|
+
placeholder?: ReactNode;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export const FileUpload = ({
|
|
17
|
+
id,
|
|
18
|
+
triggerLabel = 'Select file',
|
|
19
|
+
placeholder = 'No file selected',
|
|
20
|
+
className,
|
|
21
|
+
onChange,
|
|
22
|
+
...props
|
|
23
|
+
}: FileUploadProps) => {
|
|
24
|
+
const inputRef = useRef<HTMLInputElement>(null);
|
|
25
|
+
const [displayName, setDisplayName] = useState<string | null>(null);
|
|
26
|
+
|
|
27
|
+
const handleChange = (event: ChangeEvent<HTMLInputElement>) => {
|
|
28
|
+
const files = event.target.files;
|
|
29
|
+
|
|
30
|
+
if (!files || files.length === 0) {
|
|
31
|
+
setDisplayName(null);
|
|
32
|
+
} else if (files.length === 1 && files[0]) {
|
|
33
|
+
setDisplayName(files[0].name);
|
|
34
|
+
} else {
|
|
35
|
+
setDisplayName(`${files.length} files selected`);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
onChange?.(event);
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const handleClear = () => {
|
|
42
|
+
const input = inputRef.current;
|
|
43
|
+
|
|
44
|
+
if (!input) {
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// A file input's value can't be set programmatically, but clearing it
|
|
49
|
+
// back to empty is allowed. Dispatching a real change event (rather than
|
|
50
|
+
// calling setDisplayName directly) reuses handleChange as the single
|
|
51
|
+
// source of truth, so the consumer's own onChange fires too, exactly as
|
|
52
|
+
// if the file had been cleared through the picker itself.
|
|
53
|
+
input.value = '';
|
|
54
|
+
input.dispatchEvent(new Event('change', { bubbles: true }));
|
|
55
|
+
|
|
56
|
+
// The clear button disappears once displayName clears, move focus
|
|
57
|
+
// somewhere sensible rather than letting it fall back to <body>.
|
|
58
|
+
input.focus();
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
return (
|
|
62
|
+
<div
|
|
63
|
+
data-slot="file-upload"
|
|
64
|
+
data-state={displayName ? 'has-file' : 'empty'}
|
|
65
|
+
className={cn('cn-file-upload', className)}
|
|
66
|
+
>
|
|
67
|
+
<label htmlFor={id} className="cn-file-upload-trigger">
|
|
68
|
+
{triggerLabel}
|
|
69
|
+
</label>
|
|
70
|
+
|
|
71
|
+
<div className="cn-file-upload-field">
|
|
72
|
+
<input
|
|
73
|
+
ref={inputRef}
|
|
74
|
+
type="file"
|
|
75
|
+
id={id}
|
|
76
|
+
className="cn-file-upload-input"
|
|
77
|
+
onChange={handleChange}
|
|
78
|
+
{...props}
|
|
79
|
+
/>
|
|
80
|
+
|
|
81
|
+
<span className="cn-file-upload-filename" aria-live="polite">
|
|
82
|
+
{displayName ?? placeholder}
|
|
83
|
+
</span>
|
|
84
|
+
|
|
85
|
+
{displayName ? (
|
|
86
|
+
<Button
|
|
87
|
+
variant="icon"
|
|
88
|
+
appearance="ghost"
|
|
89
|
+
aria-label="Remove selected file"
|
|
90
|
+
onClick={handleClear}
|
|
91
|
+
className="cn-file-upload-clear"
|
|
92
|
+
>
|
|
93
|
+
<LuX aria-hidden />
|
|
94
|
+
</Button>
|
|
95
|
+
) : null}
|
|
96
|
+
</div>
|
|
97
|
+
</div>
|
|
98
|
+
);
|
|
99
|
+
};
|
|
@@ -1,11 +1,13 @@
|
|
|
1
|
-
import { ExternalLink } from '
|
|
1
|
+
import { ExternalLink } from '../ExternalLink/ExternalLink';
|
|
2
2
|
|
|
3
3
|
export const Copyright = () => {
|
|
4
4
|
const currentYear = new Date().getFullYear();
|
|
5
5
|
|
|
6
6
|
return (
|
|
7
7
|
<ExternalLink
|
|
8
|
-
className="text-
|
|
8
|
+
className="text-balance sm:text-nowrap text-center sm:text-left justify-start"
|
|
9
|
+
appearance="contrast"
|
|
10
|
+
variant="plain"
|
|
9
11
|
href="https://www.nationalarchives.gov.uk/information-management/re-using-public-sector-information/uk-government-licensing-framework/crown-copyright/"
|
|
10
12
|
>
|
|
11
13
|
© Crown Copyright {currentYear}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react-vite';
|
|
2
|
+
|
|
3
|
+
import { Footer } from './Footer';
|
|
4
|
+
|
|
5
|
+
const meta = {
|
|
6
|
+
title: 'Components/Footer',
|
|
7
|
+
component: Footer,
|
|
8
|
+
parameters: {
|
|
9
|
+
layout: 'fullscreen',
|
|
10
|
+
docs: {
|
|
11
|
+
description: {
|
|
12
|
+
component:
|
|
13
|
+
'Full site footer with support links, the Open Government Licence notice, and crown copyright.',
|
|
14
|
+
},
|
|
15
|
+
},
|
|
16
|
+
},
|
|
17
|
+
tags: ['autodocs'],
|
|
18
|
+
} satisfies Meta<typeof Footer>;
|
|
19
|
+
|
|
20
|
+
export default meta;
|
|
21
|
+
|
|
22
|
+
type Story = StoryObj<typeof meta>;
|
|
23
|
+
|
|
24
|
+
export const Default: Story = {};
|