From 154c5b89272876117eb6921304b131c36fdd123d Mon Sep 17 00:00:00 2001 From: mikkojamG Date: Mon, 20 Jan 2025 12:18:02 +0200 Subject: [PATCH] deps: upgrade HDS to 4.7.1 Ref: LINK-1759 chore: cleanup and refactoring Ref: LINK-1759 chore: optimize selects with search Ref: LINK-1759 deps: compatible react types Ref: LINK-1759 chore: type fixes Ref: LINK-1759 chore: select values should be arrays Ref: LINK-1759 chore: fixing merge conflicts Ref: LINK-1759 fix: publisher field tooltip Ref: LINK-1759 --- e2e/tests/events.spec.ts | 4 +- package.json | 21 +- src/assets/theme/theme.ts | 23 +- .../components/breadcrumb/Breadcrumb.tsx | 4 +- src/common/components/button/Button.tsx | 2 +- .../components/buttonPanel/ButtonPanel.tsx | 6 +- .../components/closeButton/CloseButton.tsx | 4 +- .../components/collapsible/Collapsible.tsx | 8 +- src/common/components/combobox/Combobox.tsx | 83 ---- .../components/combobox/combobox.module.scss | 10 - .../components/deleteButton/DeleteButton.tsx | 8 +- .../deleteButton/deleteButton.module.scss | 4 - .../dialog/confirmModal/ConfirmModal.tsx | 14 +- .../components/dialog/infoModal/InfoModal.tsx | 10 +- .../dropdown/searchInput/SearchInput.tsx | 4 +- src/common/components/errorPage/ErrorPage.tsx | 8 +- .../__snapshots__/ErrorPage.test.tsx.snap | 32 +- .../eventSelector/EventSelector.tsx | 106 +++-- .../eventSelector/__mocks__/eventSelector.ts | 18 +- .../__tests__/EventSelector.test.tsx | 45 +- .../feedbackButton/FeedbackButton.tsx | 13 +- .../__snapshots__/FieldLabel.test.tsx.snap | 2 +- src/common/components/filterTag/FilterTag.tsx | 5 +- .../EventStatusFilterTag.tsx | 3 +- .../checkboxGroupField/CheckboxGroupField.tsx | 8 +- .../useMultiSelectFieldProps.test.tsx | 16 +- .../useSingleSelectFieldProps.test.tsx | 22 +- .../hooks/useMultiSelectFieldProps.ts | 27 +- .../hooks/useSingleSelectFieldProps.ts | 40 +- .../KeywordSelectorField.tsx | 17 +- .../placeSelectorField/PlaceSelectorField.tsx | 13 +- .../PublisherSelectorField.tsx | 13 +- .../RegistrationEventSelectorField.tsx | 15 +- .../SingleKeywordSelectorField.tsx | 13 +- .../SingleOrganizationClassSelectorField.tsx | 13 +- .../SingleOrganizationSelectorField.tsx | 13 +- .../singleSelectField/SingleSelectField.tsx | 32 +- .../UmbrellaEventSelectorField.tsx | 13 +- .../userSelectorField/UserSelectorField.tsx | 17 +- .../components/imagePreview/ImagePreview.tsx | 4 +- .../imageSelector/ImageSelector.tsx | 8 +- .../imageUploader/ImageUploader.tsx | 6 +- .../keywordSelector/KeywordSelector.tsx | 134 +++--- .../__mocks__/keywordSelector.ts | 13 + .../__tests__/KeywordSelector.test.tsx | 57 ++- .../loadingButton/LoadingButton.tsx | 2 +- .../components/menuDropdown/MenuDropdown.tsx | 13 +- .../MultiSelectDropdown.tsx | 4 +- .../pagination/__tests__/Pagination.test.tsx | 4 +- .../placeSelector/PlaceSelector.tsx | 124 ++++-- .../placeSelector/__mocks__/placeSelector.ts | 4 +- .../__tests__/PlaceSelector.test.tsx | 34 +- .../publisherSelector/PublisherSelector.tsx | 40 +- .../__tests__/PublisherSelector.test.tsx | 25 +- .../components/searchPanel/SearchPanel.tsx | 7 +- src/common/components/select/Select.tsx | 44 ++ .../components/select/select.module.scss | 16 +- .../{select => selectLabel}/SelectLabel.tsx | 2 +- .../__tests__/SelectLabel.test.tsx | 0 .../__snapshots__/SelectLabel.test.tsx.snap | 0 .../selectLabel/selectLabel.module.scss | 8 + .../SelectLoadingSpinner.tsx} | 12 +- .../selectLoadingSpinner.module.scss} | 2 +- .../serverErrorSummary/ServerErrorSummary.tsx | 4 +- .../SingleKeywordSelector.tsx | 112 +++-- .../__tests__/SingleKeywordSelector.test.tsx | 43 +- .../SingleOrganizationClassSelector.tsx | 43 +- .../SingleOrganizationSelector.tsx | 41 +- .../components/singleSelect/SingleSelect.tsx | 32 +- .../__tests__/SingleSelect.test.tsx | 8 +- .../__snapshots__/SingleSelect.test.tsx.snap | 107 +++-- .../singleSelect/singleSelect.module.scss | 2 + src/common/components/table/CustomTable.tsx | 4 +- .../__snapshots__/Table.test.tsx.snap | 2 - .../table/collapseButton/CollapseButton.tsx | 4 +- .../__snapshots__/Tabs.test.tsx.snap | 2 +- src/common/components/tabs/tab/Tab.tsx | 4 +- .../__snapshots__/TextWithIcon.test.tsx.snap | 2 +- .../components/userSelector/UserSelector.tsx | 23 +- src/constants.ts | 1 + src/domain/app/App.tsx | 46 +- src/domain/app/__tests__/App.test.tsx | 109 ++--- .../app/cookieConsent/CookieConsent.tsx | 115 ----- src/domain/app/footer/Footer.tsx | 14 +- .../__snapshots__/Footer.test.tsx.snap | 60 ++- src/domain/app/header/Header.tsx | 11 +- .../__snapshots__/Header.test.tsx.snap | 105 +++-- src/domain/app/hooks/data/siteSettings.json | 350 +++++++++++++++ .../app/hooks/useCookieConsentSettings.ts | 40 ++ src/domain/app/i18n/en.json | 19 +- src/domain/app/i18n/fi.json | 19 +- src/domain/app/i18n/sv.json | 19 +- .../MaintenanceNotification.tsx | 2 +- .../app/layout/pageWrapper/PageWrapper.tsx | 9 +- .../NotificationsContext.tsx | 3 +- .../app/routes/localeRoutes/LocaleRoutes.tsx | 2 + src/domain/app/theme/Theme.tsx | 1 + .../event/__tests__/CreateEventPage.test.tsx | 2 +- .../event/__tests__/EditEventPage.test.tsx | 30 +- src/domain/event/__tests__/eventTestUtils.ts | 10 +- .../createButtonPanel/CreateButtonPanel.tsx | 4 +- .../event/editButtonPanel/EditButtonPanel.tsx | 6 +- src/domain/event/eventForm/EventForm.tsx | 2 +- .../eventHierarchyRow/EventHierarchyRow.tsx | 6 +- .../__tests__/ChannelsSection.test.tsx | 4 +- .../externalLinks/ExternalLink.tsx | 6 +- .../externalLinks/NewExternalLink.tsx | 14 +- .../ClassificationSection.tsx | 6 +- .../__tests__/ClassificationSection.test.tsx | 41 +- .../imageSection/ImageSection.tsx | 4 +- .../placeSection/PlaceSection.tsx | 8 +- .../__tests__/PlaceSection.test.tsx | 2 +- .../__tests__/PriceSection.test.tsx | 20 +- .../priceSection/offers/Offers.tsx | 6 +- .../offerPriceGroups/OfferPriceGroups.tsx | 6 +- .../RegistrationSection.tsx | 2 +- .../ResponsibilitiesSection.tsx | 18 +- .../ResponsibilitiesSection.test.tsx | 4 +- .../addEventTimeForm/AddEventTimeForm.tsx | 2 +- .../AddRecurringEventForm.tsx | 2 +- .../editEventTimeModal/EditEventTimeModal.tsx | 14 +- .../__tests__/EventTimeTab.test.tsx | 8 +- .../__tests__/RecurringEventTab.test.tsx | 4 +- .../formSections/typeSection/TypeSection.tsx | 6 +- .../videoSection/VideoSection.tsx | 6 +- src/domain/event/hooks/utils.ts | 2 +- src/domain/eventSaved/EventSavedPage.tsx | 8 +- .../eventSearch/searchPanel/SearchPanel.tsx | 3 +- .../__tests__/SearchPanel.test.tsx | 16 +- .../EventStatusSelector.tsx | 2 +- .../placeSelector/PlaceSelector.tsx | 2 +- .../searchPanel/typeSelector/TypeSelector.tsx | 2 +- src/domain/events/EventsPage.tsx | 6 +- .../events/__tests__/EventsPage.test.tsx | 4 +- src/domain/events/eventCard/EventCard.tsx | 7 +- src/domain/events/eventList/EventList.tsx | 21 +- .../eventList/__tests__/EventList.test.tsx | 8 +- src/domain/help/constants.ts | 4 +- .../askPermissionPage/AskPermissionPage.tsx | 10 +- .../__tests__/AskPermissionPage.test.tsx | 4 +- .../help/pages/contactPage/ContactPage.tsx | 6 +- .../__tests__/ContactPage.test.tsx | 19 +- src/domain/help/utils.ts | 12 +- src/domain/image/EditImagePage.tsx | 5 +- .../image/__tests__/CreateImagePage.test.tsx | 10 +- .../image/addImageForm/AddImageForm.tsx | 3 +- src/domain/image/imageForm/ImageForm.tsx | 6 +- src/domain/images/ImagesPage.tsx | 6 +- .../images/__tests__/ImagesPage.test.tsx | 2 +- .../imageList/__tests__/ImageList.test.tsx | 2 +- src/domain/images/imagesTable/ImagesTable.tsx | 4 +- .../__tests__/ImagesTable.test.tsx | 6 +- .../keyword/__tests__/validation.test.ts | 2 +- .../keyword/keywordForm/KeywordForm.tsx | 8 +- .../keywordSetForm/KeywordSetForm.tsx | 8 +- .../__tests__/KeywordSetsPage.test.tsx | 2 +- .../__tests__/KeywordSetsTable.test.tsx | 4 +- .../keywords/__tests__/KeywordsPage.test.tsx | 2 +- .../__tests__/KeywordsTable.test.tsx | 4 +- src/domain/landingPage/hero/Hero.tsx | 8 +- .../landingPage/serviceCard/ServiceCard.tsx | 4 +- src/domain/language/utils.ts | 6 +- .../organization/EditOrganizationPage.tsx | 5 +- .../__tests__/CreateOrganizationPage.test.tsx | 15 +- .../__tests__/EditOrganizationPage.test.tsx | 4 +- .../organizationForm/OrganizationForm.tsx | 24 +- .../__tests__/OrganizationForm.test.tsx | 57 ++- .../organizationForm/accounts/Accounts.tsx | 6 +- .../organizationForm/merchants/Merchants.tsx | 6 +- .../__tests__/SubOrganizationTable.test.tsx | 3 +- .../organizations/OrganizationsPage.tsx | 6 +- .../__tests__/OrganizationsPage.test.tsx | 2 +- src/domain/place/EditPlacePage.tsx | 6 +- .../place/__tests__/CreatePlacePage.test.tsx | 11 +- src/domain/place/constants.tsx | 2 +- src/domain/place/placeForm/PlaceForm.tsx | 2 +- src/domain/place/validation.ts | 6 +- src/domain/places/PlacesPage.tsx | 6 +- .../places/__tests__/PlacesPage.test.tsx | 2 +- .../__tests__/PlacesTable.test.tsx | 4 +- src/domain/priceGroup/EditPriceGroupPage.tsx | 6 +- .../__tests__/CreatePriceGroupPage.test.tsx | 12 +- .../priceGroup/__tests__/validation.test.ts | 2 +- src/domain/priceGroup/constants.tsx | 2 +- .../priceGroupForm/PriceGroupForm.tsx | 2 +- src/domain/priceGroup/utils.ts | 2 +- src/domain/priceGroup/validation.ts | 6 +- src/domain/priceGroups/PriceGroupsPage.tsx | 6 +- .../__tests__/PriceGroupsPage.test.tsx | 2 +- .../__tests__/PriceGroupList.test.tsx | 2 +- .../__tests__/CreateRegistrationPage.test.tsx | 2 +- .../registration/__tests__/validation.test.ts | 7 +- .../editButtonPanel/EditButtonPanel.tsx | 2 +- .../eventSection/EventSection.tsx | 8 +- .../__tests__/EventSection.test.tsx | 35 +- .../formSections/priceGroups/PriceGroups.tsx | 6 +- .../__tests__/PriceGroupSection.test.tsx | 20 +- .../priceGroups/accountField/AccountField.tsx | 10 +- .../merchantField/MerchantField.tsx | 10 +- .../priceGroups/priceGroup/PriceGroup.tsx | 16 +- .../vatPercentageField/VatPercentageField.tsx | 10 +- .../RegistrationUserAccessesSection.tsx | 6 +- .../RegistrationUserAccess.tsx | 14 +- src/domain/registration/utils.ts | 4 +- .../RegistrationSavedPage.tsx | 11 +- .../registrations/RegistrationsPage.tsx | 6 +- .../registrationList/RegistrationList.tsx | 3 +- .../signup/__tests__/EditSignupPage.test.tsx | 4 +- .../sendMessageModal/SendMessageModal.tsx | 18 +- src/domain/signupGroup/__tests__/testUtils.ts | 4 +- src/domain/signupGroup/constants.tsx | 5 +- .../signupGroup/eventInfo/EventInfo.tsx | 7 +- .../ParticipantAmountSelector.tsx | 3 +- .../SignupGroupFormFields.tsx | 24 +- .../signups/signup/Signup.tsx | 6 +- .../signups/signup/__tests__/Signup.test.tsx | 6 +- .../signupIsEnded/SignupIsEnded.tsx | 6 +- src/domain/signupGroup/validation.ts | 3 +- src/domain/signups/SignupsPage.tsx | 6 +- src/hooks/useSelectLanguage.ts | 6 +- src/setupTests.ts | 4 + src/types.ts | 7 +- .../__tests__/accessibilityUtils.test.ts | 70 --- src/utils/accessibilityUtils.ts | 33 -- src/utils/testUtils.tsx | 15 +- src/utils/validationUtils.ts | 3 +- vite.config.ts | 2 +- yarn.lock | 411 +++++++++--------- 228 files changed, 2312 insertions(+), 1811 deletions(-) delete mode 100644 src/common/components/combobox/Combobox.tsx delete mode 100644 src/common/components/combobox/combobox.module.scss create mode 100644 src/common/components/select/Select.tsx rename src/common/components/{select => selectLabel}/SelectLabel.tsx (92%) rename src/common/components/{select => selectLabel}/__tests__/SelectLabel.test.tsx (100%) rename src/common/components/{select => selectLabel}/__tests__/__snapshots__/SelectLabel.test.tsx.snap (100%) create mode 100644 src/common/components/selectLabel/selectLabel.module.scss rename src/common/components/{comboboxLoadingSpinner/ComboboxLoadingSpinner.tsx => selectLoadingSpinner/SelectLoadingSpinner.tsx} (69%) rename src/common/components/{comboboxLoadingSpinner/comboboxLoadingSpinner.module.scss => selectLoadingSpinner/selectLoadingSpinner.module.scss} (88%) delete mode 100644 src/domain/app/cookieConsent/CookieConsent.tsx create mode 100644 src/domain/app/hooks/data/siteSettings.json create mode 100644 src/domain/app/hooks/useCookieConsentSettings.ts delete mode 100644 src/utils/__tests__/accessibilityUtils.test.ts delete mode 100644 src/utils/accessibilityUtils.ts diff --git a/e2e/tests/events.spec.ts b/e2e/tests/events.spec.ts index 997cc0a3f..37ab0d80e 100644 --- a/e2e/tests/events.spec.ts +++ b/e2e/tests/events.spec.ts @@ -73,9 +73,9 @@ test.describe('Event search page', () => { // Results await expect(page.getByLabel('Viimeksi muokattu, laskeva')).toBeVisible(); await expect( - page.getByTestId('hds-pagination-previous-button') + page.getByRole('button', { name: 'Edellinen' }) ).toBeDisabled(); - await expect(page.getByTestId('hds-pagination-next-button')).toBeVisible(); + await expect(page.getByRole('button', { name: 'Seuraava' })).toBeVisible(); await expect( page.getByRole('button', { name: 'Anna palautetta' }) ).toBeVisible(); diff --git a/package.json b/package.json index f08d54223..2dab5fe36 100644 --- a/package.json +++ b/package.json @@ -6,23 +6,24 @@ "node": ">=22.13.0" }, "dependencies": { - "@apollo/client": "^3.9.8", + "@apollo/client": "^3.10.1", "@ckeditor/ckeditor5-build-classic": "43.1.1", "@ckeditor/ckeditor5-react": "^6.2.0", "@emotion/react": "^11.11.4", "@faker-js/faker": "^8.4.1", "@nabla/vite-plugin-eslint": "^2.0.5", "@sentry/react": "^9.15.0", + "@testing-library/dom": "^10.4.1", "@testing-library/jest-dom": "^6.4.2", - "@testing-library/react": "15.0.7", - "@testing-library/user-event": "14.5.2", + "@testing-library/react": "16.2.0", + "@testing-library/user-event": "14.6.1", "@types/dompurify": "^3.0.5", "@types/leaflet": "1.9.8", "@types/leaflet-draw": "^1.0.11", "@types/lodash": "^4.17.0", "@types/node": "^22.13.0", - "@types/react": "18.2.65", - "@types/react-dom": "18.2.22", + "@types/react": "^18.3.0", + "@types/react-dom": "^18.3.0", "@types/react-helmet": "^6.1.11", "@types/react-modal": "^3.16.3", "@types/react-scroll": "^1.8.10", @@ -45,8 +46,8 @@ "focus-visible": "^5.2.0", "formik": "2.4.5", "gzipper": "^7.2.0", - "hds-core": "^3.8.0", - "hds-react": "^3.8.0", + "hds-core": "^4.7.1", + "hds-react": "^4.7.1", "history": "^5.3.0", "html-tags": "^3.3.1", "i18next": "^23.10.1", @@ -56,8 +57,8 @@ "object-keys-normalizer": "^1.0.1", "pretty": "^2.0.0", "qs": "^6.12.1", - "react": "^18.2.0", - "react-dom": "^18.2.0", + "react": "^18.3.1", + "react-dom": "^18.3.1", "react-helmet": "^6.1.0", "react-i18next": "^14.1.0", "react-image-crop": "^11.0.5", @@ -113,10 +114,12 @@ "@playwright/test": "^1.51.1", "@vitest/coverage-istanbul": "^3.0.9", "eslint": "^8.57.0", + "fake-indexeddb": "^6.0.0", "graphql": "16.8.1", "husky": "^9.0.11", "jsdom": "^26.1.0", "prettier": "^3.2.5", + "shadow-dom-testing-library": "^1.11.3", "snyk": "^1.1297.3", "vite-bundle-visualizer": "^1.2.1" }, diff --git a/src/assets/theme/theme.ts b/src/assets/theme/theme.ts index fb0ca0b06..b0843d7e3 100644 --- a/src/assets/theme/theme.ts +++ b/src/assets/theme/theme.ts @@ -327,32 +327,23 @@ const theme: Theme = { '--dropdown-border-color-hover-invalid': 'var(--color-error-dark)', '--dropdown-border-color-focus': 'var(--color-black-90)', '--dropdown-border-color-invalid': 'var(--color-error)', - '--dropdown-border-color-disabled': 'var(--color-black-10)', '--dropdown-color-default': 'var(--color-black-90)', '--dropdown-color-disabled': 'var(--color-black-40)', '--focus-outline-color': 'var(--color-coat-of-arms)', - '--helper-color-default': 'var(--color-black-60)', + '--assistive-color': 'var(--color-black-60)', '--menu-divider-color': 'var(--color-black-20)', - '--menu-item-background-default': 'var(--color-white)', - '--menu-item-background-hover': 'var(--color-black-10)', - '--menu-item-background-selected': 'var(--color-white)', - '--menu-item-background-selected-hover': 'var(--color-black-10)', - '--menu-item-background-disabled': 'var(--color-white)', + '--menu-item-background-color-default': 'var(--color-white)', + '--menu-item-background-color-hover': 'var(--color-black-10)', + '--menu-item-background-color-selected': 'var(--color-white)', + '--menu-item-background-color-selected-hover': 'var(--color-black-10)', + '--menu-item-background-color-disabled': 'var(--color-white)', '--menu-item-color-default': 'var(--color-black-90)', '--menu-item-color-hover': 'var(--color-black-90)', '--menu-item-color-selected': 'var(--color-black-90)', '--menu-item-color-selected-hover': 'var(--color-black-90)', '--menu-item-color-disabled': 'var(--color-black-40)', - '--menu-item-icon-color-selected': 'var(--color-white)', + '--menu-item-icon-color-selected': 'var(--color-black-90)', '--menu-item-icon-color-disabled': 'var(--color-black-40)', - '--multiselect-checkbox-background-selected': 'var(--color-bus)', - '--multiselect-checkbox-background-disabled': 'var(--color-black-10)', - '--multiselect-checkbox-border-default': 'var(--color-black-50)', - '--multiselect-checkbox-border-hover': 'var(--color-black-90)', - '--multiselect-checkbox-border-disabled': 'var(--color-black-10)', - '--multiselect-checkbox-color-default': 'transparent', - '--multiselect-checkbox-color-selected': 'var(--color-white)', - '--multiselect-checkbox-color-selected-disabled': 'var(--color-white)', '--placeholder-color': 'var(--color-black-60)', }, sideNavigation: { diff --git a/src/common/components/breadcrumb/Breadcrumb.tsx b/src/common/components/breadcrumb/Breadcrumb.tsx index 9e1c8fbf2..99ba8fc92 100644 --- a/src/common/components/breadcrumb/Breadcrumb.tsx +++ b/src/common/components/breadcrumb/Breadcrumb.tsx @@ -10,7 +10,7 @@ import { useTheme } from '../../../domain/app/theme/Theme'; export type BreadcrumbProps = { ariaLabel?: string } & Omit< HdsBreadcrumbProps, - 'ariaLabel' + 'aria-label' >; const Breadcrumb = ({ @@ -23,7 +23,7 @@ const Breadcrumb = ({ return ( ); diff --git a/src/common/components/button/Button.tsx b/src/common/components/button/Button.tsx index d8eae3a23..5e6a28e21 100644 --- a/src/common/components/button/Button.tsx +++ b/src/common/components/button/Button.tsx @@ -6,7 +6,7 @@ import { useTheme } from '../../../domain/app/theme/Theme'; import styles from './button.module.scss'; const Button = React.forwardRef( - ({ className, variant = 'primary', ...rest }, ref) => { + ({ className, variant = ButtonVariant.Primary, ...rest }, ref) => { const { theme } = useTheme(); return ( diff --git a/src/common/components/buttonPanel/ButtonPanel.tsx b/src/common/components/buttonPanel/ButtonPanel.tsx index ec044bb63..1eeeb4d74 100644 --- a/src/common/components/buttonPanel/ButtonPanel.tsx +++ b/src/common/components/buttonPanel/ButtonPanel.tsx @@ -1,5 +1,5 @@ import classNames from 'classnames'; -import { IconArrowLeft, IconMenuDots } from 'hds-react'; +import { ButtonVariant, IconArrowLeft, IconMenuDots } from 'hds-react'; import React from 'react'; import { useTranslation } from 'react-i18next'; @@ -86,11 +86,11 @@ const ButtonPanel: React.FC = ({ {onBack && ( diff --git a/src/common/components/closeButton/CloseButton.tsx b/src/common/components/closeButton/CloseButton.tsx index 55753a967..20d1f2ad9 100644 --- a/src/common/components/closeButton/CloseButton.tsx +++ b/src/common/components/closeButton/CloseButton.tsx @@ -1,5 +1,5 @@ import classNames from 'classnames'; -import { IconCross } from 'hds-react'; +import { IconCross, IconSize } from 'hds-react'; import React from 'react'; import styles from './closeButton.module.scss'; @@ -25,7 +25,7 @@ const CloseButton: React.FC = ({ onClick={onClick} type={type} > - + ); }; diff --git a/src/common/components/collapsible/Collapsible.tsx b/src/common/components/collapsible/Collapsible.tsx index 3a0084ec3..d1503a2bf 100644 --- a/src/common/components/collapsible/Collapsible.tsx +++ b/src/common/components/collapsible/Collapsible.tsx @@ -1,5 +1,5 @@ import { ClassNames } from '@emotion/react'; -import { IconAngleDown, IconAngleUp } from 'hds-react'; +import { IconAngleDown, IconAngleUp, IconSize } from 'hds-react'; import React from 'react'; import { useTheme } from '../../../domain/app/theme/Theme'; @@ -45,7 +45,11 @@ const Collabsible: React.FC> = ({ onClick={togglePanel} type="button" > - {isOpen ? : } + {isOpen ? ( + + ) : ( + + )} {title} diff --git a/src/common/components/combobox/Combobox.tsx b/src/common/components/combobox/Combobox.tsx deleted file mode 100644 index 29b1ba15a..000000000 --- a/src/common/components/combobox/Combobox.tsx +++ /dev/null @@ -1,83 +0,0 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ -import classNames from 'classnames'; -import { - Combobox as BaseCombobox, - ComboboxProps, - MultiSelectProps, - SingleSelectProps, -} from 'hds-react'; -import React from 'react'; -import { useTranslation } from 'react-i18next'; - -import { useTheme } from '../../../domain/app/theme/Theme'; -import { OptionType } from '../../../types'; -import { - getA11ySelectionMessage, - getA11yStatusMessage, -} from '../../../utils/accessibilityUtils'; -import ComboboxLoadingSpinner, { - ComboboxLoadingSpinnerProps, -} from '../comboboxLoadingSpinner/ComboboxLoadingSpinner'; -import styles from './combobox.module.scss'; - -type FilterFunction = ( - options: OptionType[], - search: string -) => OptionType[]; - -type CommonComboboxProps = { - 'aria-describedby'?: string; - catchEscapeKey?: boolean; - filter?: FilterFunction; - showToggleButton?: boolean; - toggleButtonAriaLabel: string; -} & ComboboxLoadingSpinnerProps; - -export type MultiComboboxProps = Omit< - MultiSelectProps, - 'aria-labelledby' | 'options' | 'value' -> & - CommonComboboxProps & { - name: string; - value: ValueType[]; - }; - -export type SingleComboboxProps = Omit< - SingleSelectProps, - 'aria-labelledby' | 'options' | 'value' -> & - CommonComboboxProps & { - name: string; - value: ValueType; - }; - -type Props = ComboboxLoadingSpinnerProps & ComboboxProps; - -const Combobox: React.FC = ({ - alignedLabel, - isLoading, - className, - ...rest -}) => { - const { theme } = useTheme(); - const { t } = useTranslation(); - - return ( - - getA11yStatusMessage(options, t)} - getA11ySelectionMessage={ - /* istanbul ignore next */ - (options) => getA11ySelectionMessage(options, t) - } - theme={theme.select} - virtualized={true} - /> - - ); -}; - -export default Combobox; diff --git a/src/common/components/combobox/combobox.module.scss b/src/common/components/combobox/combobox.module.scss deleted file mode 100644 index e8b29a1b2..000000000 --- a/src/common/components/combobox/combobox.module.scss +++ /dev/null @@ -1,10 +0,0 @@ -.combobox { - > div:focus-within { - box-shadow: none; - - input:global(.focus-visible) { - outline: var(--focus-outline-width) solid var(--focus-outline-color); - outline-offset: 1px; - } - } -} diff --git a/src/common/components/deleteButton/DeleteButton.tsx b/src/common/components/deleteButton/DeleteButton.tsx index 7133721af..e6e87d561 100644 --- a/src/common/components/deleteButton/DeleteButton.tsx +++ b/src/common/components/deleteButton/DeleteButton.tsx @@ -1,5 +1,5 @@ import { ClassNames } from '@emotion/react'; -import { IconMinusCircle } from 'hds-react'; +import { ButtonVariant, IconMinusCircle } from 'hds-react'; import React from 'react'; import { useTranslation } from 'react-i18next'; @@ -36,12 +36,12 @@ const DeleteButton: React.FC = ({ className, css(theme.deleteButton) )} - iconLeft={} + iconStart={} onClick={onClick} type={type} - variant="supplementary" + variant={ButtonVariant.Supplementary} > - {label ?? t('common.delete')} + {label ?? t('common.delete')} )} diff --git a/src/common/components/deleteButton/deleteButton.module.scss b/src/common/components/deleteButton/deleteButton.module.scss index db5c0b162..ef464d7e0 100644 --- a/src/common/components/deleteButton/deleteButton.module.scss +++ b/src/common/components/deleteButton/deleteButton.module.scss @@ -7,9 +7,5 @@ color: var(--delete-button-color-icon) !important; height: var(--delete-button-height); - .label { - color: var(--delete-button-color); - } - @include focus-outline(); } diff --git a/src/common/components/dialog/confirmModal/ConfirmModal.tsx b/src/common/components/dialog/confirmModal/ConfirmModal.tsx index 99f2b1751..4ab80a559 100644 --- a/src/common/components/dialog/confirmModal/ConfirmModal.tsx +++ b/src/common/components/dialog/confirmModal/ConfirmModal.tsx @@ -1,4 +1,6 @@ import { + ButtonPresetTheme, + ButtonVariant, Dialog, DialogVariant, IconAlertCircle, @@ -75,7 +77,7 @@ const ConfirmModal: React.FC = ({ > ) : ( @@ -101,16 +103,20 @@ const ConfirmModal: React.FC = ({ loading={isSaving} onClick={handleConfirm} type="button" - variant={variant === 'danger' ? 'danger' : 'primary'} + variant={ + variant === ButtonVariant.Danger + ? ButtonVariant.Danger + : ButtonVariant.Primary + } > {confirmButtonText} diff --git a/src/common/components/dialog/infoModal/InfoModal.tsx b/src/common/components/dialog/infoModal/InfoModal.tsx index e4e86115b..82bc029fb 100644 --- a/src/common/components/dialog/infoModal/InfoModal.tsx +++ b/src/common/components/dialog/infoModal/InfoModal.tsx @@ -1,4 +1,4 @@ -import { Dialog, IconInfoCircle } from 'hds-react'; +import { ButtonVariant, Dialog, IconInfoCircle } from 'hds-react'; import React from 'react'; import Button from '../../button/Button'; @@ -47,14 +47,18 @@ const InfoModal: React.FC = ({ > } + iconStart={} title={heading} />

{description}

- diff --git a/src/common/components/dropdown/searchInput/SearchInput.tsx b/src/common/components/dropdown/searchInput/SearchInput.tsx index 2c997400e..8f12373fa 100644 --- a/src/common/components/dropdown/searchInput/SearchInput.tsx +++ b/src/common/components/dropdown/searchInput/SearchInput.tsx @@ -1,4 +1,4 @@ -import { IconSearch } from 'hds-react'; +import { IconSearch, IconSize } from 'hds-react'; import React from 'react'; import FieldLabel from '../../fieldLabel/FieldLabel'; @@ -15,7 +15,7 @@ const SearchInput = React.forwardRef( ({ id, onChange, placeholder, value }, ref) => { return (
- +
diff --git a/src/common/components/formFields/hooks/__tests__/useMultiSelectFieldProps.test.tsx b/src/common/components/formFields/hooks/__tests__/useMultiSelectFieldProps.test.tsx index ae8d617c7..eb985f50d 100644 --- a/src/common/components/formFields/hooks/__tests__/useMultiSelectFieldProps.test.tsx +++ b/src/common/components/formFields/hooks/__tests__/useMultiSelectFieldProps.test.tsx @@ -7,7 +7,6 @@ import useMultiSelectFieldProps, { } from '../useMultiSelectFieldProps'; const defaultProps: UseMultiSelectFieldPropsProps = { - disabled: false, name: 'name', onBlur: vi.fn(), onChange: vi.fn(), @@ -37,26 +36,13 @@ describe('useMultiSelectFieldProps', () => { it('should call onChange if hook is not disabled', async () => { const onChange = vi.fn(); const { result } = renderMultiSelectFieldPropsHook({ - disabled: false, onChange, }); - result.current.handleChange([]); + result.current.handleClose([]); await waitFor(() => expect(onChange).toBeCalledWith({ target: { id: 'name', value: [] } }) ); }); - - it('should not call onChange if hook is disabled', async () => { - const onChange = vi.fn(); - const { result } = renderMultiSelectFieldPropsHook({ - disabled: true, - onChange, - }); - - result.current.handleChange([]); - - expect(onChange).not.toBeCalledWith({ target: { id: 'name', value: [] } }); - }); }); diff --git a/src/common/components/formFields/hooks/__tests__/useSingleSelectFieldProps.test.tsx b/src/common/components/formFields/hooks/__tests__/useSingleSelectFieldProps.test.tsx index 647a1d3cb..acaeb420a 100644 --- a/src/common/components/formFields/hooks/__tests__/useSingleSelectFieldProps.test.tsx +++ b/src/common/components/formFields/hooks/__tests__/useSingleSelectFieldProps.test.tsx @@ -7,7 +7,6 @@ import useSingleSelectFieldProps, { } from '../useSingleSelectFieldProps'; const defaultProps: UseSingleSelectFieldPropsProps = { - disabled: false, name: 'name', onBlur: vi.fn(), onChange: vi.fn(), @@ -37,28 +36,15 @@ describe('useSingleSelectFieldProps', () => { it('should call onChange if hook is not disabled', async () => { const onChange = vi.fn(); const { result } = renderSingleSelectFieldPropsHook({ - disabled: false, onChange, }); - result.current.handleChange(null); + result.current.handleChange([], { label: 'Label', value: 'value' }); await waitFor(() => - expect(onChange).toBeCalledWith({ target: { id: 'name', value: null } }) + expect(onChange).toBeCalledWith({ + target: { id: 'name', value: 'value' }, + }) ); }); - - it('should not call onChange if hook is disabled', async () => { - const onChange = vi.fn(); - const { result } = renderSingleSelectFieldPropsHook({ - disabled: true, - onChange, - }); - - result.current.handleChange(null); - - expect(onChange).not.toBeCalledWith({ - target: { id: 'name', value: null }, - }); - }); }); diff --git a/src/common/components/formFields/hooks/useMultiSelectFieldProps.ts b/src/common/components/formFields/hooks/useMultiSelectFieldProps.ts index a65088d1c..2dc2cd838 100644 --- a/src/common/components/formFields/hooks/useMultiSelectFieldProps.ts +++ b/src/common/components/formFields/hooks/useMultiSelectFieldProps.ts @@ -7,18 +7,16 @@ import { getErrorText } from '../../../../utils/validationUtils'; type UseMultiSelectFieldPropsState = { errorText: string; handleBlur: () => void; - handleChange: (selected: OptionType[]) => void; + handleClose: (selectedOptions: OptionType[]) => void; }; export type UseMultiSelectFieldPropsProps = { - disabled?: boolean; name: string; onBlur: FormikHandlers['handleBlur']; onChange: FormikHandlers['handleChange']; value: string[]; }; const useMultiSelectFieldProps = ({ - disabled, name, onBlur, onChange, @@ -33,20 +31,19 @@ const useMultiSelectFieldProps = ({ onBlur({ target: { id: name, value } }); }; - const handleChange = (selected: OptionType[]) => { - // TODO: HDS Combobox component allowes to remove value even if component - // is disabled. Remove if statement when that behaviour is fixed to HDS - if (!disabled) { - // Set timeout to prevent Android devices to end up to an infinite loop when changing value - setTimeout(() => { - onChange({ - target: { id: name, value: selected.map((item) => item.value) }, - }); - }, 5); - } + const handleClose = (selectedOptions: OptionType[]) => { + // Set timeout to prevent Android devices to end up to an infinite loop when changing value + setTimeout(() => { + onChange({ + target: { + id: name, + value: selectedOptions.map((item) => item.value), + }, + }); + }, 5); }; - return { errorText, handleBlur, handleChange }; + return { errorText, handleBlur, handleClose }; }; export default useMultiSelectFieldProps; diff --git a/src/common/components/formFields/hooks/useSingleSelectFieldProps.ts b/src/common/components/formFields/hooks/useSingleSelectFieldProps.ts index fa234fb5f..342ebfc0c 100644 --- a/src/common/components/formFields/hooks/useSingleSelectFieldProps.ts +++ b/src/common/components/formFields/hooks/useSingleSelectFieldProps.ts @@ -8,11 +8,13 @@ import { getErrorText } from '../../../../utils/validationUtils'; type UseSingleSelectFieldPropsState = { errorText: string; handleBlur: () => void; - handleChange: (selected: OptionType | null) => void; + handleChange: ( + selectedOptions: OptionType[], + clickedOption: OptionType + ) => void; }; export type UseSingleSelectFieldPropsProps = { - disabled?: boolean; name: string; onBlur: FormikHandlers['handleBlur']; onChange: FormikHandlers['handleChange']; @@ -20,7 +22,6 @@ export type UseSingleSelectFieldPropsProps = { value: string; }; const useSingleSelectFieldProps = ({ - disabled, name, onBlur, onChange, @@ -36,23 +37,22 @@ const useSingleSelectFieldProps = ({ onBlur({ target: { id: name, value } }); }; - const handleChange = (selected: OptionType | null) => { - // TODO: HDS Combobox component allowes to remove value even if component - // is disabled. Remove if statement when that behaviour is fixed to HDS - if (!disabled) { - const newValue = getValue(selected?.value, null); - - // Set timeout to prevent Android devices to end up to an infinite loop when changing value - setTimeout(() => { - onChange({ - target: { id: name, value: newValue }, - }); - - if (onChangeCb) { - onChangeCb(newValue); - } - }, 5); - } + const handleChange = ( + _selectedOptions: OptionType[], + clickedOption: OptionType + ) => { + const newValue = getValue(clickedOption?.value, null); + + // Set timeout to prevent Android devices to end up to an infinite loop when changing value + setTimeout(() => { + onChange({ + target: { id: name, value: newValue }, + }); + + if (onChangeCb) { + onChangeCb(newValue); + } + }, 5); }; return { errorText, handleBlur, handleChange }; diff --git a/src/common/components/formFields/keywordSelectorField/KeywordSelectorField.tsx b/src/common/components/formFields/keywordSelectorField/KeywordSelectorField.tsx index 3bef30bf6..f1ecc16f2 100644 --- a/src/common/components/formFields/keywordSelectorField/KeywordSelectorField.tsx +++ b/src/common/components/formFields/keywordSelectorField/KeywordSelectorField.tsx @@ -1,22 +1,20 @@ import { FieldProps } from 'formik'; import React from 'react'; -import KeywordSelector, { - KeywordSelectorProps, -} from '../../keywordSelector/KeywordSelector'; +import KeywordSelector from '../../keywordSelector/KeywordSelector'; +import { MultiSelectPropsWithValue } from '../../select/Select'; import useMultiSelectFieldProps from '../hooks/useMultiSelectFieldProps'; -type Props = KeywordSelectorProps & FieldProps; +type Props = MultiSelectPropsWithValue & FieldProps; const KeywordSelectorField: React.FC = ({ field: { name, onBlur, onChange, value, ...field }, form, - helper, + texts, disabled, ...rest }) => { - const { errorText, handleBlur, handleChange } = useMultiSelectFieldProps({ - disabled, + const { errorText, handleBlur, handleClose } = useMultiSelectFieldProps({ name, onBlur, onChange, @@ -30,10 +28,9 @@ const KeywordSelectorField: React.FC = ({ disabled={disabled} name={name} onBlur={handleBlur} - onChange={handleChange} + handleClose={handleClose} value={value} - helper={helper} - error={errorText} + texts={{ ...texts, error: errorText }} invalid={!!errorText} /> ); diff --git a/src/common/components/formFields/placeSelectorField/PlaceSelectorField.tsx b/src/common/components/formFields/placeSelectorField/PlaceSelectorField.tsx index 3fa240714..45dab309a 100644 --- a/src/common/components/formFields/placeSelectorField/PlaceSelectorField.tsx +++ b/src/common/components/formFields/placeSelectorField/PlaceSelectorField.tsx @@ -1,22 +1,20 @@ import { FieldProps } from 'formik'; import React from 'react'; -import PlaceSelector, { - PlaceSelectorProps, -} from '../../placeSelector/PlaceSelector'; +import PlaceSelector from '../../placeSelector/PlaceSelector'; +import { SelectPropsWithValue } from '../../select/Select'; import useSingleSelectFieldProps from '../hooks/useSingleSelectFieldProps'; -type Props = PlaceSelectorProps & FieldProps; +type Props = SelectPropsWithValue & FieldProps; const PlaceSelectorField: React.FC = ({ field: { name, onBlur, onChange, value, ...field }, form, - helper, + texts, disabled, ...rest }) => { const { errorText, handleBlur, handleChange } = useSingleSelectFieldProps({ - disabled, name, onBlur, onChange, @@ -32,8 +30,7 @@ const PlaceSelectorField: React.FC = ({ onBlur={handleBlur} onChange={handleChange} value={value} - helper={helper} - error={errorText} + texts={{ ...texts, error: errorText }} invalid={!!errorText} /> ); diff --git a/src/common/components/formFields/publisherSelectorField/PublisherSelectorField.tsx b/src/common/components/formFields/publisherSelectorField/PublisherSelectorField.tsx index 94c84209e..4596870a0 100644 --- a/src/common/components/formFields/publisherSelectorField/PublisherSelectorField.tsx +++ b/src/common/components/formFields/publisherSelectorField/PublisherSelectorField.tsx @@ -1,22 +1,20 @@ import { FieldProps } from 'formik'; import React from 'react'; -import PublisherSelector, { - PublisherSelectorProps, -} from '../../publisherSelector/PublisherSelector'; +import PublisherSelector from '../../publisherSelector/PublisherSelector'; +import { SelectPropsWithValue } from '../../select/Select'; import useSingleSelectFieldProps from '../hooks/useSingleSelectFieldProps'; -type Props = PublisherSelectorProps & FieldProps; +type Props = SelectPropsWithValue & FieldProps; const PublisherSelectorField: React.FC = ({ field: { name, onBlur, onChange, value, ...field }, form, - helper, + texts, disabled, ...rest }) => { const { errorText, handleBlur, handleChange } = useSingleSelectFieldProps({ - disabled, name, onBlur, onChange, @@ -32,8 +30,7 @@ const PublisherSelectorField: React.FC = ({ onBlur={handleBlur} onChange={handleChange} value={value} - helper={helper} - error={errorText} + texts={{ ...texts, error: errorText }} invalid={!!errorText} /> ); diff --git a/src/common/components/formFields/registrationEventSelectorField/RegistrationEventSelectorField.tsx b/src/common/components/formFields/registrationEventSelectorField/RegistrationEventSelectorField.tsx index 82d32f960..8d3eb998c 100644 --- a/src/common/components/formFields/registrationEventSelectorField/RegistrationEventSelectorField.tsx +++ b/src/common/components/formFields/registrationEventSelectorField/RegistrationEventSelectorField.tsx @@ -10,24 +10,25 @@ import { } from '../../../../domain/events/utils'; import { EventFieldsFragment, + EventsQueryVariables, PublicationStatus, } from '../../../../generated/graphql'; import { Language, OptionType } from '../../../../types'; import skipFalsyType from '../../../../utils/skipFalsyType'; -import EventSelector, { - EventSelectorProps, -} from '../../eventSelector/EventSelector'; +import EventSelector from '../../eventSelector/EventSelector'; +import { SelectPropsWithValue } from '../../select/Select'; import useSingleSelectFieldProps from '../hooks/useSingleSelectFieldProps'; type Props = { onChangeCb?: (val: string | null) => void; -} & EventSelectorProps & + variables: EventsQueryVariables; +} & SelectPropsWithValue & FieldProps; const RegistrationEventSelectorField: React.FC = ({ field: { name, onBlur, onChange, value, ...field }, form, - helper, + texts, disabled, onChangeCb, variables, @@ -35,7 +36,6 @@ const RegistrationEventSelectorField: React.FC = ({ }) => { const { t } = useTranslation(); const { errorText, handleBlur, handleChange } = useSingleSelectFieldProps({ - disabled, name, onBlur, onChange, @@ -79,8 +79,7 @@ const RegistrationEventSelectorField: React.FC = ({ onBlur={handleBlur} onChange={handleChange} value={value} - helper={helper} - error={errorText} + texts={{ ...texts, error: errorText }} invalid={!!errorText} getOption={getEventOption} /> diff --git a/src/common/components/formFields/singleKeywordSelectorField/SingleKeywordSelectorField.tsx b/src/common/components/formFields/singleKeywordSelectorField/SingleKeywordSelectorField.tsx index 6f3dc7ada..cfb8fce67 100644 --- a/src/common/components/formFields/singleKeywordSelectorField/SingleKeywordSelectorField.tsx +++ b/src/common/components/formFields/singleKeywordSelectorField/SingleKeywordSelectorField.tsx @@ -1,22 +1,20 @@ import { FieldProps } from 'formik'; import React from 'react'; -import SingleKeywordSelector, { - SingleKeywordSelectorProps, -} from '../../singleKeywordSelector/SingleKeywordSelector'; +import { SelectPropsWithValue } from '../../select/Select'; +import SingleKeywordSelector from '../../singleKeywordSelector/SingleKeywordSelector'; import useSingleSelectFieldProps from '../hooks/useSingleSelectFieldProps'; -type Props = SingleKeywordSelectorProps & FieldProps; +type Props = SelectPropsWithValue & FieldProps; const SingleKeywordSelectorField: React.FC = ({ field: { name, onBlur, onChange, value, ...field }, form, - helper, + texts, disabled, ...rest }) => { const { errorText, handleBlur, handleChange } = useSingleSelectFieldProps({ - disabled, name, onBlur, onChange, @@ -32,8 +30,7 @@ const SingleKeywordSelectorField: React.FC = ({ onBlur={handleBlur} onChange={handleChange} value={value} - helper={helper} - error={errorText} + texts={{ ...texts, error: errorText }} invalid={!!errorText} /> ); diff --git a/src/common/components/formFields/singleOrganizationClassSelectorField/SingleOrganizationClassSelectorField.tsx b/src/common/components/formFields/singleOrganizationClassSelectorField/SingleOrganizationClassSelectorField.tsx index 19af58ecd..55c3bb4ca 100644 --- a/src/common/components/formFields/singleOrganizationClassSelectorField/SingleOrganizationClassSelectorField.tsx +++ b/src/common/components/formFields/singleOrganizationClassSelectorField/SingleOrganizationClassSelectorField.tsx @@ -1,22 +1,20 @@ import { FieldProps } from 'formik'; import React from 'react'; -import SingleOrganizationClassSelector, { - SingleOrganizationClassSelectorProps, -} from '../../singleOrganizationClassSelector/SingleOrganizationClassSelector'; +import { SelectPropsWithValue } from '../../select/Select'; +import SingleOrganizationClassSelector from '../../singleOrganizationClassSelector/SingleOrganizationClassSelector'; import useSingleSelectFieldProps from '../hooks/useSingleSelectFieldProps'; -type Props = SingleOrganizationClassSelectorProps & FieldProps; +type Props = SelectPropsWithValue & FieldProps; const SingleOrganizationClassSelectorField: React.FC = ({ field: { name, onBlur, onChange, value, ...field }, form, - helper, + texts, disabled, ...rest }) => { const { errorText, handleBlur, handleChange } = useSingleSelectFieldProps({ - disabled, name, onBlur, onChange, @@ -32,8 +30,7 @@ const SingleOrganizationClassSelectorField: React.FC = ({ onBlur={handleBlur} onChange={handleChange} value={value} - helper={helper} - error={errorText} + texts={{ ...texts, error: errorText }} invalid={!!errorText} /> ); diff --git a/src/common/components/formFields/singleOrganizationSelectorField/SingleOrganizationSelectorField.tsx b/src/common/components/formFields/singleOrganizationSelectorField/SingleOrganizationSelectorField.tsx index 2e46da4ef..7661c306a 100644 --- a/src/common/components/formFields/singleOrganizationSelectorField/SingleOrganizationSelectorField.tsx +++ b/src/common/components/formFields/singleOrganizationSelectorField/SingleOrganizationSelectorField.tsx @@ -1,22 +1,20 @@ import { FieldProps } from 'formik'; import React from 'react'; -import SingleOrganizationSelector, { - SingleOrganizationSelectorProps, -} from '../../singleOrganizationSelector/SingleOrganizationSelector'; +import { SelectPropsWithValue } from '../../select/Select'; +import SingleOrganizationSelector from '../../singleOrganizationSelector/SingleOrganizationSelector'; import useSingleSelectFieldProps from '../hooks/useSingleSelectFieldProps'; -type Props = SingleOrganizationSelectorProps & FieldProps; +type Props = SelectPropsWithValue & FieldProps; const SingleOrganizationSelectorField: React.FC = ({ field: { name, onBlur, onChange, value, ...field }, form, - helper, + texts, disabled, ...rest }) => { const { errorText, handleBlur, handleChange } = useSingleSelectFieldProps({ - disabled, name, onBlur, onChange, @@ -32,8 +30,7 @@ const SingleOrganizationSelectorField: React.FC = ({ onChange={handleChange} disabled={disabled} value={value} - helper={helper} - error={errorText} + texts={{ ...texts, error: errorText }} invalid={!!errorText} /> ); diff --git a/src/common/components/formFields/singleSelectField/SingleSelectField.tsx b/src/common/components/formFields/singleSelectField/SingleSelectField.tsx index e9bb36c96..94c45a6fe 100644 --- a/src/common/components/formFields/singleSelectField/SingleSelectField.tsx +++ b/src/common/components/formFields/singleSelectField/SingleSelectField.tsx @@ -1,7 +1,8 @@ import { FieldProps } from 'formik'; -import React from 'react'; +import React, { useMemo } from 'react'; import { useTranslation } from 'react-i18next'; +import { OptionType } from '../../../../types'; import SingleSelect, { SingleSelectProps, } from '../../singleSelect/SingleSelect'; @@ -12,8 +13,7 @@ type Props = SingleSelectProps & const SingleSelectField: React.FC = ({ field: { name, onBlur, onChange, value, ...field }, - form, - helper, + texts, onChangeCb, options, disabled, @@ -21,7 +21,6 @@ const SingleSelectField: React.FC = ({ }) => { const { t } = useTranslation(); const { errorText, handleBlur, handleChange } = useSingleSelectFieldProps({ - disabled, name, onBlur, onChange, @@ -29,12 +28,17 @@ const SingleSelectField: React.FC = ({ value, }); + const selected = useMemo(() => { + const option = options?.find( + (option): option is OptionType => + typeof option !== 'string' && option.value === value + ); + + return option ? [option] : []; + }, [options, value]); + return ( = ({ onBlur={handleBlur} onChange={handleChange} options={options} - value={ - options.find((option) => option.value === value) ?? - (null as unknown as undefined) - } - helper={helper} - error={errorText} + value={selected} + texts={{ + ...texts, + error: errorText, + clearButtonAriaLabel_one: t('common.clear') ?? undefined, + }} invalid={!!errorText} /> ); diff --git a/src/common/components/formFields/umbrellaEventSelectorField/UmbrellaEventSelectorField.tsx b/src/common/components/formFields/umbrellaEventSelectorField/UmbrellaEventSelectorField.tsx index 19b6a1ea6..aaba7b893 100644 --- a/src/common/components/formFields/umbrellaEventSelectorField/UmbrellaEventSelectorField.tsx +++ b/src/common/components/formFields/umbrellaEventSelectorField/UmbrellaEventSelectorField.tsx @@ -5,12 +5,11 @@ import { getEventFields } from '../../../../domain/event/utils'; import { EVENT_SORT_OPTIONS } from '../../../../domain/events/constants'; import { EventFieldsFragment } from '../../../../generated/graphql'; import { Language, OptionType } from '../../../../types'; -import EventSelector, { - EventSelectorProps, -} from '../../eventSelector/EventSelector'; +import EventSelector from '../../eventSelector/EventSelector'; +import { SelectPropsWithValue } from '../../select/Select'; import useSingleSelectFieldProps from '../hooks/useSingleSelectFieldProps'; -type Props = EventSelectorProps & FieldProps; +type Props = SelectPropsWithValue & FieldProps; const getEventOption = ( event: EventFieldsFragment, @@ -23,12 +22,11 @@ const getEventOption = ( const UmbrellaEventSelectorField: React.FC = ({ field: { name, onBlur, onChange, value, ...field }, form, - helper, + texts, disabled, ...rest }) => { const { errorText, handleBlur, handleChange } = useSingleSelectFieldProps({ - disabled, name, onBlur, onChange, @@ -48,8 +46,7 @@ const UmbrellaEventSelectorField: React.FC = ({ onBlur={handleBlur} onChange={handleChange} value={value} - helper={helper} - error={errorText} + texts={{ ...texts, error: errorText }} invalid={!!errorText} getOption={getEventOption} /> diff --git a/src/common/components/formFields/userSelectorField/UserSelectorField.tsx b/src/common/components/formFields/userSelectorField/UserSelectorField.tsx index 57b343945..4aef5648b 100644 --- a/src/common/components/formFields/userSelectorField/UserSelectorField.tsx +++ b/src/common/components/formFields/userSelectorField/UserSelectorField.tsx @@ -1,22 +1,20 @@ import { FieldProps } from 'formik'; import React from 'react'; -import UserSelector, { - UserSelectorProps, -} from '../../userSelector/UserSelector'; +import { MultiSelectPropsWithValue } from '../../select/Select'; +import UserSelector from '../../userSelector/UserSelector'; import useMultiSelectFieldProps from '../hooks/useMultiSelectFieldProps'; -type Props = UserSelectorProps & FieldProps; +type Props = MultiSelectPropsWithValue & FieldProps; const UserSelectorField: React.FC = ({ field: { name, onBlur, onChange, value, ...field }, form, - helper, + texts, disabled, ...rest }) => { - const { errorText, handleBlur, handleChange } = useMultiSelectFieldProps({ - disabled, + const { errorText, handleBlur, handleClose } = useMultiSelectFieldProps({ name, onBlur, onChange, @@ -30,10 +28,9 @@ const UserSelectorField: React.FC = ({ disabled={disabled} name={name} onBlur={handleBlur} - onChange={handleChange} + onClose={handleClose} value={value} - helper={helper} - error={errorText} + texts={{ ...texts, error: errorText }} invalid={!!errorText} /> ); diff --git a/src/common/components/imagePreview/ImagePreview.tsx b/src/common/components/imagePreview/ImagePreview.tsx index f52d02cec..4350a10c5 100644 --- a/src/common/components/imagePreview/ImagePreview.tsx +++ b/src/common/components/imagePreview/ImagePreview.tsx @@ -1,6 +1,6 @@ import { ResizeObserver } from '@juggle/resize-observer'; import classNames from 'classnames'; -import { IconPhoto } from 'hds-react'; +import { IconPhoto, IconSize } from 'hds-react'; import React from 'react'; import useMeasure from 'react-use-measure'; @@ -73,7 +73,7 @@ const ImagePreview: React.FC = ({ > {showPlaceholder ? (
- +
) : (
= ({ onClick={() => onClick(image)} onDoubleClick={() => onDoubleClick(image)} > - {showPlaceholder && } + {showPlaceholder && } )} @@ -212,9 +212,9 @@ const ImageSelector: React.FC = ({
diff --git a/src/common/components/imageUploader/ImageUploader.tsx b/src/common/components/imageUploader/ImageUploader.tsx index 3a6637df4..69b9581cc 100644 --- a/src/common/components/imageUploader/ImageUploader.tsx +++ b/src/common/components/imageUploader/ImageUploader.tsx @@ -1,4 +1,4 @@ -import { IconMinusCircle } from 'hds-react'; +import { ButtonVariant, IconMinusCircle } from 'hds-react'; import React, { useState } from 'react'; import { useTranslation } from 'react-i18next'; import { PixelCrop } from 'react-image-crop'; @@ -66,10 +66,10 @@ const ImageUploader: React.FC = ({
diff --git a/src/common/components/keywordSelector/KeywordSelector.tsx b/src/common/components/keywordSelector/KeywordSelector.tsx index 2863125eb..556a5d0fb 100644 --- a/src/common/components/keywordSelector/KeywordSelector.tsx +++ b/src/common/components/keywordSelector/KeywordSelector.tsx @@ -1,14 +1,14 @@ /* eslint-disable no-undef */ import { ApolloClient, + ApolloError, NormalizedCacheObject, useApolloClient, } from '@apollo/client'; -import React from 'react'; +import { Option, SearchFunction, SearchResult, SelectData } from 'hds-react'; +import React, { useEffect } from 'react'; import { useTranslation } from 'react-i18next'; -import { useDebounce } from 'use-debounce'; -import { COMBOBOX_DEBOUNCE_TIME_MS } from '../../../constants'; import { getKeywordFields, getKeywordQueryResult, @@ -20,13 +20,12 @@ import { useKeywordsQuery, } from '../../../generated/graphql'; import useLocale from '../../../hooks/useLocale'; -import useMountedState from '../../../hooks/useMountedState'; import { Language, OptionType } from '../../../types'; import getPathBuilder from '../../../utils/getPathBuilder'; import getValue from '../../../utils/getValue'; import parseIdFromAtId from '../../../utils/parseIdFromAtId'; import skipFalsyType from '../../../utils/skipFalsyType'; -import Combobox, { MultiComboboxProps } from '../combobox/Combobox'; +import Select, { MultiSelectPropsWithValue } from '../select/Select'; const getOption = ({ keyword, @@ -40,51 +39,43 @@ const getOption = ({ return { label, value }; }; -export type KeywordSelectorProps = Omit< - MultiComboboxProps, - 'toggleButtonAriaLabel' ->; +export type KeywordSelectorProps = MultiSelectPropsWithValue & { + handleClose: (selectedOptions: OptionType[]) => void; +}; const KeywordSelector: React.FC = ({ - label, + texts, name, value, + handleClose, ...rest }) => { - const timer = React.useRef(); const apolloClient = useApolloClient() as ApolloClient; const { t } = useTranslation(); const locale = useLocale(); - const [search, setSearch] = useMountedState(''); - const [debouncedSearch] = useDebounce(search, COMBOBOX_DEBOUNCE_TIME_MS); const [selectedKeywords, setSelectedKeywords] = React.useState( [] ); + const [options, setOptions] = React.useState([]); + + const initialOptions = React.useRef([]); const { data: keywordsData, loading, previousData: previousKeywordsData, + refetch, } = useKeywordsQuery({ variables: { createPath: getPathBuilder(keywordsPathBuilder), dataSource: ['yso', 'helsinki'], showAllKeywords: true, - freeText: debouncedSearch, + freeText: '', }, }); - const handleFilter = (items: OptionType[], inputValue: string) => { - clearTimeout(timer.current); - timer.current = setTimeout(() => { - setSearch(inputValue); - }); - - return items; - }; - - const options: OptionType[] = React.useMemo( + const keywordsOptions = React.useMemo( () => getValue( (keywordsData || previousKeywordsData)?.keywords.data.map((keyword) => @@ -95,42 +86,87 @@ const KeywordSelector: React.FC = ({ [keywordsData, locale, previousKeywordsData] ); + useEffect(() => { + setOptions(keywordsOptions); + + if (keywordsData && !initialOptions?.current.length) { + initialOptions.current = keywordsOptions; + } + }, [keywordsOptions, keywordsData]); + + const handleSearch: SearchFunction = React.useCallback( + async (searchValue: string): Promise => { + try { + const { error, data: newKeywordsData } = await refetch({ + freeText: searchValue, + }); + + if (error) { + throw error; + } + + return Promise.resolve({ + options: getValue( + newKeywordsData?.keywords.data.map((keyword) => + getOption({ keyword: keyword as KeywordFieldsFragment, locale }) + ), + [] + ), + }); + } catch (error) { + return Promise.reject(error as ApolloError); + } + }, + [refetch, locale] + ); + + const onClose = React.useCallback( + ( + selectedOptions: Option[], + _clickedOption: undefined, + _data: SelectData + ) => { + setOptions(initialOptions.current); + + if (handleClose) { + handleClose(selectedOptions); + } + }, + [handleClose] + ); + React.useEffect(() => { - const getSelectedKeywordsFromCache = async () => - setSelectedKeywords( - ( - await Promise.all( - value.map(async (atId) => { - const keyword = await getKeywordQueryResult( - getValue(parseIdFromAtId(atId), ''), - apolloClient - ); - /* istanbul ignore next */ - return keyword ? getOption({ keyword: keyword, locale }) : null; - }) - ) - ).filter(skipFalsyType) + const getSelectedKeywordsFromCache = async () => { + const selectedOptions = await Promise.all( + value.map(async (atId) => { + const keyword = await getKeywordQueryResult( + getValue(parseIdFromAtId(atId), ''), + apolloClient + ); + /* istanbul ignore next */ + return keyword ? getOption({ keyword: keyword, locale }) : null; + }) ); + setSelectedKeywords(selectedOptions.filter(skipFalsyType)); + }; + getSelectedKeywordsFromCache(); }, [apolloClient, locale, value]); - React.useEffect(() => { - return () => clearTimeout(timer.current); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - return ( - ); diff --git a/src/common/components/keywordSelector/__mocks__/keywordSelector.ts b/src/common/components/keywordSelector/__mocks__/keywordSelector.ts index 4593df6b4..058c095f6 100644 --- a/src/common/components/keywordSelector/__mocks__/keywordSelector.ts +++ b/src/common/components/keywordSelector/__mocks__/keywordSelector.ts @@ -40,11 +40,24 @@ const mockedKeywordsResponse = { result: keywordsResponse, }; +const filteredKeywordsVariables = { + ...keywordsVariables, + freeText: keywordName, +}; +const filteredKeywords = keywords; +const filteredEventsResponse = keywordsResponse; +const mockedFilteredKeywordsResponse = { + request: { query: KeywordDocument, variables: filteredKeywordsVariables }, + result: filteredEventsResponse, +}; + export { + filteredKeywords, keyword, keywordAtId, keywordName, keywordNames, + mockedFilteredKeywordsResponse, mockedKeywordResponse, mockedKeywordsResponse, }; diff --git a/src/common/components/keywordSelector/__tests__/KeywordSelector.test.tsx b/src/common/components/keywordSelector/__tests__/KeywordSelector.test.tsx index 570eee5cd..b84fc6fe7 100644 --- a/src/common/components/keywordSelector/__tests__/KeywordSelector.test.tsx +++ b/src/common/components/keywordSelector/__tests__/KeywordSelector.test.tsx @@ -1,13 +1,17 @@ +import getValue from '../../../../utils/getValue'; import { configure, render, screen, shouldOpenMenuAndSelectOption, + userEvent, } from '../../../../utils/testUtils'; import { + filteredKeywords, keywordAtId, keywordName, keywordNames, + mockedFilteredKeywordsResponse, mockedKeywordResponse, mockedKeywordsResponse, } from '../__mocks__/keywordSelector'; @@ -19,29 +23,40 @@ const helper = 'Helper text'; const label = 'Select keyword'; const name = 'keyword'; -const mocks = [mockedKeywordResponse, mockedKeywordsResponse]; +const mocks = [ + mockedKeywordResponse, + mockedKeywordsResponse, + mockedFilteredKeywordsResponse, +]; const clearButtonAriaLabel = 'Poista kaikki'; const selectedItemRemoveButtonAriaLabel = 'Poista valinta'; const defaultProps: KeywordSelectorProps = { - clearButtonAriaLabel, - helper, - label, - multiselect: true, + texts: { + clearButtonAriaLabel_multiple: clearButtonAriaLabel, + assistive: helper, + label, + tagRemoveSelectionAriaLabel: selectedItemRemoveButtonAriaLabel, + }, + multiSelect: true, name, - selectedItemRemoveButtonAriaLabel, value: [keywordAtId], + onChange: vi.fn(), + handleClose: vi.fn(), }; const renderComponent = (props?: Partial) => render(, { mocks }); -test('should combobox input value to be selected keyword option label', async () => { - renderComponent(); - - await screen.findByText(keywordName, undefined, { timeout: 2000 }); -}); +const getElement = (key: 'inputField' | 'toggleButton') => { + switch (key) { + case 'inputField': + return screen.getByRole('combobox', { name: new RegExp(label) }); + case 'toggleButton': + return screen.getByRole('button', { name: new RegExp(label) }); + } +}; test('should open menu by clickin toggle button and list of options should be visible', async () => { renderComponent(); @@ -51,3 +66,23 @@ test('should open menu by clickin toggle button and list of options should be vi toggleButtonLabel: new RegExp(label), }); }); + +test('should search for keywords', async () => { + const user = userEvent.setup(); + renderComponent(); + + const toggleButton = getElement('toggleButton'); + + await user.click(toggleButton); + + const input = getElement('inputField'); + + await user.type(input, keywordName); + + for (const option of filteredKeywords.data) { + await screen.findByRole('option', { + hidden: true, + name: getValue(option?.name?.fi, ''), + }); + } +}); diff --git a/src/common/components/loadingButton/LoadingButton.tsx b/src/common/components/loadingButton/LoadingButton.tsx index 2e16e91bf..0e17fb2c8 100644 --- a/src/common/components/loadingButton/LoadingButton.tsx +++ b/src/common/components/loadingButton/LoadingButton.tsx @@ -14,7 +14,7 @@ const LoadingButton = React.forwardRef( {...rest} ref={ref} disabled={loading || disabled} - iconLeft={ + iconStart={ loading ? ( = ({ {...commonProps} ref={toggleButton} fullWidth={true} - iconRight={ + iconEnd={ menuOpen ? : } - variant="secondary" + variant={ButtonVariant.Secondary} > - {buttonLabel} + {buttonLabel} ); }; diff --git a/src/common/components/multiSelectDropdown/MultiSelectDropdown.tsx b/src/common/components/multiSelectDropdown/MultiSelectDropdown.tsx index 3641c3a32..8358855b7 100644 --- a/src/common/components/multiSelectDropdown/MultiSelectDropdown.tsx +++ b/src/common/components/multiSelectDropdown/MultiSelectDropdown.tsx @@ -19,7 +19,7 @@ export interface MultiselectDropdownProps { loadingSpinnerText?: string; onChange: (values: OptionType[]) => void; options: OptionType[]; - renderOptionText?: (option: OptionType) => React.ReactChild; + renderOptionText?: (option: OptionType) => string | undefined; searchPlaceholder?: string; searchValue?: string; setSearchValue?: (newVal: string) => void; @@ -59,7 +59,7 @@ const MultiSelectDropdown: React.FC = ({ const filteredOptions = React.useMemo(() => { return [ ...options.filter((option) => - option.label.toLowerCase().includes(searchValue.toLowerCase()) + option.label?.toLowerCase().includes(searchValue.toLowerCase()) ), ].filter(skipFalsyType); }, [options, searchValue]); diff --git a/src/common/components/pagination/__tests__/Pagination.test.tsx b/src/common/components/pagination/__tests__/Pagination.test.tsx index 2d64fd1c4..b387342d9 100644 --- a/src/common/components/pagination/__tests__/Pagination.test.tsx +++ b/src/common/components/pagination/__tests__/Pagination.test.tsx @@ -67,7 +67,9 @@ test('should call onChange when clicking page link', async () => { const user = userEvent.setup(); renderComponent({ pageIndex: 0, onChange }); - const page2Link = screen.getByTestId('hds-pagination-page-2'); + const links = screen.getAllByRole('listitem'); + const page2Link = links[1].querySelector('a') as HTMLAnchorElement; + await user.click(page2Link); expect(onChange).toBeCalledWith(expect.objectContaining({}), 1); diff --git a/src/common/components/placeSelector/PlaceSelector.tsx b/src/common/components/placeSelector/PlaceSelector.tsx index a18753ae7..39b5dc8a4 100644 --- a/src/common/components/placeSelector/PlaceSelector.tsx +++ b/src/common/components/placeSelector/PlaceSelector.tsx @@ -1,10 +1,10 @@ /* eslint-disable no-undef */ +import { ApolloError } from '@apollo/client'; +import { Option, SearchFunction, SearchResult, SelectData } from 'hds-react'; import { TFunction } from 'i18next'; -import React from 'react'; +import React, { useCallback, useEffect } from 'react'; import { useTranslation } from 'react-i18next'; -import { useDebounce } from 'use-debounce'; -import { COMBOBOX_DEBOUNCE_TIME_MS } from '../../../constants'; import { getPlaceFields, placePathBuilder, @@ -16,13 +16,12 @@ import { usePlacesQuery, } from '../../../generated/graphql'; import useLocale from '../../../hooks/useLocale'; -import useMountedState from '../../../hooks/useMountedState'; import { Language, OptionType } from '../../../types'; import getPathBuilder from '../../../utils/getPathBuilder'; import getValue from '../../../utils/getValue'; import parseIdFromAtId from '../../../utils/parseIdFromAtId'; import skipFalsyType from '../../../utils/skipFalsyType'; -import Combobox, { SingleComboboxProps } from '../combobox/Combobox'; +import Select, { SelectPropsWithValue } from '../select/Select'; import styles from './placeSelector.module.scss'; export type GetOptionArgs = { @@ -58,32 +57,31 @@ export const getOption = ({ }; }; -export type PlaceSelectorProps = Omit< - SingleComboboxProps, - 'toggleButtonAriaLabel' ->; +export type PlaceSelectorProps = SelectPropsWithValue; const PlaceSelector: React.FC = ({ - label, - name, + texts, value, + name, + onChange, ...rest }) => { - const timer = React.useRef(); const { t } = useTranslation(); const locale = useLocale(); - const [search, setSearch] = useMountedState(''); - const [debouncedSearch] = useDebounce(search, COMBOBOX_DEBOUNCE_TIME_MS); + + const [options, setOptions] = React.useState([]); + const initialOptions = React.useRef([]); const { data: placesData, loading, previousData: previousPlacesData, + refetch, } = usePlacesQuery({ variables: { createPath: getPathBuilder(placesPathBuilder), showAllPlaces: true, - text: debouncedSearch, + text: '', }, }); @@ -95,16 +93,7 @@ const PlaceSelector: React.FC = ({ }, }); - const handleFilter = (items: OptionType[], inputValue: string) => { - clearTimeout(timer.current); - timer.current = setTimeout(() => { - setSearch(inputValue); - }); - - return items; - }; - - const options = React.useMemo( + const getPlacesData = useCallback( () => getValue( (placesData || previousPlacesData)?.places.data.map((place) => @@ -115,39 +104,84 @@ const PlaceSelector: React.FC = ({ [locale, placesData, previousPlacesData, t] ); - React.useEffect(() => { - return () => clearTimeout(timer.current); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); + const placesOptions = React.useMemo(() => getPlacesData(), [getPlacesData]); + + useEffect(() => { + setOptions(placesOptions); + + if (placesData && !initialOptions?.current.length) { + initialOptions.current = placesOptions; + } + }, [placesOptions, placesData]); + + const handleSearch: SearchFunction = React.useCallback( + async (searchValue: string): Promise => { + try { + const { error, data: newPlacesData } = await refetch({ + text: searchValue, + }); + + if (error) { + throw error; + } + + return { + options: getValue( + newPlacesData?.places.data.map((place) => + getOption({ place: place as PlaceFieldsFragment, locale, t }) + ), + [] + ), + }; + } catch (error) { + return Promise.reject(error as ApolloError); + } + }, + [refetch, locale, t] + ); + + const handleChange = React.useCallback( + (selectedOptions: Option[], clickedOption: Option, data: SelectData) => { + setOptions(initialOptions.current); + + if (onChange) { + onChange(selectedOptions, clickedOption, data); + } + }, + [onChange] + ); const selectedPlace = React.useMemo( () => placeData?.place - ? getOption({ - locale, - place: placeData.place, - showEventAmount: false, - t, - }) - : null, + ? [ + getOption({ + locale, + place: placeData.place, + showEventAmount: false, + t, + }), + ] + : [], [locale, placeData, t] ); return ( - ); }; diff --git a/src/common/components/placeSelector/__mocks__/placeSelector.ts b/src/common/components/placeSelector/__mocks__/placeSelector.ts index ebd163901..fef36a8c6 100644 --- a/src/common/components/placeSelector/__mocks__/placeSelector.ts +++ b/src/common/components/placeSelector/__mocks__/placeSelector.ts @@ -23,7 +23,7 @@ const mockedPlaceResponse = { result: placeResponse, }; -const places = fakePlaces(0); +const places = fakePlaces(1, [place]); const placesVariables = { createPath: undefined, showAllPlaces: true, @@ -35,7 +35,7 @@ const mockedPlacesResponse = { result: placesResponse, }; -const filteredPlaces = fakePlaces(1, [place]); +const filteredPlaces = places; const filteredPlacesVariables = { createPath: undefined, showAllPlaces: true, diff --git a/src/common/components/placeSelector/__tests__/PlaceSelector.test.tsx b/src/common/components/placeSelector/__tests__/PlaceSelector.test.tsx index 19995adec..125d37f02 100644 --- a/src/common/components/placeSelector/__tests__/PlaceSelector.test.tsx +++ b/src/common/components/placeSelector/__tests__/PlaceSelector.test.tsx @@ -10,7 +10,6 @@ import { render, screen, userEvent, - waitFor, } from '../../../../utils/testUtils'; import { filteredPlaces, @@ -39,10 +38,10 @@ const label = 'Select place'; const name = 'place'; const defaultProps: PlaceSelectorProps = { - helper, - label, + texts: { assistive: helper, label }, name, value: place.atId, + onChange: vi.fn(), }; const renderComponent = (props?: Partial) => @@ -51,31 +50,42 @@ const renderComponent = (props?: Partial) => const getElement = (key: 'inputField' | 'toggleButton') => { switch (key) { case 'inputField': - return screen.getByRole('combobox', { name: new RegExp(helper) }); + return screen.getByRole('combobox', { name: new RegExp(label) }); case 'toggleButton': return screen.getByRole('button', { name: new RegExp(label) }); } }; -test('should combobox input value to be selected place option label', async () => { +test('should open menu by clickin toggle button and list of options should be visible', async () => { + const user = userEvent.setup(); renderComponent(); - const inputField = getElement('inputField'); + const toggleButton = getElement('toggleButton'); + + await user.click(toggleButton); - await waitFor(() => expect(inputField).toHaveValue(selectedPlaceText)); + expect(toggleButton.getAttribute('aria-expanded')).toBe('true'); + + for (const option of filteredPlaces.data) { + await screen.findByRole('option', { + hidden: true, + name: new RegExp(getValue(option?.name?.fi, '')), + }); + } }); -test('should open menu by clickin toggle button and list of options should be visible', async () => { +test('should search for places', async () => { const user = userEvent.setup(); renderComponent(); - const inputField = getElement('inputField'); - expect(inputField.getAttribute('aria-expanded')).toBe('false'); - const toggleButton = getElement('toggleButton'); + await user.click(toggleButton); - expect(inputField.getAttribute('aria-expanded')).toBe('true'); + const input = getElement('inputField'); + + await user.type(input, selectedPlaceText); + for (const option of filteredPlaces.data) { await screen.findByRole('option', { hidden: true, diff --git a/src/common/components/publisherSelector/PublisherSelector.tsx b/src/common/components/publisherSelector/PublisherSelector.tsx index 699275f7d..11bc4f91b 100644 --- a/src/common/components/publisherSelector/PublisherSelector.tsx +++ b/src/common/components/publisherSelector/PublisherSelector.tsx @@ -9,18 +9,17 @@ import useUser from '../../../domain/user/hooks/useUser'; import useUserOrganizations from '../../../domain/user/hooks/useUserOrganizations'; import { useOrganizationQuery } from '../../../generated/graphql'; import useLocale from '../../../hooks/useLocale'; -import { OptionType } from '../../../types'; import getPathBuilder from '../../../utils/getPathBuilder'; import getValue from '../../../utils/getValue'; -import Combobox, { SingleComboboxProps } from '../combobox/Combobox'; +import Select, { SelectPropsWithValue } from '../select/Select'; export type PublisherSelectorProps = { publisher?: string | null; -} & Omit, 'toggleButtonAriaLabel'>; +} & SelectPropsWithValue; const PublisherSelector: React.FC = ({ clearable = false, - label, + texts, name, publisher, value, @@ -43,19 +42,21 @@ const PublisherSelector: React.FC = ({ const selectedOrganization = React.useMemo( () => organizationData?.organization - ? getOrganizationOption({ - idPath: 'id', - locale, - organization: organizationData.organization, - t, - }) - : null, + ? [ + getOrganizationOption({ + idPath: 'id', + locale, + organization: organizationData.organization, + t, + }), + ] + : [], [locale, organizationData?.organization, t] ); const options = useMemo(() => { if (publisher) { - return selectedOrganization ? [selectedOrganization] : []; + return selectedOrganization.length ? selectedOrganization : []; } return organizations.map((org) => getOrganizationOption({ @@ -68,19 +69,18 @@ const PublisherSelector: React.FC = ({ }, [locale, organizations, publisher, selectedOrganization, t]); return ( - ); }; diff --git a/src/common/components/publisherSelector/__tests__/PublisherSelector.test.tsx b/src/common/components/publisherSelector/__tests__/PublisherSelector.test.tsx index 957e87657..1d3904008 100644 --- a/src/common/components/publisherSelector/__tests__/PublisherSelector.test.tsx +++ b/src/common/components/publisherSelector/__tests__/PublisherSelector.test.tsx @@ -11,7 +11,6 @@ import { render, screen, userEvent, - waitFor, } from '../../../../utils/testUtils'; import PublisherSelector, { PublisherSelectorProps, @@ -94,9 +93,10 @@ const mocks = [ ]; const defaultProps: PublisherSelectorProps = { - label, + texts: { label }, name: 'publisher-selector', value: publisherId, + onChange: vi.fn(), }; const renderComponent = (props?: Partial) => @@ -104,36 +104,29 @@ const renderComponent = (props?: Partial) => mocks, }); -const getElement = (key: 'searchInput' | 'toggleButton') => { - switch (key) { - case 'searchInput': - return screen.getByRole('combobox', { name: label }); - case 'toggleButton': - return screen.getByRole('button', { name: `${label}: Valikko` }); - } -}; +const getToggleButton = () => + screen.getByRole('combobox', { name: new RegExp(label) }); test('should show users organizations as menu options', async () => { const user = userEvent.setup(); - renderComponent({ publisher: null, value: null }); + renderComponent({ publisher: null, value: undefined }); - getElement('searchInput'); + const toggleButton = getToggleButton(); - const toggleButton = getElement('toggleButton'); await user.click(toggleButton); await screen.findByRole('option', { name: organizationName }); + screen.getByRole('option', { name: adminOrganizationName }); }); test('should show publisher as menu option', async () => { const user = userEvent.setup(); + renderComponent({ publisher: publisherId }); - const searchinput = getElement('searchInput'); - await waitFor(() => expect(searchinput).toHaveValue(publisherName)); + const toggleButton = getToggleButton(); - const toggleButton = getElement('toggleButton'); await user.click(toggleButton); await screen.findByRole('option', { name: publisherName }); diff --git a/src/common/components/searchPanel/SearchPanel.tsx b/src/common/components/searchPanel/SearchPanel.tsx index b3e4f9f43..094d57516 100644 --- a/src/common/components/searchPanel/SearchPanel.tsx +++ b/src/common/components/searchPanel/SearchPanel.tsx @@ -1,4 +1,5 @@ import classNames from 'classnames'; +import { ButtonVariant } from 'hds-react'; import { FC, PropsWithChildren, ReactElement } from 'react'; import Button from '../button/Button'; @@ -65,7 +66,11 @@ const SearchRow: FC = ({ )} - diff --git a/src/common/components/select/Select.tsx b/src/common/components/select/Select.tsx new file mode 100644 index 000000000..2ced50000 --- /dev/null +++ b/src/common/components/select/Select.tsx @@ -0,0 +1,44 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import classNames from 'classnames'; +import { Select as HDSSelect } from 'hds-react'; +import React from 'react'; +import { useTranslation } from 'react-i18next'; + +import { useTheme } from '../../../domain/app/theme/Theme'; +import SelectLoadingSpinner from '../selectLoadingSpinner/SelectLoadingSpinner'; +import { SingleSelectProps } from '../singleSelect/SingleSelect'; +import styles from './select.module.scss'; + +export type SelectPropsWithValue = SingleSelectProps & { + name: string; + value: ValueType; +}; + +export type MultiSelectPropsWithValue = SingleSelectProps & { + name: string; + value: ValueType[]; +}; + +const Select: React.FC = ({ + alignedLabel, + isLoading, + className, + texts, + ...rest +}) => { + const { theme } = useTheme(); + const { t } = useTranslation(); + + return ( + + + + ); +}; + +export default Select; diff --git a/src/common/components/select/select.module.scss b/src/common/components/select/select.module.scss index d41f74b94..f7125c54b 100644 --- a/src/common/components/select/select.module.scss +++ b/src/common/components/select/select.module.scss @@ -1,8 +1,12 @@ -.label { - .text { - color: var(--color-black-90); - } - .amount { - color: var(--color-black-60); +.select { + max-width: 100% !important; + + > div:focus-within { + box-shadow: none; + + input:global(.focus-visible) { + outline: var(--focus-outline-width) solid var(--focus-outline-color); + outline-offset: 1px; + } } } diff --git a/src/common/components/select/SelectLabel.tsx b/src/common/components/selectLabel/SelectLabel.tsx similarity index 92% rename from src/common/components/select/SelectLabel.tsx rename to src/common/components/selectLabel/SelectLabel.tsx index 80cd62bcd..6c91bd542 100644 --- a/src/common/components/select/SelectLabel.tsx +++ b/src/common/components/selectLabel/SelectLabel.tsx @@ -1,7 +1,7 @@ import React from 'react'; import { useTranslation } from 'react-i18next'; -import styles from './select.module.scss'; +import styles from './selectLabel.module.scss'; interface SelectLabelProps { nEvents: number; diff --git a/src/common/components/select/__tests__/SelectLabel.test.tsx b/src/common/components/selectLabel/__tests__/SelectLabel.test.tsx similarity index 100% rename from src/common/components/select/__tests__/SelectLabel.test.tsx rename to src/common/components/selectLabel/__tests__/SelectLabel.test.tsx diff --git a/src/common/components/select/__tests__/__snapshots__/SelectLabel.test.tsx.snap b/src/common/components/selectLabel/__tests__/__snapshots__/SelectLabel.test.tsx.snap similarity index 100% rename from src/common/components/select/__tests__/__snapshots__/SelectLabel.test.tsx.snap rename to src/common/components/selectLabel/__tests__/__snapshots__/SelectLabel.test.tsx.snap diff --git a/src/common/components/selectLabel/selectLabel.module.scss b/src/common/components/selectLabel/selectLabel.module.scss new file mode 100644 index 000000000..d41f74b94 --- /dev/null +++ b/src/common/components/selectLabel/selectLabel.module.scss @@ -0,0 +1,8 @@ +.label { + .text { + color: var(--color-black-90); + } + .amount { + color: var(--color-black-60); + } +} diff --git a/src/common/components/comboboxLoadingSpinner/ComboboxLoadingSpinner.tsx b/src/common/components/selectLoadingSpinner/SelectLoadingSpinner.tsx similarity index 69% rename from src/common/components/comboboxLoadingSpinner/ComboboxLoadingSpinner.tsx rename to src/common/components/selectLoadingSpinner/SelectLoadingSpinner.tsx index 39d1261ba..1022f75cc 100644 --- a/src/common/components/comboboxLoadingSpinner/ComboboxLoadingSpinner.tsx +++ b/src/common/components/selectLoadingSpinner/SelectLoadingSpinner.tsx @@ -2,18 +2,18 @@ import classNames from 'classnames'; import { FC, PropsWithChildren } from 'react'; import LoadingSpinner from '../loadingSpinner/LoadingSpinner'; -import styles from './comboboxLoadingSpinner.module.scss'; +import styles from './selectLoadingSpinner.module.scss'; -export type ComboboxLoadingSpinnerProps = { +export type SelectLoadingSpinnerProps = { alignedLabel?: boolean; isLoading?: boolean; }; -const ComboboxLoadingSpinner: FC< - PropsWithChildren +const SelectLoadingSpinner: FC< + PropsWithChildren > = ({ alignedLabel, children, isLoading }) => { return ( -
+
{children} {isLoading && (
= ({ errors, id: _id }) => {
    {errors.map(({ label, message }) => ( diff --git a/src/common/components/singleKeywordSelector/SingleKeywordSelector.tsx b/src/common/components/singleKeywordSelector/SingleKeywordSelector.tsx index c655e488c..8f00d48bb 100644 --- a/src/common/components/singleKeywordSelector/SingleKeywordSelector.tsx +++ b/src/common/components/singleKeywordSelector/SingleKeywordSelector.tsx @@ -1,9 +1,9 @@ /* eslint-disable no-undef */ -import React from 'react'; +import { ApolloError } from '@apollo/client'; +import { Option, SearchFunction, SearchResult, SelectData } from 'hds-react'; +import React, { useCallback } from 'react'; import { useTranslation } from 'react-i18next'; -import { useDebounce } from 'use-debounce'; -import { COMBOBOX_DEBOUNCE_TIME_MS } from '../../../constants'; import { getKeywordFields, keywordPathBuilder, @@ -16,11 +16,10 @@ import { useKeywordsQuery, } from '../../../generated/graphql'; import useLocale from '../../../hooks/useLocale'; -import useMountedState from '../../../hooks/useMountedState'; import { Language, OptionType } from '../../../types'; import getPathBuilder from '../../../utils/getPathBuilder'; import getValue from '../../../utils/getValue'; -import Combobox, { SingleComboboxProps } from '../combobox/Combobox'; +import Select, { SelectPropsWithValue } from '../select/Select'; const getOption = ({ keyword, @@ -34,32 +33,31 @@ const getOption = ({ return { label, value }; }; -export type SingleKeywordSelectorProps = Omit< - SingleComboboxProps, - 'toggleButtonAriaLabel' ->; +export type SingleKeywordSelectorProps = SelectPropsWithValue; const SingleKeywordSelector: React.FC = ({ - label, + texts, name, value, + onChange, ...rest }) => { - const timer = React.useRef(); const { t } = useTranslation(); const locale = useLocale(); - const [search, setSearch] = useMountedState(''); - const [debouncedSearch] = useDebounce(search, COMBOBOX_DEBOUNCE_TIME_MS); + + const [options, setOptions] = React.useState([]); + const initialOptions = React.useRef([]); const { data: keywordsData, loading, previousData: previousKeywordsData, + refetch, } = useKeywordsQuery({ variables: { createPath: getPathBuilder(keywordsPathBuilder), showAllKeywords: true, - text: debouncedSearch, + text: '', }, }); @@ -71,14 +69,7 @@ const SingleKeywordSelector: React.FC = ({ }, }); - const handleFilter = (items: OptionType[], inputValue: string) => { - clearTimeout(timer.current); - timer.current = setTimeout(() => setSearch(inputValue)); - - return items; - }; - - const options: OptionType[] = React.useMemo( + const getKeywordsData = useCallback( () => getValue( (keywordsData || previousKeywordsData)?.keywords.data.map((keyword) => @@ -89,33 +80,78 @@ const SingleKeywordSelector: React.FC = ({ [keywordsData, locale, previousKeywordsData] ); + const keywordsOptions = React.useMemo( + () => getKeywordsData(), + [getKeywordsData] + ); + + React.useEffect(() => { + setOptions(keywordsOptions); + + if (keywordsData && !initialOptions?.current.length) { + initialOptions.current = keywordsOptions; + } + }, [keywordsOptions, keywordsData]); + + const handleSearch: SearchFunction = React.useCallback( + async (searchValue: string): Promise => { + try { + const { error, data: newkeywordsData } = await refetch({ + text: searchValue, + }); + + if (error) { + throw error; + } + + return { + options: getValue( + newkeywordsData?.keywords.data.map((keyword) => + getOption({ keyword: keyword as KeywordFieldsFragment, locale }) + ), + [] + ), + }; + } catch (error) { + return Promise.reject(error as ApolloError); + } + }, + [refetch, locale] + ); + + const handleChange = React.useCallback( + (selectedOptions: Option[], clickedOption: Option, data: SelectData) => { + setOptions(initialOptions.current); + + if (onChange) { + onChange(selectedOptions, clickedOption, data); + } + }, + [onChange] + ); + const selectedKeyword = React.useMemo( () => keywordData?.keyword - ? getOption({ keyword: keywordData.keyword, locale }) - : null, + ? [getOption({ keyword: keywordData.keyword, locale })] + : [], [keywordData, locale] ); - React.useEffect(() => { - return () => clearTimeout(timer.current); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - return ( - ); }; diff --git a/src/common/components/singleKeywordSelector/__tests__/SingleKeywordSelector.test.tsx b/src/common/components/singleKeywordSelector/__tests__/SingleKeywordSelector.test.tsx index e40c41449..833a739fa 100644 --- a/src/common/components/singleKeywordSelector/__tests__/SingleKeywordSelector.test.tsx +++ b/src/common/components/singleKeywordSelector/__tests__/SingleKeywordSelector.test.tsx @@ -1,12 +1,14 @@ import { TEST_KEYWORD_ID } from '../../../../domain/keyword/constants'; +import getValue from '../../../../utils/getValue'; import { configure, render, screen, shouldOpenMenuAndSelectOption, - waitFor, + userEvent, } from '../../../../utils/testUtils'; import { + filteredKeywords, keywordName, keywordNames, mockedKeywordResponse, @@ -31,21 +33,24 @@ const mocks = [ ]; const defaultProps: SingleKeywordSelectorProps = { - label, + texts: { label }, name, value: TEST_KEYWORD_ID, + onChange: vi.fn(), +}; + +const getElement = (key: 'inputField' | 'toggleButton') => { + switch (key) { + case 'inputField': + return screen.getByRole('combobox', { name: new RegExp(label) }); + case 'toggleButton': + return screen.getByRole('button', { name: new RegExp(label) }); + } }; const renderComponent = (props?: Partial) => render(, { mocks }); -test('should set input value to equal label of selected keyword', async () => { - renderComponent(); - - const combobox = screen.getByRole('combobox', { name: new RegExp(label) }); - await waitFor(() => expect(combobox).toHaveValue(keywordName)); -}); - test('should open menu by clickin toggle button and list of options should be visible', async () => { renderComponent(); @@ -54,3 +59,23 @@ test('should open menu by clickin toggle button and list of options should be vi toggleButtonLabel: new RegExp(label), }); }); + +test('should search for keywords', async () => { + const user = userEvent.setup(); + renderComponent(); + + const toggleButton = getElement('toggleButton'); + + await user.click(toggleButton); + + const input = getElement('inputField'); + + await user.type(input, keywordName); + + for (const option of filteredKeywords.data) { + await screen.findByRole('option', { + hidden: true, + name: getValue(option?.name?.fi, ''), + }); + } +}); diff --git a/src/common/components/singleOrganizationClassSelector/SingleOrganizationClassSelector.tsx b/src/common/components/singleOrganizationClassSelector/SingleOrganizationClassSelector.tsx index 95d73e623..a2120e6f7 100644 --- a/src/common/components/singleOrganizationClassSelector/SingleOrganizationClassSelector.tsx +++ b/src/common/components/singleOrganizationClassSelector/SingleOrganizationClassSelector.tsx @@ -13,7 +13,7 @@ import { import { OptionType } from '../../../types'; import getPathBuilder from '../../../utils/getPathBuilder'; import getValue from '../../../utils/getValue'; -import Combobox, { SingleComboboxProps } from '../combobox/Combobox'; +import Select, { SelectPropsWithValue } from '../select/Select'; const getOption = ({ organizationClass, @@ -26,18 +26,24 @@ const getOption = ({ return { label, value }; }; -export type SingleOrganizationClassSelectorProps = SingleComboboxProps< - string | null ->; +type SingleOrganizationClassSelectorProps = SelectPropsWithValue; const SingleOrganizationClassSelector: React.FC< SingleOrganizationClassSelectorProps -> = ({ label, name, value, ...rest }) => { +> = ({ texts, name, value, ...rest }) => { const { t } = useTranslation(); const { loading, organizationClasses } = useAllOrganizationClasses(); - const options: OptionType[] = React.useMemo( + const { data: organizationClassData } = useOrganizationClassQuery({ + skip: !value, + variables: { + id: getValue(value, ''), + createPath: getPathBuilder(organizationClassPathBuilder), + }, + }); + + const options = React.useMemo( () => getValue( organizationClasses.map((organizationClass) => @@ -48,32 +54,23 @@ const SingleOrganizationClassSelector: React.FC< [organizationClasses] ); - const { data: organizationClassData } = useOrganizationClassQuery({ - skip: !value, - variables: { - id: getValue(value, ''), - createPath: getPathBuilder(organizationClassPathBuilder), - }, - }); - const selectedOrganizationClass = React.useMemo(() => { const organizationClass = organizationClassData?.organizationClass; - return organizationClass ? getOption({ organizationClass }) : null; + return organizationClass ? [getOption({ organizationClass })] : []; }, [organizationClassData]); return ( - ); }; diff --git a/src/common/components/singleOrganizationSelector/SingleOrganizationSelector.tsx b/src/common/components/singleOrganizationSelector/SingleOrganizationSelector.tsx index 049e61f79..24ebcf1e0 100644 --- a/src/common/components/singleOrganizationSelector/SingleOrganizationSelector.tsx +++ b/src/common/components/singleOrganizationSelector/SingleOrganizationSelector.tsx @@ -9,7 +9,7 @@ import { OrganizationFieldsFragment } from '../../../generated/graphql'; import useLocale from '../../../hooks/useLocale'; import { Language, OptionType } from '../../../types'; import getValue from '../../../utils/getValue'; -import Combobox, { SingleComboboxProps } from '../combobox/Combobox'; +import Select, { SelectPropsWithValue } from '../select/Select'; const getOption = ({ locale, @@ -29,12 +29,10 @@ const getOption = ({ return { label, value }; }; -export type SingleOrganizationSelectorProps = SingleComboboxProps< - string | null ->; +type SingleOrganizationSelectorProps = SelectPropsWithValue; const SingleOrganizationSelector: React.FC = ({ - label, + texts, name, value, ...rest @@ -44,7 +42,7 @@ const SingleOrganizationSelector: React.FC = ({ const { loading, organizations } = useAllOrganizations(); - const options: OptionType[] = React.useMemo( + const options = React.useMemo( () => sortBy( getValue( @@ -58,28 +56,27 @@ const SingleOrganizationSelector: React.FC = ({ [locale, organizations, t] ); - const selectedOrganization = React.useMemo( - () => - getValue( - options.find((o) => o.value === value), - null - ), - [options, value] - ); + const selectedOrganization = React.useMemo(() => { + const option = getValue( + options.find((o) => o.value === value), + null + ); + + return option ? [option] : []; + }, [options, value]); return ( - ); }; diff --git a/src/common/components/singleSelect/SingleSelect.tsx b/src/common/components/singleSelect/SingleSelect.tsx index 413c3fa1f..f62e8360f 100644 --- a/src/common/components/singleSelect/SingleSelect.tsx +++ b/src/common/components/singleSelect/SingleSelect.tsx @@ -1,21 +1,16 @@ import classNames from 'classnames'; -import { Select, SingleSelectProps as HdsSingleSelectProps } from 'hds-react'; +import { Select as HDSSelect, SelectProps } from 'hds-react'; import React from 'react'; -import { useTranslation } from 'react-i18next'; import { useTheme } from '../../../domain/app/theme/Theme'; -import { OptionType } from '../../../types'; -import { - getA11ySelectionMessage, - getA11yStatusMessage, -} from '../../../utils/accessibilityUtils'; -import ComboboxLoadingSpinner, { - ComboboxLoadingSpinnerProps, -} from '../comboboxLoadingSpinner/ComboboxLoadingSpinner'; +import SelectLoadingSpinner, { + SelectLoadingSpinnerProps, +} from '../selectLoadingSpinner/SelectLoadingSpinner'; import styles from './singleSelect.module.scss'; -export type SingleSelectProps = ComboboxLoadingSpinnerProps & - HdsSingleSelectProps; +export type SingleSelectProps = SelectLoadingSpinnerProps & { + className?: string; +} & SelectProps; const SingleSelect: React.FC = ({ alignedLabel, @@ -24,21 +19,16 @@ const SingleSelect: React.FC = ({ ...rest }) => { const { theme } = useTheme(); - const { t } = useTranslation(); return ( - -