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
return (
<div
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 className={styles.summary}>
<div
className={cx({
[styles.confirmButtons]: isConfirmation,
[styles.backgroundBox]: isConfirmation || isSingleItem,
})}
>
{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>
{(isSingleItem || isConfirmation) && (
<PriceDetails
totalPrice={totalPrice}
totalPoints={totalPoints}
selectedAncillary={selectedAncillary}
/>
)}
{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}>
<Button
typography="Body/Supporting text (caption)/smBold"
type="button"
variant="Text"
size="Small"
color="Primary"
onPress={() => prevStep(isMobile)}
>
{intl.formatMessage({
id: "common.back",
defaultMessage: "Back",
})}
</Button>
<div className={styles.buttons}>
<Button
typography="Body/Supporting text (caption)/smBold"
type="button"
variant="Text"
size="Small"
color="Primary"
onPress={() => prevStep(isMobile)}
>
{intl.formatMessage({
id: "common.back",
defaultMessage: "Back",
})}
</Button>
<Button
typography="Body/Supporting text (caption)/smBold"
size="Small"
isDisabled={isSubmitting}
isPending={isSubmitting}
{...buttonProps}
>
{buttonLabel}
</Button>
<Button
typography="Body/Supporting text (caption)/smBold"
size="Small"
isDisabled={isSubmitting}
isPending={isSubmitting}
{...buttonProps}
>
{buttonLabel}
</Button>
</div>
</div>
</div>
</div>

View File

@@ -1,8 +1,10 @@
.summmary {
.summary {
display: flex;
flex-direction: column;
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 {

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 { trackUpdatePaymentMethod } from "@/utils/tracking"
import Summary from "../Summary"
import styles from "./confirmationStep.module.css"
import type { ConfirmationStepProps } from "@/types/components/myPages/myStay/ancillaries"
@@ -228,7 +226,6 @@ export default function ConfirmationStep({
</Typography>
)}
<TermsAndConditions />
<Summary isConfirmation />
</div>
)
}

View File

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

View File

@@ -18,10 +18,7 @@ export default function Mobile({ user, savedCreditCards, error }: StepsProps) {
if (currentStep === AncillaryStepEnum.selectQuantity) {
return (
<>
<SelectQuantityStep
user={user}
hideSummary={selectedAncillary?.requiresDeliveryTime}
/>
<SelectQuantityStep user={user} />
{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 { getErrorMessage } from "@/utils/getErrorMessage"
import Summary from "../Summary"
import { BreakfastInfo } from "./BreakfastInfo"
import styles from "./selectQuantityStep.module.css"
@@ -26,10 +25,7 @@ const cardQuantityOptions = Array.from({ length: 7 }, (_, i) => ({
value: i,
}))
export default function SelectQuantityStep({
user,
hideSummary = false,
}: SelectQuantityStepProps) {
export default function SelectQuantityStep({ user }: SelectQuantityStepProps) {
const { isBreakfast, selectedAncillary } = useAddAncillaryStore((state) => ({
isBreakfast: state.isBreakfast,
selectedAncillary: state.selectedAncillary,
@@ -50,12 +46,7 @@ export default function SelectQuantityStep({
)
}
return (
<div className={styles.container}>
{content}
{!hideSummary && <Summary />}
</div>
)
return <div className={styles.container}>{content}</div>
}
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,
} from "@/utils/tracking/myStay"
import { isAncillaryError } from "../../../utils"
import { type AncillaryFormData, ancillaryFormSchema } from "../schema"
import Description from "./Description"
import Steps from "./Steps"
import { isAncillaryError } from "../../utils"
import AddAncillaryFlow from "./Modal"
import { type AncillaryFormData, ancillaryFormSchema } from "./schema"
import {
buildBreakfastPackages,
calculateBreakfastData,
@@ -315,14 +314,11 @@ export default function AddAncillaryFlowModal({
className={styles.form}
id="add-ancillary-form-id"
>
<div className={styles.modalScrollable}>
<Description />
<Steps
user={user}
savedCreditCards={savedCreditCards}
error={errorMessage}
/>
</div>
<AddAncillaryFlow
error={errorMessage}
savedCreditCards={savedCreditCards}
user={user}
/>
</form>
</FormProvider>
)

View File

@@ -12,7 +12,7 @@ import type { IntlShape } from "react-intl"
import type { AncillaryErrorMessage } from "@/types/components/myPages/myStay/ancillaries"
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.

View File

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

View File

@@ -1,13 +1,10 @@
"use client"
import { Dialog, DialogTrigger } from "react-aria-components"
import { useState } from "react"
import { useIntl } from "react-intl"
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 { Typography } from "@scandic-hotels/design-system/Typography"
import Modal from "@/components/HotelReservation/MyStay/Modal"
import Modal from "@scandic-hotels/design-system/Modal"
import Actions from "./Actions"
import Info from "./Info"
@@ -15,6 +12,7 @@ import Info from "./Info"
import styles from "./manageStay.module.css"
export default function ManageStay() {
const [isOpen, setIsOpen] = useState(false)
const intl = useIntl()
const manageStay = intl.formatMessage({
@@ -22,36 +20,26 @@ export default function ManageStay() {
defaultMessage: "Manage stay",
})
function onClose() {
setIsOpen(false)
}
return (
<DialogTrigger>
<>
<Button
size="Medium"
variant="Tertiary"
typography="Body/Supporting text (caption)/smBold"
onPress={() => setIsOpen(true)}
>
<span>{manageStay}</span>
<MaterialIcon color="CurrentColor" icon="keyboard_arrow_down" />
</Button>
<Modal>
<Dialog className={styles.dialog}>
{({ close }) => (
<>
<header className={styles.header}>
<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 isOpen={isOpen} onToggle={onClose} title={manageStay}>
<div className={styles.content}>
<Actions onClose={onClose} />
<Info />
</div>
</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 {
padding-top: var(--Space-x1);
display: grid;
gap: var(--Space-x2);
width: 100%;
}
@media screen and (min-width: 768px) {
.dialog {
gap: var(--Space-x3);
}
.content {
gap: var(--Space-x3);
grid-template-columns: 1fr 1fr;

View File

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

View File

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

View File

@@ -33,7 +33,7 @@
display: flex;
align-items: flex-start;
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 {
@@ -49,13 +49,13 @@
}
.contentWithoutActions {
padding: 0 var(--Space-x3) var(--Space-x4);
padding: 0 var(--Space-x2) var(--Space-x3);
}
.close {
position: absolute;
top: var(--Space-x2);
right: var(--Space-x2);
top: var(--Space-x1);
right: var(--Space-x1);
}
.verticalCenter {
@@ -80,4 +80,17 @@
.dialog {
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
hideHeader?: boolean
className?: string
contentClassName?: string
} & (
| {
trigger: JSX.Element