Merged develop into feat/setup-hotel-api-call

This commit is contained in:
Chuma Mcphoy (We Ahead)
2024-07-04 09:37:31 +00:00
30 changed files with 588 additions and 134 deletions

View File

@@ -7,6 +7,7 @@ import { languageSelect } from "@/constants/languages"
import CountrySelect from "@/components/TempDesignSystem/Form/Country"
import DateSelect from "@/components/TempDesignSystem/Form/Date"
import Input from "@/components/TempDesignSystem/Form/Input"
import NewPassword from "@/components/TempDesignSystem/Form/NewPassword"
import Phone from "@/components/TempDesignSystem/Form/Phone"
import Select from "@/components/TempDesignSystem/Form/Select"
import Body from "@/components/TempDesignSystem/Text/Body"
@@ -21,8 +22,8 @@ export default function FormContent() {
const country = formatMessage({ id: "Country" })
const email = `${formatMessage({ id: "Email" })} ${formatMessage({ id: "Address" }).toLowerCase()}`
const street = formatMessage({ id: "Address" })
const phoneNumber = formatMessage({ id: "Phone number" })
const password = formatMessage({ id: "Current password" })
const newPassword = formatMessage({ id: "New password" })
const retypeNewPassword = formatMessage({ id: "Retype new password" })
const zipCode = formatMessage({ id: "Zip code" })
@@ -46,7 +47,7 @@ export default function FormContent() {
label={zipCode}
name="address.zipCode"
placeholder={zipCode}
required
registerOptions={{ required: true }}
/>
<CountrySelect
label={country}
@@ -55,12 +56,17 @@ export default function FormContent() {
registerOptions={{ required: true }}
/>
</div>
<Input label={email} name="email" placeholder={email} required />
<Phone
label={formatMessage({ id: "Phone number" })}
name="phoneNumber"
placeholder={formatMessage({ id: "Phone number" })}
<Input
label={email}
name="email"
placeholder={email}
registerOptions={{ required: true }}
type="email"
/>
<Phone
label={phoneNumber}
name="phoneNumber"
placeholder={phoneNumber}
/>
<Select
items={languageSelect}
@@ -79,12 +85,7 @@ export default function FormContent() {
placeholder={password}
type="password"
/>
<Input
label={newPassword}
name="newPassword"
placeholder={newPassword}
type="password"
/>
<NewPassword />
<Input
label={retypeNewPassword}
name="retypeNewPassword"

View File

@@ -1,13 +1,16 @@
"use client"
import { zodResolver } from "@hookform/resolvers/zod"
import { useParams } from "next/navigation"
import { useFormState as useReactFormState } from "react-dom"
import { FormProvider, useForm } from "react-hook-form"
import { useIntl } from "react-intl"
import { profile } from "@/constants/routes/myPages"
import { editProfile } from "@/actions/editProfile"
import Header from "@/components/Profile/Header"
import Button from "@/components/TempDesignSystem/Button"
import Divider from "@/components/TempDesignSystem/Divider"
import Link from "@/components/TempDesignSystem/Link"
import Title from "@/components/TempDesignSystem/Text/Title"
import FormContent from "./FormContent"
@@ -19,11 +22,14 @@ import type {
EditFormProps,
State,
} from "@/types/components/myPages/myProfile/edit"
import type { Lang } from "@/constants/languages"
const formId = "edit-profile"
export default function Form({ user }: EditFormProps) {
const { formatMessage } = useIntl()
const params = useParams()
const lang = params.lang as Lang
/**
* like react, react-hook-form also exports a useFormState hook,
* we want to clearly keep them separate by naming.
@@ -33,12 +39,14 @@ export default function Form({ user }: EditFormProps) {
null
)
const form = useForm<EditProfileSchema>({
const methods = useForm<EditProfileSchema>({
defaultValues: {
"address.city": user.address.city ?? "",
"address.countryCode": user.address.countryCode ?? "",
"address.streetAddress": user.address.streetAddress ?? "",
"address.zipCode": user.address.zipCode ?? "",
address: {
city: user.address.city ?? "",
countryCode: user.address.countryCode ?? "",
streetAddress: user.address.streetAddress ?? "",
zipCode: user.address.zipCode ?? "",
},
dateOfBirth: user.dateOfBirth,
email: user.email,
language: user.language,
@@ -53,7 +61,7 @@ export default function Form({ user }: EditFormProps) {
})
return (
<FormProvider {...form}>
<>
<Header>
<hgroup>
<Title as="h4" color="red" level="h1">
@@ -64,17 +72,15 @@ export default function Form({ user }: EditFormProps) {
</Title>
</hgroup>
<div className={styles.btns}>
<Button
form={formId}
intent="secondary"
size="small"
theme="base"
type="reset"
>
{formatMessage({ id: "Discard changes" })}
<Button asChild intent="secondary" size="small" theme="base">
<Link href={profile[lang]}>
{formatMessage({ id: "Discard changes" })}
</Link>
</Button>
<Button
disabled={!form.formState.isValid || form.formState.isSubmitting}
disabled={
!methods.formState.isValid || methods.formState.isSubmitting
}
form={formId}
intent="primary"
size="small"
@@ -85,10 +91,11 @@ export default function Form({ user }: EditFormProps) {
</Button>
</div>
</Header>
<Divider color="burgundy" opacity={8} />
<form action={formAction} className={styles.form} id={formId}>
<FormContent />
<FormProvider {...methods}>
<FormContent />
</FormProvider>
</form>
</FormProvider>
</>
)
}

View File

@@ -1,24 +1,98 @@
import { z } from "zod"
// import { phoneValidator } from "@/utils/phoneValidator"
import { Key } from "@/components/TempDesignSystem/Form/NewPassword/newPassword"
import { phoneValidator } from "@/utils/phoneValidator"
export const editProfileSchema = z.object({
"address.city": z.string().optional(),
"address.countryCode": z.string().min(1),
"address.streetAddress": z.string().optional(),
"address.zipCode": z.string().min(1),
dateOfBirth: z.string().min(1),
email: z.string().email(),
language: z.string(),
phoneNumber: z.string(),
// phoneValidator(
// "Phone is required",
// "Please enter a valid phone number"
// ),
const countryRequiredMsg = "Country is required"
export const editProfileSchema = z
.object({
address: z.object({
city: z.string().optional(),
countryCode: z
.string({
required_error: countryRequiredMsg,
invalid_type_error: countryRequiredMsg,
})
.min(1, countryRequiredMsg),
streetAddress: z.string().optional(),
zipCode: z.string().min(1, "Zip code is required"),
}),
dateOfBirth: z.string().min(1),
email: z.string().email(),
language: z.string(),
phoneNumber: phoneValidator(
"Phone is required",
"Please enter a valid phone number"
),
currentPassword: z.string().optional(),
newPassword: z.string().optional(),
retypeNewPassword: z.string().optional(),
})
currentPassword: z.string().optional(),
newPassword: z.string().optional(),
retypeNewPassword: z.string().optional(),
})
.superRefine((data, ctx) => {
if (data.currentPassword) {
if (!data.newPassword) {
ctx.addIssue({
code: "custom",
message: "New password is required",
path: ["newPassword"],
})
}
if (!data.retypeNewPassword) {
ctx.addIssue({
code: "custom",
message: "Retype new password is required",
path: ["retypeNewPassword"],
})
}
} else {
if (data.newPassword || data.retypeNewPassword) {
ctx.addIssue({
code: "custom",
message: "Current password is required",
path: ["currentPassword"],
})
}
}
if (data.newPassword) {
const msgs = []
if (data.newPassword.length < 10 || data.newPassword.length > 40) {
msgs.push(Key.CHAR_LENGTH)
}
if (!data.newPassword.match(/[A-Z]/g)) {
msgs.push(Key.UPPERCASE)
}
if (!data.newPassword.match(/[0-9]/g)) {
msgs.push(Key.NUM)
}
if (!data.newPassword.match(/[^A-Za-z0-9]/g)) {
msgs.push(Key.SPECIAL_CHAR)
}
if (msgs.length) {
ctx.addIssue({
code: "custom",
message: msgs.join(","),
path: ["newPassword"],
})
}
}
if (data.newPassword && !data.retypeNewPassword) {
ctx.addIssue({
code: "custom",
message: "Retype new password is required",
path: ["retypeNewPassword"],
})
}
if (data.retypeNewPassword !== data.newPassword) {
ctx.addIssue({
code: "custom",
message: "Retype new password does not match new password",
path: ["retypeNewPassword"],
})
}
})
export type EditProfileSchema = z.infer<typeof editProfileSchema>

View File

@@ -0,0 +1,36 @@
import { iconVariants } from "./variants"
import type { IconProps } from "@/types/components/icon"
export default function CloseIcon({ className, color, ...props }: IconProps) {
const classNames = iconVariants({ className, color })
return (
<svg
className={classNames}
fill="none"
height="20"
viewBox="0 0 20 20"
width="20"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<mask
height="20"
id="mask0_7531_17458"
maskUnits="userSpaceOnUse"
style={{ maskType: "alpha" }}
width="20"
x="0"
y="0"
>
<rect width="20" height="20" fill="#D9D9D9" />
</mask>
<g mask="url(#mask0_7531_17458)">
<path
d="M10 11.0936L7.58333 13.5207C7.43056 13.6734 7.24653 13.7498 7.03125 13.7498C6.81597 13.7498 6.63194 13.6734 6.47917 13.5207C6.32639 13.3679 6.25 13.1839 6.25 12.9686C6.25 12.7533 6.32639 12.5693 6.47917 12.4165L8.90625 9.99984L6.47917 7.59359C6.32639 7.44081 6.25 7.25678 6.25 7.0415C6.25 6.82623 6.32639 6.6422 6.47917 6.48942C6.63194 6.33664 6.81597 6.26025 7.03125 6.26025C7.24653 6.26025 7.43056 6.33664 7.58333 6.48942L10 8.9165L12.4062 6.48942C12.559 6.33664 12.7431 6.26025 12.9583 6.26025C13.1736 6.26025 13.3576 6.33664 13.5104 6.48942C13.6701 6.64914 13.75 6.83491 13.75 7.04671C13.75 7.25852 13.6701 7.44081 13.5104 7.59359L11.0833 9.99984L13.5104 12.4165C13.6632 12.5693 13.7396 12.7533 13.7396 12.9686C13.7396 13.1839 13.6632 13.3679 13.5104 13.5207C13.3507 13.6804 13.1649 13.7603 12.9531 13.7603C12.7413 13.7603 12.559 13.6804 12.4062 13.5207L10 11.0936Z"
fill="#CD0921"
/>
</g>
</svg>
)
}

View File

@@ -6,6 +6,7 @@ export { default as CheckIcon } from "./Check"
export { default as CheckCircleIcon } from "./CheckCircle"
export { default as ChevronDownIcon } from "./ChevronDown"
export { default as ChevronRightIcon } from "./ChevronRight"
export { default as CloseIcon } from "./Close"
export { default as EmailIcon } from "./Email"
export { default as GlobeIcon } from "./Globe"
export { default as HouseIcon } from "./House"

View File

@@ -19,6 +19,13 @@
padding: var(--Spacing-x1) var(--Spacing-x2);
}
.comboBoxContainer:has(
.input[data-invalid="true"],
.input[aria-invalid="true"]
) {
border-color: var(--Scandic-Red-60);
}
.label {
grid-area: label;
}
@@ -66,6 +73,8 @@
var(--Spacing-x2);
}
.listBoxItem[data-focused="true"],
.listBoxItem[data-focus-visible="true"],
.listBoxItem[data-selected="true"],
.listBoxItem:hover {
background-color: var(--Scandic-Blue-00);

View File

@@ -1,10 +1,8 @@
"use client"
import { ErrorMessage } from "@hookform/error-message"
import { useState } from "react"
import {
Button,
ComboBox,
FieldError,
Input,
type Key,
ListBox,
@@ -18,6 +16,7 @@ import Label from "@/components/TempDesignSystem/Form/Label"
import SelectChevron from "@/components/TempDesignSystem/Form/SelectChevron"
import Body from "@/components/TempDesignSystem/Text/Body"
import ErrorMessage from "../ErrorMessage"
import { countries } from "./countries"
import styles from "./country.module.css"
@@ -42,7 +41,7 @@ export default function CountrySelect({
}
}
const { control, setValue } = useFormContext()
const { field } = useController({
const { field, fieldState, formState } = useController({
control,
name,
rules: registerOptions,
@@ -60,6 +59,7 @@ export default function CountrySelect({
aria-label={formatMessage({ id: "Select country of residence" })}
className={styles.select}
isRequired={!!registerOptions?.required}
isInvalid={fieldState.invalid}
name={field.name}
onBlur={field.onBlur}
onSelectionChange={handleChange}
@@ -85,9 +85,7 @@ export default function CountrySelect({
<SelectChevron />
</Button>
</div>
<FieldError>
<ErrorMessage name={name} />
</FieldError>
<ErrorMessage errors={formState.errors} name={name} />
<Popover
className={styles.popover}
placement="bottom"

View File

@@ -8,5 +8,5 @@ export const enum DateName {
export interface DateProps
extends React.SelectHTMLAttributes<HTMLSelectElement> {
name: string
registerOptions: RegisterOptions
registerOptions?: RegisterOptions
}

View File

@@ -1,11 +1,6 @@
"use client"
import { parseDate } from "@internationalized/date"
import {
DateInput,
DatePicker,
DateSegment,
Group,
} from "react-aria-components"
import { DateInput, DatePicker, Group } from "react-aria-components"
import { useController, useFormContext, useWatch } from "react-hook-form"
import { useIntl } from "react-intl"
@@ -23,7 +18,7 @@ import type { Key } from "react-aria-components"
import type { DateProps } from "./date"
/** TODO: Get selecting with Enter-key to work */
export default function DateSelect({ name, registerOptions }: DateProps) {
export default function DateSelect({ name, registerOptions = {} }: DateProps) {
const { formatMessage } = useIntl()
const d = useWatch({ name })
const { control, setValue } = useFormContext()
@@ -84,7 +79,7 @@ export default function DateSelect({ name, registerOptions }: DateProps) {
)
}
return (
<DateSegment className={styles.day} segment={segment}>
<div className={styles.day}>
<Select
aria-label={dayLabel}
items={days}
@@ -92,13 +87,15 @@ export default function DateSelect({ name, registerOptions }: DateProps) {
name={DateName.date}
onSelect={createOnSelect(DateName.date)}
placeholder="DD"
required
tabIndex={3}
value={segment.value}
/>
</DateSegment>
</div>
)
case "month":
return (
<DateSegment className={styles.month} segment={segment}>
<div className={styles.month}>
<Select
aria-label={monthLabel}
items={months}
@@ -106,13 +103,15 @@ export default function DateSelect({ name, registerOptions }: DateProps) {
name={DateName.month}
onSelect={createOnSelect(DateName.month)}
placeholder="MM"
required
tabIndex={2}
value={segment.value}
/>
</DateSegment>
</div>
)
case "year":
return (
<DateSegment className={styles.year} segment={segment}>
<div className={styles.year}>
<Select
aria-label={yearLabel}
items={years}
@@ -120,9 +119,11 @@ export default function DateSelect({ name, registerOptions }: DateProps) {
name={DateName.year}
onSelect={createOnSelect(DateName.year)}
placeholder="YYYY"
required
tabIndex={1}
value={segment.value}
/>
</DateSegment>
</div>
)
default:
/** DateInput forces return of ReactElement */

View File

@@ -0,0 +1,13 @@
import { InfoCircleIcon } from "@/components/Icons"
import Caption from "@/components/TempDesignSystem/Text/Caption"
import styles from "./error.module.css"
export default function Error({ children }: React.PropsWithChildren) {
return (
<Caption className={styles.message} fontOnly>
<InfoCircleIcon color="red" />
{children}
</Caption>
)
}

View File

@@ -1,9 +1,6 @@
import { ErrorMessage as RHFErrorMessage } from "@hookform/error-message"
import { InfoCircleIcon } from "@/components/Icons"
import Caption from "@/components/TempDesignSystem/Text/Caption"
import styles from "./error.module.css"
import Error from "./Error"
import type { ErrorMessageProps } from "./errorMessage"
@@ -15,12 +12,7 @@ export default function ErrorMessage<T>({
<RHFErrorMessage
errors={errors}
name={name}
render={({ message }) => (
<Caption className={styles.message} fontOnly>
<InfoCircleIcon color="red" />
{message}
</Caption>
)}
render={({ message }) => <Error>{message}</Error>}
/>
)
}

View File

@@ -2,66 +2,86 @@
import {
Input as AriaInput,
Label as AriaLabel,
Text,
TextField,
} from "react-aria-components"
import { useController, useFormContext } from "react-hook-form"
import { Controller, useFormContext } from "react-hook-form"
import ErrorMessage from "@/components/TempDesignSystem/Form/ErrorMessage"
import { CheckIcon, InfoCircleIcon } from "@/components/Icons"
import Label from "@/components/TempDesignSystem/Form/Label"
import Body from "@/components/TempDesignSystem/Text/Body"
import Caption from "@/components/TempDesignSystem/Text/Caption"
import styles from "./input.module.css"
import type { HTMLAttributes, WheelEvent } from "react"
import type { InputProps } from "./input"
export default function Input({
"aria-label": ariaLabel,
disabled,
disabled = false,
helpText = "",
label,
name,
placeholder = "",
registerOptions = {},
required = false,
type = "text",
}: InputProps) {
const { control } = useFormContext()
const rules = {
...registerOptions,
required:
"required" in registerOptions ? !!registerOptions.required : required,
let numberAttributes: HTMLAttributes<HTMLInputElement> = {}
if (type === "number") {
numberAttributes.onWheel = function (evt: WheelEvent<HTMLInputElement>) {
evt.currentTarget.blur()
}
}
const { field, fieldState, formState } = useController({
control,
name,
rules,
})
return (
<TextField
aria-label={ariaLabel}
defaultValue={field.value}
isDisabled={disabled ?? field.disabled}
isInvalid={fieldState.invalid}
isRequired={!!registerOptions?.required}
name={field.name}
type={type}
>
<AriaLabel className={styles.container} htmlFor={field.name}>
<Body asChild fontOnly>
<AriaInput
className={styles.input}
id={field.name}
name={field.name}
onBlur={field.onBlur}
onChange={field.onChange}
placeholder={placeholder}
ref={field.ref}
required={rules.required}
/>
</Body>
<Label required={rules.required}>{label}</Label>
</AriaLabel>
<ErrorMessage errors={formState.errors} name={field.name} />
</TextField>
<Controller
disabled={disabled}
control={control}
name={name}
rules={registerOptions}
render={({ field, fieldState }) => (
<TextField
aria-label={ariaLabel}
isDisabled={field.disabled}
isInvalid={fieldState.invalid}
isRequired={!!registerOptions.required}
name={field.name}
onBlur={field.onBlur}
onChange={field.onChange}
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>
{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>
)}
/>
)
}

View File

@@ -22,6 +22,16 @@
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;
@@ -42,3 +52,21 @@
height: 18px;
outline: none;
}
.input:disabled {
color: var(--Main-Grey-40);
}
.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;
}

View File

@@ -1,7 +1,8 @@
import type { RegisterOptions } from "react-hook-form"
import type { RegisterOptions, UseFormRegister } from "react-hook-form"
export interface InputProps
extends React.InputHTMLAttributes<HTMLInputElement> {
helpText?: string
label: string
name: string
registerOptions?: RegisterOptions

View File

@@ -28,3 +28,7 @@ input:placeholder-shown ~ .label {
align-self: center;
grid-row: 1/-1;
}
input:disabled ~ .label {
color: var(--Main-Grey-40);
}

View File

@@ -0,0 +1,113 @@
"use client"
import {
Input as AriaInput,
Label as AriaLabel,
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 Caption from "@/components/TempDesignSystem/Text/Caption"
import { type IconProps, Key, type NewPasswordProps } from "./newPassword"
import styles from "./newPassword.module.css"
export default function NewPassword({
"aria-label": ariaLabel,
disabled = false,
placeholder = "",
registerOptions = {},
}: NewPasswordProps) {
const { control } = useFormContext()
const { formatMessage } = useIntl()
return (
<Controller
disabled={disabled}
control={control}
name="newPassword"
rules={registerOptions}
render={({ field, fieldState }) => {
const messages = (fieldState.error?.message?.split(",") ?? []) as Key[]
return (
<TextField
aria-label={ariaLabel}
isDisabled={field.disabled}
isInvalid={fieldState.invalid}
isRequired={!!registerOptions.required}
name={field.name}
onBlur={field.onBlur}
onChange={field.onChange}
validationBehavior="aria"
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>
{field.value ? (
<div className={styles.errors}>
<Caption asChild color="black">
<Text className={styles.helpText} slot="description">
<Icon matcher={Key.CHAR_LENGTH} messages={messages} />
10 {formatMessage({ id: "to" })} 40{" "}
{formatMessage({ id: "characters" })}
</Text>
</Caption>
<Caption asChild color="black">
<Text className={styles.helpText} slot="description">
<Icon matcher={Key.UPPERCASE} messages={messages} />1{" "}
{formatMessage({ id: "uppercase letter" })}
</Text>
</Caption>
<Caption asChild color="black">
<Text className={styles.helpText} slot="description">
<Icon matcher={Key.NUM} messages={messages} />1{" "}
{formatMessage({ id: "number" })}
</Text>
</Caption>
<Caption asChild color="black">
<Text className={styles.helpText} slot="description">
<Icon matcher={Key.SPECIAL_CHAR} messages={messages} />1{" "}
{formatMessage({ id: "special character" })}
</Text>
</Caption>
</div>
) : null}
{!field.value && fieldState.error ? (
<Error>
<Text className={styles.helpText} slot="description">
{fieldState.error.message}
</Text>
</Error>
) : null}
</TextField>
)
}}
/>
)
}
function Icon({ matcher, messages }: IconProps) {
return messages.includes(matcher) ? (
<CloseIcon color="red" height={20} width={20} />
) : (
<CheckIcon color="green" height={20} width={20} />
)
}

View File

@@ -0,0 +1,71 @@
.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;
gap: var(--Spacing-x-half);
grid-template-rows: auto auto;
height: 60px;
padding: var(--Spacing-x1) var(--Spacing-x2);
transition: border-color 200ms ease;
}
.container:has(.input:not(:focus):placeholder-shown) {
gap: 0;
grid-template-rows: 1fr;
}
.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;
}
.input:focus,
.input:focus:placeholder-shown {
height: 18px;
outline: none;
}
.input:disabled {
color: var(--Main-Grey-40);
}
.errors {
display: flex;
flex-wrap: wrap;
gap: var(--Spacing-x-one-and-half) var(--Spacing-x1);
padding-top: var(--Spacing-x1);
}
.helpText {
align-items: flex-start;
display: flex;
gap: var(--Spacing-x-half);
}

View File

@@ -0,0 +1,19 @@
import type { RegisterOptions } from "react-hook-form"
export enum Key {
CHAR_LENGTH = "CHAR_LENGTH",
NUM = "NUM",
SPECIAL_CHAR = "SPECIAL_CHAR",
UPPERCASE = "UPPERCASE",
}
export interface NewPasswordProps
extends React.InputHTMLAttributes<HTMLInputElement> {
label?: string
registerOptions?: RegisterOptions
}
export interface IconProps {
matcher: Key
messages: Key[]
}

View File

@@ -132,7 +132,6 @@ export default function Phone({
</AriaLabel>
<ErrorMessage errors={formState.errors} name={field.name} />
</TextField>
<ErrorMessage errors={formState.errors} name={name} />
</div>
)
}

View File

@@ -108,6 +108,10 @@
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;

View File

@@ -31,6 +31,8 @@ export default function Select({
name,
onSelect,
placeholder,
required = false,
tabIndex,
value,
}: SelectProps) {
const [rootDiv, setRootDiv] = useState<SelectPortalContainer>(undefined)
@@ -58,8 +60,10 @@ export default function Select({
>
<Body asChild fontOnly>
<Button className={styles.input}>
<div className={styles.inputContentWrapper}>
<Label size="small">{label}</Label>
<div className={styles.inputContentWrapper} tabIndex={tabIndex}>
<Label required={required} size="small">
{label}
</Label>
<SelectValue />
</div>
<SelectChevron />

View File

@@ -58,7 +58,8 @@
padding: var(--Spacing-x2);
}
.listBoxItem[data-focused="true"] {
.listBoxItem[data-focused="true"],
.listBoxItem[data-selected="true"] {
background: var(--UI-Input-Controls-Surface-Hover, var(--Scandic-Blue-00));
outline: none;
}

View File

@@ -1,4 +1,4 @@
import type { Key } from "react-aria-components"
import type { Key, SelectProps as AriaSelectProps } from "react-aria-components"
export interface SelectProps
extends Omit<React.SelectHTMLAttributes<HTMLSelectElement>, "onSelect"> {