fix: responsivity of fields and order of signup form

This commit is contained in:
Christel Westerberg
2024-11-18 15:07:23 +01:00
parent 9094b08345
commit d5c6b6809c
17 changed files with 264 additions and 262 deletions

View File

@@ -3,12 +3,14 @@
align-self: flex-start;
display: grid;
gap: var(--Spacing-x2);
container-name: addressContainer;
container-type: inline-size;
}
.container {
display: grid;
gap: var(--Spacing-x2);
grid-template-columns: max(164px) 1fr;
grid-template-columns: minmax(100px, 164px) 1fr;
}
@media (min-width: 768px) {
@@ -16,3 +18,9 @@
display: none;
}
}
@container addressContainer (max-width: 350px) {
.container {
grid-template-columns: 1fr;
}
}

View File

@@ -48,7 +48,6 @@ export default function SignupForm({ link, subtitle, title }: SignUpFormProps) {
zipCode: "",
},
password: "",
termsAccepted: false,
},
mode: "all",
criteriaMode: "all",

View File

@@ -4,14 +4,8 @@ import { useEffect, useState } from "react"
import { useWatch } from "react-hook-form"
import { useIntl } from "react-intl"
import { privacyPolicy } from "@/constants/currentWebHrefs"
import Checkbox from "@/components/TempDesignSystem/Form/Checkbox"
import DateSelect from "@/components/TempDesignSystem/Form/Date"
import Input from "@/components/TempDesignSystem/Form/Input"
import JoinScandicFriendsCard from "@/components/TempDesignSystem/Form/JoinScandicFriendsCard"
import Link from "@/components/TempDesignSystem/Link"
import Body from "@/components/TempDesignSystem/Text/Body"
import Caption from "@/components/TempDesignSystem/Text/Caption"
import useLang from "@/hooks/useLang"
@@ -31,49 +25,27 @@ export default function Signup({ name }: { name: string }) {
setIsJoinChecked(joinValue)
}, [joinValue])
return (
<div className={styles.container}>
<JoinScandicFriendsCard
name={name}
difference={{ price: 1000, currency: "SEK" }}
return isJoinChecked ? (
<div className={styles.additionalFormData}>
<Input
name="zipCode"
label={intl.formatMessage({ id: "Zip code" })}
registerOptions={{ required: true }}
/>
{isJoinChecked ? (
<div className={styles.additionalFormData}>
<div className={styles.dateField}>
<header>
<Caption type="bold">
{intl.formatMessage({ id: "Birth date" })} *
</Caption>
</header>
<DateSelect
name="dateOfBirth"
registerOptions={{ required: true }}
/>
<Input
name="zipCode"
label={intl.formatMessage({ id: "Zip code" })}
registerOptions={{ required: true }}
/>
</div>
<div>
<Checkbox name="termsAccepted" registerOptions={{ required: true }}>
<Body>
{intl.formatMessage({
id: "Yes, I accept the Terms and conditions for Scandic Friends and understand that Scandic will process my personal data in accordance with",
})}{" "}
<Link
variant="underscored"
color="peach80"
target="_blank"
href={privacyPolicy[lang]}
>
{intl.formatMessage({ id: "Scandic's Privacy Policy." })}
</Link>
</Body>
</Checkbox>
</div>
</div>
) : null}
<div className={styles.dateField}>
<header>
<Caption type="bold">
{intl.formatMessage({ id: "Birth date" })} *
</Caption>
</header>
<DateSelect name="dateOfBirth" registerOptions={{ required: true }} />
</div>
</div>
) : (
<Input
label={intl.formatMessage({ id: "Membership no" })}
name="membershipNo"
type="tel"
/>
)
}

View File

