Merged in feat/LOY-169-use-new-country-select (pull request #1832)
feat(LOY-169): use new filter select for country select Approved-by: Erik Tiekstra
This commit is contained in:
@@ -1,80 +0,0 @@
|
|||||||
.container {
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.comboBoxContainer {
|
|
||||||
position: relative;
|
|
||||||
height: 60px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.label {
|
|
||||||
position: absolute;
|
|
||||||
left: var(--Spacing-x2);
|
|
||||||
top: var(--Spacing-x-one-and-half);
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.input {
|
|
||||||
background-color: var(--Main-Grey-White);
|
|
||||||
border-color: var(--Scandic-Beige-40);
|
|
||||||
border-radius: var(--Corner-radius-Medium);
|
|
||||||
border-style: solid;
|
|
||||||
border-width: 1px;
|
|
||||||
padding: var(--Spacing-x4) var(--Spacing-x2) var(--Spacing-x1);
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
|
|
||||||
&[aria-invalid="true"],
|
|
||||||
&[data-invalid="true"] {
|
|
||||||
border-color: var(--Scandic-Red-60);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.input,
|
|
||||||
.listBoxItem {
|
|
||||||
color: var(--Main-Grey-100);
|
|
||||||
}
|
|
||||||
|
|
||||||
.button {
|
|
||||||
background: none;
|
|
||||||
border: none;
|
|
||||||
cursor: pointer;
|
|
||||||
grid-area: chevron;
|
|
||||||
height: 100%;
|
|
||||||
justify-self: flex-end;
|
|
||||||
padding-left: var(--Spacing-x2);
|
|
||||||
padding-right: var(--Spacing-x2);
|
|
||||||
position: absolute;
|
|
||||||
right: 0;
|
|
||||||
bottom: 0;
|
|
||||||
outline: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.popover {
|
|
||||||
background-color: var(--Main-Grey-White);
|
|
||||||
border-color: var(--Scandic-Beige-40);
|
|
||||||
border-style: solid;
|
|
||||||
border-width: 1px;
|
|
||||||
border-radius: var(--Corner-radius-Medium);
|
|
||||||
left: 0px;
|
|
||||||
max-height: 400px;
|
|
||||||
overflow: auto;
|
|
||||||
padding: var(--Spacing-x2);
|
|
||||||
top: calc(60px + var(--Spacing-x1));
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.listBoxItem {
|
|
||||||
padding: var(--Spacing-x1) var(--Spacing-x1) var(--Spacing-x1)
|
|
||||||
var(--Spacing-x2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.listBoxItem[data-focused="true"],
|
|
||||||
.listBoxItem[data-focus-visible="true"],
|
|
||||||
.listBoxItem[data-selected="true"],
|
|
||||||
.listBoxItem:hover {
|
|
||||||
background-color: var(--Scandic-Blue-00);
|
|
||||||
border-radius: var(--Corner-radius-Medium);
|
|
||||||
cursor: pointer;
|
|
||||||
outline: none;
|
|
||||||
}
|
|
||||||
@@ -1,33 +1,19 @@
|
|||||||
"use client"
|
"use client"
|
||||||
import { useState } from "react"
|
|
||||||
import {
|
import { useMemo, useState } from "react"
|
||||||
Button,
|
import { useFilter } from "react-aria-components"
|
||||||
ComboBox,
|
|
||||||
Input,
|
|
||||||
type Key,
|
|
||||||
ListBox,
|
|
||||||
ListBoxItem,
|
|
||||||
Popover,
|
|
||||||
} from "react-aria-components"
|
|
||||||
import { useController, useFormContext } from "react-hook-form"
|
import { useController, useFormContext } from "react-hook-form"
|
||||||
import { useIntl } from "react-intl"
|
import { useIntl } from "react-intl"
|
||||||
|
|
||||||
|
import { Select } from "@scandic-hotels/design-system/Select"
|
||||||
|
|
||||||
import { countries } from "@/constants/countries"
|
import { countries } from "@/constants/countries"
|
||||||
|
|
||||||
import Label from "@/components/TempDesignSystem/Form/Label"
|
|
||||||
import SelectChevron from "@/components/TempDesignSystem/Form/SelectChevron"
|
|
||||||
import Body from "@/components/TempDesignSystem/Text/Body"
|
|
||||||
import useLang from "@/hooks/useLang"
|
import useLang from "@/hooks/useLang"
|
||||||
|
|
||||||
import ErrorMessage from "../ErrorMessage"
|
import ErrorMessage from "../ErrorMessage"
|
||||||
|
|
||||||
import styles from "./country.module.css"
|
import type { CountryProps } from "./country"
|
||||||
|
|
||||||
import type {
|
|
||||||
CountryPortalContainer,
|
|
||||||
CountryPortalContainerArgs,
|
|
||||||
CountryProps,
|
|
||||||
} from "./country"
|
|
||||||
|
|
||||||
export default function CountrySelect({
|
export default function CountrySelect({
|
||||||
className = "",
|
className = "",
|
||||||
@@ -38,104 +24,45 @@ export default function CountrySelect({
|
|||||||
}: CountryProps) {
|
}: CountryProps) {
|
||||||
const lang = useLang()
|
const lang = useLang()
|
||||||
const intl = useIntl()
|
const intl = useIntl()
|
||||||
const [rootDiv, setRootDiv] = useState<CountryPortalContainer>(undefined)
|
|
||||||
|
|
||||||
function setRef(node: CountryPortalContainerArgs) {
|
const { startsWith } = useFilter({ sensitivity: "base" })
|
||||||
if (node) {
|
const [filterValue, setFilterValue] = useState("")
|
||||||
setRootDiv(node)
|
const { control } = useFormContext()
|
||||||
}
|
const { field, formState, fieldState } = useController({
|
||||||
}
|
|
||||||
const { control, setValue } = useFormContext()
|
|
||||||
const { field, formState } = useController({
|
|
||||||
control,
|
control,
|
||||||
name,
|
name,
|
||||||
rules: registerOptions,
|
rules: registerOptions,
|
||||||
})
|
})
|
||||||
|
|
||||||
function handleChange(country: Key | null) {
|
const items = useMemo(() => {
|
||||||
setValue(name, country ?? "")
|
|
||||||
}
|
|
||||||
|
|
||||||
const selectCountryLabel = intl.formatMessage({
|
|
||||||
defaultMessage: "Select a country",
|
|
||||||
})
|
|
||||||
const collator = new Intl.Collator(lang)
|
const collator = new Intl.Collator(lang)
|
||||||
|
return countries
|
||||||
return (
|
|
||||||
<div className={`${styles.container} ${className}`} ref={setRef}>
|
|
||||||
<ComboBox
|
|
||||||
aria-label={intl.formatMessage({
|
|
||||||
defaultMessage: "Select country of residence",
|
|
||||||
})}
|
|
||||||
isReadOnly={readOnly}
|
|
||||||
isRequired={!!registerOptions?.required}
|
|
||||||
name={field.name}
|
|
||||||
onBlur={field.onBlur}
|
|
||||||
onSelectionChange={handleChange}
|
|
||||||
ref={field.ref}
|
|
||||||
selectedKey={field.value}
|
|
||||||
data-testid={name}
|
|
||||||
>
|
|
||||||
<div className={styles.comboBoxContainer}>
|
|
||||||
<Label
|
|
||||||
className={styles.label}
|
|
||||||
size="small"
|
|
||||||
required={!!registerOptions.required}
|
|
||||||
>
|
|
||||||
{label}
|
|
||||||
</Label>
|
|
||||||
<Body asChild fontOnly>
|
|
||||||
<Input
|
|
||||||
aria-label={selectCountryLabel}
|
|
||||||
className={styles.input}
|
|
||||||
placeholder={selectCountryLabel}
|
|
||||||
/>
|
|
||||||
</Body>
|
|
||||||
<Button className={styles.button}>
|
|
||||||
<SelectChevron />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<ErrorMessage errors={formState.errors} name={name} />
|
|
||||||
<Popover
|
|
||||||
className={styles.popover}
|
|
||||||
placement="bottom"
|
|
||||||
shouldFlip={false}
|
|
||||||
shouldUpdatePosition={false}
|
|
||||||
/**
|
|
||||||
* react-aria uses portals to render Popover in body
|
|
||||||
* unless otherwise specified. We need it to be contained
|
|
||||||
* by this component to both access css variables assigned
|
|
||||||
* on the container as well as to not overflow it at any time.
|
|
||||||
*/
|
|
||||||
UNSTABLE_portalContainer={rootDiv}
|
|
||||||
>
|
|
||||||
<ListBox>
|
|
||||||
{countries
|
|
||||||
.map((country) => ({
|
.map((country) => ({
|
||||||
...country,
|
value: country.code,
|
||||||
localizedDisplayName:
|
label:
|
||||||
intl.formatDisplayName(country.code, { type: "region" }) ||
|
intl.formatDisplayName(country.code, { type: "region" }) ||
|
||||||
country.name,
|
country.name,
|
||||||
}))
|
}))
|
||||||
.sort((a, b) =>
|
.filter((item) => startsWith(item.label, filterValue))
|
||||||
collator.compare(a.localizedDisplayName, b.localizedDisplayName)
|
.sort((a, b) => collator.compare(a.label, b.label))
|
||||||
)
|
}, [filterValue, intl, lang, startsWith])
|
||||||
.map((country, idx) => {
|
|
||||||
return (
|
return (
|
||||||
<Body asChild fontOnly key={`${country.code}-${idx}`}>
|
<div className={className}>
|
||||||
<ListBoxItem
|
<Select
|
||||||
aria-label={country.name}
|
label={label}
|
||||||
className={styles.listBoxItem}
|
items={items}
|
||||||
id={country.code}
|
enableFiltering
|
||||||
>
|
isRequired={Boolean(registerOptions?.required)}
|
||||||
{country.localizedDisplayName}
|
isInvalid={fieldState.invalid}
|
||||||
</ListBoxItem>
|
name={field.name}
|
||||||
</Body>
|
onBlur={field.onBlur}
|
||||||
)
|
defaultSelectedKey={field.value}
|
||||||
})}
|
data-testid={name}
|
||||||
</ListBox>
|
isReadOnly={readOnly}
|
||||||
</Popover>
|
onInputChange={setFilterValue}
|
||||||
</ComboBox>
|
/>
|
||||||
|
<ErrorMessage errors={formState.errors} name={name} />
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ export default function DateSelect({ name, registerOptions = {} }: DateProps) {
|
|||||||
const lang = useLang()
|
const lang = useLang()
|
||||||
|
|
||||||
const { control, setValue, formState, watch } = useFormContext()
|
const { control, setValue, formState, watch } = useFormContext()
|
||||||
const { field } = useController({
|
const { field, fieldState } = useController({
|
||||||
control,
|
control,
|
||||||
name,
|
name,
|
||||||
rules: registerOptions,
|
rules: registerOptions,
|
||||||
@@ -127,6 +127,8 @@ export default function DateSelect({ name, registerOptions = {} }: DateProps) {
|
|||||||
onSelectionChange={(key) => setValue(DateName.day, Number(key))}
|
onSelectionChange={(key) => setValue(DateName.day, Number(key))}
|
||||||
isRequired
|
isRequired
|
||||||
enableFiltering
|
enableFiltering
|
||||||
|
isInvalid={fieldState.invalid}
|
||||||
|
onBlur={field.onBlur}
|
||||||
defaultSelectedKey={dateValue?.day}
|
defaultSelectedKey={dateValue?.day}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -140,6 +142,8 @@ export default function DateSelect({ name, registerOptions = {} }: DateProps) {
|
|||||||
onSelectionChange={(key) => setValue(DateName.month, Number(key))}
|
onSelectionChange={(key) => setValue(DateName.month, Number(key))}
|
||||||
isRequired
|
isRequired
|
||||||
enableFiltering
|
enableFiltering
|
||||||
|
isInvalid={fieldState.invalid}
|
||||||
|
onBlur={field.onBlur}
|
||||||
defaultSelectedKey={dateValue?.month}
|
defaultSelectedKey={dateValue?.month}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -153,6 +157,8 @@ export default function DateSelect({ name, registerOptions = {} }: DateProps) {
|
|||||||
onSelectionChange={(key) => setValue(DateName.year, Number(key))}
|
onSelectionChange={(key) => setValue(DateName.year, Number(key))}
|
||||||
isRequired
|
isRequired
|
||||||
enableFiltering
|
enableFiltering
|
||||||
|
isInvalid={fieldState.invalid}
|
||||||
|
onBlur={field.onBlur}
|
||||||
defaultSelectedKey={dateValue?.year}
|
defaultSelectedKey={dateValue?.year}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
.message {
|
.message {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
color: var(--Scandic-Red-60);
|
color: var(--Text-Interactive-Error);
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: var(--Spacing-x-half);
|
gap: var(--Spacing-x-half);
|
||||||
margin: var(--Spacing-x1) 0 0;
|
margin: var(--Spacing-x1) 0 0;
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import { ErrorMessage as RHFErrorMessage } from "@hookform/error-message"
|
import { ErrorMessage as RHFErrorMessage } from "@hookform/error-message"
|
||||||
|
import { useIntl } from "react-intl"
|
||||||
|
|
||||||
|
import { getErrorMessage } from "../Input/errors"
|
||||||
import Error from "./Error"
|
import Error from "./Error"
|
||||||
|
|
||||||
import type { ErrorMessageProps } from "./errorMessage"
|
import type { ErrorMessageProps } from "./errorMessage"
|
||||||
@@ -8,11 +10,12 @@ export default function ErrorMessage<T>({
|
|||||||
errors,
|
errors,
|
||||||
name,
|
name,
|
||||||
}: ErrorMessageProps<T>) {
|
}: ErrorMessageProps<T>) {
|
||||||
|
const intl = useIntl()
|
||||||
return (
|
return (
|
||||||
<RHFErrorMessage
|
<RHFErrorMessage
|
||||||
errors={errors}
|
errors={errors}
|
||||||
name={name}
|
name={name}
|
||||||
render={({ message }) => <Error>{message}</Error>}
|
render={({ message }) => <Error>{getErrorMessage(intl, message)}</Error>}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -83,7 +83,7 @@ const Input = forwardRef<HTMLInputElement, InputProps>(function Input(
|
|||||||
) : null}
|
) : null}
|
||||||
{fieldState.error && !hideError ? (
|
{fieldState.error && !hideError ? (
|
||||||
<Caption className={styles.error} fontOnly>
|
<Caption className={styles.error} fontOnly>
|
||||||
<MaterialIcon icon="info" color="Icon/Interactive/Accent" />
|
<MaterialIcon icon="info" color="Icon/Feedback/Error" />
|
||||||
{getErrorMessage(intl, fieldState.error.message)}
|
{getErrorMessage(intl, fieldState.error.message)}
|
||||||
</Caption>
|
</Caption>
|
||||||
) : null}
|
) : null}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
|
|
||||||
.error {
|
.error {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
color: var(--Scandic-Red-60);
|
color: var(--Text-Interactive-Error);
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: var(--Spacing-x-half);
|
gap: var(--Spacing-x-half);
|
||||||
margin: var(--Spacing-x1) 0 0;
|
margin: var(--Spacing-x1) 0 0;
|
||||||
|
|||||||
@@ -120,7 +120,6 @@ export const countriesMap = {
|
|||||||
"Korea, Republic of": "KR",
|
"Korea, Republic of": "KR",
|
||||||
Kuwait: "KW",
|
Kuwait: "KW",
|
||||||
Kyrgyzstan: "KG",
|
Kyrgyzstan: "KG",
|
||||||
'Lao People"S Democratic Republic': "LA",
|
|
||||||
Laos: "LA",
|
Laos: "LA",
|
||||||
Latvia: "LV",
|
Latvia: "LV",
|
||||||
Lebanon: "LB",
|
Lebanon: "LB",
|
||||||
@@ -241,7 +240,6 @@ export const countriesMap = {
|
|||||||
Uzbekistan: "UZ",
|
Uzbekistan: "UZ",
|
||||||
Vanuatu: "VU",
|
Vanuatu: "VU",
|
||||||
Venezuela: "VE",
|
Venezuela: "VE",
|
||||||
"Viet Nam": "VN",
|
|
||||||
Vietnam: "VN",
|
Vietnam: "VN",
|
||||||
"Virgin Islands, British": "VG",
|
"Virgin Islands, British": "VG",
|
||||||
"Virgin Islands, U.S.": "VI",
|
"Virgin Islands, U.S.": "VI",
|
||||||
|
|||||||
@@ -24,7 +24,9 @@ export function SelectFilter({
|
|||||||
icon,
|
icon,
|
||||||
itemIcon,
|
itemIcon,
|
||||||
defaultSelectedKey,
|
defaultSelectedKey,
|
||||||
onSelectionChange,
|
onSelectionChange = () => undefined,
|
||||||
|
onFocus = () => undefined,
|
||||||
|
onBlur = () => undefined,
|
||||||
...props
|
...props
|
||||||
}: SelectFilterProps) {
|
}: SelectFilterProps) {
|
||||||
const [focus, setFocus] = useState(false)
|
const [focus, setFocus] = useState(false)
|
||||||
@@ -39,12 +41,16 @@ export function SelectFilter({
|
|||||||
isDisabled={isDisabled}
|
isDisabled={isDisabled}
|
||||||
onSelectionChange={(val) => {
|
onSelectionChange={(val) => {
|
||||||
setValue(val)
|
setValue(val)
|
||||||
if (onSelectionChange) {
|
|
||||||
onSelectionChange(val)
|
onSelectionChange(val)
|
||||||
}
|
|
||||||
}}
|
}}
|
||||||
onFocus={() => setFocus(true)}
|
onFocus={(e) => {
|
||||||
onBlur={() => setFocus(false)}
|
setFocus(true)
|
||||||
|
onFocus(e)
|
||||||
|
}}
|
||||||
|
onBlur={(e) => {
|
||||||
|
setFocus(false)
|
||||||
|
onBlur(e)
|
||||||
|
}}
|
||||||
defaultSelectedKey={defaultSelectedKey}
|
defaultSelectedKey={defaultSelectedKey}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
|
|||||||
Reference in New Issue
Block a user