Merged in fix/STAY-133 (pull request #3313)

Fix/STAY-133

* fix: Add static summary buttons row on add ancillary flow

* fix: refactor handling of modals

* fix: refactor file structure for add ancillary flow

* Merged in chore/replace-deprecated-body (pull request #3300)

Replace deprecated <Body> with <Typography>

* chore: replace deprecated body component

* refactor: replace Body component with Typography across various components

* merge

Approved-by: Bianca Widstam
Approved-by: Matilda Landström


Approved-by: Bianca Widstam
Approved-by: Matilda Landström
This commit is contained in:
Christel Westerberg
2025-12-11 07:29:36 +00:00
parent 5bcbc23732
commit cd8b30f2ec
35 changed files with 208 additions and 214 deletions

View File

@@ -1,15 +0,0 @@
.form {
display: flex;
flex-direction: column;
flex-grow: 1;
overflow-y: auto;
}
.modalScrollable {
display: flex;
flex-direction: column;
}
.form::-webkit-scrollbar {
display: none;
}

View File

@@ -1,22 +0,0 @@
import Modal from "@scandic-hotels/design-system/Modal"
import { useAddAncillaryStore } from "@/stores/my-stay/add-ancillary-flow"
import styles from "./wrapper.module.css"
export default function AncillaryFlowModalWrapper({
children,
}: React.PropsWithChildren) {
const { isOpen, closeModal, selectedAncillaryTitle } = useAddAncillaryStore(
(state) => ({
isOpen: state.isOpen,
closeModal: state.closeModal,
selectedAncillaryTitle: state.selectedAncillary?.title,
})
)
return (
<Modal isOpen={isOpen} onToggle={closeModal} title={selectedAncillaryTitle}>
<div className={styles.modalWrapper}>{children}</div>
</Modal>
)
}

View File

@@ -1,13 +0,0 @@
.modalWrapper {
display: flex;
flex-direction: column;
max-height: 70dvh;
width: 100%;
margin-top: var(--Space-x3);
}
@media screen and (min-width: 768px) {
.modalWrapper {
width: 492px;
}
}

View File

@@ -136,77 +136,81 @@ export default function Summary({
: null : null
return ( return (
<div <div className={styles.summary}>
className={cx(styles.summary, {
[styles.backgroundBox]: isConfirmation || isSingleItem,
})}
>
{(isSingleItem || isConfirmation) && (
<PriceDetails
totalPrice={totalPrice}
totalPoints={totalPoints}
selectedAncillary={selectedAncillary}
/>
)}
{isConfirmation && isPriceDetailsOpen && (
<PriceSummary
totalPrice={totalPrice}
totalPoints={totalPoints}
items={items}
/>
)}
<div <div
className={cx({ className={cx({
[styles.confirmButtons]: isConfirmation, [styles.backgroundBox]: isConfirmation || isSingleItem,
})} })}
> >
{isConfirmation && ( {(isSingleItem || isConfirmation) && (
<Button <PriceDetails
type="button" totalPrice={totalPrice}
typography="Body/Supporting text (caption)/smBold" totalPoints={totalPoints}
size="Small" selectedAncillary={selectedAncillary}
variant="Text" />
onPress={togglePriceDetails}
className={styles.priceButton}
>
{intl.formatMessage({
id: "commonpriceDetails",
defaultMessage: "Price details",
})}
<MaterialIcon
icon={
isPriceDetailsOpen ? "keyboard_arrow_up" : "keyboard_arrow_down"
}
size={20}
color="CurrentColor"
/>
</Button>
)} )}
{isConfirmation && isPriceDetailsOpen && (
<PriceSummary
totalPrice={totalPrice}
totalPoints={totalPoints}
items={items}
/>
)}
<div
className={cx({
[styles.confirmButtons]: isConfirmation,
})}
>
{isConfirmation && (
<Button
type="button"
typography="Body/Supporting text (caption)/smBold"
size="Small"
variant="Text"
onPress={togglePriceDetails}
className={styles.priceButton}
>
{intl.formatMessage({
id: "commonpriceDetails",
defaultMessage: "Price details",
})}
<MaterialIcon
icon={
isPriceDetailsOpen
? "keyboard_arrow_up"
: "keyboard_arrow_down"
}
size={20}
color="CurrentColor"
/>
</Button>
)}
<div className={styles.buttons}> <div className={styles.buttons}>
<Button <Button
typography="Body/Supporting text (caption)/smBold" typography="Body/Supporting text (caption)/smBold"
type="button" type="button"
variant="Text" variant="Text"
size="Small" size="Small"
color="Primary" color="Primary"
onPress={() => prevStep(isMobile)} onPress={() => prevStep(isMobile)}
> >
{intl.formatMessage({ {intl.formatMessage({
id: "common.back", id: "common.back",
defaultMessage: "Back", defaultMessage: "Back",
})} })}
</Button> </Button>
<Button <Button
typography="Body/Supporting text (caption)/smBold" typography="Body/Supporting text (caption)/smBold"
size="Small" size="Small"
isDisabled={isSubmitting} isDisabled={isSubmitting}
isPending={isSubmitting} isPending={isSubmitting}
{...buttonProps} {...buttonProps}
> >
{buttonLabel} {buttonLabel}
</Button> </Button>
</div>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -1,8 +1,10 @@
.summmary { .summary {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
justify-content: space-between; justify-content: space-between;
margin-top: var(--Space-x2); padding: var(--Space-x2) var(--Space-x2) var(--Space-x3);
width: 100%;
border-top: 1px solid var(--Border-Default);
} }
.backgroundBox { .backgroundBox {

View File

@@ -0,0 +1,25 @@
.modal {
width: 100%;
}
.modalContent {
gap: unset;
}
.modalScrollable {
width: 100%;
display: flex;
flex-direction: column;
overflow-y: auto;
padding: var(--Space-x2);
}
@media screen and (min-width: 768px) {
.modal {
width: 492px;
}
.modalScrollable {
padding: var(--Space-x3);
}
}

View File

@@ -0,0 +1,47 @@
import Modal from "@scandic-hotels/design-system/Modal"
import {
AncillaryStepEnum,
useAddAncillaryStore,
} from "@/stores/my-stay/add-ancillary-flow"
import Description from "../Description"
import Steps from "../Steps"
import Summary from "./Summary"
import styles from "./addAncillaryModal.module.css"
import type { StepsProps } from "@/types/components/myPages/myStay/ancillaries"
export default function AddAncillaryModal({
error,
savedCreditCards,
user,
}: StepsProps) {
const { isOpen, closeModal, selectedAncillaryTitle, currentStep } =
useAddAncillaryStore((state) => ({
isOpen: state.isOpen,
closeModal: state.closeModal,
selectedAncillaryTitle: state.selectedAncillary?.title,
currentStep: state.currentStep,
}))
return (
<Modal
isOpen={isOpen}
onToggle={closeModal}
title={selectedAncillaryTitle}
withActions
contentClassName={styles.modalContent}
className={styles.modal}
>
<div className={styles.modalScrollable}>
<Description />
<Steps user={user} savedCreditCards={savedCreditCards} error={error} />
</div>
<Summary
isConfirmation={currentStep === AncillaryStepEnum.confirmation}
/>
</Modal>
)
}

View File

@@ -16,8 +16,6 @@ import { useAddAncillaryStore } from "@/stores/my-stay/add-ancillary-flow"
import TermsAndConditions from "@/components/HotelReservation/MyStay/TermsAndConditions" import TermsAndConditions from "@/components/HotelReservation/MyStay/TermsAndConditions"
import { trackUpdatePaymentMethod } from "@/utils/tracking" import { trackUpdatePaymentMethod } from "@/utils/tracking"
import Summary from "../Summary"
import styles from "./confirmationStep.module.css" import styles from "./confirmationStep.module.css"
import type { ConfirmationStepProps } from "@/types/components/myPages/myStay/ancillaries" import type { ConfirmationStepProps } from "@/types/components/myPages/myStay/ancillaries"
@@ -228,7 +226,6 @@ export default function ConfirmationStep({
</Typography> </Typography>
)} )}
<TermsAndConditions /> <TermsAndConditions />
<Summary isConfirmation />
</div> </div>
) )
} }

View File

@@ -6,8 +6,6 @@ import { generateDeliveryOptions } from "@/components/HotelReservation/MyStay/ut
import Input from "@/components/TempDesignSystem/Form/Input" import Input from "@/components/TempDesignSystem/Form/Input"
import Select from "@/components/TempDesignSystem/Form/Select" import Select from "@/components/TempDesignSystem/Form/Select"
import Summary from "../Summary"
import styles from "./deliveryDetailsStep.module.css" import styles from "./deliveryDetailsStep.module.css"
export default function DeliveryMethodStep() { export default function DeliveryMethodStep() {
@@ -28,7 +26,7 @@ export default function DeliveryMethodStep() {
</Typography> </Typography>
<Select <Select
name="deliveryTime" name="deliveryTime"
label="" label={""}
items={deliveryTimeOptions} items={deliveryTimeOptions}
registerOptions={{ required: true }} registerOptions={{ required: true }}
isNestedInModal isNestedInModal
@@ -61,7 +59,6 @@ export default function DeliveryMethodStep() {
</Typography> </Typography>
</div> </div>
</div> </div>
<Summary />
</div> </div>
) )
} }

View File

@@ -18,10 +18,7 @@ export default function Mobile({ user, savedCreditCards, error }: StepsProps) {
if (currentStep === AncillaryStepEnum.selectQuantity) { if (currentStep === AncillaryStepEnum.selectQuantity) {
return ( return (
<> <>
<SelectQuantityStep <SelectQuantityStep user={user} />
user={user}
hideSummary={selectedAncillary?.requiresDeliveryTime}
/>
{selectedAncillary?.requiresDeliveryTime && <DeliveryMethodStep />} {selectedAncillary?.requiresDeliveryTime && <DeliveryMethodStep />}
</> </>
) )

View File

@@ -11,7 +11,6 @@ import { useAddAncillaryStore } from "@/stores/my-stay/add-ancillary-flow"
import Select from "@/components/TempDesignSystem/Form/Select" import Select from "@/components/TempDesignSystem/Form/Select"
import { getErrorMessage } from "@/utils/getErrorMessage" import { getErrorMessage } from "@/utils/getErrorMessage"
import Summary from "../Summary"
import { BreakfastInfo } from "./BreakfastInfo" import { BreakfastInfo } from "./BreakfastInfo"
import styles from "./selectQuantityStep.module.css" import styles from "./selectQuantityStep.module.css"
@@ -26,10 +25,7 @@ const cardQuantityOptions = Array.from({ length: 7 }, (_, i) => ({
value: i, value: i,
})) }))
export default function SelectQuantityStep({ export default function SelectQuantityStep({ user }: SelectQuantityStepProps) {
user,
hideSummary = false,
}: SelectQuantityStepProps) {
const { isBreakfast, selectedAncillary } = useAddAncillaryStore((state) => ({ const { isBreakfast, selectedAncillary } = useAddAncillaryStore((state) => ({
isBreakfast: state.isBreakfast, isBreakfast: state.isBreakfast,
selectedAncillary: state.selectedAncillary, selectedAncillary: state.selectedAncillary,
@@ -50,12 +46,7 @@ export default function SelectQuantityStep({
) )
} }
return ( return <div className={styles.container}>{content}</div>
<div className={styles.container}>
{content}
{!hideSummary && <Summary />}
</div>
)
} }
function InnerSelectQuantityStep({ function InnerSelectQuantityStep({

View File

@@ -0,0 +1,6 @@
.form {
display: flex;
flex-direction: column;
flex-grow: 1;
width: 100%;
}

View File

@@ -29,10 +29,9 @@ import {
trackGlaAncillaryAttempt, trackGlaAncillaryAttempt,
} from "@/utils/tracking/myStay" } from "@/utils/tracking/myStay"
import { isAncillaryError } from "../../../utils" import { isAncillaryError } from "../../utils"
import { type AncillaryFormData, ancillaryFormSchema } from "../schema" import AddAncillaryFlow from "./Modal"
import Description from "./Description" import { type AncillaryFormData, ancillaryFormSchema } from "./schema"
import Steps from "./Steps"
import { import {
buildBreakfastPackages, buildBreakfastPackages,
calculateBreakfastData, calculateBreakfastData,
@@ -315,14 +314,11 @@ export default function AddAncillaryFlowModal({
className={styles.form} className={styles.form}
id="add-ancillary-form-id" id="add-ancillary-form-id"
> >
<div className={styles.modalScrollable}> <AddAncillaryFlow
<Description /> error={errorMessage}
<Steps savedCreditCards={savedCreditCards}
user={user} user={user}
savedCreditCards={savedCreditCards} />
error={errorMessage}
/>
</div>
</form> </form>
</FormProvider> </FormProvider>
) )

View File

@@ -12,7 +12,7 @@ import type { IntlShape } from "react-intl"
import type { AncillaryErrorMessage } from "@/types/components/myPages/myStay/ancillaries" import type { AncillaryErrorMessage } from "@/types/components/myPages/myStay/ancillaries"
import type { BreakfastData } from "@/stores/my-stay/add-ancillary-flow" import type { BreakfastData } from "@/stores/my-stay/add-ancillary-flow"
import type { AncillaryFormData } from "../schema" import type { AncillaryFormData } from "./schema"
/** /**
* This function calculates some breakfast data in the store. * This function calculates some breakfast data in the store.

View File

@@ -9,8 +9,7 @@ import { Carousel } from "@/components/Carousel"
import { useAncillaries } from "@/hooks/useAncillaries" import { useAncillaries } from "@/hooks/useAncillaries"
import { AddAncillaryProvider } from "@/providers/AddAncillaryProvider" import { AddAncillaryProvider } from "@/providers/AddAncillaryProvider"
import AddAncillaryFlowModal from "./AddAncillaryFlow/AddAncillaryFlowModal" import AddAncillaryFlow from "./AddAncillaryFlow"
import AncillaryFlowModalWrapper from "./AddAncillaryFlow/AncillaryFlowModalWrapper"
import { AddedAncillaries } from "./AddedAncillaries" import { AddedAncillaries } from "./AddedAncillaries"
import AllAncillariesModal from "./AllAncillariesModal" import AllAncillariesModal from "./AllAncillariesModal"
import WrappedAncillaryCard from "./Card" import WrappedAncillaryCard from "./Card"
@@ -92,15 +91,12 @@ export function Ancillaries({
booking={bookedRoom} booking={bookedRoom}
ancillaries={ancillaries.allUnique} ancillaries={ancillaries.allUnique}
/> />
<AddAncillaryFlow
<AncillaryFlowModalWrapper> user={user}
<AddAncillaryFlowModal booking={bookedRoom}
user={user} packages={packages}
booking={bookedRoom} savedCreditCards={savedCreditCards}
packages={packages} />
savedCreditCards={savedCreditCards}
/>
</AncillaryFlowModalWrapper>
</div> </div>
</AddAncillaryProvider> </AddAncillaryProvider>
) )

View File

@@ -1,13 +1,10 @@
"use client" "use client"
import { Dialog, DialogTrigger } from "react-aria-components" import { useState } from "react"
import { useIntl } from "react-intl" import { useIntl } from "react-intl"
import { Button } from "@scandic-hotels/design-system/Button" import { Button } from "@scandic-hotels/design-system/Button"
import { IconButton } from "@scandic-hotels/design-system/IconButton"
import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon" import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
import { Typography } from "@scandic-hotels/design-system/Typography" import Modal from "@scandic-hotels/design-system/Modal"
import Modal from "@/components/HotelReservation/MyStay/Modal"
import Actions from "./Actions" import Actions from "./Actions"
import Info from "./Info" import Info from "./Info"
@@ -15,6 +12,7 @@ import Info from "./Info"
import styles from "./manageStay.module.css" import styles from "./manageStay.module.css"
export default function ManageStay() { export default function ManageStay() {
const [isOpen, setIsOpen] = useState(false)
const intl = useIntl() const intl = useIntl()
const manageStay = intl.formatMessage({ const manageStay = intl.formatMessage({
@@ -22,36 +20,26 @@ export default function ManageStay() {
defaultMessage: "Manage stay", defaultMessage: "Manage stay",
}) })
function onClose() {
setIsOpen(false)
}
return ( return (
<DialogTrigger> <>
<Button <Button
size="Medium" size="Medium"
variant="Tertiary" variant="Tertiary"
typography="Body/Supporting text (caption)/smBold" typography="Body/Supporting text (caption)/smBold"
onPress={() => setIsOpen(true)}
> >
<span>{manageStay}</span> <span>{manageStay}</span>
<MaterialIcon color="CurrentColor" icon="keyboard_arrow_down" /> <MaterialIcon color="CurrentColor" icon="keyboard_arrow_down" />
</Button> </Button>
<Modal> <Modal isOpen={isOpen} onToggle={onClose} title={manageStay}>
<Dialog className={styles.dialog}> <div className={styles.content}>
{({ close }) => ( <Actions onClose={onClose} />
<> <Info />
<header className={styles.header}> </div>
<Typography variant="Title/Subtitle/lg">
<span className={styles.title}>{manageStay}</span>
</Typography>
<IconButton onPress={close} theme="Inverted">
<MaterialIcon color="CurrentColor" icon="close" />
</IconButton>
</header>
<div className={styles.content}>
<Actions onClose={close} />
<Info />
</div>
</>
)}
</Dialog>
</Modal> </Modal>
</DialogTrigger> </>
) )
} }

View File

@@ -1,30 +1,11 @@
.dialog {
display: grid;
flex: 1;
gap: var(--Space-x2);
}
.header {
align-items: center;
display: flex;
gap: var(--Space-x2);
justify-content: space-between;
}
.title {
color: var(--Text-Default);
}
.content { .content {
padding-top: var(--Space-x1);
display: grid; display: grid;
gap: var(--Space-x2); gap: var(--Space-x2);
width: 100%;
} }
@media screen and (min-width: 768px) { @media screen and (min-width: 768px) {
.dialog {
gap: var(--Space-x3);
}
.content { .content {
gap: var(--Space-x3); gap: var(--Space-x3);
grid-template-columns: 1fr 1fr; grid-template-columns: 1fr 1fr;

View File

@@ -40,7 +40,6 @@ export interface AddAncillaryFlowModalProps {
export interface SelectQuantityStepProps { export interface SelectQuantityStepProps {
user: User | null user: User | null
hideSummary?: boolean
} }
export interface InnerSelectQuantityStepProps { export interface InnerSelectQuantityStepProps {
user: User | null user: User | null

View File

@@ -41,11 +41,13 @@ function InnerModal({
withActions, withActions,
hideHeader, hideHeader,
className, className,
contentClassName,
}: PropsWithChildren<InnerModalProps>) { }: PropsWithChildren<InnerModalProps>) {
const intl = useIntl() const intl = useIntl()
const contentClassNames = modalContentVariants({ const contentClassNames = modalContentVariants({
withActions, withActions,
className: contentClassName,
}) })
function modalStateHandler(newAnimationState: AnimationState) { function modalStateHandler(newAnimationState: AnimationState) {
@@ -155,6 +157,7 @@ export default function Modal({
withActions = false, withActions = false,
hideHeader = false, hideHeader = false,
className = '', className = '',
contentClassName = '',
}: PropsWithChildren<ModalProps>) { }: PropsWithChildren<ModalProps>) {
const [animation, setAnimation] = useState<AnimationState>( const [animation, setAnimation] = useState<AnimationState>(
AnimationStateEnum.visible AnimationStateEnum.visible
@@ -188,6 +191,7 @@ export default function Modal({
withActions={withActions} withActions={withActions}
hideHeader={hideHeader} hideHeader={hideHeader}
className={className} className={className}
contentClassName={contentClassName}
> >
{children} {children}
</InnerModal> </InnerModal>

View File

@@ -33,7 +33,7 @@
display: flex; display: flex;
align-items: flex-start; align-items: flex-start;
position: relative; position: relative;
padding: var(--Space-x3) var(--Space-x7) 0 var(--Space-x3); padding: var(--Space-x2) var(--Space-x7) var(--Space-x1) var(--Space-x2);
} }
.content { .content {
@@ -49,13 +49,13 @@
} }
.contentWithoutActions { .contentWithoutActions {
padding: 0 var(--Space-x3) var(--Space-x4); padding: 0 var(--Space-x2) var(--Space-x3);
} }
.close { .close {
position: absolute; position: absolute;
top: var(--Space-x2); top: var(--Space-x1);
right: var(--Space-x2); right: var(--Space-x1);
} }
.verticalCenter { .verticalCenter {
@@ -80,4 +80,17 @@
.dialog { .dialog {
max-height: 90dvh; max-height: 90dvh;
} }
.header {
padding: var(--Space-x3) var(--Space-x7) var(--Space-x1) var(--Space-x3);
}
.contentWithoutActions {
padding: 0 var(--Space-x3) var(--Space-x4);
}
.close {
top: var(--Space-x2);
right: var(--Space-x2);
}
} }

View File

@@ -15,6 +15,7 @@ export type ModalProps = {
withActions?: boolean withActions?: boolean
hideHeader?: boolean hideHeader?: boolean
className?: string className?: string
contentClassName?: string
} & ( } & (
| { | {
trigger: JSX.Element trigger: JSX.Element