fix: special requests

This commit is contained in:
Christel Westerberg
2024-12-10 14:40:26 +01:00
parent 854563d099
commit 241e354fc5
16 changed files with 393 additions and 18 deletions
@@ -9,11 +9,7 @@
width: min(100%, 600px);
}
.header,
.country,
.email,
.signup,
.phone {
.fullWidth {
grid-column: 1/-1;
}
@@ -10,10 +10,17 @@ import Button from "@/components/TempDesignSystem/Button"
import CountrySelect from "@/components/TempDesignSystem/Form/Country"
import Input from "@/components/TempDesignSystem/Form/Input"
import Phone from "@/components/TempDesignSystem/Form/Phone"
import Select from "@/components/TempDesignSystem/Form/Select"
import TextArea from "@/components/TempDesignSystem/Form/TextArea"
import Footnote from "@/components/TempDesignSystem/Text/Footnote"
import JoinScandicFriendsCard from "./JoinScandicFriendsCard"
import { guestDetailsSchema, signedInDetailsSchema } from "./schema"
import {
ElevatorPreference,
FloorPreference,
guestDetailsSchema,
signedInDetailsSchema,
} from "./schema"
import Signup from "./Signup"
import styles from "./details.module.css"
@@ -71,7 +78,7 @@ export default function Details({ user, memberPrice }: DetailsProps) {
color="uiTextHighContrast"
textTransform="uppercase"
type="label"
className={styles.header}
className={styles.fullWidth}
>
{intl.formatMessage({ id: "Guest information" })}
</Footnote>
@@ -90,31 +97,92 @@ export default function Details({ user, memberPrice }: DetailsProps) {
registerOptions={{ required: true }}
/>
<CountrySelect
className={styles.country}
className={styles.fullWidth}
label={intl.formatMessage({ id: "Country" })}
name="countryCode"
readOnly={!!user}
registerOptions={{ required: true }}
/>
<Input
className={styles.email}
className={styles.fullWidth}
label={intl.formatMessage({ id: "Email address" })}
name="email"
readOnly={!!user}
registerOptions={{ required: true }}
/>
<Phone
className={styles.phone}
className={styles.fullWidth}
label={intl.formatMessage({ id: "Phone number" })}
name="phoneNumber"
readOnly={!!user}
registerOptions={{ required: true }}
/>
{user ? null : (
<div className={styles.signup}>
<div className={styles.fullWidth}>
<Signup name="join" />
</div>
)}
<Footnote
color="uiTextHighContrast"
textTransform="uppercase"
type="label"
className={styles.fullWidth}
>
{intl.formatMessage({ id: "Special requests" })}
</Footnote>
<Select
className={styles.fullWidth}
label={intl.formatMessage({ id: "Floor preference" })}
name="specialRequests.floorPreference"
items={[
{
value: "",
label: intl.formatMessage({
id: "No preference",
}),
},
{
value: FloorPreference.HIGH,
label: intl.formatMessage({ id: FloorPreference.HIGH }),
},
{
value: FloorPreference.LOW,
label: intl.formatMessage({ id: FloorPreference.LOW }),
},
]}
/>
<Select
className={styles.fullWidth}
label={intl.formatMessage({ id: "Elevator preference" })}
name="specialRequests.elevatorPreference"
items={[
{
value: "",
label: intl.formatMessage({
id: "No preference",
}),
},
{
value: ElevatorPreference.AWAY_FROM_ELEVATOR,
label: intl.formatMessage({
id: ElevatorPreference.AWAY_FROM_ELEVATOR,
}),
},
{
value: ElevatorPreference.NEAR_ELEVATOR,
label: intl.formatMessage({
id: ElevatorPreference.NEAR_ELEVATOR,
}),
},
]}
/>
<TextArea
label={intl.formatMessage({
id: "Is there anything else you would like us to know before your arrival?",
})}
name="specialRequests.comments"
className={styles.fullWidth}
/>
</div>
<footer className={styles.footer}>
<Button
@@ -8,6 +8,24 @@ const stringMatcher =
const isValidString = (key: string) => stringMatcher.test(key)
export enum FloorPreference {
LOW = "Low level",
HIGH = "High level",
}
export enum ElevatorPreference {
AWAY_FROM_ELEVATOR = "Away from elevator",
NEAR_ELEVATOR = "Near elevator",
}
const specialRequestsSchema = z
.object({
floorPreference: z.nativeEnum(FloorPreference).optional(),
elevatorPreference: z.nativeEnum(ElevatorPreference).optional(),
comments: z.string().optional(),
})
.optional()
export const baseDetailsSchema = z.object({
countryCode: z.string().min(1, { message: "Country is required" }),
email: z.string().email({ message: "Email address is required" }),
@@ -24,6 +42,7 @@ export const baseDetailsSchema = z.object({
message: "Last name can't contain any special characters",
}),
phoneNumber: phoneValidator(),
specialRequests: specialRequestsSchema,
})
export const notJoinDetailsSchema = baseDetailsSchema.merge(
@@ -78,4 +97,5 @@ export const signedInDetailsSchema = z.object({
.transform((_) => false),
dateOfBirth: z.string().default(""),
zipCode: z.string().default(""),
specialRequests: specialRequestsSchema,
})
@@ -46,3 +46,26 @@ input:placeholder-shown:active ~ .label {
input:disabled ~ .label {
color: var(--Main-Grey-40);
}
textarea:active ~ .label,
textarea:not(:placeholder-shown) ~ .label {
display: block;
font-size: 12px;
}
textarea:focus ~ .label {
font-size: 12px;
}
textarea:placeholder-shown ~ .label {
grid-row: 1/-1;
}
textarea:placeholder-shown:focus ~ .label,
textarea:placeholder-shown:active ~ .label {
margin-bottom: var(--Spacing-x-half);
}
textarea:disabled ~ .label {
color: var(--Main-Grey-40);
}
@@ -6,6 +6,7 @@ import ReactAriaSelect from "@/components/TempDesignSystem/Select"
import type { SelectProps } from "./select"
export default function Select({
className,
items,
label,
name,
@@ -21,6 +22,7 @@ export default function Select({
return (
<ReactAriaSelect
className={className}
defaultSelectedKey={field.value}
disabled={field.disabled}
items={items}
@@ -0,0 +1,90 @@
"use client"
import {
Label as AriaLabel,
Text,
TextArea as AriaTextArea,
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 Caption from "@/components/TempDesignSystem/Text/Caption"
import Body from "../../Text/Body"
import styles from "./textarea.module.css"
import type { HTMLAttributes, WheelEvent } from "react"
import type { TextAreaProps } from "./input"
export default function TextArea({
"aria-label": ariaLabel,
className = "",
disabled = false,
helpText = "",
label,
name,
placeholder = "",
readOnly = false,
registerOptions = {},
type = "text",
}: TextAreaProps) {
const { control } = useFormContext()
let numberAttributes: HTMLAttributes<HTMLTextAreaElement> = {}
if (type === "number") {
numberAttributes.onWheel = function (evt: WheelEvent<HTMLTextAreaElement>) {
evt.currentTarget.blur()
}
}
return (
<Controller
disabled={disabled}
control={control}
name={name}
rules={registerOptions}
render={({ field, fieldState }) => (
<TextField
aria-label={ariaLabel}
className={className}
isDisabled={field.disabled}
isRequired={!!registerOptions.required}
onBlur={field.onBlur}
onChange={field.onChange}
validationBehavior="aria"
value={field.value}
>
<AriaLabel className={styles.container} htmlFor={name}>
<Body asChild fontOnly>
<AriaTextArea
{...field}
aria-labelledby={field.name}
id={name}
placeholder={placeholder}
readOnly={readOnly}
required={!!registerOptions.required}
className={styles.textarea}
/>
</Body>
<Label required={!!registerOptions.required}>{label}</Label>
</AriaLabel>
{helpText && !fieldState.error ? (
<Caption asChild color="black">
<Text className={styles.helpText} slot="description">
<CheckIcon height={20} width={30} />
{helpText}
</Text>
</Caption>
) : null}
{fieldState.error ? (
<Caption className={styles.error} fontOnly>
<InfoCircleIcon color="red" />
{fieldState.error.message}
</Caption>
) : null}
</TextField>
)}
/>
)
}
@@ -0,0 +1,9 @@
import type { RegisterOptions } from "react-hook-form"
export interface TextAreaProps
extends React.InputHTMLAttributes<HTMLTextAreaElement> {
helpText?: string
label: string
name: string
registerOptions?: RegisterOptions
}
@@ -0,0 +1,72 @@
.helpText {
align-items: flex-start;
display: flex;
gap: var(--Spacing-x-half);
}
.error {
align-items: center;
color: var(--Scandic-Red-60);
display: flex;
gap: var(--Spacing-x-half);
margin: var(--Spacing-x1) 0 0;
}
.container {
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;
min-width: 0; /* allow shrinkage */
grid-template-rows: auto 1fr;
height: 138px;
padding: var(--Spacing-x3) var(--Spacing-x2) 0 var(--Spacing-x2);
transition: border-color 200ms ease;
}
.container:has(.textarea:active, .textarea:focus) {
border-color: var(--Scandic-Blue-90);
}
.container:has(.textarea:disabled) {
background-color: var(--Main-Grey-10);
border: none;
color: var(--Main-Grey-40);
}
.container:has(.textarea[data-invalid="true"], .textarea[aria-invalid="true"]) {
border-color: var(--Scandic-Red-60);
}
.textarea {
background: none;
border: none;
color: var(--Main-Grey-100);
height: 100%;
width: 100%;
margin: 0;
order: 2;
overflow: visible;
padding: 0;
resize: none;
}
.textarea:not(:active, :focus):placeholder-shown {
height: 88px;
transition: height 150ms ease;
}
.textarea:focus,
.textarea:focus:placeholder-shown,
.textarea:active,
.textarea:active:placeholder-shown {
height: 94px;
transition: height 150ms ease;
outline: none;
}
.textarea:disabled {
color: var(--Main-Grey-40);
}
+22 -6
View File
@@ -25,6 +25,7 @@ import type {
} from "./select"
export default function Select({
className = "",
"aria-label": ariaLabel,
defaultSelectedKey,
items,
@@ -54,7 +55,7 @@ export default function Select({
}
return (
<div className={styles.container} ref={setRef}>
<div className={`${styles.container} ${className}`} ref={setRef}>
<ReactAriaSelect
aria-label={ariaLabel}
className={`${styles.select} ${discreet && styles.discreet}`}
@@ -68,11 +69,26 @@ export default function Select({
<Body asChild fontOnly>
<Button className={styles.input} data-testid={name}>
<span className={styles.inputContentWrapper} tabIndex={tabIndex}>
<Label required={required} size={discreet ? "discreet" : "small"}>
{label}
{discreet && `:`}
</Label>
<SelectValue />
<SelectValue>
{({ isPlaceholder, selectedText }) => (
<>
<Label
required={required}
size={discreet ? "discreet" : "small"}
>
{label}
{discreet && `:`}
</Label>
{isPlaceholder ? (
placeholder ? (
<Body color="uiTextPlaceholder"> {placeholder}</Body>
) : null
) : (
selectedText
)}
</>
)}
</SelectValue>
</span>
<SelectChevron
{...(discreet ? { color: "baseButtonTextOnFillNormal" } : {})}
+1 -1
View File
@@ -1,4 +1,4 @@
import type { Key, SelectProps as AriaSelectProps } from "react-aria-components"
import type { Key } from "react-aria-components"
export interface SelectProps
extends Omit<React.SelectHTMLAttributes<HTMLSelectElement>, "onSelect"> {