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"
|
||||
import { useState } from "react"
|
||||
import {
|
||||
Button,
|
||||
ComboBox,
|
||||
Input,
|
||||
type Key,
|
||||
ListBox,
|
||||
ListBoxItem,
|
||||
Popover,
|
||||
} from "react-aria-components"
|
||||
|
||||
import { useMemo, useState } from "react"
|
||||
import { useFilter } from "react-aria-components"
|
||||
import { useController, useFormContext } from "react-hook-form"
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import { Select } from "@scandic-hotels/design-system/Select"
|
||||
|
||||
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 ErrorMessage from "../ErrorMessage"
|
||||
|
||||
import styles from "./country.module.css"
|
||||
|
||||
import type {
|
||||
CountryPortalContainer,
|
||||
CountryPortalContainerArgs,
|
||||
CountryProps,
|
||||
} from "./country"
|
||||
import type { CountryProps } from "./country"
|
||||
|
||||
export default function CountrySelect({
|
||||
className = "",
|
||||
@@ -38,104 +24,45 @@ export default function CountrySelect({
|
||||
}: CountryProps) {
|
||||
const lang = useLang()
|
||||
const intl = useIntl()
|
||||
const [rootDiv, setRootDiv] = useState<CountryPortalContainer>(undefined)
|
||||
|
||||
function setRef(node: CountryPortalContainerArgs) {
|
||||
if (node) {
|
||||
setRootDiv(node)
|
||||
}
|
||||
}
|
||||
const { control, setValue } = useFormContext()
|
||||
const { field, formState } = useController({
|
||||
const { startsWith } = useFilter({ sensitivity: "base" })
|
||||
const [filterValue, setFilterValue] = useState("")
|
||||
const { control } = useFormContext()
|
||||
const { field, formState, fieldState } = useController({
|
||||
control,
|
||||
name,
|
||||
rules: registerOptions,
|
||||
})
|
||||
|
||||
function handleChange(country: Key | null) {
|
||||
setValue(name, country ?? "")
|
||||
}
|
||||
|
||||
const selectCountryLabel = intl.formatMessage({
|
||||
defaultMessage: "Select a country",
|
||||
})
|
||||
const collator = new Intl.Collator(lang)
|
||||
const items = useMemo(() => {
|
||||
const collator = new Intl.Collator(lang)
|
||||
return countries
|
||||
.map((country) => ({
|
||||
value: country.code,
|
||||
label:
|
||||
intl.formatDisplayName(country.code, { type: "region" }) ||
|
||||
country.name,
|
||||
}))
|
||||
.filter((item) => startsWith(item.label, filterValue))
|
||||
.sort((a, b) => collator.compare(a.label, b.label))
|
||||
}, [filterValue, intl, lang, startsWith])
|
||||
|
||||
return (
|
||||
<div className={`${styles.container} ${className}`} ref={setRef}>
|
||||
<ComboBox
|
||||
aria-label={intl.formatMessage({
|
||||
defaultMessage: "Select country of residence",
|
||||
})}
|
||||
isReadOnly={readOnly}
|
||||
isRequired={!!registerOptions?.required}
|
||||
<div className={className}>
|
||||
<Select
|
||||
label={label}
|
||||
items={items}
|
||||
enableFiltering
|
||||
isRequired={Boolean(registerOptions?.required)}
|
||||
isInvalid={fieldState.invalid}
|
||||
name={field.name}
|
||||
onBlur={field.onBlur}
|
||||
onSelectionChange={handleChange}
|
||||
ref={field.ref}
|
||||
selectedKey={field.value}
|
||||
defaultSelectedKey={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) => ({
|
||||
...country,
|
||||
localizedDisplayName:
|
||||
intl.formatDisplayName(country.code, { type: "region" }) ||
|
||||
country.name,
|
||||
}))
|
||||
.sort((a, b) =>
|
||||
collator.compare(a.localizedDisplayName, b.localizedDisplayName)
|
||||
)
|
||||
.map((country, idx) => {
|
||||
return (
|
||||
<Body asChild fontOnly key={`${country.code}-${idx}`}>
|
||||
<ListBoxItem
|
||||
aria-label={country.name}
|
||||
className={styles.listBoxItem}
|
||||
id={country.code}
|
||||
>
|
||||
{country.localizedDisplayName}
|
||||
</ListBoxItem>
|
||||
</Body>
|
||||
)
|
||||
})}
|
||||
</ListBox>
|
||||
</Popover>
|
||||
</ComboBox>
|
||||
isReadOnly={readOnly}
|
||||
onInputChange={setFilterValue}
|
||||
/>
|
||||
<ErrorMessage errors={formState.errors} name={name} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ export default function DateSelect({ name, registerOptions = {} }: DateProps) {
|
||||
const lang = useLang()
|
||||
|
||||
const { control, setValue, formState, watch } = useFormContext()
|
||||
const { field } = useController({
|
||||
const { field, fieldState } = useController({
|
||||
control,
|
||||
name,
|
||||
rules: registerOptions,
|
||||
@@ -127,6 +127,8 @@ export default function DateSelect({ name, registerOptions = {} }: DateProps) {
|
||||
onSelectionChange={(key) => setValue(DateName.day, Number(key))}
|
||||
isRequired
|
||||
enableFiltering
|
||||
isInvalid={fieldState.invalid}
|
||||
onBlur={field.onBlur}
|
||||
defaultSelectedKey={dateValue?.day}
|
||||
/>
|
||||
</div>
|
||||
@@ -140,6 +142,8 @@ export default function DateSelect({ name, registerOptions = {} }: DateProps) {
|
||||
onSelectionChange={(key) => setValue(DateName.month, Number(key))}
|
||||
isRequired
|
||||
enableFiltering
|
||||
isInvalid={fieldState.invalid}
|
||||
onBlur={field.onBlur}
|
||||
defaultSelectedKey={dateValue?.month}
|
||||
/>
|
||||
</div>
|
||||
@@ -153,6 +157,8 @@ export default function DateSelect({ name, registerOptions = {} }: DateProps) {
|
||||
onSelectionChange={(key) => setValue(DateName.year, Number(key))}
|
||||
isRequired
|
||||
enableFiltering
|
||||
isInvalid={fieldState.invalid}
|
||||
onBlur={field.onBlur}
|
||||
defaultSelectedKey={dateValue?.year}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
.message {
|
||||
align-items: center;
|
||||
color: var(--Scandic-Red-60);
|
||||
color: var(--Text-Interactive-Error);
|
||||
display: flex;
|
||||
gap: var(--Spacing-x-half);
|
||||
margin: var(--Spacing-x1) 0 0;
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { ErrorMessage as RHFErrorMessage } from "@hookform/error-message"
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import { getErrorMessage } from "../Input/errors"
|
||||
import Error from "./Error"
|
||||
|
||||
import type { ErrorMessageProps } from "./errorMessage"
|
||||
@@ -8,11 +10,12 @@ export default function ErrorMessage<T>({
|
||||
errors,
|
||||
name,
|
||||
}: ErrorMessageProps<T>) {
|
||||
const intl = useIntl()
|
||||
return (
|
||||
<RHFErrorMessage
|
||||
errors={errors}
|
||||
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}
|
||||
{fieldState.error && !hideError ? (
|
||||
<Caption className={styles.error} fontOnly>
|
||||
<MaterialIcon icon="info" color="Icon/Interactive/Accent" />
|
||||
<MaterialIcon icon="info" color="Icon/Feedback/Error" />
|
||||
{getErrorMessage(intl, fieldState.error.message)}
|
||||
</Caption>
|
||||
) : null}
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
.error {
|
||||
align-items: center;
|
||||
color: var(--Scandic-Red-60);
|
||||
color: var(--Text-Interactive-Error);
|
||||
display: flex;
|
||||
gap: var(--Spacing-x-half);
|
||||
margin: var(--Spacing-x1) 0 0;
|
||||
|
||||
@@ -120,7 +120,6 @@ export const countriesMap = {
|
||||
"Korea, Republic of": "KR",
|
||||
Kuwait: "KW",
|
||||
Kyrgyzstan: "KG",
|
||||
'Lao People"S Democratic Republic': "LA",
|
||||
Laos: "LA",
|
||||
Latvia: "LV",
|
||||
Lebanon: "LB",
|
||||
@@ -241,7 +240,6 @@ export const countriesMap = {
|
||||
Uzbekistan: "UZ",
|
||||
Vanuatu: "VU",
|
||||
Venezuela: "VE",
|
||||
"Viet Nam": "VN",
|
||||
Vietnam: "VN",
|
||||
"Virgin Islands, British": "VG",
|
||||
"Virgin Islands, U.S.": "VI",
|
||||
|
||||
@@ -24,7 +24,9 @@ export function SelectFilter({
|
||||
icon,
|
||||
itemIcon,
|
||||
defaultSelectedKey,
|
||||
onSelectionChange,
|
||||
onSelectionChange = () => undefined,
|
||||
onFocus = () => undefined,
|
||||
onBlur = () => undefined,
|
||||
...props
|
||||
}: SelectFilterProps) {
|
||||
const [focus, setFocus] = useState(false)
|
||||
@@ -39,12 +41,16 @@ export function SelectFilter({
|
||||
isDisabled={isDisabled}
|
||||
onSelectionChange={(val) => {
|
||||
setValue(val)
|
||||
if (onSelectionChange) {
|
||||
onSelectionChange(val)
|
||||
}
|
||||
onSelectionChange(val)
|
||||
}}
|
||||
onFocus={(e) => {
|
||||
setFocus(true)
|
||||
onFocus(e)
|
||||
}}
|
||||
onBlur={(e) => {
|
||||
setFocus(false)
|
||||
onBlur(e)
|
||||
}}
|
||||
onFocus={() => setFocus(true)}
|
||||
onBlur={() => setFocus(false)}
|
||||
defaultSelectedKey={defaultSelectedKey}
|
||||
{...props}
|
||||
>
|
||||
|
||||
Reference in New Issue
Block a user