@@ -1,34 +1,30 @@
.form {
display: grid;
gap: var(--Spacing-x1);
padding: var(--Spacing-x3) 0px;
gap: var(--Spacing-x3);
margin-bottom: var(--Spacing-x3);
}
.container {
display: grid;
gap: var(--Spacing-x1);
grid-template-columns: 1fr 1fr;
gap: var(--Spacing-x2);
width: min(100%, 600px);
}
.header,
.country,
.email,
.membershipNo,
.signup,
.phone {
grid-column: 1/-1;
}
.footer {
display: grid;
gap: var(--Spacing-x3);
justify-items: flex-start;
margin-top: var(--Spacing-x1);
}
@media screen and (min-width: 1367px) {
@media screen and (min-width: 768px) {
.form {
gap: var(--Spacing-x2);
padding: var(--Spacing-x3) 0px;
gap: var(--Spacing-x3);
}
.container {
@@ -36,18 +32,4 @@
grid-template-columns: 1fr 1fr;
width: min(100%, 600px);
}
.country,
.email,
.membershipNo,
.phone {
grid-column: 1/-1;
}
.footer {
display: grid;
gap: var(--Spacing-x3);
justify-items: flex-start;
margin-top: var(--Spacing-x1);
}
}

View File

@@ -10,6 +10,7 @@ import { useStepsStore } from "@/stores/steps"
import Button from "@/components/TempDesignSystem/Button"
import CountrySelect from "@/components/TempDesignSystem/Form/Country"
import Input from "@/components/TempDesignSystem/Form/Input"
import JoinScandicFriendsCard from "@/components/TempDesignSystem/Form/JoinScandicFriendsCard"
import Phone from "@/components/TempDesignSystem/Form/Phone"
import Footnote from "@/components/TempDesignSystem/Text/Footnote"
@@ -35,7 +36,6 @@ export default function Details({ user }: DetailsProps) {
join: state.data.join,
dateOfBirth: state.data.dateOfBirth,
zipCode: state.data.zipCode,
termsAccepted: state.data.termsAccepted,
membershipNo: state.data.membershipNo,
}))
@@ -52,7 +52,6 @@ export default function Details({ user }: DetailsProps) {
join: initialData.join,
dateOfBirth: initialData.dateOfBirth,
zipCode: initialData.zipCode,
termsAccepted: initialData.termsAccepted,
membershipNo: initialData.membershipNo,
},
criteriaMode: "all",
@@ -69,6 +68,7 @@ export default function Details({ user }: DetailsProps) {
[completeStep, updateDetails]
)
const joinValue = methods.watch("join")
return (
<FormProvider {...methods}>
<form
@@ -76,15 +76,21 @@ export default function Details({ user }: DetailsProps) {
id={formID}
onSubmit={methods.handleSubmit(onSubmit)}
>
{user ? null : <Signup name="join" />}
<Footnote
color="uiTextHighContrast"
textTransform="uppercase"
type="label"
>
{intl.formatMessage({ id: "Guest information" })}
</Footnote>
{user ? null : (
<JoinScandicFriendsCard
name="join"
difference={{ price: 1000, currency: "SEK" }}
/>
)}
<div className={styles.container}>
<Footnote
color="uiTextHighContrast"
textTransform="uppercase"
type="label"
className={styles.header}
>
{intl.formatMessage({ id: "Guest information" })}
</Footnote>
<Input
label={intl.formatMessage({ id: "First name" })}
name="firstName"
@@ -118,13 +124,10 @@ export default function Details({ user }: DetailsProps) {
readOnly={!!user}
registerOptions={{ required: true }}
/>
{user || methods.watch("join") ? null : (
<Input
className={styles.membershipNo}
label={intl.formatMessage({ id: "Membership no" })}
name="membershipNo"
type="tel"
/>
{user ? null : (
<div className={styles.signup}>
<Signup name="join" />
</div>
)}
</div>
<footer className={styles.footer}>

View File

@@ -15,7 +15,6 @@ export const notJoinDetailsSchema = baseDetailsSchema.merge(
join: z.literal<boolean>(false),
zipCode: z.string().optional(),
dateOfBirth: z.string().optional(),
termsAccepted: z.boolean().default(false),
membershipNo: z
.string()
.optional()
@@ -39,15 +38,6 @@ export const joinDetailsSchema = baseDetailsSchema.merge(
join: z.literal<boolean>(true),
zipCode: z.string().min(1, { message: "Zip code is required" }),
dateOfBirth: z.string().min(1, { message: "Date of birth is required" }),
termsAccepted: z.literal<boolean>(true, {
errorMap: (err, ctx) => {
switch (err.code) {
case "invalid_literal":
return { message: "You must accept the terms and conditions" }
}
return { message: ctx.defaultError }
},
}),
membershipNo: z.string().optional(),
})
)

View File

@@ -66,7 +66,7 @@ export default function SectionAccordion({
const textColor =
isComplete || isOpen ? "uiTextHighContrast" : "baseTextDisabled"
return (
<section className={styles.wrapper} data-open={isOpen} data-step={step}>
<div className={styles.main} data-open={isOpen} data-step={step}>
<div className={styles.iconWrapper}>
<div className={styles.circle} data-checked={isComplete}>
{isComplete ? (
@@ -74,31 +74,27 @@ export default function SectionAccordion({
) : null}
</div>
</div>
<div className={styles.main}>
<header>
<button onClick={onModify} className={styles.modifyButton}>
<Footnote
className={styles.title}
asChild
textTransform="uppercase"
type="label"
color={textColor}
>
<h2>{header}</h2>
</Footnote>
<Subtitle className={styles.selection} type="two" color={textColor}>
{title}
</Subtitle>
<header className={styles.header}>
<button onClick={onModify} className={styles.modifyButton}>
<Footnote
className={styles.title}
asChild
textTransform="uppercase"
type="label"
color={textColor}
>
<h2>{header}</h2>
</Footnote>
<Subtitle className={styles.selection} type="two" color={textColor}>
{title}
</Subtitle>
{isComplete && !isOpen && (
<ChevronDownIcon className={styles.button} color="burgundy" />
)}
</button>
</header>
<div className={styles.content}>
<div className={styles.contentWrapper}>{children}</div>
</div>
</div>
</section>
{isComplete && !isOpen && (
<ChevronDownIcon className={styles.button} color="burgundy" />
)}
</button>
</header>
<div className={styles.content}>{children}</div>
</div>
)
}

View File

@@ -1,15 +1,25 @@
.wrapper {
position: relative;
display: flex;
flex-direction: row;
gap: var(--Spacing-x-one-and-half);
.main {
gap: var(--Spacing-x3);
width: 100%;
padding-top: var(--Spacing-x3);
transition: 0.4s ease-out;
display: grid;
grid-template-areas: "circle header" "content content";
grid-template-columns: auto 1fr;
grid-template-rows: 2.4em 0fr;
column-gap: var(--Spacing-x-one-and-half);
}
.wrapper:last-child .main {
.main:last-child .main {
border-bottom: none;
}
.header {
grid-area: header;
}
.modifyButton {
display: grid;
grid-template-areas: "title button" "selection button";
@@ -17,6 +27,7 @@
background-color: transparent;
border: none;
width: 100%;
padding: 0;
}
.title {
@@ -29,15 +40,6 @@
justify-self: flex-end;
}
.main {
display: grid;
width: 100%;
border-bottom: 1px solid var(--Primary-Light-On-Surface-Divider-subtle);
padding-bottom: var(--Spacing-x3);
transition: 0.4s ease-out;
grid-template-rows: 2em 0fr;
}
.selection {
font-weight: 450;
font-size: var(--typography-Title-4-fontSize);
@@ -46,6 +48,7 @@
.iconWrapper {
position: relative;
grid-area: circle;
}
.circle {
@@ -63,42 +66,42 @@
background-color: var(--UI-Input-Controls-Fill-Selected);
}
.wrapper[data-open="true"] .circle[data-checked="false"] {
.main[data-open="true"] .circle[data-checked="false"] {
background-color: var(--UI-Text-Placeholder);
}
.wrapper[data-open="false"] .circle[data-checked="false"] {
.main[data-open="false"] .circle[data-checked="false"] {
background-color: var(--Base-Surface-Subtle-Hover);
}
.wrapper[data-open="true"] .main {
grid-template-rows: 2em 1fr;
.main[data-open="true"] {
grid-template-rows: 2.4em 1fr;
gap: var(--Spacing-x3);
}
.content {
overflow: hidden;
grid-area: content;
border-bottom: 1px solid var(--Primary-Light-On-Surface-Divider-subtle);
}
.contentWrapper {
padding-top: var(--Spacing-x3);
}
@media screen and (min-width: 1367px) {
.wrapper {
@media screen and (min-width: 768px) {
.main {
gap: var(--Spacing-x3);
grid-template-areas: "circle header" "circle content";
}
.iconWrapper {
top: var(--Spacing-x1);
}
.wrapper:not(:last-child)::after {
.main:not(:last-child) .iconWrapper::after {
position: absolute;
left: 12px;
bottom: 0;
top: var(--Spacing-x7);
height: 100%;
bottom: calc(0px - var(--Spacing-x7));
top: 24px;
content: "";
border-left: 1px solid var(--Primary-Light-On-Surface-Divider-subtle);
}
}
}

View File

@@ -63,7 +63,7 @@
justify-content: flex-start;
}
@media screen and (min-width: 1367px) {
@media screen and (min-width: 768px) {
.wrapper {
gap: var(--Spacing-x3);
padding-top: var(--Spacing-x3);

View File

@@ -9,6 +9,7 @@
padding: var(--Spacing-x-one-and-half) var(--Spacing-x2);
transition: all 200ms ease;
width: min(100%, 600px);
height: 100%;
}
.label:hover {

View File

@@ -1,4 +1,8 @@
/* Leaving, will most likely get deleted */
.datePicker {
container-name: datePickerContainer;
container-type: inline-size;
}
.container {
display: grid;
gap: var(--Spacing-x2);
@@ -27,3 +31,10 @@
.year.invalid > div > div {
border-color: var(--Scandic-Red-60);
}
@container datePickerContainer (max-width: 350px) {
.container {
display: flex;
flex-direction: column;
}
}

View File

@@ -115,6 +115,7 @@ export default function DateSelect({ name, registerOptions = {} }: DateProps) {
ref={field.ref}
value={dateValue}
data-testid={name}
className={styles.datePicker}
>
<Group>
<DateInput className={styles.container}>

View File

@@ -35,43 +35,41 @@ export default function JoinScandicFriendsCard({
return (
<div className={styles.cardContainer}>
<header className={styles.header}>
<Checkbox name={name} className={styles.checkBox}>
<div>
<Caption type="label" textTransform="uppercase" color="red">
{intl.formatMessage(
{
id: "Only pay {amount} {currency}",
},
{
amount: intl.formatNumber(difference.price),
currency: difference.currency,
}
)}
</Caption>
<Caption
type="label"
textTransform="uppercase"
color="uiTextHighContrast"
>
{intl.formatMessage({ id: "Join Scandic Friends" })}
</Caption>
</div>
</Checkbox>
<Footnote color="uiTextHighContrast">
{intl.formatMessage({ id: "Already a friend?" })}{" "}
<LoginButton
color="burgundy"
position="enter details"
trackingId="join-scandic-friends-enter-details"
variant="breadcrumb"
target="_blank"
<Checkbox name={name} className={styles.checkBox}>
<div>
<Caption type="label" textTransform="uppercase" color="red">
{intl.formatMessage(
{
id: "Only pay {amount} {currency}",
},
{
amount: intl.formatNumber(difference.price),
currency: difference.currency,
}
)}
</Caption>
<Caption
type="label"
textTransform="uppercase"
color="uiTextHighContrast"
>
{intl.formatMessage({ id: "Log in" })}
</LoginButton>
</Footnote>
</header>
{intl.formatMessage({ id: "Join Scandic Friends" })}
</Caption>
</div>
</Checkbox>
<Footnote color="uiTextHighContrast" className={styles.login}>
{intl.formatMessage({ id: "Already a friend?" })}{" "}
<LoginButton
color="burgundy"
position="enter details"
trackingId="join-scandic-friends-enter-details"
variant="breadcrumb"
target="_blank"
>
{intl.formatMessage({ id: "Log in" })}
</LoginButton>
</Footnote>
<div className={styles.list}>
{list.map((item) => (
@@ -84,7 +82,7 @@ export default function JoinScandicFriendsCard({
</Caption>
))}
</div>
<Footnote color="uiTextPlaceholder">
<Footnote color="uiTextPlaceholder" className={styles.terms}>
{intl.formatMessage<React.ReactNode>(
{
id: "signup.terms",

View File

@@ -4,26 +4,52 @@
border: 1px solid var(--Base-Border-Subtle);
border-radius: var(--Corner-radius-Large);
display: grid;
gap: var(--Spacing-x2);
gap: var(--Spacing-x-one-and-half);
padding: var(--Spacing-x-one-and-half) var(--Spacing-x2);
grid-template-areas:
"checkbox"
"list"
"login"
"terms";
width: min(100%, 600px);
}
.header {
display: grid;
gap: var(--Spacing-x-one-and-half);
grid-template-columns: 1fr auto;
.login {
grid-area: login;
}
.checkBox {
align-self: center;
grid-area: checkbox;
}
.list {
display: flex;
display: grid;
grid-area: list;
gap: var(--Spacing-x1);
}
.listItem {
display: flex;
}
.terms {
border-top: 1px solid var(--Base-Border-Normal);
grid-area: terms;
padding-top: var(--Spacing-x1);
}
@media screen and (min-width: 768px) {
.cardContainer {
grid-template-columns: 1fr auto;
gap: var(--Spacing-x2);
grid-template-areas:
"checkbox login"
"list list"
"terms terms";
}
.list {
display: flex;
gap: var(--Spacing-x1);
}
}

View File

@@ -78,67 +78,69 @@ export default function Phone({
}
return (
<div className={`${styles.phone} ${className}`}>
<CountrySelector
disabled={readOnly}
dropdownArrowClassName={styles.arrow}
flagClassName={styles.flag}
onSelect={handleSelectCountry}
preferredCountries={["de", "dk", "fi", "no", "se", "gb"]}
selectedCountry={country.iso2}
renderButtonWrapper={(props) => (
<button
{...props.rootProps}
className={styles.select}
tabIndex={0}
type="button"
data-testid="country-selector"
>
<Label required={!!registerOptions.required} size="small">
{intl.formatMessage({ id: "Country code" })}
</Label>
<span className={styles.selectContainer}>
{props.children}
<Body asChild fontOnly>
<DialCodePreview
className={styles.dialCode}
dialCode={country.dialCode}
prefix="+"
<div className={`${styles.wrapper} ${className}`}>
<div className={styles.phone}>
<CountrySelector
disabled={readOnly}
dropdownArrowClassName={styles.arrow}
flagClassName={styles.flag}
onSelect={handleSelectCountry}
preferredCountries={["de", "dk", "fi", "no", "se", "gb"]}
selectedCountry={country.iso2}
renderButtonWrapper={(props) => (
<button
{...props.rootProps}
className={styles.select}
tabIndex={0}
type="button"
data-testid="country-selector"
>
<Label required={!!registerOptions.required} size="small">
{intl.formatMessage({ id: "Country code" })}
</Label>
<span className={styles.selectContainer}>
{props.children}
<Body asChild fontOnly>
<DialCodePreview
className={styles.dialCode}
dialCode={country.dialCode}
prefix="+"
/>
</Body>
<ChevronDownIcon
className={styles.chevron}
color="grey80"
height={18}
width={18}
/>
</Body>
<ChevronDownIcon
className={styles.chevron}
color="grey80"
height={18}
width={18}
/>
</span>
</button>
)}
/>
<TextField
aria-label={ariaLabel}
defaultValue={field.value}
isDisabled={disabled ?? field.disabled}
isInvalid={fieldState.invalid}
isRequired={!!registerOptions?.required}
isReadOnly={readOnly}
name={field.name}
type="tel"
>
<AriaInputWithLabel
{...field}
id={field.name}
label={label}
onChange={handleChange}
placeholder={placeholder}
readOnly={readOnly}
required={!!registerOptions.required}
type="tel"
value={inputValue}
</span>
</button>
)}
/>
<ErrorMessage errors={formState.errors} name={field.name} />
</TextField>
<TextField
aria-label={ariaLabel}
defaultValue={field.value}
isDisabled={disabled ?? field.disabled}
isInvalid={fieldState.invalid}
isRequired={!!registerOptions?.required}
isReadOnly={readOnly}
name={field.name}
type="tel"
>
<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>
</div>
)
}

View File

@@ -1,3 +1,7 @@
.wrapper {
container-name: phoneContainer;
container-type: inline-size;
}
.phone {
display: grid;
gap: var(--Spacing-x2);
@@ -100,3 +104,10 @@
justify-self: flex-start;
padding: 0;
}
@container phoneContainer (max-width: 350px) {
.phone {
display: flex;
flex-direction: column;
}
}

View File

@@ -94,7 +94,6 @@ export function createDetailsStore(
state.data.membershipNo = data.membershipNo
}
state.data.phoneNumber = data.phoneNumber
state.data.termsAccepted = data.termsAccepted
state.data.zipCode = data.zipCode
})
)