feat: guest information form enter details
This commit is contained in:
@@ -10,6 +10,7 @@ import type { ClearSearchButtonProps } from "@/types/components/search"
|
||||
|
||||
export default function ClearSearchButton({
|
||||
getItemProps,
|
||||
handleClearSearchHistory,
|
||||
highlightedIndex,
|
||||
index,
|
||||
}: ClearSearchButtonProps) {
|
||||
@@ -18,13 +19,6 @@ export default function ClearSearchButton({
|
||||
variant: index === highlightedIndex ? "active" : "default",
|
||||
})
|
||||
|
||||
function handleClick() {
|
||||
// noop
|
||||
// the click bubbles to handleOnSelect
|
||||
// where selectedItem = "clear-search"
|
||||
// which is the value for item below
|
||||
}
|
||||
|
||||
return (
|
||||
<button
|
||||
{...getItemProps({
|
||||
@@ -34,7 +28,7 @@ export default function ClearSearchButton({
|
||||
item: "clear-search",
|
||||
role: "button",
|
||||
})}
|
||||
onClick={handleClick}
|
||||
onClick={handleClearSearchHistory}
|
||||
tabIndex={0}
|
||||
type="button"
|
||||
>
|
||||
|
||||
@@ -20,6 +20,7 @@ import type { SearchListProps } from "@/types/components/search"
|
||||
export default function SearchList({
|
||||
getItemProps,
|
||||
getMenuProps,
|
||||
handleClearSearchHistory,
|
||||
highlightedIndex,
|
||||
isOpen,
|
||||
locations,
|
||||
@@ -125,6 +126,7 @@ export default function SearchList({
|
||||
<Divider className={styles.divider} color="beige" />
|
||||
<ClearSearchButton
|
||||
getItemProps={getItemProps}
|
||||
handleClearSearchHistory={handleClearSearchHistory}
|
||||
highlightedIndex={highlightedIndex}
|
||||
index={searchHistory.length}
|
||||
/>
|
||||
@@ -161,6 +163,7 @@ export default function SearchList({
|
||||
<Divider className={styles.divider} color="beige" />
|
||||
<ClearSearchButton
|
||||
getItemProps={getItemProps}
|
||||
handleClearSearchHistory={handleClearSearchHistory}
|
||||
highlightedIndex={highlightedIndex}
|
||||
index={searchHistory.length}
|
||||
/>
|
||||
|
||||
@@ -43,6 +43,11 @@ export default function Search({ locations }: SearchProps) {
|
||||
[locations]
|
||||
)
|
||||
|
||||
function handleClearSearchHistory() {
|
||||
localStorage.removeItem(localStorageKey)
|
||||
dispatch({ type: ActionType.CLEAR_HISTORY_LOCATIONS })
|
||||
}
|
||||
|
||||
function handleOnBlur() {
|
||||
if (!value && state.searchData?.name) {
|
||||
setValue(name, state.searchData.name)
|
||||
@@ -79,11 +84,8 @@ export default function Search({ locations }: SearchProps) {
|
||||
}
|
||||
}
|
||||
|
||||
function handleOnSelect(selectedItem: Location | null | "clear-search") {
|
||||
if (selectedItem === "clear-search") {
|
||||
localStorage.removeItem(localStorageKey)
|
||||
dispatch({ type: ActionType.CLEAR_HISTORY_LOCATIONS })
|
||||
} else if (selectedItem) {
|
||||
function handleOnSelect(selectedItem: Location | null) {
|
||||
if (selectedItem) {
|
||||
const stringified = JSON.stringify(selectedItem)
|
||||
setValue("location", encodeURIComponent(stringified))
|
||||
sessionStorage.setItem(sessionStorageKey, stringified)
|
||||
@@ -167,6 +169,7 @@ export default function Search({ locations }: SearchProps) {
|
||||
<SearchList
|
||||
getItemProps={getItemProps}
|
||||
getMenuProps={getMenuProps}
|
||||
handleClearSearchHistory={handleClearSearchHistory}
|
||||
highlightedIndex={highlightedIndex}
|
||||
isOpen={isOpen}
|
||||
locations={state.locations}
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
.container {
|
||||
display: grid;
|
||||
gap: var(--Spacing-x2);
|
||||
padding: var(--Spacing-x3) 0px;
|
||||
}
|
||||
|
||||
.form {
|
||||
display: grid;
|
||||
gap: var(--Spacing-x2);
|
||||
grid-template-columns: 1fr 1fr;
|
||||
width: min(100%, 600px);
|
||||
}
|
||||
|
||||
.country,
|
||||
.email,
|
||||
.phone {
|
||||
grid-column: 1/-1;
|
||||
}
|
||||
|
||||
.footer {
|
||||
display: grid;
|
||||
gap: var(--Spacing-x3);
|
||||
justify-items: flex-start;
|
||||
margin-top: var(--Spacing-x1);
|
||||
}
|
||||
117
components/HotelReservation/EnterDetails/Details/index.tsx
Normal file
117
components/HotelReservation/EnterDetails/Details/index.tsx
Normal file
@@ -0,0 +1,117 @@
|
||||
"use client"
|
||||
import { zodResolver } from "@hookform/resolvers/zod"
|
||||
import { FormProvider, useForm } from "react-hook-form"
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import Button from "@/components/TempDesignSystem/Button"
|
||||
import CheckboxCard from "@/components/TempDesignSystem/Form/Checkbox/Card"
|
||||
import CountrySelect from "@/components/TempDesignSystem/Form/Country"
|
||||
import Input from "@/components/TempDesignSystem/Form/Input"
|
||||
import Phone from "@/components/TempDesignSystem/Form/Phone"
|
||||
import Body from "@/components/TempDesignSystem/Text/Body"
|
||||
|
||||
import { detailsSchema, signedInDetailsSchema } from "./schema"
|
||||
|
||||
import styles from "./details.module.css"
|
||||
|
||||
import type {
|
||||
DetailsProps,
|
||||
DetailsSchema,
|
||||
} from "@/types/components/enterDetails/details"
|
||||
|
||||
export default function Details({ user }: DetailsProps) {
|
||||
const intl = useIntl()
|
||||
|
||||
const list = [
|
||||
{ title: intl.formatMessage({ id: "Earn bonus nights & points" }) },
|
||||
{ title: intl.formatMessage({ id: "Get member benefits & offers" }) },
|
||||
{ title: intl.formatMessage({ id: "Join at no cost" }) },
|
||||
]
|
||||
|
||||
const methods = useForm<DetailsSchema>({
|
||||
defaultValues: {
|
||||
countryCode: user?.address?.countryCode ?? "",
|
||||
email: user?.email ?? "",
|
||||
firstname: user?.firstName ?? "",
|
||||
lastname: user?.lastName ?? "",
|
||||
phoneNumber: user?.phoneNumber ?? "",
|
||||
},
|
||||
mode: "all",
|
||||
resolver: zodResolver(user ? signedInDetailsSchema : detailsSchema),
|
||||
reValidateMode: "onChange",
|
||||
})
|
||||
|
||||
return (
|
||||
<FormProvider {...methods}>
|
||||
<section className={styles.container}>
|
||||
<header>
|
||||
<Body color="uiTextHighContrast" textTransform="bold">
|
||||
{intl.formatMessage({ id: "Guest information" })}
|
||||
</Body>
|
||||
</header>
|
||||
<form className={styles.form}>
|
||||
<Input
|
||||
label={intl.formatMessage({ id: "Firstname" })}
|
||||
name="firstname"
|
||||
readOnly={!!user}
|
||||
registerOptions={{ required: true }}
|
||||
/>
|
||||
<Input
|
||||
label={intl.formatMessage({ id: "Lastname" })}
|
||||
name="lastname"
|
||||
readOnly={!!user}
|
||||
registerOptions={{ required: true }}
|
||||
/>
|
||||
<CountrySelect
|
||||
className={styles.country}
|
||||
label={intl.formatMessage({ id: "Country" })}
|
||||
name="countryCode"
|
||||
readOnly={!!user}
|
||||
registerOptions={{ required: true }}
|
||||
/>
|
||||
<Input
|
||||
className={styles.email}
|
||||
label={intl.formatMessage({ id: "Email address" })}
|
||||
name="email"
|
||||
readOnly={!!user}
|
||||
registerOptions={{ required: true }}
|
||||
/>
|
||||
<Phone
|
||||
className={styles.phone}
|
||||
label={intl.formatMessage({ id: "Phone number" })}
|
||||
name="phoneNumber"
|
||||
readOnly={!!user}
|
||||
registerOptions={{ required: true }}
|
||||
/>
|
||||
</form>
|
||||
<footer className={styles.footer}>
|
||||
{user ? null : (
|
||||
<CheckboxCard
|
||||
list={list}
|
||||
saving
|
||||
subtitle={intl.formatMessage(
|
||||
{
|
||||
id: "{difference}{amount} {currency}",
|
||||
},
|
||||
{
|
||||
amount: "491",
|
||||
currency: "SEK",
|
||||
difference: "-",
|
||||
}
|
||||
)}
|
||||
title={intl.formatMessage({ id: "Join Scandic Friends" })}
|
||||
/>
|
||||
)}
|
||||
<Button
|
||||
disabled={!methods.formState.isValid}
|
||||
intent="secondary"
|
||||
size="small"
|
||||
theme="base"
|
||||
>
|
||||
{intl.formatMessage({ id: "Proceed to payment method" })}
|
||||
</Button>
|
||||
</footer>
|
||||
</section>
|
||||
</FormProvider>
|
||||
)
|
||||
}
|
||||
19
components/HotelReservation/EnterDetails/Details/schema.ts
Normal file
19
components/HotelReservation/EnterDetails/Details/schema.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { z } from "zod"
|
||||
|
||||
import { phoneValidator } from "@/utils/phoneValidator"
|
||||
|
||||
export const detailsSchema = z.object({
|
||||
countryCode: z.string(),
|
||||
email: z.string().email(),
|
||||
firstname: z.string(),
|
||||
lastname: z.string(),
|
||||
phoneNumber: phoneValidator(),
|
||||
})
|
||||
|
||||
export const signedInDetailsSchema = z.object({
|
||||
countryCode: z.string().optional(),
|
||||
email: z.string().email().optional(),
|
||||
firstname: z.string().optional(),
|
||||
lastname: z.string().optional(),
|
||||
phoneNumber: phoneValidator().optional(),
|
||||
})
|
||||
@@ -48,10 +48,10 @@ export default async function HotelCard({ hotel }: HotelCardProps) {
|
||||
<Title as="h4" textTransform="capitalize">
|
||||
{hotelData.name}
|
||||
</Title>
|
||||
<Footnote color="textMediumContrast">
|
||||
<Footnote color="uiTextMediumContrast">
|
||||
{`${hotelData.address.streetAddress}, ${hotelData.address.city}`}
|
||||
</Footnote>
|
||||
<Footnote color="textMediumContrast">
|
||||
<Footnote color="uiTextMediumContrast">
|
||||
{`${hotelData.location.distanceToCentre} ${intl.formatMessage({ id: "km to city center" })}`}
|
||||
</Footnote>
|
||||
</section>
|
||||
@@ -79,7 +79,7 @@ export default async function HotelCard({ hotel }: HotelCardProps) {
|
||||
{price?.regularAmount} {price?.currency} /
|
||||
{intl.formatMessage({ id: "night" })}
|
||||
</Caption>
|
||||
<Footnote color="textMediumContrast">approx 280 eur</Footnote>
|
||||
<Footnote color="uiTextMediumContrast">approx 280 eur</Footnote>
|
||||
</div>
|
||||
<div>
|
||||
<Chip intent="primary" className={styles.member}>
|
||||
@@ -90,7 +90,7 @@ export default async function HotelCard({ hotel }: HotelCardProps) {
|
||||
{price?.memberAmount} {price?.currency} /
|
||||
{intl.formatMessage({ id: "night" })}
|
||||
</Caption>
|
||||
<Footnote color="textMediumContrast">approx 280 eur</Footnote>
|
||||
<Footnote color="uiTextMediumContrast">approx 280 eur</Footnote>
|
||||
</div>
|
||||
<Button
|
||||
asChild
|
||||
|
||||
36
components/Icons/Heart.tsx
Normal file
36
components/Icons/Heart.tsx
Normal file
@@ -0,0 +1,36 @@
|
||||
import { iconVariants } from "./variants"
|
||||
|
||||
import type { IconProps } from "@/types/components/icon"
|
||||
|
||||
export default function HeartIcon({ className, color, ...props }: IconProps) {
|
||||
const classNames = iconVariants({ className, color })
|
||||
return (
|
||||
<svg
|
||||
className={classNames}
|
||||
fill="none"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
width="24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
{...props}
|
||||
>
|
||||
<mask
|
||||
height="24"
|
||||
id="mask0_69_3298"
|
||||
maskUnits="userSpaceOnUse"
|
||||
style={{ maskType: "alpha" }}
|
||||
width="24"
|
||||
x="0"
|
||||
y="0"
|
||||
>
|
||||
<rect width="24" height="24" fill="#D9D9D9" />
|
||||
</mask>
|
||||
<g mask="url(#mask0_69_3298)">
|
||||
<path
|
||||
d="M12 20.0875C11.775 20.0875 11.5521 20.0479 11.3313 19.9687C11.1104 19.8896 10.9125 19.7666 10.7375 19.6L9.1625 18.1625C7.3625 16.5208 5.76042 14.9125 4.35625 13.3375C2.95208 11.7625 2.25 10.0416 2.25 8.17498C2.25 6.65816 2.75865 5.39145 3.77595 4.37485C4.79327 3.35827 6.06087 2.84998 7.57875 2.84998C8.43458 2.84998 9.24792 3.03539 10.0188 3.40623C10.7896 3.77706 11.45 4.29998 12 4.97498C12.5667 4.29998 13.231 3.77706 13.993 3.40623C14.755 3.03539 15.5657 2.84998 16.425 2.84998C17.9418 2.84998 19.2085 3.35827 20.2251 4.37485C21.2417 5.39145 21.75 6.65816 21.75 8.17498C21.75 10.0416 21.05 11.7646 19.65 13.3437C18.25 14.9229 16.6458 16.5291 14.8375 18.1625L13.2625 19.6C13.0875 19.7666 12.8896 19.8896 12.6687 19.9687C12.4479 20.0479 12.225 20.0875 12 20.0875ZM11.107 6.89753C10.6773 6.20749 10.1729 5.67289 9.59375 5.29373C9.01458 4.91456 8.33962 4.72498 7.56885 4.72498C6.5849 4.72498 5.76493 5.05206 5.10895 5.70623C4.45298 6.36039 4.125 7.18202 4.125 8.1711C4.125 9.0283 4.42917 9.93908 5.0375 10.9035C5.64583 11.8678 6.37083 12.8041 7.2125 13.7125C8.05417 14.6208 8.92083 15.4687 9.8125 16.2562C10.7042 17.0437 11.4333 17.6916 12 18.2C12.5667 17.6916 13.2958 17.0437 14.1875 16.2562C15.0792 15.4687 15.9458 14.6208 16.7875 13.7125C17.6292 12.8041 18.3542 11.8678 18.9625 10.9035C19.5708 9.93908 19.875 9.0283 19.875 8.1711C19.875 7.18202 19.547 6.36039 18.8911 5.70623C18.2351 5.05206 17.4151 4.72498 16.4311 4.72498C15.6604 4.72498 14.9833 4.91456 14.4 5.29373C13.8167 5.67289 13.3102 6.20749 12.8805 6.89753C12.7768 7.06583 12.6466 7.18956 12.4899 7.26873C12.3331 7.34789 12.1685 7.38748 11.9961 7.38748C11.8237 7.38748 11.6583 7.34789 11.5 7.26873C11.3417 7.18956 11.2107 7.06583 11.107 6.89753Z"
|
||||
fill="#26201E"
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
@@ -2,6 +2,11 @@
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.baseIconLowContrast,
|
||||
.baseIconLowContrast * {
|
||||
fill: var(--Base-Icon-Low-contrast);
|
||||
}
|
||||
|
||||
.black,
|
||||
.black * {
|
||||
fill: #000;
|
||||
@@ -46,3 +51,13 @@
|
||||
.white * {
|
||||
fill: var(--UI-Opacity-White-100);
|
||||
}
|
||||
|
||||
.uiTextHighContrast,
|
||||
.uiTextHighContrast * {
|
||||
fill: var(--UI-Text-High-contrast);
|
||||
}
|
||||
|
||||
.uiTextMediumContrast,
|
||||
.uiTextMediumContrast * {
|
||||
fill: var(--UI-Text-Medium-contrast);
|
||||
}
|
||||
|
||||
@@ -30,6 +30,7 @@ export { default as ErrorCircleIcon } from "./ErrorCircle"
|
||||
export { default as FitnessIcon } from "./Fitness"
|
||||
export { default as GiftIcon } from "./Gift"
|
||||
export { default as GlobeIcon } from "./Globe"
|
||||
export { default as HeartIcon } from "./Heart"
|
||||
export { default as HouseIcon } from "./House"
|
||||
export { default as ImageIcon } from "./Image"
|
||||
export { default as InfoCircleIcon } from "./InfoCircle"
|
||||
|
||||
@@ -5,6 +5,7 @@ import styles from "./icon.module.css"
|
||||
const config = {
|
||||
variants: {
|
||||
color: {
|
||||
baseIconLowContrast: styles.baseIconLowContrast,
|
||||
black: styles.black,
|
||||
burgundy: styles.burgundy,
|
||||
grey80: styles.grey80,
|
||||
@@ -14,6 +15,8 @@ const config = {
|
||||
red: styles.red,
|
||||
green: styles.green,
|
||||
white: styles.white,
|
||||
uiTextHighContrast: styles.uiTextHighContrast,
|
||||
uiTextMediumContrast: styles.uiTextMediumContrast,
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
.checkbox {
|
||||
background-color: var(--Base-Surface-Primary-light-Normal);
|
||||
border: 1px solid var(--Base-Border-Subtle);
|
||||
border-radius: var(--Corner-radius-Large);
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--Spacing-x1);
|
||||
padding: var(--Spacing-x-one-and-half) var(--Spacing-x2);
|
||||
transition: all 200ms ease;
|
||||
width: min(100%, 600px);
|
||||
}
|
||||
|
||||
.checkbox:hover {
|
||||
background-color: var(--Base-Surface-Secondary-light-Hover);
|
||||
}
|
||||
|
||||
.checkbox:has(:checked) {
|
||||
background-color: var(--Primary-Light-Surface-Normal);
|
||||
border-color: var(--Base-Border-Hover);
|
||||
}
|
||||
|
||||
.header {
|
||||
align-items: center;
|
||||
display: grid;
|
||||
gap: 0px var(--Spacing-x1);
|
||||
grid-template-areas:
|
||||
"title icon"
|
||||
"subtitle icon";
|
||||
}
|
||||
|
||||
.icon {
|
||||
grid-area: icon;
|
||||
justify-self: flex-end;
|
||||
transition: fill 200ms ease;
|
||||
}
|
||||
|
||||
.checkbox:hover .icon,
|
||||
.checkbox:hover .icon *,
|
||||
.checkbox:has(:checked) .icon,
|
||||
.checkbox:has(:checked) .icon * {
|
||||
fill: var(--Base-Text-Medium-contrast);
|
||||
}
|
||||
|
||||
.checkbox[data-declined="true"]:hover .icon,
|
||||
.checkbox[data-declined="true"]:hover .icon *,
|
||||
.checkbox[data-declined="true"]:has(:checked) .icon,
|
||||
.checkbox[data-declined="true"]:has(:checked) .icon * {
|
||||
fill: var(--Base-Text-Disabled);
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
grid-area: subtitle;
|
||||
}
|
||||
|
||||
.title {
|
||||
grid-area: title;
|
||||
}
|
||||
|
||||
.list {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.listItem {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
gap: var(--Spacing-x-quarter);
|
||||
}
|
||||
14
components/TempDesignSystem/Form/Checkbox/Card/card.ts
Normal file
14
components/TempDesignSystem/Form/Checkbox/Card/card.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import type { IconProps } from "@/types/components/icon"
|
||||
|
||||
export interface CheckboxCardProps {
|
||||
Icon?: (props: IconProps) => JSX.Element
|
||||
declined?: boolean
|
||||
list?: {
|
||||
title: string
|
||||
}[]
|
||||
name?: string
|
||||
saving?: boolean
|
||||
subtitle?: string
|
||||
text?: string
|
||||
title: string
|
||||
}
|
||||
65
components/TempDesignSystem/Form/Checkbox/Card/index.tsx
Normal file
65
components/TempDesignSystem/Form/Checkbox/Card/index.tsx
Normal file
@@ -0,0 +1,65 @@
|
||||
"use client"
|
||||
|
||||
import { CheckIcon, CloseIcon, HeartIcon } from "@/components/Icons"
|
||||
import Caption from "@/components/TempDesignSystem/Text/Caption"
|
||||
import Footnote from "@/components/TempDesignSystem/Text/Footnote"
|
||||
|
||||
import styles from "./card.module.css"
|
||||
|
||||
import type { CheckboxCardProps } from "./card"
|
||||
|
||||
export default function CheckboxCard({
|
||||
Icon = HeartIcon,
|
||||
declined = false,
|
||||
list,
|
||||
name = "join",
|
||||
saving = false,
|
||||
subtitle,
|
||||
text,
|
||||
title,
|
||||
}: CheckboxCardProps) {
|
||||
return (
|
||||
<label className={styles.checkbox} data-declined={declined} htmlFor={name}>
|
||||
<header className={styles.header}>
|
||||
<Caption className={styles.title} textTransform="bold" uppercase>
|
||||
{title}
|
||||
</Caption>
|
||||
{subtitle ? (
|
||||
<Caption
|
||||
className={styles.subtitle}
|
||||
color={saving ? "baseTextAccent" : "uiTextHighContrast"}
|
||||
textTransform="bold"
|
||||
>
|
||||
{subtitle}
|
||||
</Caption>
|
||||
) : null}
|
||||
<Icon
|
||||
className={styles.icon}
|
||||
color="uiTextHighContrast"
|
||||
height={32}
|
||||
width={32}
|
||||
/>
|
||||
</header>
|
||||
{list ? (
|
||||
<ul className={styles.list}>
|
||||
{list.map((listItem) => (
|
||||
<li key={listItem.title} className={styles.listItem}>
|
||||
{declined ? (
|
||||
<CloseIcon
|
||||
color="uiTextMediumContrast"
|
||||
height={20}
|
||||
width={20}
|
||||
/>
|
||||
) : (
|
||||
<CheckIcon color="baseIconLowContrast" height={20} width={20} />
|
||||
)}
|
||||
<Footnote color="uiTextMediumContrast">{listItem.title}</Footnote>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
) : null}
|
||||
{text ? <Footnote color="uiTextMediumContrast">{text}</Footnote> : null}
|
||||
<input aria-hidden id={name} hidden name={name} type="checkbox" />
|
||||
</label>
|
||||
)
|
||||
}
|
||||
@@ -1,9 +1,11 @@
|
||||
import type { RegisterOptions } from "react-hook-form"
|
||||
|
||||
export type CountryProps = {
|
||||
className?: string
|
||||
label: string
|
||||
name?: string
|
||||
placeholder?: string
|
||||
readOnly?: boolean
|
||||
registerOptions?: RegisterOptions
|
||||
}
|
||||
|
||||
|
||||
@@ -28,8 +28,10 @@ import type {
|
||||
} from "./country"
|
||||
|
||||
export default function CountrySelect({
|
||||
className = "",
|
||||
label,
|
||||
name = "country",
|
||||
readOnly = false,
|
||||
registerOptions = {},
|
||||
}: CountryProps) {
|
||||
const { formatMessage } = useIntl()
|
||||
@@ -54,12 +56,13 @@ export default function CountrySelect({
|
||||
const selectCountryLabel = formatMessage({ id: "Select a country" })
|
||||
|
||||
return (
|
||||
<div className={styles.container} ref={setRef}>
|
||||
<div className={`${styles.container} ${className}`} ref={setRef}>
|
||||
<ComboBox
|
||||
aria-label={formatMessage({ id: "Select country of residence" })}
|
||||
className={styles.select}
|
||||
isRequired={!!registerOptions?.required}
|
||||
isInvalid={fieldState.invalid}
|
||||
isReadOnly={readOnly}
|
||||
isRequired={!!registerOptions?.required}
|
||||
name={field.name}
|
||||
onBlur={field.onBlur}
|
||||
onSelectionChange={handleChange}
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
import { type ForwardedRef,forwardRef } from "react"
|
||||
import { Input as AriaInput, Label as AriaLabel } from "react-aria-components"
|
||||
|
||||
import Label from "@/components/TempDesignSystem/Form/Label"
|
||||
import Body from "@/components/TempDesignSystem/Text/Body"
|
||||
|
||||
import styles from "./input.module.css"
|
||||
|
||||
import type { AriaInputWithLabelProps } from "./input"
|
||||
|
||||
const AriaInputWithLabel = forwardRef(function AriaInputWithLabelComponent(
|
||||
{ label, ...props }: AriaInputWithLabelProps,
|
||||
ref: ForwardedRef<HTMLInputElement>
|
||||
) {
|
||||
return (
|
||||
<AriaLabel className={styles.container} htmlFor={props.name}>
|
||||
<Body asChild fontOnly>
|
||||
<AriaInput {...props} className={styles.input} ref={ref} />
|
||||
</Body>
|
||||
<Label required={!!props.required}>{label}</Label>
|
||||
</AriaLabel>
|
||||
)
|
||||
})
|
||||
|
||||
export default AriaInputWithLabel
|
||||
@@ -0,0 +1,55 @@
|
||||
.container {
|
||||
align-content: center;
|
||||
background-color: var(--Main-Grey-White);
|
||||
border-color: var(--Scandic-Beige-40);
|
||||
border-style: solid;
|
||||
border-width: 1px;
|
||||
border-radius: var(--Corner-radius-Medium);
|
||||
display: grid;
|
||||
height: 60px;
|
||||
padding: var(--Spacing-x1) var(--Spacing-x2);
|
||||
transition: border-color 200ms ease;
|
||||
}
|
||||
|
||||
.container:has(.input:active, .input:focus) {
|
||||
border-color: var(--Scandic-Blue-90);
|
||||
}
|
||||
|
||||
.container:has(.input:disabled) {
|
||||
background-color: var(--Main-Grey-10);
|
||||
border: none;
|
||||
color: var(--Main-Grey-40);
|
||||
}
|
||||
|
||||
.container:has(.input[data-invalid="true"], .input[aria-invalid="true"]) {
|
||||
border-color: var(--Scandic-Red-60);
|
||||
}
|
||||
|
||||
.input {
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--Main-Grey-100);
|
||||
height: 18px;
|
||||
margin: 0;
|
||||
order: 2;
|
||||
overflow: visible;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.input:not(:active, :focus):placeholder-shown {
|
||||
height: 0px;
|
||||
transition: height 150ms ease;
|
||||
}
|
||||
|
||||
.input:focus,
|
||||
.input:focus:placeholder-shown,
|
||||
.input:active,
|
||||
.input:active:placeholder-shown {
|
||||
height: 18px;
|
||||
transition: height 150ms ease;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.input:disabled {
|
||||
color: var(--Main-Grey-40);
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
export interface AriaInputWithLabelProps
|
||||
extends React.InputHTMLAttributes<HTMLInputElement> {
|
||||
label: string
|
||||
}
|
||||
@@ -1,15 +1,9 @@
|
||||
"use client"
|
||||
import {
|
||||
Input as AriaInput,
|
||||
Label as AriaLabel,
|
||||
Text,
|
||||
TextField,
|
||||
} from "react-aria-components"
|
||||
import { Text, TextField } from "react-aria-components"
|
||||
import { Controller, useFormContext } from "react-hook-form"
|
||||
|
||||
import { CheckIcon, InfoCircleIcon } from "@/components/Icons"
|
||||
import Label from "@/components/TempDesignSystem/Form/Label"
|
||||
import Body from "@/components/TempDesignSystem/Text/Body"
|
||||
import AriaInputWithLabel from "@/components/TempDesignSystem/Form/Input/AriaInputWithLabel"
|
||||
import Caption from "@/components/TempDesignSystem/Text/Caption"
|
||||
|
||||
import styles from "./input.module.css"
|
||||
@@ -20,11 +14,13 @@ import type { InputProps } from "./input"
|
||||
|
||||
export default function Input({
|
||||
"aria-label": ariaLabel,
|
||||
className = "",
|
||||
disabled = false,
|
||||
helpText = "",
|
||||
label,
|
||||
name,
|
||||
placeholder = "",
|
||||
readOnly = false,
|
||||
registerOptions = {},
|
||||
type = "text",
|
||||
}: InputProps) {
|
||||
@@ -44,6 +40,7 @@ export default function Input({
|
||||
render={({ field, fieldState }) => (
|
||||
<TextField
|
||||
aria-label={ariaLabel}
|
||||
className={className}
|
||||
isDisabled={field.disabled}
|
||||
isInvalid={fieldState.invalid}
|
||||
isRequired={!!registerOptions.required}
|
||||
@@ -53,19 +50,16 @@ export default function Input({
|
||||
validationBehavior="aria"
|
||||
value={field.value}
|
||||
>
|
||||
<AriaLabel className={styles.container} htmlFor={field.name}>
|
||||
<Body asChild fontOnly>
|
||||
<AriaInput
|
||||
{...numberAttributes}
|
||||
aria-labelledby={field.name}
|
||||
className={styles.input}
|
||||
id={field.name}
|
||||
placeholder={placeholder}
|
||||
type={type}
|
||||
/>
|
||||
</Body>
|
||||
<Label required={!!registerOptions.required}>{label}</Label>
|
||||
</AriaLabel>
|
||||
<AriaInputWithLabel
|
||||
{...field}
|
||||
aria-labelledby={field.name}
|
||||
id={field.name}
|
||||
label={label}
|
||||
placeholder={placeholder}
|
||||
readOnly={readOnly}
|
||||
required={!!registerOptions.required}
|
||||
type={type}
|
||||
/>
|
||||
{helpText && !fieldState.error ? (
|
||||
<Caption asChild color="black">
|
||||
<Text className={styles.helpText} slot="description">
|
||||
|
||||
@@ -1,59 +1,3 @@
|
||||
.container {
|
||||
align-content: center;
|
||||
background-color: var(--Main-Grey-White);
|
||||
border-color: var(--Scandic-Beige-40);
|
||||
border-style: solid;
|
||||
border-width: 1px;
|
||||
border-radius: var(--Corner-radius-Medium);
|
||||
display: grid;
|
||||
height: 60px;
|
||||
padding: var(--Spacing-x1) var(--Spacing-x2);
|
||||
transition: border-color 200ms ease;
|
||||
}
|
||||
|
||||
.container:has(.input:active, .input:focus) {
|
||||
border-color: var(--Scandic-Blue-90);
|
||||
}
|
||||
|
||||
.container:has(.input:disabled) {
|
||||
background-color: var(--Main-Grey-10);
|
||||
border: none;
|
||||
color: var(--Main-Grey-40);
|
||||
}
|
||||
|
||||
.container:has(.input[data-invalid="true"], .input[aria-invalid="true"]) {
|
||||
border-color: var(--Scandic-Red-60);
|
||||
}
|
||||
|
||||
.input {
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--Main-Grey-100);
|
||||
height: 18px;
|
||||
margin: 0;
|
||||
order: 2;
|
||||
overflow: visible;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.input:not(:active, :focus):placeholder-shown {
|
||||
height: 0px;
|
||||
transition: height 150ms ease;
|
||||
}
|
||||
|
||||
.input:focus,
|
||||
.input:focus:placeholder-shown,
|
||||
.input:active,
|
||||
.input:active:placeholder-shown {
|
||||
height: 18px;
|
||||
transition: height 150ms ease;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.input:disabled {
|
||||
color: var(--Main-Grey-40);
|
||||
}
|
||||
|
||||
.helpText {
|
||||
align-items: flex-start;
|
||||
display: flex;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { RegisterOptions, UseFormRegister } from "react-hook-form"
|
||||
import type { RegisterOptions } from "react-hook-form"
|
||||
|
||||
export interface InputProps
|
||||
extends React.InputHTMLAttributes<HTMLInputElement> {
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
letter-spacing: 0.03px;
|
||||
line-height: 120%;
|
||||
text-align: left;
|
||||
transition: font-size 100ms ease;
|
||||
}
|
||||
|
||||
span.small {
|
||||
@@ -21,7 +22,6 @@ input:active ~ .label,
|
||||
input:not(:placeholder-shown) ~ .label {
|
||||
display: block;
|
||||
font-size: 12px;
|
||||
transition: font-size 100ms ease;
|
||||
}
|
||||
|
||||
input:focus ~ .label {
|
||||
|
||||
@@ -1,17 +1,11 @@
|
||||
"use client"
|
||||
import {
|
||||
Input as AriaInput,
|
||||
Label as AriaLabel,
|
||||
Text,
|
||||
TextField,
|
||||
} from "react-aria-components"
|
||||
import { Text, TextField } from "react-aria-components"
|
||||
import { Controller, useFormContext } from "react-hook-form"
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import { CheckIcon, CloseIcon } from "@/components/Icons"
|
||||
import Error from "@/components/TempDesignSystem/Form/ErrorMessage/Error"
|
||||
import Label from "@/components/TempDesignSystem/Form/Label"
|
||||
import Body from "@/components/TempDesignSystem/Text/Body"
|
||||
import AriaInputWithLabel from "@/components/TempDesignSystem/Form/Input/AriaInputWithLabel"
|
||||
import Caption from "@/components/TempDesignSystem/Text/Caption"
|
||||
|
||||
import { type IconProps, Key, type NewPasswordProps } from "./newPassword"
|
||||
@@ -47,20 +41,14 @@ export default function NewPassword({
|
||||
value={field.value}
|
||||
type="password"
|
||||
>
|
||||
<AriaLabel className={styles.container} htmlFor={field.name}>
|
||||
<Body asChild fontOnly>
|
||||
<AriaInput
|
||||
aria-labelledby={field.name}
|
||||
className={styles.input}
|
||||
id={field.name}
|
||||
placeholder={placeholder}
|
||||
type="password"
|
||||
/>
|
||||
</Body>
|
||||
<Label required={!!registerOptions.required}>
|
||||
{formatMessage({ id: "New password" })}
|
||||
</Label>
|
||||
</AriaLabel>
|
||||
<AriaInputWithLabel
|
||||
{...field}
|
||||
aria-labelledby={field.name}
|
||||
id={field.name}
|
||||
label={formatMessage({ id: "New password" })}
|
||||
placeholder={placeholder}
|
||||
type="password"
|
||||
/>
|
||||
{field.value ? (
|
||||
<div className={styles.errors}>
|
||||
<Caption asChild color="black">
|
||||
|
||||
@@ -1,59 +1,3 @@
|
||||
.container {
|
||||
align-content: center;
|
||||
background-color: var(--Main-Grey-White);
|
||||
border-color: var(--Scandic-Beige-40);
|
||||
border-style: solid;
|
||||
border-width: 1px;
|
||||
border-radius: var(--Corner-radius-Medium);
|
||||
display: grid;
|
||||
height: 60px;
|
||||
padding: var(--Spacing-x1) var(--Spacing-x2);
|
||||
transition: border-color 200ms ease;
|
||||
}
|
||||
|
||||
.container:has(.input:active, .input:focus) {
|
||||
border-color: var(--Scandic-Blue-90);
|
||||
}
|
||||
|
||||
.container:has(.input:disabled) {
|
||||
background-color: var(--Main-Grey-10);
|
||||
border: none;
|
||||
color: var(--Main-Grey-40);
|
||||
}
|
||||
|
||||
.container:has(.input[data-invalid="true"], .input[aria-invalid="true"]) {
|
||||
border-color: var(--Scandic-Red-60);
|
||||
}
|
||||
|
||||
.input {
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--Main-Grey-100);
|
||||
height: 18px;
|
||||
margin: 0;
|
||||
order: 2;
|
||||
overflow: visible;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.input:not(:active, :focus):placeholder-shown {
|
||||
height: 0px;
|
||||
transition: height 150ms ease;
|
||||
}
|
||||
|
||||
.input:focus,
|
||||
.input:focus:placeholder-shown,
|
||||
.input:active,
|
||||
.input:active:placeholder-shown {
|
||||
height: 18px;
|
||||
transition: height 150ms ease;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.input:disabled {
|
||||
color: var(--Main-Grey-40);
|
||||
}
|
||||
|
||||
.helpText {
|
||||
align-items: flex-start;
|
||||
display: flex;
|
||||
|
||||
@@ -2,11 +2,7 @@
|
||||
import "react-international-phone/style.css"
|
||||
|
||||
import { isValidPhoneNumber, parsePhoneNumber } from "libphonenumber-js"
|
||||
import {
|
||||
Input as AriaInput,
|
||||
Label as AriaLabel,
|
||||
TextField,
|
||||
} from "react-aria-components"
|
||||
import { TextField } from "react-aria-components"
|
||||
import { useController, useFormContext, useWatch } from "react-hook-form"
|
||||
import {
|
||||
CountrySelector,
|
||||
@@ -18,6 +14,7 @@ import { useIntl } from "react-intl"
|
||||
|
||||
import { ChevronDownIcon } from "@/components/Icons"
|
||||
import ErrorMessage from "@/components/TempDesignSystem/Form/ErrorMessage"
|
||||
import AriaInputWithLabel from "@/components/TempDesignSystem/Form/Input/AriaInputWithLabel"
|
||||
import Label from "@/components/TempDesignSystem/Form/Label"
|
||||
import Body from "@/components/TempDesignSystem/Text/Body"
|
||||
|
||||
@@ -29,10 +26,12 @@ import type { PhoneProps } from "./phone"
|
||||
|
||||
export default function Phone({
|
||||
ariaLabel = "Phone number input",
|
||||
className = "",
|
||||
disabled = false,
|
||||
label,
|
||||
name = "phoneNumber",
|
||||
placeholder = "",
|
||||
readOnly = false,
|
||||
registerOptions = {
|
||||
required: true,
|
||||
},
|
||||
@@ -72,8 +71,9 @@ export default function Phone({
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.phone}>
|
||||
<div className={`${styles.phone} ${className}`}>
|
||||
<CountrySelector
|
||||
disabled={readOnly}
|
||||
dropdownArrowClassName={styles.arrow}
|
||||
flagClassName={styles.flag}
|
||||
onSelect={handleSelectCountry}
|
||||
@@ -114,25 +114,21 @@ export default function Phone({
|
||||
isDisabled={disabled ?? field.disabled}
|
||||
isInvalid={fieldState.invalid}
|
||||
isRequired={!!registerOptions?.required}
|
||||
isReadOnly={readOnly}
|
||||
name={field.name}
|
||||
type="tel"
|
||||
>
|
||||
<AriaLabel className={styles.inputContainer} htmlFor={field.name}>
|
||||
<Body asChild fontOnly>
|
||||
<AriaInput
|
||||
className={styles.input}
|
||||
id={field.name}
|
||||
name={field.name}
|
||||
onBlur={field.onBlur}
|
||||
onChange={handleChange}
|
||||
placeholder={placeholder}
|
||||
ref={field.ref}
|
||||
required={!!registerOptions.required}
|
||||
value={inputValue}
|
||||
/>
|
||||
</Body>
|
||||
<Label required={!!registerOptions.required}>{label}</Label>
|
||||
</AriaLabel>
|
||||
<AriaInputWithLabel
|
||||
{...field}
|
||||
id={field.name}
|
||||
label={label}
|
||||
onChange={handleChange}
|
||||
placeholder={placeholder}
|
||||
readOnly={readOnly}
|
||||
required={!!registerOptions.required}
|
||||
type="tel"
|
||||
value={inputValue}
|
||||
/>
|
||||
<ErrorMessage errors={formState.errors} name={field.name} />
|
||||
</TextField>
|
||||
</div>
|
||||
|
||||
@@ -19,6 +19,9 @@
|
||||
--react-international-phone-dropdown-top: calc(
|
||||
var(--react-international-phone-height) + var(--Spacing-x1)
|
||||
);
|
||||
--react-international-phone-dial-code-preview-font-size: var(
|
||||
--typography-Body-Regular-fontSize
|
||||
);
|
||||
}
|
||||
|
||||
.phone:has(.input:active, .input:focus) {
|
||||
@@ -46,7 +49,6 @@
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
.inputContainer,
|
||||
.select {
|
||||
align-content: center;
|
||||
background-color: var(--Main-Grey-White);
|
||||
@@ -93,42 +95,8 @@
|
||||
|
||||
.select .dialCode {
|
||||
border: none;
|
||||
color: var(--Main-Grey-100);
|
||||
color: var(--UI-Text-High-contrast);
|
||||
line-height: 1;
|
||||
justify-self: flex-start;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.inputContainer:has(.input:not(:focus):placeholder-shown) {
|
||||
gap: 0;
|
||||
grid-template-rows: 1fr;
|
||||
}
|
||||
|
||||
.inputContainer:has(.input:active, .input:focus) {
|
||||
border-color: var(--Scandic-Blue-90);
|
||||
}
|
||||
|
||||
.inputContainer:has(.input[data-invalid="true"], .input[aria-invalid="true"]) {
|
||||
border-color: var(--Scandic-Red-60);
|
||||
}
|
||||
|
||||
.input {
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--Main-Grey-100);
|
||||
height: 18px;
|
||||
margin: 0;
|
||||
order: 2;
|
||||
overflow: visible;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.input:not(:active, :focus):placeholder-shown {
|
||||
height: 0px;
|
||||
}
|
||||
|
||||
.input:focus,
|
||||
.input:focus:placeholder-shown {
|
||||
height: 18px;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
@@ -2,9 +2,11 @@ import type { RegisterOptions } from "react-hook-form"
|
||||
|
||||
export type PhoneProps = {
|
||||
ariaLabel?: string
|
||||
className?: string
|
||||
disabled?: boolean
|
||||
label: string
|
||||
name?: string
|
||||
placeholder?: string
|
||||
readOnly?: boolean
|
||||
registerOptions?: RegisterOptions
|
||||
}
|
||||
|
||||
@@ -92,6 +92,10 @@
|
||||
color: var(--Base-Text-Medium-contrast);
|
||||
}
|
||||
|
||||
.uiTextHighContrast {
|
||||
color: var(--UI-Text-High-contrast);
|
||||
}
|
||||
|
||||
.uiTextPlaceholder {
|
||||
color: var(--UI-Text-Placeholder);
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ const config = {
|
||||
white: styles.white,
|
||||
peach50: styles.peach50,
|
||||
peach80: styles.peach80,
|
||||
uiTextHighContrast: styles.uiTextHighContrast,
|
||||
uiTextPlaceholder: styles.uiTextPlaceholder,
|
||||
},
|
||||
textAlign: {
|
||||
|
||||
@@ -35,6 +35,10 @@ p.caption {
|
||||
text-decoration: var(--typography-Caption-Regular-textDecoration);
|
||||
}
|
||||
|
||||
.baseTextAccent {
|
||||
color: var(--Base-Text-Accent);
|
||||
}
|
||||
|
||||
.black {
|
||||
color: var(--Main-Grey-100);
|
||||
}
|
||||
@@ -67,10 +71,14 @@ p.caption {
|
||||
color: var(--UI-Text-Medium-contrast);
|
||||
}
|
||||
|
||||
.uiTextHighContrast {
|
||||
color: var(--UI-Text-High-contrast);
|
||||
}
|
||||
|
||||
.center {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.left {
|
||||
text-align: left;
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,7 @@ export default function Caption({
|
||||
fontOnly = false,
|
||||
textAlign,
|
||||
textTransform,
|
||||
uppercase,
|
||||
...props
|
||||
}: CaptionProps) {
|
||||
const Comp = asChild ? Slot : "p"
|
||||
@@ -18,12 +19,14 @@ export default function Caption({
|
||||
? fontOnlycaptionVariants({
|
||||
className,
|
||||
textTransform,
|
||||
uppercase,
|
||||
})
|
||||
: captionVariants({
|
||||
className,
|
||||
color,
|
||||
textTransform,
|
||||
textAlign,
|
||||
uppercase,
|
||||
})
|
||||
return <Comp className={classNames} {...props} />
|
||||
}
|
||||
|
||||
@@ -5,12 +5,14 @@ import styles from "./caption.module.css"
|
||||
const config = {
|
||||
variants: {
|
||||
color: {
|
||||
baseTextAccent: styles.baseTextAccent,
|
||||
black: styles.black,
|
||||
burgundy: styles.burgundy,
|
||||
pale: styles.pale,
|
||||
textMediumContrast: styles.textMediumContrast,
|
||||
red: styles.red,
|
||||
white: styles.white,
|
||||
uiTextHighContrast: styles.uiTextHighContrast,
|
||||
uiTextActive: styles.uiTextActive,
|
||||
uiTextMediumContrast: styles.uiTextMediumContrast,
|
||||
},
|
||||
@@ -23,6 +25,9 @@ const config = {
|
||||
center: styles.center,
|
||||
left: styles.left,
|
||||
},
|
||||
uppercase: {
|
||||
true: styles.uppercase,
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
color: "black",
|
||||
@@ -39,6 +44,9 @@ const fontOnlyConfig = {
|
||||
regular: styles.regular,
|
||||
uppercase: styles.uppercase,
|
||||
},
|
||||
uppercase: {
|
||||
true: styles.uppercase,
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
textTransform: "regular",
|
||||
|
||||
@@ -59,7 +59,7 @@
|
||||
color: var(--Scandic-Peach-50);
|
||||
}
|
||||
|
||||
.textMediumContrast {
|
||||
.uiTextMediumContrast {
|
||||
color: var(--UI-Text-Medium-contrast);
|
||||
}
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ const config = {
|
||||
burgundy: styles.burgundy,
|
||||
pale: styles.pale,
|
||||
peach50: styles.peach50,
|
||||
textMediumContrast: styles.textMediumContrast,
|
||||
uiTextMediumContrast: styles.uiTextMediumContrast,
|
||||
uiTextPlaceholder: styles.uiTextPlaceholder,
|
||||
},
|
||||
textAlign: {
|
||||
|
||||
Reference in New Issue
Block a user