From 6a008ba3421ebff6f17c2815c67e19f732d1b4db Mon Sep 17 00:00:00 2001 From: Matilda Haneling Date: Mon, 12 Jan 2026 14:18:51 +0000 Subject: [PATCH] Merged in fix/book-149-incorrect-onfocus-behaviour-booking-widget (pull request #3320) Fix/book 149 incorrect onfocus behaviour booking widget * fix(BOOK-149): fixed labels shifting * fix(BOOK-149): reintroduced sticky position * fix(BOOK-149): added missing border to "where" text field * added overflow to datepicker * comment fixes * removed separate typography declaration * changed to onPress * fix(BOOK-149): moved components to separate files * fix(BOOK-149): removed desktop & mobile specific css classes * fix(BOOK-149): new implementation of date and room modals * dependencies update * fix(BOOK-149): fixed child age dropdown issue, related error message, and Rooms & Guests container height * updated info button to new variant * fix(BOOK-149): prevent scrolling of background when modals are open in Tablet mode * fixed overlay issue and added focus indicator on mobile * fixed missing space in css * rebase and fixed icon buttons after update * simplified to use explicit boolean * PR comments fixes * more PR comment fixes * PR comment fixes * fixed setIsOpen((prev) => !prev) * fixed issues with room error not showing properly on mobile * fixing pr comments * fixed flickering on GuestRoomModal Approved-by: Erik Tiekstra --- .../booking-code-error.module.css | 28 ++ .../BookingCode/BookingCodeError/index.tsx | 48 ++++ .../code-rules-modal.module.css | 11 + .../BookingCode/CodeRulesModal/index.tsx | 50 ++++ .../BookingCode/RememberCode/index.tsx | 49 ++++ .../RememberCode/remember-code.module.css | 9 + .../BookingCode/booking-code.module.css | 66 ++--- .../FormContent/BookingCode/index.tsx | 185 +++++--------- .../FormContent/RewardNight/index.tsx | 65 +++-- .../RewardNight/reward-night.module.css | 6 + .../FormContent/Search/SearchList/index.tsx | 5 +- .../Search/SearchList/searchList.module.css | 4 + .../FormContent/Search/search.module.css | 4 - .../FormContent/ValidationError/index.tsx | 44 ++-- .../validationError.module.css | 2 + .../FormContent/formContent.module.css | 70 +++-- .../BookingWidgetForm/FormContent/index.tsx | 114 +++++---- .../BookingWidgetForm/form.module.css | 6 +- .../DatePicker/Range/Desktop.tsx | 22 +- .../BookingWidget/DatePicker/Range/Mobile.tsx | 20 +- .../DatePicker/Range/desktop.module.css | 1 + .../DatePicker/Range/mobile.module.css | 2 +- .../DatePicker/date-picker.module.css | 75 +++--- .../BookingWidget/DatePicker/index.tsx | 240 +++++++++++------- .../ChildSelector/ChildInfoSelector.tsx | 91 +++++-- .../ChildSelector/child-selector.module.css | 1 + .../BookingWidget/GuestsRoomsPicker/Form.tsx | 169 ++++++------ .../ValidationError/index.tsx | 33 +++ .../validationError.module.css | 38 +++ .../guests-rooms-picker.module.css | 151 +++++------ .../BookingWidget/GuestsRoomsPicker/index.tsx | 229 +++++++++++------ .../BookingWidget/bookingWidget.module.css | 6 +- packages/booking-flow/package.json | 1 + .../lib/components/Button/button.module.css | 2 +- .../lib/components/Form/Checkbox/index.tsx | 3 +- .../lib/components/Select/Select.tsx | 7 +- .../lib/components/Select/types.ts | 2 + yarn.lock | 1 + 38 files changed, 1117 insertions(+), 743 deletions(-) create mode 100644 packages/booking-flow/lib/components/BookingWidget/BookingWidgetForm/FormContent/BookingCode/BookingCodeError/booking-code-error.module.css create mode 100644 packages/booking-flow/lib/components/BookingWidget/BookingWidgetForm/FormContent/BookingCode/BookingCodeError/index.tsx create mode 100644 packages/booking-flow/lib/components/BookingWidget/BookingWidgetForm/FormContent/BookingCode/CodeRulesModal/code-rules-modal.module.css create mode 100644 packages/booking-flow/lib/components/BookingWidget/BookingWidgetForm/FormContent/BookingCode/CodeRulesModal/index.tsx create mode 100644 packages/booking-flow/lib/components/BookingWidget/BookingWidgetForm/FormContent/BookingCode/RememberCode/index.tsx create mode 100644 packages/booking-flow/lib/components/BookingWidget/BookingWidgetForm/FormContent/BookingCode/RememberCode/remember-code.module.css create mode 100644 packages/booking-flow/lib/components/BookingWidget/GuestsRoomsPicker/ValidationError/index.tsx create mode 100644 packages/booking-flow/lib/components/BookingWidget/GuestsRoomsPicker/ValidationError/validationError.module.css diff --git a/packages/booking-flow/lib/components/BookingWidget/BookingWidgetForm/FormContent/BookingCode/BookingCodeError/booking-code-error.module.css b/packages/booking-flow/lib/components/BookingWidget/BookingWidgetForm/FormContent/BookingCode/BookingCodeError/booking-code-error.module.css new file mode 100644 index 000000000..d7073bfbf --- /dev/null +++ b/packages/booking-flow/lib/components/BookingWidget/BookingWidgetForm/FormContent/BookingCode/BookingCodeError/booking-code-error.module.css @@ -0,0 +1,28 @@ +.errorContainer { + display: flex; + flex-direction: column; + gap: var(--Space-x2); +} + +.error { + display: flex; + gap: var(--Space-x1); + text-wrap: wrap; + color: var(--UI-Text-Error); +} + +.removeButton { + width: 100%; +} + +@media screen and (min-width: 768px) { + .error { + color: var(--Text-Default); + } +} + +@media screen and (max-width: 767px) { + .removeButton { + display: none; + } +} diff --git a/packages/booking-flow/lib/components/BookingWidget/BookingWidgetForm/FormContent/BookingCode/BookingCodeError/index.tsx b/packages/booking-flow/lib/components/BookingWidget/BookingWidgetForm/FormContent/BookingCode/BookingCodeError/index.tsx new file mode 100644 index 000000000..2959d03dc --- /dev/null +++ b/packages/booking-flow/lib/components/BookingWidget/BookingWidgetForm/FormContent/BookingCode/BookingCodeError/index.tsx @@ -0,0 +1,48 @@ +import { type FieldError } from "react-hook-form" +import { useIntl } from "react-intl" + +import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon" +import { Typography } from "@scandic-hotels/design-system/Typography" + +import { useBookingFlowConfig } from "../../../../../../bookingFlowConfig/bookingFlowConfigContext" +import { getErrorMessage } from "../../../../../BookingFlowInput/errors" +import { RemoveExtraRooms } from "../../RemoveExtraRooms/RemoveExtraRooms" +import { isMultiRoomError } from "../../utils" + +import styles from "./booking-code-error.module.css" + +export function BookingCodeError({ + codeError, + isDesktop = false, +}: { + codeError: FieldError + isDesktop?: boolean +}) { + const intl = useIntl() + const isMultiroomError = isMultiRoomError(codeError.message) + const config = useBookingFlowConfig() + + return ( +
+ + + + {getErrorMessage(intl, config.variant, codeError.message)} + + + {isMultiroomError ? ( +
+ +
+ ) : null} +
+ ) +} diff --git a/packages/booking-flow/lib/components/BookingWidget/BookingWidgetForm/FormContent/BookingCode/CodeRulesModal/code-rules-modal.module.css b/packages/booking-flow/lib/components/BookingWidget/BookingWidgetForm/FormContent/BookingCode/CodeRulesModal/code-rules-modal.module.css new file mode 100644 index 000000000..f371ccc71 --- /dev/null +++ b/packages/booking-flow/lib/components/BookingWidget/BookingWidgetForm/FormContent/BookingCode/CodeRulesModal/code-rules-modal.module.css @@ -0,0 +1,11 @@ +.bookingCodeTooltip { + max-width: 560px; + margin-top: var(--Space-x2); + color: var(--Text-Secondary); +} +.infoButton { + align-self: center; + color: var( + --Icon-Interactive-Placeholder + ) !important; /* Override IconButton default color */ +} diff --git a/packages/booking-flow/lib/components/BookingWidget/BookingWidgetForm/FormContent/BookingCode/CodeRulesModal/index.tsx b/packages/booking-flow/lib/components/BookingWidget/BookingWidgetForm/FormContent/BookingCode/CodeRulesModal/index.tsx new file mode 100644 index 000000000..38101e0b2 --- /dev/null +++ b/packages/booking-flow/lib/components/BookingWidget/BookingWidgetForm/FormContent/BookingCode/CodeRulesModal/index.tsx @@ -0,0 +1,50 @@ +import { useIntl } from "react-intl" + +import { IconButton } from "@scandic-hotels/design-system/IconButton" +import Modal from "@scandic-hotels/design-system/Modal" +import { Typography } from "@scandic-hotels/design-system/Typography" + +import styles from "./code-rules-modal.module.css" + +export default function CodeRulesModal() { + const intl = useIntl() + const codeVoucher = intl.formatMessage({ + id: "booking.codeVoucher", + defaultMessage: "Code / Voucher", + }) + const bookingCodeTooltipText = intl.formatMessage({ + id: "bookingWidget.bookingCode.tooltip", + defaultMessage: + "If you're booking a promotional offer or a Corporate negotiated rate you'll need a special booking code. Don't use any special characters such as (.) (,) (-) (:). If you would like to make a booking with code VOF, please call us +46 8 517 517 20.Save your booking code for the next time you visit the page by ticking the box “Remember”. Don't tick the box if you're using a public computer to avoid unauthorized access to your booking code.", + }) + + const infoButtonAriaLabel = intl.formatMessage( + { + id: "bookingWidget.bookingCode.readMore", + defaultMessage: "Read more about using {codeVoucher}", + }, + { + codeVoucher, + } + ) + + return ( + + } + title={codeVoucher} + > + +

{bookingCodeTooltipText}

+
+
+ ) +} diff --git a/packages/booking-flow/lib/components/BookingWidget/BookingWidgetForm/FormContent/BookingCode/RememberCode/index.tsx b/packages/booking-flow/lib/components/BookingWidget/BookingWidgetForm/FormContent/BookingCode/RememberCode/index.tsx new file mode 100644 index 000000000..e7e85df60 --- /dev/null +++ b/packages/booking-flow/lib/components/BookingWidget/BookingWidgetForm/FormContent/BookingCode/RememberCode/index.tsx @@ -0,0 +1,49 @@ +import { useIntl } from "react-intl" + +import { Button } from "@scandic-hotels/design-system/Button" +import Checkbox from "@scandic-hotels/design-system/Form/Checkbox" +import { Typography } from "@scandic-hotels/design-system/Typography" + +import styles from "./remember-code.module.css" + +type CodeRememberProps = { + bookingCodeValue: string | undefined + onApplyClick: () => void +} + +export function RememberCode({ + bookingCodeValue, + onApplyClick, +}: CodeRememberProps) { + const intl = useIntl() + + const checkBoxLabel = intl.formatMessage({ + id: "bookingWidget.bookingCode.remember", + defaultMessage: "Remember code", + }) + return ( + <> + + + {checkBoxLabel} + + + + + + ) +} diff --git a/packages/booking-flow/lib/components/BookingWidget/BookingWidgetForm/FormContent/BookingCode/RememberCode/remember-code.module.css b/packages/booking-flow/lib/components/BookingWidget/BookingWidgetForm/FormContent/BookingCode/RememberCode/remember-code.module.css new file mode 100644 index 000000000..dec4446ed --- /dev/null +++ b/packages/booking-flow/lib/components/BookingWidget/BookingWidgetForm/FormContent/BookingCode/RememberCode/remember-code.module.css @@ -0,0 +1,9 @@ +.applyButton { + min-width: 100px; +} + +@media screen and (max-width: 767px) { + .applyButton { + display: none; + } +} diff --git a/packages/booking-flow/lib/components/BookingWidget/BookingWidgetForm/FormContent/BookingCode/booking-code.module.css b/packages/booking-flow/lib/components/BookingWidget/BookingWidgetForm/FormContent/BookingCode/booking-code.module.css index efe082141..2fb4bc380 100644 --- a/packages/booking-flow/lib/components/BookingWidget/BookingWidgetForm/FormContent/BookingCode/booking-code.module.css +++ b/packages/booking-flow/lib/components/BookingWidget/BookingWidgetForm/FormContent/BookingCode/booking-code.module.css @@ -10,6 +10,15 @@ background-color: var(--Background-Primary); border-radius: var(--Corner-radius-md); padding: var(--Space-x1) var(--Space-x15); + border: 2px solid transparent; +} + +.bookingCode:focus-within, +.bookingCode:has([data-focused="true"]), +.bookingCode:has([data-pressed="true"]) { + background-color: var(--Surface-Primary-Hover); + border-radius: var(--Corner-radius-md); + border-color: var(--Border-Interactive-Focus); } .bookingCodeLabel { @@ -20,21 +29,9 @@ color: var(--Text-Secondary); } -.colorSecondary { - color: var(--Text-Secondary); -} - -.errorContainer { - display: flex; - flex-direction: column; - gap: var(--Space-x2); -} - -.error { - display: flex; - gap: var(--Space-x1); - white-space: break-spaces; - color: var(--UI-Text-Error); +.input { + background-color: var(--Surface-Primary-Hover); + color: var(--Text-Interactive-Focus); } .bookingCodeRemember, @@ -48,29 +45,10 @@ width: 100%; } -.bookingCodeTooltip { - max-width: 560px; - margin-top: var(--Space-x2); - color: var(--Text-Secondary); -} - .bookingCodeRememberVisible label { align-items: center; } -.removeButton { - width: 100%; -} - -@media screen and (max-width: 767px) { - .hideOnMobile { - display: none; - } - .removeButton { - display: none; - } -} - @media screen and (min-width: 768px) { .bookingCode { height: auto; @@ -82,9 +60,6 @@ justify-content: space-between; border-radius: var(--Space-x15); } - .error { - color: var(--Text-Default); - } } @media screen and (min-width: 768px) and (max-width: 1366px) { @@ -113,24 +88,25 @@ } } +@media screen and (max-width: 767px) { + .bookingCode { + display: flex; + flex-direction: column; + justify-content: center; + } +} @media screen and (min-width: 1367px) { .container:hover { background-color: var(--Surface-Primary-Hover); border-radius: var(--Corner-radius-md); } - .container:focus-within, - .container:has([data-focused="true"]), - .container:has([data-pressed="true"]) { - background-color: var(--Surface-Primary-Hover); - border-radius: var(--Corner-radius-md); - border: 2px solid var(--Border-Interactive-Focus); - } + .bookingCodeRememberVisible { padding: var(--Space-x2); position: absolute; top: calc(100% + var(--Space-x3)); left: calc(0% - var(--Space-x05)); - width: 360px; + width: 320px; box-shadow: var(--popup-box-shadow); } } diff --git a/packages/booking-flow/lib/components/BookingWidget/BookingWidgetForm/FormContent/BookingCode/index.tsx b/packages/booking-flow/lib/components/BookingWidget/BookingWidgetForm/FormContent/BookingCode/index.tsx index 41553e974..ae11c3698 100644 --- a/packages/booking-flow/lib/components/BookingWidget/BookingWidgetForm/FormContent/BookingCode/index.tsx +++ b/packages/booking-flow/lib/components/BookingWidget/BookingWidgetForm/FormContent/BookingCode/index.tsx @@ -1,24 +1,20 @@ import { useCallback, useEffect, useRef, useState } from "react" import { Dialog, DialogTrigger, Popover } from "react-aria-components" -import { type FieldError, useFormContext } from "react-hook-form" +import { useFormContext } from "react-hook-form" import { useIntl } from "react-intl" import { useMediaQuery } from "usehooks-ts" import { Button } from "@scandic-hotels/design-system/Button" import Checkbox from "@scandic-hotels/design-system/Form/Checkbox" -import { IconButton } from "@scandic-hotels/design-system/IconButton" -import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon" -import Modal from "@scandic-hotels/design-system/Modal" import Switch from "@scandic-hotels/design-system/Switch" import { Typography } from "@scandic-hotels/design-system/Typography" import { SEARCH_TYPE_REDEMPTION } from "@scandic-hotels/trpc/constants/booking" -import { useBookingFlowConfig } from "../../../../../bookingFlowConfig/bookingFlowConfigContext" import BookingFlowInput from "../../../../BookingFlowInput" -import { getErrorMessage } from "../../../../BookingFlowInput/errors" import { Input as BookingWidgetInput } from "../Input" -import { RemoveExtraRooms } from "../RemoveExtraRooms/RemoveExtraRooms" -import { isMultiRoomError } from "../utils" +import { BookingCodeError } from "./BookingCodeError" +import CodeRulesModal from "./CodeRulesModal" +import { RememberCode } from "./RememberCode" import styles from "./booking-code.module.css" @@ -106,6 +102,21 @@ export default function BookingCode() { setShowRemember(true) } + // Only show the Remember Code Popover if there is any text in the Booking Code text field + function hideRememberCheck() { + setShowRemember(false) + } + function resetRememberCheck() { + setValue("bookingCode.remember", false, { shouldDirty: true }) + setValue("bookingCode.value", "", { shouldDirty: true }) + setValue("bookingCode.flag", false, { shouldDirty: true }) + } + + useEffect(() => { + if (bookingCode?.value === "") { + setValue("bookingCode.remember", false, { shouldDirty: true }) + } + }, [bookingCode?.value, setValue]) useEffect(() => { setIsTablet(checkIsTablet) }, [checkIsTablet]) @@ -142,27 +153,48 @@ export default function BookingCode() {
closeIfOutside(e.nativeEvent.relatedTarget as HTMLElement)} >
-
+ - {codeVoucher} + -
- updateBookingCodeFormValue(event.target.value)} - autoComplete="off" - value={bookingCode?.value} - /> + + + { + updateBookingCodeFormValue(event.target.value) + if (!!bookingCode?.value) { + showRememberCheck() + } else { + hideRememberCheck() + resetRememberCheck() + } + }} + autoComplete="off" + value={bookingCode?.value} + onFocus={() => { + if (!!bookingCode?.value) { + showRememberCheck() + } + }} + onBlur={(e) => + closeIfOutside(e.nativeEvent.relatedTarget as HTMLElement) + } + /> +
+ {isDesktop ? (
) : ( - setShowRemember(false)} /> @@ -210,106 +242,6 @@ export default function BookingCode() { ) } -type CodeRememberProps = { - bookingCodeValue: string | undefined - onApplyClick: () => void -} - -function CodeRulesModal() { - const intl = useIntl() - const codeVoucher = intl.formatMessage({ - id: "booking.codeVoucher", - defaultMessage: "Code / Voucher", - }) - const bookingCodeTooltipText = intl.formatMessage({ - id: "bookingWidget.bookingCode.tooltip", - defaultMessage: - "If you're booking a promotional offer or a Corporate negotiated rate you'll need a special booking code. Don't use any special characters such as (.) (,) (-) (:). If you would like to make a booking with code VOF, please call us +46 8 517 517 20.Save your booking code for the next time you visit the page by ticking the box “Remember”. Don't tick the box if you're using a public computer to avoid unauthorized access to your booking code.", - }) - - return ( - - } - title={codeVoucher} - > - -

{bookingCodeTooltipText}

-
-
- ) -} - -function CodeRemember({ bookingCodeValue, onApplyClick }: CodeRememberProps) { - const intl = useIntl() - - return ( - <> - - - - {intl.formatMessage({ - id: "bookingWidget.bookingCode.remember", - defaultMessage: "Remember code", - })} - - - - {bookingCodeValue ? ( - - ) : null} - - ) -} - -function BookingCodeError({ - codeError, - isDesktop = false, -}: { - codeError: FieldError - isDesktop?: boolean -}) { - const intl = useIntl() - const isMultiroomError = isMultiRoomError(codeError.message) - const config = useBookingFlowConfig() - - return ( -
- - - - {getErrorMessage(intl, config.variant, codeError.message)} - - - {isMultiroomError ? ( -
- -
- ) : null} -
- ) -} - function TabletBookingCode({ bookingCode, updateValue, @@ -340,6 +272,7 @@ function TabletBookingCode({ document.body.style.overflow = "clip !important" } } + if (!isOpen && !bookingCode?.value) { setValue("bookingCode.flag", false, { shouldDirty: true }) setIsOpen(isOpen) @@ -367,8 +300,8 @@ function TabletBookingCode({ }, })} > - - {codeVoucher} + + {codeVoucher} @@ -395,7 +328,7 @@ function TabletBookingCode({ {codeError?.message ? ( ) : ( - diff --git a/packages/booking-flow/lib/components/BookingWidget/BookingWidgetForm/FormContent/RewardNight/index.tsx b/packages/booking-flow/lib/components/BookingWidget/BookingWidgetForm/FormContent/RewardNight/index.tsx index 2f87d0afe..5da588442 100644 --- a/packages/booking-flow/lib/components/BookingWidget/BookingWidgetForm/FormContent/RewardNight/index.tsx +++ b/packages/booking-flow/lib/components/BookingWidget/BookingWidgetForm/FormContent/RewardNight/index.tsx @@ -36,6 +36,14 @@ export default function RewardNight() { const reward = getRewardMessage(config, intl) const rewardNightTooltip = getRewardNightTooltipMessage(config, intl) + const rewardLabel = intl.formatMessage( + { + id: "bookingWidget.reward.readMore", + defaultMessage: "Read more about booking with {reward}", + }, + { reward: reward } + ) + const redemptionErr = errors[SEARCH_TYPE_REDEMPTION] const isDesktop = useMediaQuery("(min-width: 767px)") @@ -45,6 +53,7 @@ export default function RewardNight() { if (value && getValues("bookingCode.value")) { setValue("bookingCode.flag", false) setValue("bookingCode.value", "", { shouldValidate: true }) + setValue("bookingCode.remember", false) // Hide the notification popup after 5 seconds by re-triggering validation // This is kept consistent with location search field error notification timeout setTimeout(() => { @@ -83,37 +92,45 @@ export default function RewardNight() { return (
closeOnBlur(e.nativeEvent)}> - { - validateRedemption(e.target.value) - }, - }} - > -
+
+ { + validateRedemption(e.target.value) + }, + }} + > {reward} - - } - title={reward} + + + } + title={reward} + > + - - {rewardNightTooltip} - - -
- + {rewardNightTooltip} + + +
+ {redemptionErr && (
{caption} - +

{body}

diff --git a/packages/booking-flow/lib/components/BookingWidget/BookingWidgetForm/FormContent/Search/SearchList/searchList.module.css b/packages/booking-flow/lib/components/BookingWidget/BookingWidgetForm/FormContent/Search/SearchList/searchList.module.css index 9e3c4b43c..62c1c5965 100644 --- a/packages/booking-flow/lib/components/BookingWidget/BookingWidgetForm/FormContent/Search/SearchList/searchList.module.css +++ b/packages/booking-flow/lib/components/BookingWidget/BookingWidgetForm/FormContent/Search/SearchList/searchList.module.css @@ -36,3 +36,7 @@ .textPlaceholderColor { color: var(--UI-Text-Placeholder); } + +.errorBody { + color: var(--UI-Text-High-contrast); +} diff --git a/packages/booking-flow/lib/components/BookingWidget/BookingWidgetForm/FormContent/Search/search.module.css b/packages/booking-flow/lib/components/BookingWidget/BookingWidgetForm/FormContent/Search/search.module.css index ba533a6c6..776401207 100644 --- a/packages/booking-flow/lib/components/BookingWidget/BookingWidgetForm/FormContent/Search/search.module.css +++ b/packages/booking-flow/lib/components/BookingWidget/BookingWidgetForm/FormContent/Search/search.module.css @@ -19,10 +19,6 @@ &:has(input:active, input:focus, input:focus-within) { background-color: var(--Surface-Primary-Hover); } - - &:has(input:active, input:focus, input:focus-within) { - border: 2px solid var(--Border-Interactive-Focus); - } } .label { diff --git a/packages/booking-flow/lib/components/BookingWidget/BookingWidgetForm/FormContent/ValidationError/index.tsx b/packages/booking-flow/lib/components/BookingWidget/BookingWidgetForm/FormContent/ValidationError/index.tsx index 77244958f..df823c028 100644 --- a/packages/booking-flow/lib/components/BookingWidget/BookingWidgetForm/FormContent/ValidationError/index.tsx +++ b/packages/booking-flow/lib/components/BookingWidget/BookingWidgetForm/FormContent/ValidationError/index.tsx @@ -1,7 +1,7 @@ import { useIntl } from "react-intl" -import Caption from "@scandic-hotels/design-system/Caption" import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon" +import { Typography } from "@scandic-hotels/design-system/Typography" import styles from "./validationError.module.css" @@ -10,20 +10,34 @@ export default function ValidationError() { return (
- - - {intl.formatMessage({ - id: "bookingWidget.validationError.destination", - defaultMessage: "Enter destination or hotel", - })} - - - {intl.formatMessage({ - id: "bookingWidget.validationError.destinationDesc", - defaultMessage: - "A destination or hotel name is needed to be able to search for a hotel room.", - })} - + + + + {intl.formatMessage({ + id: "bookingWidget.validationError.destination", + defaultMessage: "Enter destination or hotel", + })} + + + + + {intl.formatMessage({ + id: "bookingWidget.validationError.destinationDesc", + defaultMessage: + "A destination or hotel name is needed to be able to search for a hotel room.", + })} + +
) } diff --git a/packages/booking-flow/lib/components/BookingWidget/BookingWidgetForm/FormContent/ValidationError/validationError.module.css b/packages/booking-flow/lib/components/BookingWidget/BookingWidgetForm/FormContent/ValidationError/validationError.module.css index 061fb6049..462823ced 100644 --- a/packages/booking-flow/lib/components/BookingWidget/BookingWidgetForm/FormContent/ValidationError/validationError.module.css +++ b/packages/booking-flow/lib/components/BookingWidget/BookingWidgetForm/FormContent/ValidationError/validationError.module.css @@ -20,9 +20,11 @@ display: flex; align-items: center; gap: var(--Space-x1); + color: var(--UI-Text-Error); } .message { + color: var(--UI-Text-High-contrast); text-wrap: auto; } diff --git a/packages/booking-flow/lib/components/BookingWidget/BookingWidgetForm/FormContent/formContent.module.css b/packages/booking-flow/lib/components/BookingWidget/BookingWidgetForm/FormContent/formContent.module.css index d0e15ff01..7c402f108 100644 --- a/packages/booking-flow/lib/components/BookingWidget/BookingWidgetForm/FormContent/formContent.module.css +++ b/packages/booking-flow/lib/components/BookingWidget/BookingWidgetForm/FormContent/formContent.module.css @@ -12,11 +12,14 @@ .rooms, .when { position: relative; + border: 2px solid transparent; } .buttonContainer { + align-self: center; display: grid; gap: var(--Space-x1); + border: 2px solid transparent; } .showOnTablet { @@ -30,15 +33,41 @@ .label { color: var(--Text-Accent-Primary); } -.when:has([data-datepicker-open="true"]) .label, -.rooms:has([data-pressed="true"]) .label { +.when:has([data-datepicker-open="true"], [data-pressed="true"]) .label, +.rooms:has([data-rooms-open="true"], [data-pressed="true"]) .label { color: var(--Text-Interactive-Focus); } +.when:hover, +.rooms:hover { + background-color: var(--Surface-Primary-Hover); +} +.where:has( + [data-focus-visible="true"], + [data-focused="true"], + [data-pressed="true"] +), +.when:has( + [data-datepicker-open="true"], + [data-focus-visible="true"], + [data-pressed="true"] +), +.rooms:has( + [data-focus-visible="true"], + [data-rooms-open="true"], + [data-pressed="true"] +) { + background-color: var(--Surface-Primary-Hover); + border-color: var(--Border-Interactive-Focus); + color: var(--Text-Interactive-Focus); +} @media screen and (max-width: 767px) { .voucherContainer { padding: var(--Space-x2) 0 var(--Space-x4); } + .buttonContainer { + width: 100%; + } } @media screen and (max-width: 1366px) { @@ -83,16 +112,16 @@ display: flex; flex: 2; gap: var(--Space-x2); - margin-left: calc(-1 * var(--Space-x15)); } .voucherContainer { - flex: 1; + border-radius: 0 0 var(--Corner-radius-md) var(--Corner-radius-md); } .rooms, .when, .where { width: 100%; + border-radius: var(--Corner-radius-md); } .inputContainer input[type="text"] { @@ -103,18 +132,6 @@ .rooms, .when { padding: var(--Space-x1) var(--Space-x15); - border-radius: var(--Corner-radius-md); - } - - .when:hover, - .rooms:hover { - background-color: var(--Surface-Primary-Hover); - } - .when:has([data-datepicker-open="true"]), - .rooms:has([data-focus-visible="true"], [data-pressed="true"]) { - background-color: var(--Surface-Primary-Hover); - border: 2px solid var(--Border-Interactive-Focus); - color: var(--Text-Interactive-Focus); } .where { @@ -137,13 +154,18 @@ .input { flex-wrap: wrap; } + .inputRow { + display: flex; + flex-direction: row; + gap: var(--Space-x2); + width: 100%; + padding: var(--Space-x2); + } + .inputContainer { - padding: var(--Space-x2) var(--Space-x2) var(--Space-x2) - var(--Layout-Tablet-Margin-Margin-min); flex-basis: 80%; } .buttonContainer { - padding-right: var(--Layout-Tablet-Margin-Margin-min); margin: 0; } .input .buttonContainer .button { @@ -153,12 +175,11 @@ } .voucherRow { + flex: 1; display: flex; - background: var(--Base-Surface-Primary-light-Hover); + background-color: var(--Base-Surface-Primary-light-Hover); border-bottom: 1px solid var(--Primary-Light-On-Surface-Divider-subtle); - padding: var(--Space-x2) var(--Layout-Tablet-Margin-Margin-min); - border-radius: 0 0 var(--Corner-radius-lg) var(--Corner-radius-lg); - margin-left: calc(var(--Space-x15) * -1); + padding-left: var(--Space-x2); } .showOnTablet { @@ -173,6 +194,9 @@ .input { gap: var(--Space-x2); } + .inputRow { + flex: 1; + } .bookingCodeDisabled { flex: none; diff --git a/packages/booking-flow/lib/components/BookingWidget/BookingWidgetForm/FormContent/index.tsx b/packages/booking-flow/lib/components/BookingWidget/BookingWidgetForm/FormContent/index.tsx index 1dbdc2e59..46e4f2739 100644 --- a/packages/booking-flow/lib/components/BookingWidget/BookingWidgetForm/FormContent/index.tsx +++ b/packages/booking-flow/lib/components/BookingWidget/BookingWidgetForm/FormContent/index.tsx @@ -62,64 +62,66 @@ export default function FormContent({ return (
-
-
- +
+
+ + {errors.search && } +
+
+ + + + +
+
+ + + + +
+
+
+ - {errors.search && }
-
- - - - -
-
- - - - -
-
-
-
-
diff --git a/packages/booking-flow/lib/components/BookingWidget/DatePicker/Range/Mobile.tsx b/packages/booking-flow/lib/components/BookingWidget/DatePicker/Range/Mobile.tsx index fbd7ea128..e65f84922 100644 --- a/packages/booking-flow/lib/components/BookingWidget/DatePicker/Range/Mobile.tsx +++ b/packages/booking-flow/lib/components/BookingWidget/DatePicker/Range/Mobile.tsx @@ -5,8 +5,8 @@ import { useIntl } from "react-intl" import { Lang } from "@scandic-hotels/common/constants/language" import { dt } from "@scandic-hotels/common/dt" +import { Button } from "@scandic-hotels/design-system/Button" import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon" -import { OldDSButton as Button } from "@scandic-hotels/design-system/OldDSButton" import { Typography } from "@scandic-hotels/design-system/Typography" import useLang from "../../../../hooks/useLang" @@ -131,19 +131,15 @@ export default function DatePickerRangeMobile({
diff --git a/packages/booking-flow/lib/components/BookingWidget/DatePicker/Range/desktop.module.css b/packages/booking-flow/lib/components/BookingWidget/DatePicker/Range/desktop.module.css index ce9876ea4..4e842f356 100644 --- a/packages/booking-flow/lib/components/BookingWidget/DatePicker/Range/desktop.module.css +++ b/packages/booking-flow/lib/components/BookingWidget/DatePicker/Range/desktop.module.css @@ -14,6 +14,7 @@ div.months { .captionLabel { text-transform: capitalize; + color: var(--Text-Default); } td.day, diff --git a/packages/booking-flow/lib/components/BookingWidget/DatePicker/Range/mobile.module.css b/packages/booking-flow/lib/components/BookingWidget/DatePicker/Range/mobile.module.css index c9a27ee31..7d00efcb4 100644 --- a/packages/booking-flow/lib/components/BookingWidget/DatePicker/Range/mobile.module.css +++ b/packages/booking-flow/lib/components/BookingWidget/DatePicker/Range/mobile.module.css @@ -24,7 +24,7 @@ align-self: flex-end; background-color: var(--Main-Grey-White); grid-area: header; - padding: var(--Space-x3) var(--Space-x2); + padding: 0 var(--Space-x2) 0; position: sticky; top: 0; z-index: 10; diff --git a/packages/booking-flow/lib/components/BookingWidget/DatePicker/date-picker.module.css b/packages/booking-flow/lib/components/BookingWidget/DatePicker/date-picker.module.css index 80a85c665..ebd51ec0c 100644 --- a/packages/booking-flow/lib/components/BookingWidget/DatePicker/date-picker.module.css +++ b/packages/booking-flow/lib/components/BookingWidget/DatePicker/date-picker.module.css @@ -1,6 +1,7 @@ -.btn { +.triggerButton { background: none; border: none; + color: var(--Text-Default); cursor: pointer; outline: none; padding: 0; @@ -12,55 +13,63 @@ bottom: 0; right: 0; padding: 20px var(--Space-x15) 0; + border-radius: var(--Corner-radius-lg); } -.body { - color: var(--Text-Default); -} - -.hideWrapper { - background-color: var(--Main-Grey-White); - display: none; -} - -.container[data-datepicker-open="true"] .hideWrapper { +.datePicker[data-datepicker-open="true"] { display: block; } +.pickerContainer { + --header-height: 72px; + --sticky-button-height: 140px; + background-color: var(--Main-Grey-White); + border-radius: var(--Corner-radius-lg) var(--Corner-radius-lg) 0 0; + position: fixed; + left: 0; + right: 0; + bottom: 0; + top: calc(max(var(--sitewide-alert-sticky-height), 20px)); + transition: top 300ms ease; + overflow: scroll; + z-index: var(--booking-widget-z-index); +} + @media screen and (max-width: 1366px) { - .container { - z-index: 10001; + .datePicker { height: 24px; } - .hideWrapper { - bottom: 0; - left: 0; - overflow: hidden; - position: fixed; - right: 0; - top: 100%; - transition: top 300ms ease; - z-index: 10001; - } - - .container[data-datepicker-open="true"] .hideWrapper { + .datePicker[data-datepicker-open="true"] { border-radius: var(--Corner-radius-lg) var(--Corner-radius-lg) 0 0; top: calc(max(var(--sitewide-alert-sticky-height), 20px)); } } @media screen and (min-width: 1367px) { - .hideWrapper { + .datePicker { + display: block; + } + + .pickerContainer { + position: absolute; + display: grid; border-radius: var(--Corner-radius-lg); box-shadow: var(--popup-box-shadow); padding: var(--Space-x2) var(--Space-x3); - position: absolute; - /** - BookingWidget padding + - border-width + - wanted space below booking widget - */ - top: calc(100% + var(--Space-x1) + 1px + var(--Space-x4)); + max-width: calc(100vw - 20px); + max-height: 440px; + top: calc(100% + 36px); + left: auto; + right: auto; + bottom: auto; + overflow: visible; + } + + .triggerButton { + display: block; + overflow: hidden; + text-overflow: ellipsis; + border-radius: var(--Corner-radius-md); } } diff --git a/packages/booking-flow/lib/components/BookingWidget/DatePicker/index.tsx b/packages/booking-flow/lib/components/BookingWidget/DatePicker/index.tsx index 304f4764d..1f5030dac 100644 --- a/packages/booking-flow/lib/components/BookingWidget/DatePicker/index.tsx +++ b/packages/booking-flow/lib/components/BookingWidget/DatePicker/index.tsx @@ -1,10 +1,14 @@ "use client" import { useCallback, useEffect, useRef, useState } from "react" +import { FocusScope, useOverlay } from "react-aria" +import { Button as ButtonRAC } from "react-aria-components" import { useFormContext, useWatch } from "react-hook-form" import { useIntl } from "react-intl" +import { useMediaQuery } from "usehooks-ts" import { longDateFormat } from "@scandic-hotels/common/constants/dateFormats" import { dt } from "@scandic-hotels/common/dt" +import { useScrollLock } from "@scandic-hotels/common/hooks/useScrollLock" import { Typography } from "@scandic-hotels/design-system/Typography" import useLang from "../../../hooks/useLang" @@ -16,17 +20,24 @@ import styles from "./date-picker.module.css" import type { DateRange } from "react-day-picker" type DatePickerFormProps = { + ariaLabelledBy?: string name?: string } -export default function DatePickerForm({ name = "date" }: DatePickerFormProps) { +export default function DatePickerForm({ + ariaLabelledBy, + name = "date", +}: DatePickerFormProps) { const lang = useLang() - const intl = useIntl() + const checkIsDesktop = useMediaQuery("(min-width: 1367px)") + const [isDesktop, setIsDesktop] = useState(true) + const { lockScroll, unlockScroll } = useScrollLock({ + autoLock: false, + }) const [isOpen, setIsOpen] = useState(false) const selectedDate = useWatch({ name }) - const { register, setValue } = useFormContext() + const { setValue } = useFormContext() const ref = useRef(null) - const close = useCallback(() => { if (!selectedDate.toDate) { setValue( @@ -38,13 +49,21 @@ export default function DatePickerForm({ name = "date" }: DatePickerFormProps) { { shouldDirty: true } ) } - setIsOpen(false) - }, [name, setValue, selectedDate]) + unlockScroll() + }, [name, setValue, selectedDate, unlockScroll]) - function showOnFocus() { - setIsOpen(true) - } + const { overlayProps, underlayProps } = useOverlay( + { + isOpen, + onClose: () => { + setIsOpen(false) + unlockScroll() + }, + isDismissable: true, + }, + ref + ) function handleSelectDate( _nextRange: DateRange | undefined, @@ -98,34 +117,9 @@ export default function DatePickerForm({ name = "date" }: DatePickerFormProps) { } } - const closeIfOutside = useCallback( - (target: HTMLElement) => { - if (ref.current && target && !ref.current.contains(target)) { - close() - } - }, - [close, ref] - ) - - function closeOnBlur(evt: FocusEvent) { - if (isOpen) { - const target = evt.relatedTarget as HTMLElement - closeIfOutside(target) - } - } - useEffect(() => { - function handleClickOutside(evt: Event) { - if (isOpen) { - const target = evt.target as HTMLElement - closeIfOutside(target) - } - } - document.body.addEventListener("click", handleClickOutside) - return () => { - document.body.removeEventListener("click", handleClickOutside) - } - }, [closeIfOutside, isOpen]) + setIsDesktop(checkIsDesktop) + }, [checkIsDesktop]) const selectedFromDate = dt(selectedDate.fromDate) .locale(lang) @@ -134,64 +128,122 @@ export default function DatePickerForm({ name = "date" }: DatePickerFormProps) { ? dt(selectedDate.toDate).locale(lang).format(longDateFormat[lang]) : "" - return ( -
{ - closeOnBlur(e.nativeEvent) - }} - data-datepicker-open={isOpen} - ref={ref} - > - - - -
- - {isOpen && ( - - )} -
+ return isDesktop ? ( +
+ { + setIsOpen((prev) => !prev) + }} + selectedFromDate={selectedFromDate} + selectedToDate={selectedToDate} + /> + {isOpen && ( +
+ +
+ +
+
+
+ )} +
+ ) : ( +
+ { + setIsOpen((prev) => !prev) + if (!isOpen) { + lockScroll() + } else { + unlockScroll() + } + }} + selectedFromDate={selectedFromDate} + selectedToDate={selectedToDate} + /> + {isOpen && ( +
+ +
+ +
+
+
+ )}
) } + +function Trigger({ + onPress, + selectedFromDate, + selectedToDate, + ariaLabelledBy, +}: { + onPress?: () => void + selectedFromDate: string + selectedToDate: string + ariaLabelledBy?: string +}) { + const intl = useIntl() + const { register } = useFormContext() + + const triggerText = intl.formatMessage( + { + id: "booking.selectedDateRange", + defaultMessage: "{selectedFromDate} – {selectedToDate}", + }, + { + selectedFromDate, + selectedToDate, + } + ) + return ( + <> + + + {triggerText} + + + + + + ) +} diff --git a/packages/booking-flow/lib/components/BookingWidget/GuestsRoomsPicker/ChildSelector/ChildInfoSelector.tsx b/packages/booking-flow/lib/components/BookingWidget/GuestsRoomsPicker/ChildSelector/ChildInfoSelector.tsx index 110b0774c..2623ad0f7 100644 --- a/packages/booking-flow/lib/components/BookingWidget/GuestsRoomsPicker/ChildSelector/ChildInfoSelector.tsx +++ b/packages/booking-flow/lib/components/BookingWidget/GuestsRoomsPicker/ChildSelector/ChildInfoSelector.tsx @@ -1,11 +1,12 @@ "use client" +import { useEffect, useRef, useState } from "react" import { useFormContext } from "react-hook-form" import { useIntl } from "react-intl" -import Caption from "@scandic-hotels/design-system/Caption" -import DeprecatedSelect from "@scandic-hotels/design-system/DeprecatedSelect" import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon" +import { Select } from "@scandic-hotels/design-system/Select" +import { Typography } from "@scandic-hotels/design-system/Typography" import { ChildBedMapEnum } from "@scandic-hotels/trpc/enums/childBedMapEnum" import styles from "./child-selector.module.css" @@ -38,6 +39,34 @@ export default function ChildInfoSelector({ index = 0, roomIndex = 0, }: ChildInfoSelectorProps) { + const ageSelectRef = useRef(null) + const bedPrefSelectRef = useRef(null) + const [ageWidth, setAgeWidth] = useState(undefined) + const [bedWidth, setBedWidth] = useState(undefined) + + //Match width of the dropdown with width of parent select + useEffect(() => { + if (!ageSelectRef.current) return + + const observer = new ResizeObserver(() => { + setAgeWidth(ageSelectRef.current!.offsetWidth) + }) + + observer.observe(ageSelectRef.current) + return () => observer.disconnect() + }, []) + + useEffect(() => { + if (!bedPrefSelectRef.current) return + + const observer = new ResizeObserver(() => { + setBedWidth(bedPrefSelectRef.current!.offsetWidth) + }) + + observer.observe(bedPrefSelectRef.current) + return () => observer.disconnect() + }, []) + const ageFieldName = `rooms.${roomIndex}.childrenInRoom.${index}.age` const bedFieldName = `rooms.${roomIndex}.childrenInRoom.${index}.bed` const intl = useIntl() @@ -49,10 +78,12 @@ export default function ChildInfoSelector({ id: "booking.bedPreference", defaultMessage: "Bed preference", }) + const errorMessage = intl.formatMessage({ id: "bookingWidget.child.ageRequiredError", defaultMessage: "Child age is required", }) + const { setValue, formState } = useFormContext() function updateSelectedBed(bed: number) { @@ -113,50 +144,62 @@ export default function ChildInfoSelector({ return ( <>
-
- +