From 7497203df2a435754ea7d1777a3544b919035177 Mon Sep 17 00:00:00 2001 From: Christian Andolf Date: Tue, 3 Jun 2025 14:04:57 +0200 Subject: [PATCH 1/2] fix: remove filtering from date and country select for better mobile experience --- .../Form/Country/CountryCombobox.tsx | 168 ++++++++++++++++++ .../TempDesignSystem/Form/Country/index.tsx | 109 +----------- .../TempDesignSystem/Form/Date/index.tsx | 3 - 3 files changed, 176 insertions(+), 104 deletions(-) create mode 100644 apps/scandic-web/components/TempDesignSystem/Form/Country/CountryCombobox.tsx diff --git a/apps/scandic-web/components/TempDesignSystem/Form/Country/CountryCombobox.tsx b/apps/scandic-web/components/TempDesignSystem/Form/Country/CountryCombobox.tsx new file mode 100644 index 000000000..962468be9 --- /dev/null +++ b/apps/scandic-web/components/TempDesignSystem/Form/Country/CountryCombobox.tsx @@ -0,0 +1,168 @@ +"use client" + +import { type SyntheticEvent, useMemo, useState } from "react" +import { + Button, + ComboBox, + Input, + Label, + ListBox, + ListBoxItem, + Popover, + useFilter, +} from "react-aria-components" +import { useController } from "react-hook-form" +import { useIntl } from "react-intl" + +import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon" +import { Typography } from "@scandic-hotels/design-system/Typography" + +import { countries } from "@/constants/countries" + +import useLang from "@/hooks/useLang" + +import ErrorMessage from "../ErrorMessage" + +import styles from "./country.module.css" + +import type { CountryProps } from "./country" + +const prioCountryCode = ["DE", "DK", "FI", "NO", "SE"] + +export default function CountrySelect({ + // hack used since chrome does not respect autocomplete="off" + autoComplete = "nope", + className = "", + label, + name = "country", + readOnly = false, + registerOptions = {}, +}: CountryProps) { + const lang = useLang() + const intl = useIntl() + + const { startsWith } = useFilter({ sensitivity: "base" }) + const [filterValue, setFilterValue] = useState("") + const { field, formState, fieldState } = useController({ + name, + rules: registerOptions, + }) + + const items = useMemo(() => { + function mapCountry(country: (typeof countries)[number]) { + return { + value: country.code, + label: + intl.formatDisplayName(country.code, { type: "region" }) || + country.name, + } + } + + const collator = new Intl.Collator(lang) + const prioCountries = countries + .filter((c) => prioCountryCode.includes(c.code)) + .map(mapCountry) + .filter((item) => startsWith(item.label, filterValue)) + .sort((a, b) => collator.compare(a.label, b.label)) + + const restCountries = countries + .filter((c) => !prioCountryCode.includes(c.code)) + .map(mapCountry) + .filter((item) => startsWith(item.label, filterValue)) + .sort((a, b) => collator.compare(a.label, b.label)) + + return [...prioCountries, ...restCountries] + }, [filterValue, intl, lang, startsWith]) + + function handleOnInput(evt: SyntheticEvent) { + setFilterValue(evt.currentTarget.value) + const isAutoCompleteEvent = !("inputType" in evt.nativeEvent) + if (isAutoCompleteEvent) { + const { value } = evt.currentTarget + const cc = countries.find((c) => c.name === value || c.code === value) + if (cc) { + field.onChange(cc.code) + } + } + } + + return ( +
+ field.onChange(c ?? "")} + selectedKey={field.value} + menuTrigger="focus" + > + + + + {items.map((item) => ( + + {({ isSelected }) => ( + + {item.label} + + )} + + ))} + + + + +
+ ) +} diff --git a/apps/scandic-web/components/TempDesignSystem/Form/Country/index.tsx b/apps/scandic-web/components/TempDesignSystem/Form/Country/index.tsx index 962468be9..ab376647e 100644 --- a/apps/scandic-web/components/TempDesignSystem/Form/Country/index.tsx +++ b/apps/scandic-web/components/TempDesignSystem/Form/Country/index.tsx @@ -1,21 +1,10 @@ "use client" -import { type SyntheticEvent, useMemo, useState } from "react" -import { - Button, - ComboBox, - Input, - Label, - ListBox, - ListBoxItem, - Popover, - useFilter, -} from "react-aria-components" +import { useMemo } from "react" import { useController } from "react-hook-form" import { useIntl } from "react-intl" -import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon" -import { Typography } from "@scandic-hotels/design-system/Typography" +import { Select } from "@scandic-hotels/design-system/Select" import { countries } from "@/constants/countries" @@ -23,15 +12,11 @@ import useLang from "@/hooks/useLang" import ErrorMessage from "../ErrorMessage" -import styles from "./country.module.css" - import type { CountryProps } from "./country" const prioCountryCode = ["DE", "DK", "FI", "NO", "SE"] export default function CountrySelect({ - // hack used since chrome does not respect autocomplete="off" - autoComplete = "nope", className = "", label, name = "country", @@ -41,8 +26,6 @@ export default function CountrySelect({ const lang = useLang() const intl = useIntl() - const { startsWith } = useFilter({ sensitivity: "base" }) - const [filterValue, setFilterValue] = useState("") const { field, formState, fieldState } = useController({ name, rules: registerOptions, @@ -62,36 +45,21 @@ export default function CountrySelect({ const prioCountries = countries .filter((c) => prioCountryCode.includes(c.code)) .map(mapCountry) - .filter((item) => startsWith(item.label, filterValue)) .sort((a, b) => collator.compare(a.label, b.label)) const restCountries = countries .filter((c) => !prioCountryCode.includes(c.code)) .map(mapCountry) - .filter((item) => startsWith(item.label, filterValue)) .sort((a, b) => collator.compare(a.label, b.label)) return [...prioCountries, ...restCountries] - }, [filterValue, intl, lang, startsWith]) - - function handleOnInput(evt: SyntheticEvent) { - setFilterValue(evt.currentTarget.value) - const isAutoCompleteEvent = !("inputType" in evt.nativeEvent) - if (isAutoCompleteEvent) { - const { value } = evt.currentTarget - const cc = countries.find((c) => c.name === value || c.code === value) - if (cc) { - field.onChange(cc.code) - } - } - } + }, [intl, lang]) return (
- field.onChange(c ?? "")} selectedKey={field.value} - menuTrigger="focus" - > - - - - {items.map((item) => ( - - {({ isSelected }) => ( - - {item.label} - - )} - - ))} - - - + data-testid={name} + />
) diff --git a/apps/scandic-web/components/TempDesignSystem/Form/Date/index.tsx b/apps/scandic-web/components/TempDesignSystem/Form/Date/index.tsx index 2f8d27b2c..26758a70e 100644 --- a/apps/scandic-web/components/TempDesignSystem/Form/Date/index.tsx +++ b/apps/scandic-web/components/TempDesignSystem/Form/Date/index.tsx @@ -126,7 +126,6 @@ export default function DateSelect({ name, registerOptions = {} }: DateProps) { name={DateName.day} onSelectionChange={(key) => setValue(DateName.day, Number(key))} isRequired - enableFiltering isInvalid={fieldState.invalid} onBlur={field.onBlur} defaultSelectedKey={dateValue?.day} @@ -142,7 +141,6 @@ export default function DateSelect({ name, registerOptions = {} }: DateProps) { name={DateName.month} onSelectionChange={(key) => setValue(DateName.month, Number(key))} isRequired - enableFiltering isInvalid={fieldState.invalid} onBlur={field.onBlur} defaultSelectedKey={dateValue?.month} @@ -158,7 +156,6 @@ export default function DateSelect({ name, registerOptions = {} }: DateProps) { name={DateName.year} onSelectionChange={(key) => setValue(DateName.year, Number(key))} isRequired - enableFiltering isInvalid={fieldState.invalid} onBlur={field.onBlur} defaultSelectedKey={dateValue?.year} From afd3a7d9ca90ccc4885fa9fa0dfd5cc7bf143002 Mon Sep 17 00:00:00 2001 From: Christian Andolf Date: Wed, 4 Jun 2025 12:00:49 +0200 Subject: [PATCH 2/2] fix: date and country selects now still uses the filtering in desktop --- .../Form/Country/CountryCombobox.tsx | 2 +- .../Form/Country/CountrySelect.tsx | 75 +++++++++++++++++++ .../TempDesignSystem/Form/Country/index.tsx | 75 +++---------------- .../TempDesignSystem/Form/Date/index.tsx | 7 ++ .../lib/components/Select/Select.tsx | 2 +- .../lib/components/Select/types.ts | 3 +- 6 files changed, 96 insertions(+), 68 deletions(-) create mode 100644 apps/scandic-web/components/TempDesignSystem/Form/Country/CountrySelect.tsx diff --git a/apps/scandic-web/components/TempDesignSystem/Form/Country/CountryCombobox.tsx b/apps/scandic-web/components/TempDesignSystem/Form/Country/CountryCombobox.tsx index 962468be9..dc57b7b8e 100644 --- a/apps/scandic-web/components/TempDesignSystem/Form/Country/CountryCombobox.tsx +++ b/apps/scandic-web/components/TempDesignSystem/Form/Country/CountryCombobox.tsx @@ -29,7 +29,7 @@ import type { CountryProps } from "./country" const prioCountryCode = ["DE", "DK", "FI", "NO", "SE"] -export default function CountrySelect({ +export default function CountryCombobox({ // hack used since chrome does not respect autocomplete="off" autoComplete = "nope", className = "", diff --git a/apps/scandic-web/components/TempDesignSystem/Form/Country/CountrySelect.tsx b/apps/scandic-web/components/TempDesignSystem/Form/Country/CountrySelect.tsx new file mode 100644 index 000000000..ab376647e --- /dev/null +++ b/apps/scandic-web/components/TempDesignSystem/Form/Country/CountrySelect.tsx @@ -0,0 +1,75 @@ +"use client" + +import { useMemo } from "react" +import { useController } from "react-hook-form" +import { useIntl } from "react-intl" + +import { Select } from "@scandic-hotels/design-system/Select" + +import { countries } from "@/constants/countries" + +import useLang from "@/hooks/useLang" + +import ErrorMessage from "../ErrorMessage" + +import type { CountryProps } from "./country" + +const prioCountryCode = ["DE", "DK", "FI", "NO", "SE"] + +export default function CountrySelect({ + className = "", + label, + name = "country", + readOnly = false, + registerOptions = {}, +}: CountryProps) { + const lang = useLang() + const intl = useIntl() + + const { field, formState, fieldState } = useController({ + name, + rules: registerOptions, + }) + + const items = useMemo(() => { + function mapCountry(country: (typeof countries)[number]) { + return { + value: country.code, + label: + intl.formatDisplayName(country.code, { type: "region" }) || + country.name, + } + } + + const collator = new Intl.Collator(lang) + const prioCountries = countries + .filter((c) => prioCountryCode.includes(c.code)) + .map(mapCountry) + .sort((a, b) => collator.compare(a.label, b.label)) + + const restCountries = countries + .filter((c) => !prioCountryCode.includes(c.code)) + .map(mapCountry) + .sort((a, b) => collator.compare(a.label, b.label)) + + return [...prioCountries, ...restCountries] + }, [intl, lang]) + + return ( +
+ field.onChange(c ?? "")} - selectedKey={field.value} - data-testid={name} - /> - -
+ return isDesktop ? ( + + ) : ( + ) } diff --git a/apps/scandic-web/components/TempDesignSystem/Form/Date/index.tsx b/apps/scandic-web/components/TempDesignSystem/Form/Date/index.tsx index 26758a70e..5d57c889e 100644 --- a/apps/scandic-web/components/TempDesignSystem/Form/Date/index.tsx +++ b/apps/scandic-web/components/TempDesignSystem/Form/Date/index.tsx @@ -3,6 +3,7 @@ import { parseDate } from "@internationalized/date" import { useEffect } from "react" import { useController, useFormContext, useWatch } from "react-hook-form" import { useIntl } from "react-intl" +import { useMediaQuery } from "usehooks-ts" import { Select } from "@scandic-hotels/design-system/Select" @@ -20,6 +21,9 @@ import styles from "./date.module.css" export default function DateSelect({ name, registerOptions = {} }: DateProps) { const intl = useIntl() const lang = useLang() + const isDesktop = useMediaQuery("(min-width: 768px)", { + initializeWithValue: false, + }) const { control, setValue, formState, watch } = useFormContext() const { field, fieldState } = useController({ @@ -126,6 +130,7 @@ export default function DateSelect({ name, registerOptions = {} }: DateProps) { name={DateName.day} onSelectionChange={(key) => setValue(DateName.day, Number(key))} isRequired + enableFiltering={isDesktop} isInvalid={fieldState.invalid} onBlur={field.onBlur} defaultSelectedKey={dateValue?.day} @@ -141,6 +146,7 @@ export default function DateSelect({ name, registerOptions = {} }: DateProps) { name={DateName.month} onSelectionChange={(key) => setValue(DateName.month, Number(key))} isRequired + enableFiltering={isDesktop} isInvalid={fieldState.invalid} onBlur={field.onBlur} defaultSelectedKey={dateValue?.month} @@ -156,6 +162,7 @@ export default function DateSelect({ name, registerOptions = {} }: DateProps) { name={DateName.year} onSelectionChange={(key) => setValue(DateName.year, Number(key))} isRequired + enableFiltering={isDesktop} isInvalid={fieldState.invalid} onBlur={field.onBlur} defaultSelectedKey={dateValue?.year} diff --git a/packages/design-system/lib/components/Select/Select.tsx b/packages/design-system/lib/components/Select/Select.tsx index deb37342a..409b6d9de 100644 --- a/packages/design-system/lib/components/Select/Select.tsx +++ b/packages/design-system/lib/components/Select/Select.tsx @@ -28,7 +28,7 @@ export function Select({ }: SelectProps | SelectFilterProps) { const [isOpen, setIsOpen] = useState(false) - if ('enableFiltering' in props) { + if (props.enableFiltering) { return ( { name: string label: string onSelectionChange?: (key: Key | null) => void + enableFiltering?: false } export interface SelectItemProps extends ComponentProps { @@ -32,5 +33,5 @@ export interface SelectFilterProps extends ComponentProps { name: string label: string onSelectionChange?: (key: Key | null) => void - enableFiltering: boolean + enableFiltering: true }