Merged in feat/use-new-input-component (pull request #3324)

feat(SW-3659): Use new input component

* Use new input component

* Update error formatter

* Merged master into feat/use-new-input-component

* Merged master into feat/use-new-input-component

* Merge branch 'master' into feat/use-new-input-component

* Merged master into feat/use-new-input-component

* Update Input stories

* Merge branch 'feat/use-new-input-component' of bitbucket.org:scandic-swap/web into feat/use-new-input-component

* Update Storybook logo

* Add some new demo icon input story

* Fix the clear content button position

* Fix broken password input icon

* Merged master into feat/use-new-input-component

* Merged master into feat/use-new-input-component

* Add aria-hidden to required asterisk

* Merge branch 'feat/use-new-input-component' of bitbucket.org:scandic-swap/web into feat/use-new-input-component

* Merge branch 'master' into feat/use-new-input-component


Approved-by: Bianca Widstam
Approved-by: Matilda Landström
This commit is contained in:
Rasmus Langvad
2025-12-18 15:42:09 +00:00
parent 40e1efa81f
commit b9a62b5280
34 changed files with 520 additions and 1113 deletions

View File

@@ -15,9 +15,9 @@ import {
import { FormProvider, useForm } from "react-hook-form"
import { useIntl } from "react-intl"
import { FormInput } from "@scandic-hotels/design-system/Form/FormInput"
import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
import Image from "@scandic-hotels/design-system/Image"
import { Input } from "@scandic-hotels/design-system/Input"
import Modal from "@scandic-hotels/design-system/Modal"
import { OldDSButton as Button } from "@scandic-hotels/design-system/OldDSButton"
import { Typography } from "@scandic-hotels/design-system/Typography"
@@ -87,7 +87,8 @@ export function TransferPointsFormClient({
</I18nProvider>
<div className={styles.inputsWrapper}>
<TextField type="number" isDisabled={disabled}>
<Input
<FormInput
name="ebPointsToExchange"
label={intl.formatMessage({
id: "partnerSas.ebPointsToExchange",
defaultMessage: "EB points to exchange",

View File

@@ -6,17 +6,20 @@ import { getDefaultCountryFromLang } from "@scandic-hotels/common/utils/phone"
import { Divider } from "@scandic-hotels/design-system/Divider"
import CountrySelect from "@scandic-hotels/design-system/Form/Country"
import DateSelect from "@scandic-hotels/design-system/Form/Date"
import { FormInput } from "@scandic-hotels/design-system/Form/FormInput"
import Phone from "@scandic-hotels/design-system/Form/Phone"
import { FormSelect } from "@scandic-hotels/design-system/Form/Select"
import { Typography } from "@scandic-hotels/design-system/Typography"
import { getLocalizedLanguageOptions } from "@/constants/languages"
import Input from "@/components/TempDesignSystem/Form/Input"
import PasswordInput from "@/components/TempDesignSystem/Form/PasswordInput"
import useLang from "@/hooks/useLang"
import { getFormattedCountryList } from "@/utils/countries"
import { getErrorMessage } from "@/utils/getErrorMessage"
import {
formatFormErrorMessage,
getErrorMessage,
} from "@/utils/getErrorMessage"
import styles from "./formContent.module.css"
@@ -63,7 +66,7 @@ export default function FormContent({ errors }: { errors: FieldErrors }) {
name="dateOfBirth"
registerOptions={{ required: true }}
/>
<Input
<FormInput
data-hj-suppress
label={`${intl.formatMessage({
id: "common.address",
@@ -71,7 +74,7 @@ export default function FormContent({ errors }: { errors: FieldErrors }) {
})} 1`}
name="address.streetAddress"
/>
<Input
<FormInput
data-hj-suppress
label={intl.formatMessage({
id: "common.city",
@@ -80,12 +83,13 @@ export default function FormContent({ errors }: { errors: FieldErrors }) {
name="address.city"
/>
<div className={styles.container}>
<Input
<FormInput
data-hj-suppress
label={intl.formatMessage({
id: "common.zipCode",
defaultMessage: "Zip code",
})}
errorFormatter={formatFormErrorMessage}
name="address.zipCode"
registerOptions={{ required: true }}
/>
@@ -105,7 +109,7 @@ export default function FormContent({ errors }: { errors: FieldErrors }) {
registerOptions={{ required: true }}
/>
</div>
<Input
<FormInput
label={intl.formatMessage({
id: "common.emailAddress",
defaultMessage: "Email address",

View File

@@ -15,6 +15,7 @@ import { Button } from "@scandic-hotels/design-system/Button"
import Checkbox from "@scandic-hotels/design-system/Form/Checkbox"
import CountrySelect from "@scandic-hotels/design-system/Form/Country"
import DateSelect from "@scandic-hotels/design-system/Form/Date"
import { FormInput } from "@scandic-hotels/design-system/Form/FormInput"
import Phone from "@scandic-hotels/design-system/Form/Phone"
import { TextLink } from "@scandic-hotels/design-system/TextLink"
import { TextLinkButton } from "@scandic-hotels/design-system/TextLinkButton"
@@ -29,11 +30,13 @@ import {
} from "@scandic-hotels/trpc/routers/user/schemas"
import ProfilingConsentModalReadOnly from "@/components/MyPages/ProfilingConsent/Modal/ReadOnly"
import Input from "@/components/TempDesignSystem/Form/Input"
import PasswordInput from "@/components/TempDesignSystem/Form/PasswordInput"
import useLang from "@/hooks/useLang"
import { getFormattedCountryList } from "@/utils/countries"
import { getErrorMessage } from "@/utils/getErrorMessage"
import {
formatFormErrorMessage,
getErrorMessage,
} from "@/utils/getErrorMessage"
import { requestOpen } from "@/utils/profilingConsent"
import { trackLinkClick } from "@/utils/tracking/profilingConsent"
@@ -162,7 +165,8 @@ export default function SignupForm({
</Typography>
</header>
<div className={styles.nameInputs}>
<Input
<FormInput
errorFormatter={formatFormErrorMessage}
label={intl.formatMessage({
id: "common.firstName",
defaultMessage: "First name",
@@ -170,7 +174,8 @@ export default function SignupForm({
name="firstName"
registerOptions={{ required: true }}
/>
<Input
<FormInput
errorFormatter={formatFormErrorMessage}
label={intl.formatMessage({
id: "common.lastName",
defaultMessage: "Last name",
@@ -214,7 +219,8 @@ export default function SignupForm({
/>
</div>
<div className={cx(styles.container, styles.additional)}>
<Input
<FormInput
errorFormatter={formatFormErrorMessage}
label={intl.formatMessage({
id: "common.zipCode",
defaultMessage: "Zip code",
@@ -236,7 +242,8 @@ export default function SignupForm({
name="address.countryCode"
registerOptions={{ required: true }}
/>
<Input
<FormInput
errorFormatter={formatFormErrorMessage}
label={intl.formatMessage({
id: "common.emailAddress",
defaultMessage: "Email address",

View File

@@ -6,8 +6,9 @@ import { FormProvider, useForm } from "react-hook-form"
import { useIntl } from "react-intl"
import { Button } from "@scandic-hotels/design-system/Button"
import { FormInput } from "@scandic-hotels/design-system/Form/FormInput"
import Input from "@/components/TempDesignSystem/Form/Input"
import { formatFormErrorMessage } from "@/utils/getErrorMessage"
import {
type AdditionalInfoFormSchema,
@@ -53,15 +54,16 @@ export default function AdditionalInfoForm({
<form onSubmit={form.handleSubmit(onSubmit)} className={styles.form}>
<Title isAdditional />
<div className={styles.inputs}>
<Input
<FormInput
label={intl.formatMessage({
id: "common.firstName",
defaultMessage: "First name",
})}
name="firstName"
registerOptions={{ required: true }}
errorFormatter={formatFormErrorMessage}
/>
<Input
<FormInput
label={intl.formatMessage({
id: "common.email",
defaultMessage: "Email",
@@ -69,6 +71,7 @@ export default function AdditionalInfoForm({
name="email"
type="email"
registerOptions={{ required: true }}
errorFormatter={formatFormErrorMessage}
/>
</div>
<div className={styles.buttons}>

View File

@@ -11,13 +11,14 @@ import { myStay } from "@scandic-hotels/common/constants/routes/myStay"
import { logger } from "@scandic-hotels/common/logger"
import { Alert } from "@scandic-hotels/design-system/Alert"
import { Button } from "@scandic-hotels/design-system/Button"
import { FormInput } from "@scandic-hotels/design-system/Form/FormInput"
import { TextLink } from "@scandic-hotels/design-system/TextLink"
import { toast } from "@scandic-hotels/design-system/Toast"
import { Typography } from "@scandic-hotels/design-system/Typography"
import { trpc } from "@scandic-hotels/trpc/client"
import Input from "@/components/TempDesignSystem/Form/Input"
import useLang from "@/hooks/useLang"
import { formatFormErrorMessage } from "@/utils/getErrorMessage"
import { type FindMyBookingFormSchema, findMyBookingFormSchema } from "./schema"
import { Title } from "./Title"
@@ -95,31 +96,34 @@ export default function FindMyBooking({
/>
) : null}
<div className={[styles.inputs, styles.grid].join(" ")}>
<Input
<FormInput
label={intl.formatMessage({
id: "common.bookingNumber",
defaultMessage: "Booking number",
})}
name="confirmationNumber"
registerOptions={{ required: true }}
errorFormatter={formatFormErrorMessage}
/>
<Input
<FormInput
label={intl.formatMessage({
id: "common.firstName",
defaultMessage: "First name",
})}
name="firstName"
registerOptions={{ required: true }}
errorFormatter={formatFormErrorMessage}
/>
<Input
<FormInput
label={intl.formatMessage({
id: "common.lastName",
defaultMessage: "Last name",
})}
name="lastName"
registerOptions={{ required: true }}
errorFormatter={formatFormErrorMessage}
/>
<Input
<FormInput
label={intl.formatMessage({
id: "common.email",
defaultMessage: "Email",
@@ -127,6 +131,7 @@ export default function FindMyBooking({
name="email"
type="email"
registerOptions={{ required: true }}
errorFormatter={formatFormErrorMessage}
/>
</div>
<div className={styles.buttons}>

View File

@@ -7,10 +7,10 @@ import {
getDefaultCountryFromLang,
} from "@scandic-hotels/common/utils/phone"
import CountrySelect from "@scandic-hotels/design-system/Form/Country"
import { FormInput } from "@scandic-hotels/design-system/Form/FormInput"
import Phone from "@scandic-hotels/design-system/Form/Phone"
import { Typography } from "@scandic-hotels/design-system/Typography"
import Input from "@/components/TempDesignSystem/Form/Input"
import useLang from "@/hooks/useLang"
import { getFormattedCountryList } from "@/utils/countries"
import { getErrorMessage } from "@/utils/getErrorMessage"
@@ -45,7 +45,7 @@ export default function ModifyContact({
{isFirstStep ? (
<div className={styles.container}>
<div className={`${styles.row} ${styles.gridEqual}`}>
<Input
<FormInput
label={intl.formatMessage({
id: "common.firstName",
defaultMessage: "First name",
@@ -54,7 +54,7 @@ export default function ModifyContact({
name="firstName"
disabled={!!guest.firstName}
/>
<Input
<FormInput
label={intl.formatMessage({
id: "common.lastName",
defaultMessage: "Last name",
@@ -80,7 +80,7 @@ export default function ModifyContact({
/>
</div>
<div className={styles.row}>
<Input
<FormInput
label={intl.formatMessage({
id: "common.email",
defaultMessage: "Email",

View File

@@ -1,102 +0,0 @@
"use client"
import { forwardRef, type HTMLAttributes, type WheelEvent } from "react"
import { Text, TextField } from "react-aria-components"
import { Controller, useFormContext } from "react-hook-form"
import { useIntl } from "react-intl"
import Caption from "@scandic-hotels/design-system/Caption"
import { ErrorMessage } from "@scandic-hotels/design-system/Form/ErrorMessage"
import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
import { Input as InputWithLabel } from "@scandic-hotels/design-system/Input"
import { getErrorMessage } from "@/utils/getErrorMessage"
import styles from "./input.module.css"
import type { InputProps } from "./input"
const Input = forwardRef<HTMLInputElement, InputProps>(function Input(
{
"aria-label": ariaLabel,
autoComplete,
className = "",
disabled = false,
helpText = "",
label,
maxLength,
name,
placeholder,
readOnly = false,
registerOptions = {},
type = "text",
hideError,
inputMode,
},
ref
) {
const intl = useIntl()
const { control } = useFormContext()
const numberAttributes: HTMLAttributes<HTMLInputElement> = {}
if (type === "number") {
numberAttributes.onWheel = function (evt: WheelEvent<HTMLInputElement>) {
evt.currentTarget.blur()
}
}
return (
<Controller
disabled={disabled}
control={control}
name={name}
rules={registerOptions}
render={({ field, fieldState, formState }) => (
<TextField
aria-label={ariaLabel}
className={className}
isDisabled={field.disabled}
isReadOnly={readOnly}
isInvalid={fieldState.invalid}
isRequired={!!registerOptions.required}
name={field.name}
onBlur={field.onBlur}
onChange={field.onChange}
validationBehavior="aria"
value={field.value}
>
<InputWithLabel
{...field}
ref={ref}
aria-labelledby={field.name}
autoComplete={autoComplete}
id={field.name}
label={label}
maxLength={maxLength}
placeholder={placeholder}
readOnly={readOnly}
disabled={disabled}
required={!!registerOptions.required}
type={type}
inputMode={inputMode}
/>
{helpText && !fieldState.error ? (
<Caption asChild color="black">
<Text className={styles.helpText} slot="description">
<MaterialIcon icon="check" size={20} />
{helpText}
</Text>
</Caption>
) : null}
{fieldState.error && !hideError ? (
<ErrorMessage
errors={formState.errors}
name={name}
messageLabel={getErrorMessage(intl, fieldState.error.message)}
/>
) : null}
</TextField>
)}
/>
)
})
export default Input

View File

@@ -1,17 +0,0 @@
.helpText {
align-items: flex-start;
display: flex;
gap: var(--Space-x05);
}
.error {
align-items: center;
color: var(--Text-Interactive-Error);
display: flex;
gap: var(--Space-x05);
margin: var(--Space-x1) 0 0;
}
.error svg {
min-width: 20px;
}

View File

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

View File

@@ -20,8 +20,7 @@ import { NewPasswordValidation } from "./NewPasswordValidation"
import styles from "./passwordInput.module.css"
interface PasswordInputProps
extends React.InputHTMLAttributes<HTMLInputElement> {
interface PasswordInputProps extends React.InputHTMLAttributes<HTMLInputElement> {
label?: string
registerOptions?: RegisterOptions
visibilityToggleable?: boolean

View File

@@ -23,7 +23,7 @@
position: relative;
}
.toggleButton {
.inputWrapper .toggleButton {
position: absolute;
right: var(--Space-x2);
top: 50%;

View File

@@ -196,3 +196,14 @@ export function getErrorMessage(intl: IntlShape, errorCode?: string) {
return errorCode
}
}
/**
* Wrapper for getErrorMessage that ensures a string is always returned.
* Can be used directly as errorFormatter prop for FormInput components.
*/
export function formatFormErrorMessage(
intl: IntlShape,
errorMessage?: string
): string {
return getErrorMessage(intl, errorMessage) ?? ""
}