feat: guest information form enter details

This commit is contained in:
Simon Emanuelsson
2024-10-03 11:12:36 +02:00
parent 4103e3fb37
commit 451d461c7f
50 changed files with 834 additions and 442 deletions

View File

@@ -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"
>

View File

@@ -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}
/>

View File

@@ -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}

View File

@@ -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);
}

View 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>
)
}

View 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(),
})

View File

@@ -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

View 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>
)
}

View File

@@ -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);
}

View File

@@ -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"

View File

@@ -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: {

View File

@@ -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);
}

View 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
}

View 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>
)
}

View File

@@ -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
}

View File

@@ -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}

View File

@@ -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

View File

@@ -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);
}

View File

@@ -0,0 +1,4 @@
export interface AriaInputWithLabelProps
extends React.InputHTMLAttributes<HTMLInputElement> {
label: string
}

View File

@@ -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">

View File

@@ -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;

View File

@@ -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> {

View File

@@ -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 {

View File

@@ -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">

View File

@@ -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;

View File

@@ -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>

View File

@@ -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;
}

View File

@@ -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
}

View File

@@ -92,6 +92,10 @@
color: var(--Base-Text-Medium-contrast);
}
.uiTextHighContrast {
color: var(--UI-Text-High-contrast);
}
.uiTextPlaceholder {
color: var(--UI-Text-Placeholder);
}

View File

@@ -15,6 +15,7 @@ const config = {
white: styles.white,
peach50: styles.peach50,
peach80: styles.peach80,
uiTextHighContrast: styles.uiTextHighContrast,
uiTextPlaceholder: styles.uiTextPlaceholder,
},
textAlign: {

View File

@@ -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;
}
}

View File

@@ -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} />
}

View File

@@ -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",

View File

@@ -59,7 +59,7 @@
color: var(--Scandic-Peach-50);
}
.textMediumContrast {
.uiTextMediumContrast {
color: var(--UI-Text-Medium-contrast);
}

View File

@@ -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: {