@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
|
@@ -27,10 +27,9 @@ describe('Geocoder', () => {
|
|
|
27
27
|
const input = screen.getByRole('combobox');
|
|
28
28
|
const user = userEvent.setup();
|
|
29
29
|
|
|
30
|
-
//
|
|
30
|
+
// Typing alone triggers the debounced search, Enter here is just
|
|
31
|
+
// incidental (it's a no-op with no option focused yet)
|
|
31
32
|
await user.type(input, 'Lon');
|
|
32
|
-
|
|
33
|
-
// Press Enter to trigger search
|
|
34
33
|
await user.keyboard('{Enter}');
|
|
35
34
|
|
|
36
35
|
// Wait for the result to appear
|
|
@@ -111,4 +110,48 @@ describe('Geocoder', () => {
|
|
|
111
110
|
expect(screen.queryByText('London')).not.toBeInTheDocument();
|
|
112
111
|
expect(input).toHaveValue('');
|
|
113
112
|
});
|
|
113
|
+
|
|
114
|
+
it('searches automatically after the debounce delay, without pressing Enter', async () => {
|
|
115
|
+
const search = vi.fn().mockResolvedValue([{ id: '1', label: 'London', center: [0, 0] }]);
|
|
116
|
+
|
|
117
|
+
render(<Geocoder map={mockMap} search={search} typeaheadDelay={10} />);
|
|
118
|
+
|
|
119
|
+
const input = screen.getByRole('combobox');
|
|
120
|
+
const user = userEvent.setup();
|
|
121
|
+
|
|
122
|
+
await user.type(input, 'Lon');
|
|
123
|
+
|
|
124
|
+
const option = await screen.findByText('London');
|
|
125
|
+
|
|
126
|
+
expect(option).toBeInTheDocument();
|
|
127
|
+
expect(search).toHaveBeenCalledWith('Lon');
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it('selects a result, then clears it with Backspace', async () => {
|
|
131
|
+
const search = vi.fn().mockResolvedValue([{ id: '1', label: 'London', center: [0, 0] }]);
|
|
132
|
+
|
|
133
|
+
render(<Geocoder map={mockMap} search={search} typeaheadDelay={10} />);
|
|
134
|
+
|
|
135
|
+
const input = screen.getByRole('combobox');
|
|
136
|
+
const user = userEvent.setup();
|
|
137
|
+
|
|
138
|
+
await user.type(input, 'Lon');
|
|
139
|
+
|
|
140
|
+
const option = await screen.findByText('London');
|
|
141
|
+
|
|
142
|
+
await user.click(option);
|
|
143
|
+
|
|
144
|
+
// Selecting shows the result's label in the control (react-select's
|
|
145
|
+
// standard single-select behaviour), the same as any other Select in
|
|
146
|
+
// this library, rather than leaving the raw typed query behind.
|
|
147
|
+
expect(screen.getByText('London')).toBeInTheDocument();
|
|
148
|
+
|
|
149
|
+
// The visible clear (x) button itself is aria-hidden, react-select's own
|
|
150
|
+
// default, mouse/touch-only, the accessible equivalent is Backspace on
|
|
151
|
+
// an empty, focused control.
|
|
152
|
+
await user.click(input);
|
|
153
|
+
await user.keyboard('{Backspace}');
|
|
154
|
+
|
|
155
|
+
expect(screen.queryByText('London')).not.toBeInTheDocument();
|
|
156
|
+
});
|
|
114
157
|
});
|
|
@@ -3,12 +3,13 @@
|
|
|
3
3
|
import { useCallback, useEffect, useId, useRef, useState } from 'react';
|
|
4
4
|
|
|
5
5
|
import type Map from 'ol/Map';
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
6
|
+
import { LuLoaderCircle, LuSearch } from 'react-icons/lu';
|
|
7
|
+
import type { GroupBase, OptionsOrGroups } from 'react-select';
|
|
8
8
|
|
|
9
9
|
import { groupResults } from './groupResults';
|
|
10
10
|
import type { GeocoderResult } from './types';
|
|
11
|
-
import {
|
|
11
|
+
import { InputGroup, InputPrefix, InputSwatch } from '../../components/InputGroup/InputGroup';
|
|
12
|
+
import { AsyncSelect } from '../../components/Select/AsyncSelect';
|
|
12
13
|
|
|
13
14
|
// Typeahead defaults
|
|
14
15
|
const DEFAULT_TYPEAHEAD_DELAY_MS = 500;
|
|
@@ -21,6 +22,8 @@ const DEFAULT_ZOOM_OUT_DURATION = 800;
|
|
|
21
22
|
const DEFAULT_FLY_TO_DURATION = 1500;
|
|
22
23
|
const DEFAULT_FIT_PADDING = 40;
|
|
23
24
|
|
|
25
|
+
type GeocoderGroup = GroupBase<GeocoderResult>;
|
|
26
|
+
|
|
24
27
|
export type GeocoderProps = {
|
|
25
28
|
map: Map;
|
|
26
29
|
search: (query: string) => Promise<GeocoderResult[]>;
|
|
@@ -41,9 +44,7 @@ export type GeocoderProps = {
|
|
|
41
44
|
fitPadding?: number;
|
|
42
45
|
/** Debug callback for logging results and selections */
|
|
43
46
|
onDebug?: (event: { type: 'results' | 'select' | 'clear'; data?: unknown }) => void;
|
|
44
|
-
/**
|
|
45
|
-
enableTypeahead?: boolean;
|
|
46
|
-
/** Debounce delay in ms for typeahead search (default: 500) */
|
|
47
|
+
/** Debounce delay in ms before searching once the user stops typing (default: 500) */
|
|
47
48
|
typeaheadDelay?: number;
|
|
48
49
|
};
|
|
49
50
|
|
|
@@ -60,302 +61,178 @@ export const Geocoder = ({
|
|
|
60
61
|
flyToDuration = DEFAULT_FLY_TO_DURATION,
|
|
61
62
|
fitPadding = DEFAULT_FIT_PADDING,
|
|
62
63
|
onDebug,
|
|
63
|
-
enableTypeahead = false,
|
|
64
64
|
typeaheadDelay = DEFAULT_TYPEAHEAD_DELAY_MS,
|
|
65
65
|
}: GeocoderProps) => {
|
|
66
66
|
const generatedId = useId();
|
|
67
|
-
const
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
const [
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
67
|
+
const inputId = providedId ?? generatedId;
|
|
68
|
+
|
|
69
|
+
// Controlled rather than left uncontrolled: shows the selected result's
|
|
70
|
+
// label in the control (standard single-select behaviour, and the only
|
|
71
|
+
// way react-select's own clear button appears at all, it's tied to
|
|
72
|
+
// hasValue, not to whether there's typed input text) and lets clearing
|
|
73
|
+
// reset it back to null.
|
|
74
|
+
const [value, setValue] = useState<GeocoderResult | null>(null);
|
|
75
|
+
|
|
76
|
+
// Swaps the InputPrefix icon between search and spinner instead of using
|
|
77
|
+
// react-select's own indicators area (hidden via hideLoadingIndicator
|
|
78
|
+
// below), which otherwise grows/shrinks as the indicator mounts and
|
|
79
|
+
// unmounts and shifts the typed text.
|
|
78
80
|
const [isSearching, setIsSearching] = useState(false);
|
|
79
81
|
|
|
80
|
-
|
|
82
|
+
// react-select's own useAsync doesn't debounce loadOptions itself, it
|
|
83
|
+
// fires on every keystroke, so the delay is implemented here instead.
|
|
84
|
+
const debounceRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
81
85
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
if (
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
return;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
setIsSearching(true);
|
|
91
|
-
setStatus('Searching...');
|
|
92
|
-
|
|
93
|
-
try {
|
|
94
|
-
const data = await search(queryStr);
|
|
95
|
-
|
|
96
|
-
setResults(data);
|
|
97
|
-
setSelectedId(null);
|
|
98
|
-
setOpen(true);
|
|
99
|
-
setActiveIndex(-1);
|
|
100
|
-
setStatus(data.length ? `${data.length} results available` : 'No results found');
|
|
101
|
-
|
|
102
|
-
// Debug callback for results
|
|
103
|
-
onDebug?.({
|
|
104
|
-
type: 'results',
|
|
105
|
-
data: { query: queryStr, results: data, count: data.length },
|
|
106
|
-
});
|
|
107
|
-
} catch (error) {
|
|
108
|
-
console.error('Search error:', error);
|
|
109
|
-
setStatus('There was an error reaching the server');
|
|
110
|
-
setResults([]);
|
|
111
|
-
} finally {
|
|
112
|
-
setIsSearching(false);
|
|
86
|
+
useEffect(
|
|
87
|
+
() => () => {
|
|
88
|
+
if (debounceRef.current) {
|
|
89
|
+
clearTimeout(debounceRef.current);
|
|
113
90
|
}
|
|
114
91
|
},
|
|
115
|
-
[
|
|
92
|
+
[],
|
|
116
93
|
);
|
|
117
94
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
view.animate({ zoom: flyZoom, duration: zoomOutDuration }, () => {
|
|
151
|
-
view.fit(result.extent!, { padding, duration: flyToDuration });
|
|
152
|
-
});
|
|
153
|
-
} else {
|
|
154
|
-
view.fit(result.extent, { padding, duration: flyToDuration });
|
|
155
|
-
}
|
|
156
|
-
} else if (result.center) {
|
|
157
|
-
if (shouldZoomOut) {
|
|
158
|
-
// Step 1: Zoom out, then combined pan + zoom in (like ol-geocoder)
|
|
159
|
-
view.animate({ zoom: flyZoom, duration: zoomOutDuration }, () => {
|
|
95
|
+
const selectResult = useCallback(
|
|
96
|
+
(result: GeocoderResult) => {
|
|
97
|
+
const view = map.getView();
|
|
98
|
+
const currentZoom = view.getZoom() ?? 10;
|
|
99
|
+
const shouldZoomOut = currentZoom > flyOutThreshold;
|
|
100
|
+
const flyZoom = shouldZoomOut ? Math.max(currentZoom - 4, flyOutZoomMin) : currentZoom;
|
|
101
|
+
const padding = [fitPadding, fitPadding, fitPadding, fitPadding] as [
|
|
102
|
+
number,
|
|
103
|
+
number,
|
|
104
|
+
number,
|
|
105
|
+
number,
|
|
106
|
+
];
|
|
107
|
+
// Use result-specific zoom if provided, otherwise fall back to targetZoom prop
|
|
108
|
+
const finalZoom = result.zoom ?? targetZoom;
|
|
109
|
+
|
|
110
|
+
if (result.extent) {
|
|
111
|
+
if (shouldZoomOut) {
|
|
112
|
+
// Step 1: Zoom out, then fit to extent (combined pan + zoom)
|
|
113
|
+
view.animate({ zoom: flyZoom, duration: zoomOutDuration }, () => {
|
|
114
|
+
view.fit(result.extent!, { padding, duration: flyToDuration });
|
|
115
|
+
});
|
|
116
|
+
} else {
|
|
117
|
+
view.fit(result.extent, { padding, duration: flyToDuration });
|
|
118
|
+
}
|
|
119
|
+
} else if (result.center) {
|
|
120
|
+
if (shouldZoomOut) {
|
|
121
|
+
// Step 1: Zoom out, then combined pan + zoom in (like ol-geocoder)
|
|
122
|
+
view.animate({ zoom: flyZoom, duration: zoomOutDuration }, () => {
|
|
123
|
+
view.animate({ center: result.center, zoom: finalZoom, duration: flyToDuration });
|
|
124
|
+
});
|
|
125
|
+
} else {
|
|
126
|
+
// Combined pan + zoom in one smooth animation
|
|
160
127
|
view.animate({ center: result.center, zoom: finalZoom, duration: flyToDuration });
|
|
161
|
-
}
|
|
162
|
-
} else {
|
|
163
|
-
// Combined pan + zoom in one smooth animation
|
|
164
|
-
view.animate({ center: result.center, zoom: finalZoom, duration: flyToDuration });
|
|
128
|
+
}
|
|
165
129
|
}
|
|
166
|
-
}
|
|
167
130
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
131
|
+
// Debug callback for selection
|
|
132
|
+
onDebug?.({
|
|
133
|
+
type: 'select',
|
|
134
|
+
data: { result, zoom: finalZoom, currentZoom, shouldZoomOut },
|
|
135
|
+
});
|
|
136
|
+
},
|
|
137
|
+
[
|
|
138
|
+
map,
|
|
139
|
+
flyOutThreshold,
|
|
140
|
+
flyOutZoomMin,
|
|
141
|
+
fitPadding,
|
|
142
|
+
targetZoom,
|
|
143
|
+
zoomOutDuration,
|
|
144
|
+
flyToDuration,
|
|
145
|
+
onDebug,
|
|
146
|
+
],
|
|
147
|
+
);
|
|
171
148
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
149
|
+
const loadOptions = useCallback(
|
|
150
|
+
(
|
|
151
|
+
inputValue: string,
|
|
152
|
+
callback: (options: OptionsOrGroups<GeocoderResult, GeocoderGroup>) => void,
|
|
153
|
+
) => {
|
|
154
|
+
if (debounceRef.current) {
|
|
155
|
+
clearTimeout(debounceRef.current);
|
|
156
|
+
}
|
|
178
157
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
setOpen(false);
|
|
183
|
-
setActiveIndex(-1);
|
|
184
|
-
setSelectedId(null);
|
|
185
|
-
setStatus('');
|
|
186
|
-
inputRef.current?.focus();
|
|
158
|
+
if (inputValue.length < minChars) {
|
|
159
|
+
setIsSearching(false);
|
|
160
|
+
callback([]);
|
|
187
161
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
};
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
191
164
|
|
|
192
|
-
|
|
193
|
-
// Show previous results when focusing the input
|
|
194
|
-
if (results.length > 0) {
|
|
195
|
-
setOpen(true);
|
|
196
|
-
setActiveIndex(-1);
|
|
197
|
-
}
|
|
198
|
-
};
|
|
165
|
+
setIsSearching(true);
|
|
199
166
|
|
|
200
|
-
|
|
201
|
-
|
|
167
|
+
debounceRef.current = setTimeout(async () => {
|
|
168
|
+
try {
|
|
169
|
+
const results = await search(inputValue);
|
|
170
|
+
const grouped = groupResults(results);
|
|
171
|
+
const options = Object.entries(grouped).map(([label, groupOptions]) => ({
|
|
172
|
+
label,
|
|
173
|
+
options: groupOptions,
|
|
174
|
+
}));
|
|
175
|
+
|
|
176
|
+
callback(options);
|
|
177
|
+
|
|
178
|
+
// Debug callback for results
|
|
179
|
+
onDebug?.({
|
|
180
|
+
type: 'results',
|
|
181
|
+
data: { query: inputValue, results, count: results.length },
|
|
182
|
+
});
|
|
183
|
+
} catch (error) {
|
|
184
|
+
console.error('Search error:', error);
|
|
185
|
+
callback([]);
|
|
186
|
+
} finally {
|
|
187
|
+
setIsSearching(false);
|
|
188
|
+
}
|
|
189
|
+
}, typeaheadDelay);
|
|
190
|
+
},
|
|
191
|
+
[minChars, search, typeaheadDelay, onDebug],
|
|
192
|
+
);
|
|
202
193
|
|
|
203
194
|
return (
|
|
204
|
-
<
|
|
205
|
-
<
|
|
206
|
-
{status}
|
|
207
|
-
</div>
|
|
208
|
-
|
|
209
|
-
<label htmlFor={id} className="sr-only">
|
|
195
|
+
<InputGroup className="min-w-xs max-w-sm">
|
|
196
|
+
<label htmlFor={inputId} className="sr-only">
|
|
210
197
|
Search for a place
|
|
211
198
|
</label>
|
|
212
199
|
|
|
213
|
-
<
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
<IoSearch
|
|
218
|
-
size={20}
|
|
219
|
-
className="absolute left-3 top-1/2 -translate-y-1/2 pointer-events-none text-gray-400"
|
|
220
|
-
/>
|
|
221
|
-
|
|
222
|
-
<input
|
|
223
|
-
ref={inputRef}
|
|
224
|
-
id={id}
|
|
225
|
-
type="text"
|
|
226
|
-
role="combobox"
|
|
227
|
-
aria-autocomplete="list"
|
|
228
|
-
aria-expanded={open}
|
|
229
|
-
aria-controls={listboxId}
|
|
230
|
-
aria-activedescendant={
|
|
231
|
-
activeIndex >= 0 ? `${listboxId}-option-${activeIndex}` : undefined
|
|
232
|
-
}
|
|
233
|
-
value={query}
|
|
234
|
-
placeholder={placeholder}
|
|
235
|
-
onChange={(e) => setQuery(e.target.value)}
|
|
236
|
-
onFocus={handleFocus}
|
|
237
|
-
onKeyDown={(e) => {
|
|
238
|
-
if (e.key === 'ArrowDown') {
|
|
239
|
-
e.preventDefault();
|
|
240
|
-
|
|
241
|
-
if (!open && results.length > 0) {
|
|
242
|
-
setOpen(true);
|
|
243
|
-
} else {
|
|
244
|
-
setActiveIndex((i) => Math.min(i + 1, results.length - 1));
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
if (e.key === 'ArrowUp') {
|
|
249
|
-
e.preventDefault();
|
|
250
|
-
setActiveIndex((i) => Math.max(i - 1, 0));
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
if (e.key === 'Enter') {
|
|
254
|
-
e.preventDefault();
|
|
255
|
-
const activeResult = results[activeIndex];
|
|
256
|
-
|
|
257
|
-
if (activeResult && open) {
|
|
258
|
-
selectResult(activeResult);
|
|
259
|
-
} else {
|
|
260
|
-
performSearch(query);
|
|
261
|
-
}
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
if (e.key === 'Escape') {
|
|
265
|
-
e.preventDefault();
|
|
266
|
-
clearSearch();
|
|
267
|
-
}
|
|
268
|
-
}}
|
|
269
|
-
className="w-full h-12 pl-11 pr-11 border-0 bg-transparent text-base text-gray-800
|
|
270
|
-
placeholder:text-gray-500 focus:outline-none focus:ring-0"
|
|
271
|
-
/>
|
|
272
|
-
|
|
273
|
-
<div className="absolute right-2 top-1/2 -translate-y-1/2">
|
|
274
|
-
{(() => {
|
|
275
|
-
if (isSearching) {
|
|
276
|
-
return (
|
|
277
|
-
<div className="p-1">
|
|
278
|
-
<ImSpinner2 size={22} className="animate-spin text-gray-500" />
|
|
279
|
-
</div>
|
|
280
|
-
);
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
if (query) {
|
|
284
|
-
return (
|
|
285
|
-
<button
|
|
286
|
-
type="button"
|
|
287
|
-
onClick={clearSearch}
|
|
288
|
-
aria-label="Clear search"
|
|
289
|
-
className="p-1 hover:bg-gray-100 rounded transition-colors"
|
|
290
|
-
>
|
|
291
|
-
<IoClose size={20} className="text-gray-500" />
|
|
292
|
-
</button>
|
|
293
|
-
);
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
return <></>;
|
|
297
|
-
})()}
|
|
298
|
-
</div>
|
|
299
|
-
</div>
|
|
300
|
-
|
|
301
|
-
{/* Results dropdown with Mapbox styling */}
|
|
302
|
-
{open ? (
|
|
303
|
-
<div className="absolute z-1000 mt-1.5 w-full bg-white rounded shadow-lg overflow-hidden">
|
|
304
|
-
{results.length > 0 ? (
|
|
305
|
-
<div id={listboxId} role="listbox" tabIndex={-1} className="max-h-60 overflow-auto">
|
|
306
|
-
{Object.entries(grouped).map(([group, items]) => (
|
|
307
|
-
<div key={group} role="presentation">
|
|
308
|
-
<div
|
|
309
|
-
className="px-3 py-1.5 text-xs font-semibold text-gray-600 bg-gray-50 sticky
|
|
310
|
-
top-0"
|
|
311
|
-
>
|
|
312
|
-
{group}
|
|
313
|
-
</div>
|
|
314
|
-
|
|
315
|
-
<div role="group">
|
|
316
|
-
{items.map((item) => {
|
|
317
|
-
flatIndex += 1;
|
|
318
|
-
const itemIndex = flatIndex;
|
|
319
|
-
const isActive = itemIndex === activeIndex;
|
|
320
|
-
const isSelected = item.id === selectedId;
|
|
321
|
-
|
|
322
|
-
return (
|
|
323
|
-
<div
|
|
324
|
-
id={`${listboxId}-option-${itemIndex}`}
|
|
325
|
-
key={item.id}
|
|
326
|
-
role="option"
|
|
327
|
-
aria-selected={isSelected}
|
|
328
|
-
tabIndex={-1}
|
|
329
|
-
className={`scroll-mt-8 cursor-pointer px-3 py-2 text-sm text-gray-800
|
|
330
|
-
transition-colors ${isSelected ? 'bg-blue-50 font-semibold' : ''}
|
|
331
|
-
${isActive && !isSelected ? 'bg-gray-100' : ''}
|
|
332
|
-
${!isSelected && !isActive ? 'hover:bg-gray-50' : ''}`}
|
|
333
|
-
onMouseEnter={() => setActiveIndex(itemIndex)}
|
|
334
|
-
onMouseDown={() => selectResult(item)}
|
|
335
|
-
>
|
|
336
|
-
<div className="truncate">{item.label}</div>
|
|
337
|
-
</div>
|
|
338
|
-
);
|
|
339
|
-
})}
|
|
340
|
-
</div>
|
|
341
|
-
</div>
|
|
342
|
-
))}
|
|
343
|
-
</div>
|
|
200
|
+
<InputPrefix>
|
|
201
|
+
<InputSwatch>
|
|
202
|
+
{isSearching ? (
|
|
203
|
+
<LuLoaderCircle aria-hidden className="animate-spin" />
|
|
344
204
|
) : (
|
|
345
|
-
|
|
346
|
-
if (query && !isSearching) {
|
|
347
|
-
return (
|
|
348
|
-
<div className="px-3 py-6 text-center text-sm text-gray-500">
|
|
349
|
-
{status || 'No results found'}
|
|
350
|
-
</div>
|
|
351
|
-
);
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
return <></>;
|
|
355
|
-
})()
|
|
205
|
+
<LuSearch aria-hidden />
|
|
356
206
|
)}
|
|
357
|
-
</
|
|
358
|
-
|
|
359
|
-
|
|
207
|
+
</InputSwatch>
|
|
208
|
+
</InputPrefix>
|
|
209
|
+
|
|
210
|
+
<AsyncSelect<GeocoderResult, false, GeocoderGroup>
|
|
211
|
+
inputId={inputId}
|
|
212
|
+
value={value}
|
|
213
|
+
placeholder={placeholder}
|
|
214
|
+
loadOptions={loadOptions}
|
|
215
|
+
getOptionLabel={(option) => option.label}
|
|
216
|
+
getOptionValue={(option) => option.id}
|
|
217
|
+
isClearable
|
|
218
|
+
hideLoadingIndicator
|
|
219
|
+
noOptionsMessage={({ inputValue }) =>
|
|
220
|
+
inputValue.length < minChars
|
|
221
|
+
? `Enter at least ${minChars} characters`
|
|
222
|
+
: 'No results found'
|
|
223
|
+
}
|
|
224
|
+
screenReaderStatus={({ count }) => `${count} result${count === 1 ? '' : 's'} available`}
|
|
225
|
+
onChange={(option, meta) => {
|
|
226
|
+
setValue(option);
|
|
227
|
+
|
|
228
|
+
if (meta.action === 'select-option' && option) {
|
|
229
|
+
selectResult(option);
|
|
230
|
+
} else if (meta.action === 'clear') {
|
|
231
|
+
// Debug callback for clear
|
|
232
|
+
onDebug?.({ type: 'clear' });
|
|
233
|
+
}
|
|
234
|
+
}}
|
|
235
|
+
/>
|
|
236
|
+
</InputGroup>
|
|
360
237
|
);
|
|
361
238
|
};
|
package/src/map/index.ts
CHANGED
|
@@ -2,7 +2,7 @@ export * from './basemaps';
|
|
|
2
2
|
export * from './geocoder/index';
|
|
3
3
|
export * from './geometries';
|
|
4
4
|
export * from './LayerSwitcherPanel';
|
|
5
|
-
export * from './
|
|
5
|
+
export * from './LegendShim';
|
|
6
6
|
export * from './MapControlsOverlay';
|
|
7
7
|
export * from './utils';
|
|
8
8
|
export * from './MapComponent';
|
package/src/types/utils.ts
CHANGED
|
@@ -1,13 +1,3 @@
|
|
|
1
|
-
import type { ComponentProps } from 'react';
|
|
2
|
-
|
|
3
1
|
// Utility types for better developer experience
|
|
4
2
|
export type PropsWithRequired<T, K extends keyof T> = T & Required<Pick<T, K>>;
|
|
5
3
|
export type PropsWithOptional<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;
|
|
6
|
-
|
|
7
|
-
export type ExtendProps<
|
|
8
|
-
// `ComponentProps` internally constrains `Comp` to be `JSXElementConstructor<any>`,
|
|
9
|
-
// and since our type must have the same constraints to avoid errors, `any` is required here
|
|
10
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
11
|
-
Comp extends keyof React.JSX.IntrinsicElements | React.JSXElementConstructor<any>,
|
|
12
|
-
Props = object,
|
|
13
|
-
> = Props & Omit<ComponentProps<Comp>, keyof Props>;
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { cva } from 'class-variance-authority';
|
|
2
|
+
import { extendTailwindMerge } from 'tailwind-merge';
|
|
3
|
+
|
|
4
|
+
// This project's border-width scale (border-width-form/standard/wide, see
|
|
5
|
+
// @tpzdsp/eslint-config-dsp/tailwindcss/layout.css) and semantic colour scale
|
|
6
|
+
// (see .../tailwindcss/colors/defra-light.css) are custom theme tokens
|
|
7
|
+
// tailwind-merge has no way to know about by default. Without registering
|
|
8
|
+
// them, tailwind-merge fails to recognise e.g. `border-l-standard` as a
|
|
9
|
+
// border-width utility, misclassifies it as an unrecognised border-colour
|
|
10
|
+
// utility instead, and silently drops it when merged alongside a real colour
|
|
11
|
+
// utility like `border-error` (confirmed via the installed tailwind-merge:
|
|
12
|
+
// `twMerge('border-l-standard border-error')` -> `'border-error'`, the width
|
|
13
|
+
// class vanishes so the border renders at its default 0 width).
|
|
14
|
+
const BORDER_WIDTHS = ['form', 'standard', 'wide'];
|
|
15
|
+
|
|
16
|
+
const SEMANTIC_COLORS = [
|
|
17
|
+
'panel',
|
|
18
|
+
'panel-border',
|
|
19
|
+
'body',
|
|
20
|
+
'body-secondary',
|
|
21
|
+
'accent',
|
|
22
|
+
'accent-safe',
|
|
23
|
+
'accent-hover',
|
|
24
|
+
'accent-shadow',
|
|
25
|
+
'accent-subtle',
|
|
26
|
+
'on-accent',
|
|
27
|
+
'on-accent-subtle',
|
|
28
|
+
'link',
|
|
29
|
+
'link-hover',
|
|
30
|
+
'link-visited',
|
|
31
|
+
'link-active',
|
|
32
|
+
'divider',
|
|
33
|
+
'divider-input',
|
|
34
|
+
'focus',
|
|
35
|
+
'on-focus',
|
|
36
|
+
'error',
|
|
37
|
+
'error-hover',
|
|
38
|
+
'error-shadow',
|
|
39
|
+
'error-subtle',
|
|
40
|
+
'on-error',
|
|
41
|
+
'on-error-subtle',
|
|
42
|
+
'success',
|
|
43
|
+
'success-subtle',
|
|
44
|
+
'on-success-subtle',
|
|
45
|
+
'wash',
|
|
46
|
+
'neutral-mid',
|
|
47
|
+
'muted',
|
|
48
|
+
'neutral-subtle',
|
|
49
|
+
'on-neutral-subtle',
|
|
50
|
+
'surface',
|
|
51
|
+
'on-surface',
|
|
52
|
+
'surface-divider',
|
|
53
|
+
'surface-hover',
|
|
54
|
+
'warning',
|
|
55
|
+
'warning-subtle',
|
|
56
|
+
'on-warning',
|
|
57
|
+
'on-warning-subtle',
|
|
58
|
+
'disabled',
|
|
59
|
+
'on-disabled',
|
|
60
|
+
'info-subtle',
|
|
61
|
+
'on-info-subtle',
|
|
62
|
+
];
|
|
63
|
+
|
|
64
|
+
const twMerge = extendTailwindMerge({
|
|
65
|
+
extend: {
|
|
66
|
+
theme: {
|
|
67
|
+
color: SEMANTIC_COLORS,
|
|
68
|
+
},
|
|
69
|
+
classGroups: {
|
|
70
|
+
'border-w': [{ border: BORDER_WIDTHS }],
|
|
71
|
+
'border-w-x': [{ 'border-x': BORDER_WIDTHS }],
|
|
72
|
+
'border-w-y': [{ 'border-y': BORDER_WIDTHS }],
|
|
73
|
+
'border-w-t': [{ 'border-t': BORDER_WIDTHS }],
|
|
74
|
+
'border-w-r': [{ 'border-r': BORDER_WIDTHS }],
|
|
75
|
+
'border-w-b': [{ 'border-b': BORDER_WIDTHS }],
|
|
76
|
+
'border-w-l': [{ 'border-l': BORDER_WIDTHS }],
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Utility function to merge Tailwind CSS classes with conflict resolution
|
|
83
|
+
* Uses tailwind-merge to handle conflicting utility classes
|
|
84
|
+
*/
|
|
85
|
+
export const cn = (...classes: (string | undefined | null | false)[]): string => {
|
|
86
|
+
return twMerge(classes.filter(Boolean).join(' '));
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Mixture of `cva` (class-variance-authority) and `twMerge`.
|
|
91
|
+
*/
|
|
92
|
+
export const cvm = <T>(...args: Parameters<typeof cva<T>>): ReturnType<typeof cva<T>> => {
|
|
93
|
+
const variants = cva<T>(...args);
|
|
94
|
+
|
|
95
|
+
return (...props) => cn(variants(...props));
|
|
96
|
+
};
|
package/src/utils/index.ts
CHANGED