Merged in feat/SW-2703-mobile-summary-improvements (pull request #2060)

Feat/SW-2703 mobile price summary improvements

* feat(SW-2703): fixes to select rate price summary

* feat(SW-2703): fixes enter details summary mobile

* fix: z-index issue related to booking widget popover

* fix

* fix: added accessibility props to overlay div

* fix: added button inside header

* fix: rename aria button


Approved-by: Michael Zetterberg
This commit is contained in:
Tobias Johansson
2025-05-13 09:22:34 +00:00
parent 19166ec5c7
commit 13261d425c
13 changed files with 178 additions and 108 deletions

View File

@@ -20,6 +20,7 @@
bottom: 0; bottom: 0;
left: 0; left: 0;
right: 0; right: 0;
z-index: 99;
} }
@media screen and (min-width: 1367px) { @media screen and (min-width: 1367px) {

View File

@@ -32,6 +32,8 @@
.wrapper[data-open="true"] { .wrapper[data-open="true"] {
grid-template-rows: 1fr 7.5em; grid-template-rows: 1fr 7.5em;
position: relative;
z-index: var(--default-modal-z-index);
} }
.wrapper[data-open="true"] .bottomSheet { .wrapper[data-open="true"] .bottomSheet {

View File

@@ -12,8 +12,11 @@ import styles from "./mobile.module.css"
import type { SummaryProps } from "@/types/components/hotelReservation/summary" import type { SummaryProps } from "@/types/components/hotelReservation/summary"
export default function MobileSummary({ isMember }: SummaryProps) { export default function MobileSummary({ isMember }: SummaryProps) {
const toggleSummaryOpen = useEnterDetailsStore( const { isSummaryOpen, toggleSummaryOpen } = useEnterDetailsStore(
(state) => state.actions.toggleSummaryOpen (state) => ({
isSummaryOpen: state.isSummaryOpen,
toggleSummaryOpen: state.actions.toggleSummaryOpen,
})
) )
const { booking, rooms, totalPrice, vat } = useEnterDetailsStore((state) => ({ const { booking, rooms, totalPrice, vat } = useEnterDetailsStore((state) => ({
@@ -31,7 +34,21 @@ export default function MobileSummary({ isMember }: SummaryProps) {
return ( return (
<div className={styles.mobileSummary}> <div className={styles.mobileSummary}>
{showPromo ? <SignupPromoMobile /> : null} {showPromo ? (
<div className={styles.signupPromoWrapper}>
<SignupPromoMobile />
</div>
) : null}
{isSummaryOpen && (
<div
className={styles.overlay}
role="presentation"
aria-hidden="true"
onClick={toggleSummaryOpen}
/>
)}
<SummaryBottomSheet> <SummaryBottomSheet>
<div className={styles.wrapper}> <div className={styles.wrapper}>
<SummaryUI <SummaryUI

View File

@@ -9,7 +9,21 @@
border-style: solid; border-style: solid;
border-width: 1px; border-width: 1px;
border-bottom: none; border-bottom: none;
z-index: 10; }
.signupPromoWrapper {
position: relative;
z-index: var(--default-modal-z-index);
}
.overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: var(--Overlay-40);
z-index: var(--default-modal-overlay-z-index);
} }
} }

View File

@@ -4,7 +4,6 @@ import { Fragment } 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 { dt } from "@/lib/dt" import { dt } from "@/lib/dt"
@@ -104,7 +103,11 @@ export default function SummaryUI({
return ( return (
<section className={styles.summary}> <section className={styles.summary}>
<header className={styles.header}> <header
className={styles.header}
role="button"
onClick={handleToggleSummary}
>
<Subtitle className={styles.title} type="two"> <Subtitle className={styles.title} type="two">
{intl.formatMessage({ {intl.formatMessage({
defaultMessage: "Booking summary", defaultMessage: "Booking summary",
@@ -120,18 +123,12 @@ export default function SummaryUI({
{/* eslint-disable-next-line formatjs/no-literal-string-in-jsx */} {/* eslint-disable-next-line formatjs/no-literal-string-in-jsx */}
{dt(booking.toDate).locale(lang).format("ddd, D MMM")} ({nightsMsg}) {dt(booking.toDate).locale(lang).format("ddd, D MMM")} ({nightsMsg})
</Body> </Body>
<IconButton <MaterialIcon
onPress={handleToggleSummary} className={styles.chevronIcon}
className={styles.chevronButton} icon="keyboard_arrow_down"
theme="Black" size={30}
style="Muted" color="CurrentColor"
> />
<MaterialIcon
icon="keyboard_arrow_down"
size={20}
color="CurrentColor"
/>
</IconButton>
</header> </header>
<Divider color="primaryLightSubtle" /> <Divider color="primaryLightSubtle" />
{rooms.map(({ room }, idx) => { {rooms.map(({ room }, idx) => {

View File

@@ -9,18 +9,17 @@
.header { .header {
display: grid; display: grid;
grid-template-areas: "title button" "date button"; grid-template-areas: "title button" "date date";
grid-template-columns: 1fr auto;
align-items: center;
} }
.title { .title {
grid-area: title; grid-area: title;
} }
.chevronButton { .chevronIcon {
grid-area: button; grid-area: button;
justify-self: end;
align-items: center;
margin-right: calc(0px - var(--Spacing-x2));
} }
.date { .date {

View File

@@ -1,9 +1,9 @@
"use client" "use client"
import { Fragment } from "react" import { Fragment } from "react"
import { Button as ButtonRAC } from "react-aria-components"
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 { dt } from "@/lib/dt" import { dt } from "@/lib/dt"
@@ -74,28 +74,29 @@ export default function Summary({
return ( return (
<section className={styles.summary}> <section className={styles.summary}>
<header className={styles.header}> <header className={styles.header}>
<Subtitle className={styles.title} type="two"> <ButtonRAC onPress={toggleSummaryOpen}>
{intl.formatMessage({ <Subtitle className={styles.title} type="two">
defaultMessage: "Booking summary", {intl.formatMessage({
})} defaultMessage: "Booking summary",
</Subtitle> })}
<Body className={styles.date} color="baseTextMediumContrast"> </Subtitle>
{dt(booking.fromDate).locale(lang).format("ddd, D MMM")} <Body className={styles.date} color="baseTextMediumContrast">
<MaterialIcon {dt(booking.fromDate).locale(lang).format("ddd, D MMM")}
icon="arrow_forward" <MaterialIcon
size={15} icon="arrow_forward"
color="Icon/Interactive/Secondary" size={15}
/> color="Icon/Interactive/Secondary"
{/* eslint-disable-next-line formatjs/no-literal-string-in-jsx */} />
{dt(booking.toDate).locale(lang).format("ddd, D MMM")} ({nights}) {/* eslint-disable-next-line formatjs/no-literal-string-in-jsx */}
</Body> {dt(booking.toDate).locale(lang).format("ddd, D MMM")} ({nights})
<IconButton onPress={toggleSummaryOpen} theme="Black" style="Muted"> </Body>
<MaterialIcon <MaterialIcon
className={styles.chevronIcon}
icon="keyboard_arrow_down" icon="keyboard_arrow_down"
size={20} size={30}
color="CurrentColor" color="CurrentColor"
/> />
</IconButton> </ButtonRAC>
</header> </header>
<Divider color="primaryLightSubtle" /> <Divider color="primaryLightSubtle" />
{rooms.map((room, idx) => { {rooms.map((room, idx) => {

View File

@@ -4,6 +4,7 @@ import { useIntl } from "react-intl"
import { useRatesStore } from "@/stores/select-rate" import { useRatesStore } from "@/stores/select-rate"
import SignupPromoMobile from "@/components/HotelReservation/SignupPromo/Mobile"
import Button from "@/components/TempDesignSystem/Button" import Button from "@/components/TempDesignSystem/Button"
import Caption from "@/components/TempDesignSystem/Text/Caption" import Caption from "@/components/TempDesignSystem/Text/Caption"
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle" import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
@@ -22,6 +23,7 @@ export default function MobileSummary({
isAllRoomsSelected, isAllRoomsSelected,
isUserLoggedIn, isUserLoggedIn,
totalPriceToShow, totalPriceToShow,
showMemberDiscountBanner,
}: MobileSummaryProps) { }: MobileSummaryProps) {
const intl = useIntl() const intl = useIntl()
const scrollY = useRef(0) const scrollY = useRef(0)
@@ -80,64 +82,79 @@ export default function MobileSummary({
const showDiscounted = containsBookingCodeRate || isUserLoggedIn const showDiscounted = containsBookingCodeRate || isUserLoggedIn
return ( return (
<div className={styles.wrapper} data-open={isSummaryOpen}> <>
<div className={styles.content}> {isSummaryOpen && (
<div className={styles.summaryAccordion}> <div
<Summary className={styles.overlay}
booking={booking} role="presentation"
rooms={rooms} aria-hidden="true"
isMember={isUserLoggedIn} onClick={toggleSummaryOpen}
totalPrice={totalPriceToShow} />
vat={vat} )}
toggleSummaryOpen={toggleSummaryOpen} {showMemberDiscountBanner ? (
/> <div className={styles.signupPromoWrapper}>
<SignupPromoMobile />
</div>
) : null}
<div className={styles.wrapper} data-open={isSummaryOpen}>
<div className={styles.content}>
<div className={styles.summaryAccordion}>
<Summary
booking={booking}
rooms={rooms}
isMember={isUserLoggedIn}
totalPrice={totalPriceToShow}
vat={vat}
toggleSummaryOpen={toggleSummaryOpen}
/>
</div>
</div>
<div className={styles.bottomSheet}>
<button
data-open={isSummaryOpen}
onClick={(e) => {
e.preventDefault()
toggleSummaryOpen()
}}
className={styles.priceDetailsButton}
>
<Caption>
{intl.formatMessage({
defaultMessage: "Total price",
})}
</Caption>
<Subtitle
color={showDiscounted ? "red" : "uiTextHighContrast"}
className={styles.wrappedText}
>
{formatPrice(
intl,
totalPriceToShow.local.price,
totalPriceToShow.local.currency,
totalPriceToShow.local.additionalPrice,
totalPriceToShow.local.additionalPriceCurrency
)}
</Subtitle>
<Caption color="baseTextHighContrast" type="underline">
{intl.formatMessage({
defaultMessage: "See details",
})}
</Caption>
</button>
<Button
intent="primary"
theme="base"
size="large"
type="submit"
fullWidth
disabled={!isAllRoomsSelected}
>
{intl.formatMessage({
defaultMessage: "Continue",
})}
</Button>
</div> </div>
</div> </div>
<div className={styles.bottomSheet}> </>
<button
data-open={isSummaryOpen}
onClick={(e) => {
e.preventDefault()
toggleSummaryOpen()
}}
className={styles.priceDetailsButton}
>
<Caption>
{intl.formatMessage({
defaultMessage: "Total price",
})}
</Caption>
<Subtitle
color={showDiscounted ? "red" : "uiTextHighContrast"}
className={styles.wrappedText}
>
{formatPrice(
intl,
totalPriceToShow.local.price,
totalPriceToShow.local.currency,
totalPriceToShow.local.additionalPrice,
totalPriceToShow.local.additionalPriceCurrency
)}
</Subtitle>
<Caption color="baseTextHighContrast" type="underline">
{intl.formatMessage({
defaultMessage: "See details",
})}
</Caption>
</button>
<Button
intent="primary"
theme="base"
size="large"
type="submit"
fullWidth
disabled={!isAllRoomsSelected}
>
{intl.formatMessage({
defaultMessage: "Continue",
})}
</Button>
</div>
</div>
) )
} }

View File

@@ -1,4 +1,5 @@
.wrapper { .wrapper {
position: relative;
display: grid; display: grid;
grid-template-rows: 0fr 7.5em; grid-template-rows: 0fr 7.5em;
@@ -6,6 +7,22 @@
border-top: 1px solid var(--Base-Border-Subtle); border-top: 1px solid var(--Base-Border-Subtle);
background: var(--Base-Surface-Primary-light-Normal); background: var(--Base-Surface-Primary-light-Normal);
align-content: end; align-content: end;
z-index: var(--default-modal-z-index);
}
.signupPromoWrapper {
position: relative;
z-index: var(--default-modal-z-index);
}
.overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: var(--Overlay-40);
z-index: var(--default-modal-overlay-z-index);
} }
.bottomSheet { .bottomSheet {

View File

@@ -7,20 +7,25 @@
height: 100%; height: 100%;
} }
.header { .header button {
display: grid; display: grid;
grid-template-areas: "title button" "date button"; grid-template-areas: "title button" "date date";
grid-template-columns: 1fr auto;
align-items: center;
width: 100%;
background-color: transparent;
border: none;
padding: 0;
margin: 0;
} }
.title { .title {
grid-area: title; grid-area: title;
} }
.chevronButton { .chevronIcon {
grid-area: button; grid-area: button;
justify-self: end;
align-items: center;
margin-right: calc(0px - var(--Spacing-x2));
} }
.date { .date {

View File

@@ -8,7 +8,6 @@ import { dt } from "@/lib/dt"
import { useRatesStore } from "@/stores/select-rate" import { useRatesStore } from "@/stores/select-rate"
import SignupPromoDesktop from "@/components/HotelReservation/SignupPromo/Desktop" import SignupPromoDesktop from "@/components/HotelReservation/SignupPromo/Desktop"
import SignupPromoMobile from "@/components/HotelReservation/SignupPromo/Mobile"
import Button from "@/components/TempDesignSystem/Button" import Button from "@/components/TempDesignSystem/Button"
import Body from "@/components/TempDesignSystem/Text/Body" import Body from "@/components/TempDesignSystem/Text/Body"
import Caption from "@/components/TempDesignSystem/Text/Caption" import Caption from "@/components/TempDesignSystem/Text/Caption"
@@ -409,11 +408,11 @@ export default function RateSummary() {
</div> </div>
</div> </div>
<div className={styles.mobileSummary}> <div className={styles.mobileSummary}>
{showMemberDiscountBanner ? <SignupPromoMobile /> : null}
<MobileSummary <MobileSummary
isAllRoomsSelected={isAllRoomsSelected} isAllRoomsSelected={isAllRoomsSelected}
isUserLoggedIn={isUserLoggedIn} isUserLoggedIn={isUserLoggedIn}
totalPriceToShow={totalPriceToShow} totalPriceToShow={totalPriceToShow}
showMemberDiscountBanner={showMemberDiscountBanner}
/> />
</div> </div>
</div> </div>

View File

@@ -17,7 +17,7 @@
left: 0; left: 0;
position: fixed; position: fixed;
right: 0; right: 0;
z-index: 10; z-index: 99;
} }
.content { .content {

View File

@@ -4,4 +4,5 @@ export interface MobileSummaryProps {
isAllRoomsSelected: boolean isAllRoomsSelected: boolean
isUserLoggedIn: boolean isUserLoggedIn: boolean
totalPriceToShow: Price totalPriceToShow: Price
showMemberDiscountBanner: boolean
} }