Merged in chore/SW-3145-move-date-to-design-system (pull request #2556)

feat: SW-3145 Moved date component to design system

* chore: SW-3145 Moved date component to design system


Approved-by: Anton Gunnarsson
Approved-by: Matilda Landström
This commit is contained in:
Hrishikesh Vaipurkar
2025-07-28 09:05:25 +00:00
parent 36e8ac11d1
commit 5ff4234552
15 changed files with 120 additions and 64 deletions

View File

@@ -4,6 +4,7 @@ import { useIntl } from "react-intl"
import { Divider } from "@scandic-hotels/design-system/Divider" import { Divider } from "@scandic-hotels/design-system/Divider"
import CountrySelect from "@scandic-hotels/design-system/Form/Country" import CountrySelect from "@scandic-hotels/design-system/Form/Country"
import DateSelect from "@scandic-hotels/design-system/Form/Date"
import Phone from "@scandic-hotels/design-system/Form/Phone" import Phone from "@scandic-hotels/design-system/Form/Phone"
import { Select } from "@scandic-hotels/design-system/Select" import { Select } from "@scandic-hotels/design-system/Select"
import { Typography } from "@scandic-hotels/design-system/Typography" import { Typography } from "@scandic-hotels/design-system/Typography"
@@ -13,7 +14,6 @@ import {
getLocalizedLanguageOptions, getLocalizedLanguageOptions,
} from "@/constants/languages" } from "@/constants/languages"
import DateSelect from "@/components/TempDesignSystem/Form/Date"
import Input from "@/components/TempDesignSystem/Form/Input" import Input from "@/components/TempDesignSystem/Form/Input"
import PasswordInput from "@/components/TempDesignSystem/Form/PasswordInput" import PasswordInput from "@/components/TempDesignSystem/Form/PasswordInput"
import useLang from "@/hooks/useLang" import useLang from "@/hooks/useLang"
@@ -41,7 +41,20 @@ export default function FormContent({ errors }: { errors: FieldErrors }) {
</p> </p>
</Typography> </Typography>
</header> </header>
<DateSelect name="dateOfBirth" registerOptions={{ required: true }} /> <DateSelect
labels={{
day: intl.formatMessage({ defaultMessage: "Day" }),
month: intl.formatMessage({ defaultMessage: "Month" }),
year: intl.formatMessage({ defaultMessage: "Year" }),
errorMessage: getErrorMessage(
intl,
errors["dateOfBirth"]?.message?.toString()
),
}}
lang={lang}
name="dateOfBirth"
registerOptions={{ required: true }}
/>
<Input <Input
data-hj-suppress data-hj-suppress
label={`${intl.formatMessage({ label={`${intl.formatMessage({

View File

@@ -10,6 +10,7 @@ import { logger } from "@scandic-hotels/common/logger"
import { Button } from "@scandic-hotels/design-system/Button" import { Button } from "@scandic-hotels/design-system/Button"
import Checkbox from "@scandic-hotels/design-system/Form/Checkbox" import Checkbox from "@scandic-hotels/design-system/Form/Checkbox"
import CountrySelect from "@scandic-hotels/design-system/Form/Country" import CountrySelect from "@scandic-hotels/design-system/Form/Country"
import DateSelect from "@scandic-hotels/design-system/Form/Date"
import Phone from "@scandic-hotels/design-system/Form/Phone" import Phone from "@scandic-hotels/design-system/Form/Phone"
import { Typography } from "@scandic-hotels/design-system/Typography" import { Typography } from "@scandic-hotels/design-system/Typography"
import { trpc } from "@scandic-hotels/trpc/client" import { trpc } from "@scandic-hotels/trpc/client"
@@ -25,7 +26,6 @@ import {
privacyPolicy, privacyPolicy,
} from "@/constants/webHrefs" } from "@/constants/webHrefs"
import DateSelect from "@/components/TempDesignSystem/Form/Date"
import Input from "@/components/TempDesignSystem/Form/Input" import Input from "@/components/TempDesignSystem/Form/Input"
import PasswordInput from "@/components/TempDesignSystem/Form/PasswordInput" import PasswordInput from "@/components/TempDesignSystem/Form/PasswordInput"
import Link from "@/components/TempDesignSystem/Link" import Link from "@/components/TempDesignSystem/Link"
@@ -162,6 +162,16 @@ export default function SignupForm({ title }: SignUpFormProps) {
</p> </p>
</Typography> </Typography>
<DateSelect <DateSelect
labels={{
day: intl.formatMessage({ defaultMessage: "Day" }),
month: intl.formatMessage({ defaultMessage: "Month" }),
year: intl.formatMessage({ defaultMessage: "Year" }),
errorMessage: getErrorMessage(
intl,
errors.dateOfBirth?.message?.toString()
),
}}
lang={lang}
name="dateOfBirth" name="dateOfBirth"
registerOptions={{ required: true }} registerOptions={{ required: true }}
/> />

View File

@@ -1,24 +1,33 @@
"use client" "use client"
import { useEffect, useState } from "react" import { useEffect, useState } from "react"
import { type RegisterOptions, useWatch } from "react-hook-form" import {
type FieldErrors,
type RegisterOptions,
useWatch,
} from "react-hook-form"
import { useIntl } from "react-intl" import { useIntl } from "react-intl"
import Caption from "@scandic-hotels/design-system/Caption" import Caption from "@scandic-hotels/design-system/Caption"
import DateSelect from "@scandic-hotels/design-system/Form/Date"
import DateSelect from "@/components/TempDesignSystem/Form/Date"
import Input from "@/components/TempDesignSystem/Form/Input" import Input from "@/components/TempDesignSystem/Form/Input"
import useLang from "@/hooks/useLang"
import { getErrorMessage } from "@/utils/getErrorMessage"
import styles from "./signup.module.css" import styles from "./signup.module.css"
export default function Signup({ export default function Signup({
errors,
name, name,
registerOptions, registerOptions,
}: { }: {
errors: FieldErrors
name: string name: string
registerOptions?: RegisterOptions registerOptions?: RegisterOptions
}) { }) {
const intl = useIntl() const intl = useIntl()
const lang = useLang()
const [isJoinChecked, setIsJoinChecked] = useState(false) const [isJoinChecked, setIsJoinChecked] = useState(false)
@@ -50,6 +59,16 @@ export default function Signup({
</Caption> </Caption>
</header> </header>
<DateSelect <DateSelect
labels={{
day: intl.formatMessage({ defaultMessage: "Day" }),
month: intl.formatMessage({ defaultMessage: "Month" }),
year: intl.formatMessage({ defaultMessage: "Year" }),
errorMessage: getErrorMessage(
intl,
errors["dateOfBirth"]?.message?.toString()
),
}}
lang={lang}
name="dateOfBirth" name="dateOfBirth"
registerOptions={{ required: true, ...registerOptions }} registerOptions={{ required: true, ...registerOptions }}
/> />

View File

@@ -214,6 +214,7 @@ export default function Details({ user }: DetailsProps) {
{user ? null : ( {user ? null : (
<div className={styles.fullWidth}> <div className={styles.fullWidth}>
<Signup <Signup
errors={formState.errors}
name="join" name="join"
registerOptions={{ onBlur: updateDetailsStore }} registerOptions={{ onBlur: updateDetailsStore }}
/> />

View File

@@ -1,4 +1,5 @@
import { dt } from "@scandic-hotels/common/dt" import { dt } from "@scandic-hotels/common/dt"
import { getNumberOfNights } from "@scandic-hotels/common/utils/dateFormatting"
import { Typography } from "@scandic-hotels/design-system/Typography" import { Typography } from "@scandic-hotels/design-system/Typography"
import { RoomPackageCodeEnum } from "@scandic-hotels/trpc/enums/roomFilter" import { RoomPackageCodeEnum } from "@scandic-hotels/trpc/enums/roomFilter"
@@ -6,7 +7,6 @@ import { longDateWithYearFormat } from "@/constants/dateFormats"
import { getIntl } from "@/i18n" import { getIntl } from "@/i18n"
import { getLang } from "@/i18n/serverContext" import { getLang } from "@/i18n/serverContext"
import { getNumberOfNights } from "@/utils/dateFormatting"
import styles from "./footer.module.css" import styles from "./footer.module.css"

View File

@@ -1,13 +0,0 @@
import type { RegisterOptions } from "react-hook-form"
export const enum DateName {
date = "date",
day = "day",
month = "month",
year = "year",
}
export interface DateProps
extends React.SelectHTMLAttributes<HTMLSelectElement> {
name: string
registerOptions?: RegisterOptions
}

View File

@@ -7,11 +7,10 @@ import { afterEach, describe, expect, test } from "vitest"
import { Lang } from "@scandic-hotels/common/constants/language" import { Lang } from "@scandic-hotels/common/constants/language"
import { dt } from "@scandic-hotels/common/dt" import { dt } from "@scandic-hotels/common/dt"
import { getLocalizedMonthName } from "@scandic-hotels/common/utils/dateFormatting"
import Date from "@scandic-hotels/design-system/Form/Date"
import { cleanup, render, screen } from "@/tests/utils" import { cleanup, render, screen } from "@/tests/utils"
import { getLocalizedMonthName } from "@/utils/dateFormatting"
import Date from "./index"
const testUtilUser = new User({ interactionType: "touch" }) const testUtilUser = new User({ interactionType: "touch" })
@@ -116,7 +115,16 @@ describe("Date input", () => {
expect(data).toEqual(expectedOutput) expect(data).toEqual(expectedOutput)
}} }}
> >
<Date name="dateOfBirth" /> <Date
labels={{
day: "day",
month: "Month",
year: "Year",
errorMessage: "Date is required",
}}
lang={Lang.en}
name="dateOfBirth"
/>
</FormWrapper> </FormWrapper>
) )

View File

@@ -25,6 +25,8 @@
"./utils/chunk": "./utils/chunk.ts", "./utils/chunk": "./utils/chunk.ts",
"./utils/isDefined": "./utils/isDefined.ts", "./utils/isDefined": "./utils/isDefined.ts",
"./utils/maskValue": "./utils/maskValue.ts", "./utils/maskValue": "./utils/maskValue.ts",
"./utils/dateFormatting": "./utils/dateFormatting.ts",
"./utils/rangeArray": "./utils/rangeArray.ts",
"./utils/zod/*": "./utils/zod/*.ts", "./utils/zod/*": "./utils/zod/*.ts",
"./constants/language": "./constants/language.ts", "./constants/language": "./constants/language.ts",
"./constants/membershipLevels": "./constants/membershipLevels.ts", "./constants/membershipLevels": "./constants/membershipLevels.ts",

View File

@@ -1,6 +1,6 @@
import { dt } from "@scandic-hotels/common/dt" import { dt } from "../dt/dt"
import type { Lang } from "@scandic-hotels/common/constants/language" import type { Lang } from "../constants/language"
/** /**
* Get the localized month name for a given month index and language * Get the localized month name for a given month index and language

View File

@@ -1,6 +1,6 @@
export function rangeArray(start: number, stop: number, step: number = 1) { export function rangeArray(start: number, stop: number, step: number = 1) {
return Array.from( return Array.from(
{ length: (stop - start) / step + 1 }, { length: (stop - start) / step + 1 },
(value, index) => start + index * step (_, index) => start + index * step
) )
} }

View File

@@ -0,0 +1,21 @@
import { Lang } from '@scandic-hotels/common/constants/language'
import type { RegisterOptions } from 'react-hook-form'
export const enum DateName {
date = 'date',
day = 'day',
month = 'month',
year = 'year',
}
export interface DateProps
extends React.SelectHTMLAttributes<HTMLSelectElement> {
labels: {
day: string
month: string
year: string
errorMessage?: string
}
lang: Lang
name: string
registerOptions?: RegisterOptions
}

View File

@@ -1,28 +1,27 @@
"use client" 'use client'
import { parseDate } from "@internationalized/date" import { parseDate } from '@internationalized/date'
import { useEffect } from "react" import { useEffect } from 'react'
import { useController, useFormContext, useWatch } from "react-hook-form" import { useController, useFormContext, useWatch } from 'react-hook-form'
import { useIntl } from "react-intl" import { useMediaQuery } from 'usehooks-ts'
import { useMediaQuery } from "usehooks-ts"
import { dt } from "@scandic-hotels/common/dt" import { dt } from '@scandic-hotels/common/dt'
import { logger } from "@scandic-hotels/common/logger" import { logger } from '@scandic-hotels/common/logger'
import { ErrorMessage } from "@scandic-hotels/design-system/Form/ErrorMessage" import { getLocalizedMonthName } from '@scandic-hotels/common/utils/dateFormatting'
import { Select } from "@scandic-hotels/design-system/Select" import { rangeArray } from '@scandic-hotels/common/utils/rangeArray'
import { ErrorMessage } from '../../Form/ErrorMessage'
import { Select } from '../../Select'
import useLang from "@/hooks/useLang" import { DateName, type DateProps } from './date'
import { getLocalizedMonthName } from "@/utils/dateFormatting"
import { getErrorMessage } from "@/utils/getErrorMessage"
import { rangeArray } from "@/utils/rangeArray"
import { DateName, type DateProps } from "./date" import styles from './date.module.css'
import styles from "./date.module.css" export default function DateSelect({
labels,
export default function DateSelect({ name, registerOptions = {} }: DateProps) { lang,
const intl = useIntl() name,
const lang = useLang() registerOptions = {},
const isDesktop = useMediaQuery("(min-width: 768px)", { }: DateProps) {
const isDesktop = useMediaQuery('(min-width: 768px)', {
initializeWithValue: false, initializeWithValue: false,
}) })
@@ -38,7 +37,7 @@ export default function DateSelect({ name, registerOptions = {} }: DateProps) {
const month = watch(DateName.month) const month = watch(DateName.month)
const day = watch(DateName.day) const day = watch(DateName.day)
const minAgeDate = dt().subtract(18, "year").toDate() // age 18 const minAgeDate = dt().subtract(18, 'year').toDate() // age 18
const minAgeYear = minAgeDate.getFullYear() const minAgeYear = minAgeDate.getFullYear()
const minAgeMonth = year === minAgeYear ? minAgeDate.getMonth() + 1 : null const minAgeMonth = year === minAgeYear ? minAgeDate.getMonth() + 1 : null
const minAgeDay = const minAgeDay =
@@ -87,7 +86,7 @@ export default function DateSelect({ name, registerOptions = {} }: DateProps) {
.date(Number(day)) .date(Number(day))
if (newDate.isValid()) { if (newDate.isValid()) {
setValue(name, newDate.format("YYYY-MM-DD"), { setValue(name, newDate.format('YYYY-MM-DD'), {
shouldDirty: true, shouldDirty: true,
shouldTouch: true, shouldTouch: true,
shouldValidate: true, shouldValidate: true,
@@ -106,7 +105,7 @@ export default function DateSelect({ name, registerOptions = {} }: DateProps) {
? parseDate(currentDateValue) ? parseDate(currentDateValue)
: null : null
} catch (error) { } catch (error) {
logger.warn("Known error for parse date in DateSelect: ", error) logger.warn('Known error for parse date in DateSelect: ', error)
} }
useEffect(() => { useEffect(() => {
@@ -125,9 +124,7 @@ export default function DateSelect({ name, registerOptions = {} }: DateProps) {
<div className={styles.segment}> <div className={styles.segment}>
<Select <Select
items={days} items={days}
label={intl.formatMessage({ label={labels.day}
defaultMessage: "Day",
})}
name={DateName.day} name={DateName.day}
onSelectionChange={(key) => setValue(DateName.day, Number(key))} onSelectionChange={(key) => setValue(DateName.day, Number(key))}
isRequired isRequired
@@ -142,9 +139,7 @@ export default function DateSelect({ name, registerOptions = {} }: DateProps) {
<div className={styles.segment}> <div className={styles.segment}>
<Select <Select
items={months} items={months}
label={intl.formatMessage({ label={labels.month}
defaultMessage: "Month",
})}
name={DateName.month} name={DateName.month}
onSelectionChange={(key) => setValue(DateName.month, Number(key))} onSelectionChange={(key) => setValue(DateName.month, Number(key))}
isRequired isRequired
@@ -158,9 +153,7 @@ export default function DateSelect({ name, registerOptions = {} }: DateProps) {
<div className={styles.segment}> <div className={styles.segment}>
<Select <Select
items={years} items={years}
label={intl.formatMessage({ label={labels.year}
defaultMessage: "Year",
})}
name={DateName.year} name={DateName.year}
onSelectionChange={(key) => setValue(DateName.year, Number(key))} onSelectionChange={(key) => setValue(DateName.year, Number(key))}
isRequired isRequired
@@ -176,10 +169,7 @@ export default function DateSelect({ name, registerOptions = {} }: DateProps) {
<ErrorMessage <ErrorMessage
errors={formState.errors} errors={formState.errors}
name={field.name} name={field.name}
messageLabel={getErrorMessage( messageLabel={labels.errorMessage}
intl,
formState.errors[field.name]?.message?.toString()
)}
/> />
</> </>
) )

View File

@@ -16,6 +16,7 @@
"./Footnote": "./dist/components/Footnote/index.js", "./Footnote": "./dist/components/Footnote/index.js",
"./Form/Checkbox": "./dist/components/Form/Checkbox/index.js", "./Form/Checkbox": "./dist/components/Form/Checkbox/index.js",
"./Form/Country": "./dist/components/Form/Country/index.js", "./Form/Country": "./dist/components/Form/Country/index.js",
"./Form/Date": "./dist/components/Form/Date/index.js",
"./Form/ErrorMessage": "./dist/components/Form/ErrorMessage/index.js", "./Form/ErrorMessage": "./dist/components/Form/ErrorMessage/index.js",
"./Form/Phone": "./dist/components/Form/Phone/index.js", "./Form/Phone": "./dist/components/Form/Phone/index.js",
"./Form/RadioCard": "./dist/components/Form/RadioCard/index.js", "./Form/RadioCard": "./dist/components/Form/RadioCard/index.js",
@@ -149,12 +150,14 @@
"check-types": "tsc --noEmit" "check-types": "tsc --noEmit"
}, },
"peerDependencies": { "peerDependencies": {
"@internationalized/date": "^3.8.0",
"@radix-ui/react-slot": "^1.2.2", "@radix-ui/react-slot": "^1.2.2",
"react": "^19.1.0", "react": "^19.1.0",
"react-aria-components": "^1.8.0", "react-aria-components": "^1.8.0",
"react-dom": "^19.1.0", "react-dom": "^19.1.0",
"react-hook-form": "^7.56.2", "react-hook-form": "^7.56.2",
"react-international-phone": "^4.5.0" "react-international-phone": "^4.5.0",
"usehooks-ts": "3.1.1"
}, },
"devDependencies": { "devDependencies": {
"@eslint/eslintrc": "^3.3.1", "@eslint/eslintrc": "^3.3.1",

View File

@@ -6729,12 +6729,14 @@ __metadata:
vite-plugin-lib-inject-css: "npm:^2.2.2" vite-plugin-lib-inject-css: "npm:^2.2.2"
vitest: "npm:^3.2.4" vitest: "npm:^3.2.4"
peerDependencies: peerDependencies:
"@internationalized/date": ^3.8.0
"@radix-ui/react-slot": ^1.2.2 "@radix-ui/react-slot": ^1.2.2
react: ^19.1.0 react: ^19.1.0
react-aria-components: ^1.8.0 react-aria-components: ^1.8.0
react-dom: ^19.1.0 react-dom: ^19.1.0
react-hook-form: ^7.56.2 react-hook-form: ^7.56.2
react-international-phone: ^4.5.0 react-international-phone: ^4.5.0
usehooks-ts: 3.1.1
languageName: unknown languageName: unknown
linkType: soft linkType: soft