Merge branch 'develop'

This commit is contained in:
Linus Flood
2024-11-07 10:20:12 +01:00
261 changed files with 5132 additions and 2106 deletions

View File

@@ -18,16 +18,13 @@
font-weight: var(--typography-Body-Bold-fontWeight);
transition: background-color 0.3s;
}
.summary:hover,
.accordionItem details[open] .summary {
background-color: var(--Base-Surface-Primary-light-Hover-alt, #f2ece6);
.summary:hover {
background-color: var(--Base-Surface-Primary-light-Hover-alt);
}
.accordionItem.light .summary:hover,
.accordionItem.light details[open] .summary {
background-color: var(--Base-Surface-Primary-light-Hover, #f9f6f4);
.accordionItem.light .summary:hover {
background-color: var(--Base-Surface-Primary-light-Hover);
}
.accordionItem.subtle .summary:hover,
.accordionItem.subtle details[open] .summary {
.accordionItem.subtle .summary:hover {
background-color: var(--Base-Surface-Primary-light-Normal);
}

View File

@@ -16,3 +16,10 @@
.accordion li:last-child {
border: none;
}
.accordion details > summary {
list-style: none;
}
.accordion details > summary::-webkit-details-marker {
display: none;
}

View File

@@ -15,6 +15,7 @@
.imageWrapper {
display: flex;
width: 100%;
}
.imageWrapper::after {
@@ -41,6 +42,7 @@
.content {
margin: var(--Spacing-x0) var(--Spacing-x4);
position: absolute;
display: grid;
gap: var(--Spacing-x2);
}

View File

@@ -33,12 +33,12 @@ export default function Checkbox({
>
{({ isSelected }) => (
<>
<div className={styles.checkboxContainer}>
<div className={styles.checkbox}>
<span className={styles.checkboxContainer}>
<span className={styles.checkbox}>
{isSelected && <CheckIcon color="white" />}
</div>
</span>
{children}
</div>
</span>
{fieldState.error ? (
<Caption className={styles.error} fontOnly>
<InfoCircleIcon color="red" />

View File

@@ -26,7 +26,12 @@ interface TextCardProps extends BaseCardProps {
text: React.ReactNode
}
export type CardProps = ListCardProps | TextCardProps
interface CleanCardProps extends BaseCardProps {
list?: never
text?: never
}
export type CardProps = ListCardProps | TextCardProps | CleanCardProps
export type CheckboxProps =
| Omit<ListCardProps, "type">
@@ -34,6 +39,7 @@ export type CheckboxProps =
export type RadioProps =
| Omit<ListCardProps, "type">
| Omit<TextCardProps, "type">
| Omit<CleanCardProps, "type">
export interface ListProps extends Pick<ListCardProps, "declined"> {
list?: ListCardProps["list"]

View File

@@ -18,3 +18,12 @@
.year {
grid-area: year;
}
/* TODO: Handle this in Select component.
- out of scope for now.
*/
.day.invalid > div > div,
.month.invalid > div > div,
.year.invalid > div > div {
border-color: var(--Scandic-Red-60);
}

View File

@@ -2,6 +2,7 @@ import type { RegisterOptions } from "react-hook-form"
export const enum DateName {
date = "date",
day = "day",
month = "month",
year = "year",
}

View File

@@ -1,6 +1,6 @@
"use client"
import { parseDate } from "@internationalized/date"
import { useState } from "react"
import { useEffect } from "react"
import { DateInput, DatePicker, Group } from "react-aria-components"
import { useController, useFormContext, useWatch } from "react-hook-form"
import { useIntl } from "react-intl"
@@ -8,8 +8,11 @@ import { useIntl } from "react-intl"
import { dt } from "@/lib/dt"
import Select from "@/components/TempDesignSystem/Select"
import useLang from "@/hooks/useLang"
import { getLocalizedMonthName } from "@/utils/dateFormatting"
import { rangeArray } from "@/utils/rangeArray"
import ErrorMessage from "../ErrorMessage"
import { DateName } from "./date"
import styles from "./date.module.css"
@@ -20,51 +23,75 @@ import type { DateProps } from "./date"
export default function DateSelect({ name, registerOptions = {} }: DateProps) {
const intl = useIntl()
const currentValue = useWatch({ name })
const { control, setValue, trigger } = useFormContext()
const { field } = useController({
const { control, setValue, formState, watch } = useFormContext()
const { field, fieldState } = useController({
control,
name,
rules: registerOptions,
})
const currentYear = new Date().getFullYear()
const currentDateValue = useWatch({ name })
const year = watch(DateName.year)
const month = watch(DateName.month)
const day = watch(DateName.day)
const lang = useLang()
const months = rangeArray(1, 12).map((month) => ({
value: month,
label: `${month}`,
label: getLocalizedMonthName(month, lang),
}))
const currentYear = new Date().getFullYear()
const years = rangeArray(1900, currentYear - 18)
.reverse()
.map((year) => ({ value: year, label: year.toString() }))
// Ensure the user can't select a date that doesn't exist.
const daysInMonth = dt(currentValue).daysInMonth()
// Calculate available days based on selected year and month
const daysInMonth = getDaysInMonth(
year ? Number(year) : null,
month ? Number(month) - 1 : null
)
const days = rangeArray(1, daysInMonth).map((day) => ({
value: day,
label: `${day}`,
}))
function createOnSelect(selector: DateName) {
/**
* Months are 0 index based and therefore we
* must subtract by 1 to get the selected month
*/
return (select: Key) => {
if (selector === DateName.month) {
select = Number(select) - 1
}
const newDate = dt(currentValue).set(selector, Number(select))
setValue(name, newDate.format("YYYY-MM-DD"))
trigger(name)
}
}
const dayLabel = intl.formatMessage({ id: "Day" })
const monthLabel = intl.formatMessage({ id: "Month" })
const yearLabel = intl.formatMessage({ id: "Year" })
useEffect(() => {
if (formState.isSubmitting) return
if (month && day) {
const maxDays = getDaysInMonth(
year ? Number(year) : null,
Number(month) - 1
)
const adjustedDay = Number(day) > maxDays ? maxDays : Number(day)
if (adjustedDay !== Number(day)) {
setValue(DateName.day, adjustedDay)
}
}
if (year && month && day) {
const newDate = dt()
.year(Number(year))
.month(Number(month) - 1)
.date(Number(day))
if (newDate.isValid()) {
setValue(name, newDate.format("YYYY-MM-DD"), {
shouldDirty: true,
shouldTouch: true,
shouldValidate: true,
})
}
}
}, [year, month, day, setValue, name, formState.isSubmitting])
let dateValue = null
try {
/**
@@ -72,7 +99,9 @@ export default function DateSelect({ name, registerOptions = {} }: DateProps) {
* date, but we can't check isNan since
* we recieve the date as "1999-01-01"
*/
dateValue = dt(currentValue).isValid() ? parseDate(currentValue) : null
dateValue = dt(currentDateValue).isValid()
? parseDate(currentDateValue)
: null
} catch (error) {
console.warn("Known error for parse date in DateSelect: ", error)
}
@@ -81,6 +110,7 @@ export default function DateSelect({ name, registerOptions = {} }: DateProps) {
<DatePicker
aria-label={intl.formatMessage({ id: "Select date of birth" })}
isRequired={!!registerOptions.required}
isInvalid={!formState.isValid}
name={name}
ref={field.ref}
value={dateValue}
@@ -92,57 +122,60 @@ export default function DateSelect({ name, registerOptions = {} }: DateProps) {
switch (segment.type) {
case "day":
return (
<div className={styles.day}>
<div
className={`${styles.day} ${fieldState.invalid ? styles.invalid : ""}`}
>
<Select
aria-label={dayLabel}
items={days}
label={dayLabel}
name={DateName.date}
onSelect={createOnSelect(DateName.date)}
placeholder="DD"
name={DateName.day}
onSelect={(key: Key) =>
setValue(DateName.day, Number(key))
}
placeholder={dayLabel}
required
tabIndex={3}
defaultSelectedKey={
segment.isPlaceholder ? undefined : segment.value
}
value={segment.isPlaceholder ? undefined : segment.value}
/>
</div>
)
case "month":
return (
<div className={styles.month}>
<div
className={`${styles.month} ${fieldState.invalid ? styles.invalid : ""}`}
>
<Select
aria-label={monthLabel}
items={months}
label={monthLabel}
name={DateName.month}
onSelect={createOnSelect(DateName.month)}
placeholder="MM"
onSelect={(key: Key) =>
setValue(DateName.month, Number(key))
}
placeholder={monthLabel}
required
tabIndex={2}
defaultSelectedKey={
segment.isPlaceholder ? undefined : segment.value
}
value={segment.isPlaceholder ? undefined : segment.value}
/>
</div>
)
case "year":
return (
<div className={styles.year}>
<div
className={`${styles.year} ${fieldState.invalid ? styles.invalid : ""}`}
>
<Select
aria-label={yearLabel}
items={years}
label={yearLabel}
name={DateName.year}
onSelect={createOnSelect(DateName.year)}
placeholder="YYYY"
onSelect={(key: Key) =>
setValue(DateName.year, Number(key))
}
placeholder={yearLabel}
required
tabIndex={1}
defaultSelectedKey={
segment.isPlaceholder ? undefined : segment.value
}
value={segment.isPlaceholder ? undefined : segment.value}
/>
</div>
@@ -154,6 +187,21 @@ export default function DateSelect({ name, registerOptions = {} }: DateProps) {
}}
</DateInput>
</Group>
<ErrorMessage errors={formState.errors} name={field.name} />
</DatePicker>
)
}
function getDaysInMonth(year: number | null, month: number | null): number {
if (month === null) {
return 31
}
// If month is February and no year selected, return minimum.
if (month === 1 && !year) {
return 28
}
const yearToUse = year ?? new Date().getFullYear()
return dt(`${yearToUse}-${month + 1}-01`).daysInMonth()
}

View File

@@ -58,8 +58,14 @@ export default function Phone({
forceDialCode: true,
value: phone,
onChange: (value) => {
setValue(name, value.phone)
trigger(name)
// If not checked trigger(name) forces validation on mount
// which shows error message before user even can see the form
if (value.inputValue) {
setValue(name, value.phone)
trigger(name)
} else {
setValue(name, "")
}
},
})

View File

@@ -21,6 +21,14 @@
line-height: var(--typography-Footnote-Bold-lineHeight);
}
.link.breadcrumb {
font-family: var(--typography-Footnote-Bold-fontFamily);
font-size: var(--typography-Footnote-Bold-fontSize);
font-weight: var(--typography-Footnote-Bold-fontWeight);
letter-spacing: var(--typography-Footnote-Bold-letterSpacing);
line-height: var(--typography-Footnote-Bold-lineHeight);
}
.icon {
align-items: center;
display: flex;

View File

@@ -35,7 +35,7 @@ export default function LoyaltyCard({
focalPoint={image.focalPoint}
/>
) : null}
<Title as="h5" level="h3" textAlign="center">
<Title as="h4" level="h3" textAlign="center">
{heading}
</Title>
{bodyText ? <Body textAlign="center">{bodyText}</Body> : null}

View File

@@ -6,7 +6,7 @@ import { Dialog, Modal, ModalOverlay } from "react-aria-components"
import { useIntl } from "react-intl"
import { CloseLargeIcon } from "@/components/Icons"
import { SidePeekContext } from "@/components/SidePeekProvider"
import { SidePeekContext } from "@/components/SidePeeks/SidePeekProvider"
import Button from "../Button"
import Title from "../Text/Title"
@@ -41,7 +41,7 @@ function SidePeek({
if (isSSR) {
return (
<div>
<div className={styles.visuallyHidden}>
<h2>{title}</h2>
{children}
</div>

View File

@@ -21,6 +21,12 @@
}
}
.visuallyHidden {
position: absolute;
opacity: 0;
visibility: hidden;
}
.overlay {
position: fixed;
top: 0;
@@ -40,7 +46,7 @@
height: 100vh;
background-color: var(--Base-Background-Primary-Normal);
z-index: var(--sidepeek-z-index);
box-shadow: 0 0 10px rgba(0, 0, 0, 0.85);
outline: none;
}
.modal[data-entering] {

View File

@@ -21,7 +21,7 @@ export default function TeaserCardSidepeek({
const { heading, content, primary_button, secondary_button } = sidePeekContent
return (
<>
<div>
<Button
onPress={() => setSidePeekIsOpen(true)}
theme="base"
@@ -77,6 +77,6 @@ export default function TeaserCardSidepeek({
)}
</div>
</SidePeek>
</>
</div>
)
}

View File

@@ -19,25 +19,23 @@ export default function TeaserCard({
sidePeekButton,
sidePeekContent,
image,
style = "default",
intent,
alwaysStack = false,
className,
}: TeaserCardProps) {
const cardClasses = teaserCardVariants({ style, alwaysStack, className })
const classNames = teaserCardVariants({ intent, alwaysStack, className })
return (
<article className={cardClasses}>
<article className={classNames}>
{image && (
<div className={styles.imageContainer}>
<Image
src={image.url}
alt={image.meta?.alt || ""}
className={styles.backgroundImage}
width={399}
height={201}
focalPoint={image.focalPoint}
/>
</div>
<Image
src={image.url}
alt={image.meta?.alt || ""}
className={styles.image}
width={Math.ceil(image.dimensions.aspectRatio * 200)}
height={200}
focalPoint={image.focalPoint}
/>
)}
<div className={styles.content}>
<Subtitle textAlign="left" type="two" color="black">

View File

@@ -18,24 +18,17 @@
border: 1px solid var(--Base-Border-Subtle);
}
.imageContainer {
.image {
width: 100%;
height: 12.58625rem; /* 201.38px / 16 = 12.58625rem */
overflow: hidden;
}
.backgroundImage {
width: 100%;
height: 100%;
object-fit: cover;
height: 12.5rem; /* 200px */
}
.content {
display: flex;
flex-direction: column;
display: grid;
gap: var(--Spacing-x-one-and-half);
align-items: flex-start;
padding: var(--Spacing-x2) var(--Spacing-x3);
grid-template-rows: auto 1fr auto;
flex-grow: 1;
}
.description {
@@ -53,17 +46,6 @@
width: 100%;
}
.body {
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 3;
line-clamp: 3;
overflow: hidden;
text-overflow: ellipsis;
/* line-height variables are in %, so using the value in rem instead */
max-height: calc(3 * 1.5rem);
}
@media (min-width: 1367px) {
.card:not(.alwaysStack) .ctaContainer {
grid-template-columns: repeat(auto-fit, minmax(0, 1fr));

View File

@@ -4,7 +4,7 @@ import styles from "./teaserCard.module.css"
export const teaserCardVariants = cva(styles.card, {
variants: {
style: {
intent: {
default: styles.default,
featured: styles.featured,
},
@@ -14,7 +14,7 @@ export const teaserCardVariants = cva(styles.card, {
},
},
defaultVariants: {
style: "default",
intent: "default",
alwaysStack: false,
},
})

View File

@@ -92,7 +92,7 @@
color: var(--Primary-Dark-On-Surface-Accent);
}
.peach80 {
.baseTextMediumContrast {
color: var(--Base-Text-Medium-contrast);
}

View File

@@ -16,7 +16,6 @@ const config = {
textHighContrast: styles.textHighContrast,
white: styles.white,
peach50: styles.peach50,
peach80: styles.peach80,
uiTextHighContrast: styles.uiTextHighContrast,
uiTextMediumContrast: styles.uiTextMediumContrast,
uiTextPlaceholder: styles.uiTextPlaceholder,

View File

@@ -4,7 +4,9 @@
font-weight: 500;
}
.h1 {
/* Temporarily remove h1 styling until design tokens är updated */
/* .h1 {
font-family: var(--typography-Title-1-fontFamily);
font-size: clamp(
var(--typography-Title-1-Mobile-fontSize),
@@ -14,9 +16,9 @@
letter-spacing: var(--typography-Title-1-letterSpacing);
line-height: var(--typography-Title-1-lineHeight);
text-decoration: var(--typography-Title-1-textDecoration);
}
} */
.h2 {
.h1 {
font-family: var(--typography-Title-2-fontFamily);
font-size: clamp(
var(--typography-Title-2-Mobile-fontSize),
@@ -26,9 +28,10 @@
letter-spacing: var(--typography-Title-2-letterSpacing);
line-height: var(--typography-Title-2-lineHeight);
text-decoration: var(--typography-Title-2-textDecoration);
font-weight: var(--typography-Title-2-fontWeight);
}
.h3 {
.h2 {
font-family: var(--typography-Title-3-fontFamily);
font-size: clamp(
var(--typography-Title-3-Mobile-fontSize),
@@ -38,9 +41,10 @@
letter-spacing: var(--typography-Title-3-letterSpacing);
line-height: var(--typography-Title-3-lineHeight);
text-decoration: var(--typography-Title-3-textDecoration);
font-weight: var(--typography-Title-3-fontWeight);
}
.h4 {
.h3 {
font-family: var(--typography-Title-4-fontFamily);
font-size: clamp(
var(--typography-Title-4-Mobile-fontSize),
@@ -50,9 +54,10 @@
letter-spacing: var(--typography-Title-4-letterSpacing);
line-height: var(--typography-Title-4-lineHeight);
text-decoration: var(--typography-Title-4-textDecoration);
font-weight: var(--typography-Title-4-fontWeight);
}
.h5 {
.h4 {
font-family: var(--typography-Title-5-fontFamily);
font-size: clamp(
var(--typography-Title-5-Mobile-fontSize),
@@ -62,6 +67,7 @@
letter-spacing: var(--typography-Title-5-letterSpacing);
line-height: var(--typography-Title-5-lineHeight);
text-decoration: var(--typography-Title-5-textDecoration);
font-weight: var(--typography-Title-5-fontWeight);
}
.capitalize {

View File

@@ -26,7 +26,7 @@ const config = {
h2: styles.h2,
h3: styles.h3,
h4: styles.h4,
h5: styles.h5,
h5: styles.h4,
},
},
defaultVariants: {