Merged in fix/book-149-ui-fixes (pull request #3463)

Fix/book 149 ui fixes

* fixed text-overflow issue in datepicker trigger

* fixed X missing in booking code text field

* fixed toDate not setting properly

* fixed spacing issues and placeholder text not fitting

* added error message to child age if none is added

* spacing fixes

* Revert "map link alignment fix"

This reverts commit d38cc5b007bc05a1d48ce6661b1052fe714961c3.

* fixed EB points padding issue on SAS tablet

* maxWidth on BookingCode/voucher

* spacing fixes

* fixed icons in error message

* spacing fixes

* scroll to child age picker updates

* feat(SW-3706): fix heatmap issue for langswitcher and booking widget

* fixed tablet lineup issue


Approved-by: Linus Flood
This commit is contained in:
Matilda Haneling
2026-01-22 12:50:24 +00:00
parent f79ff9b570
commit 665ca210c0
19 changed files with 129 additions and 41 deletions

View File

@@ -70,10 +70,12 @@
left: 0; left: 0;
bottom: 0; bottom: 0;
transform: translateY(100%); transform: translateY(100%);
visibility: hidden;
} }
.footer .dropdown.isExpanded { .footer .dropdown.isExpanded {
transform: translateY(0); transform: translateY(0);
visibility: visible;
} }
@media screen and (max-width: 767px) { @media screen and (max-width: 767px) {

View File

@@ -11,13 +11,7 @@ import { isMultiRoomError } from "../../utils"
import styles from "./booking-code-error.module.css" import styles from "./booking-code-error.module.css"
export function BookingCodeError({ export function BookingCodeError({ codeError }: { codeError: FieldError }) {
codeError,
isDesktop = false,
}: {
codeError: FieldError
isDesktop?: boolean
}) {
const intl = useIntl() const intl = useIntl()
const isMultiroomError = isMultiRoomError(codeError.message) const isMultiroomError = isMultiRoomError(codeError.message)
const config = useBookingFlowConfig() const config = useBookingFlowConfig()
@@ -33,7 +27,7 @@ export function BookingCodeError({
size={20} size={20}
icon="error" icon="error"
color="Icon/Feedback/Error" color="Icon/Feedback/Error"
isFilled={!isDesktop} isFilled
/> />
{getErrorMessage(intl, config.variant, codeError.message)} {getErrorMessage(intl, config.variant, codeError.message)}
</span> </span>

View File

@@ -11,6 +11,7 @@
border-radius: var(--Corner-Radius-md); border-radius: var(--Corner-Radius-md);
padding: var(--Space-x1) var(--Space-x15); padding: var(--Space-x1) var(--Space-x15);
border: 2px solid transparent; border: 2px solid transparent;
flex-shrink: 0;
} }
.bookingCode:focus-within, .bookingCode:focus-within,
@@ -27,6 +28,9 @@
gap: var(--Space-x05); gap: var(--Space-x05);
position: relative; position: relative;
color: var(--Text-Secondary); color: var(--Text-Secondary);
flex-shrink: 0;
text-wrap: nowrap;
width: 100%;
} }
.input { .input {

View File

@@ -167,7 +167,7 @@ export default function BookingCode() {
<span className={styles.inputWrapper}> <span className={styles.inputWrapper}>
<BookingWidgetInput <BookingWidgetInput
className={styles.input} className={styles.input}
type="text" type="search"
placeholder={addCode} placeholder={addCode}
aria-labelledby="bookingCodeLabel" aria-labelledby="bookingCodeLabel"
name="bookingCode.value" name="bookingCode.value"
@@ -204,7 +204,7 @@ export default function BookingCode() {
} }
> >
{codeError?.message ? ( {codeError?.message ? (
<BookingCodeError codeError={codeError} isDesktop /> <BookingCodeError codeError={codeError} />
) : ( ) : (
<RememberCode <RememberCode
bookingCodeValue={bookingCode?.value} bookingCodeValue={bookingCode?.value}

View File

@@ -10,7 +10,9 @@
color: var(--UI-Text-Error); color: var(--UI-Text-Error);
white-space: break-spaces; white-space: break-spaces;
} }
.label {
text-wrap: nowrap;
}
.errorIcon { .errorIcon {
min-width: 20px; min-width: 20px;
} }
@@ -18,7 +20,6 @@
align-items: center; align-items: center;
display: flex; display: flex;
color: var(--Text-Secondary); color: var(--Text-Secondary);
min-width: 160px;
gap: var(--Space-x1); gap: var(--Space-x1);
} }

View File

@@ -3,7 +3,7 @@
border-style: solid; border-style: solid;
border-width: 1px; border-width: 1px;
border-radius: var(--Corner-Radius-md); border-radius: var(--Corner-Radius-md);
padding: var(--Space-x1) var(--Space-x15); padding: var(--Space-x15);
position: relative; position: relative;
height: 60px; height: 60px;

View File

@@ -15,7 +15,12 @@ export default function ValidationError() {
variant="Body/Supporting text (caption)/smBold" variant="Body/Supporting text (caption)/smBold"
> >
<span> <span>
<MaterialIcon icon="error" color="Icon/Feedback/Error" size={20} /> <MaterialIcon
icon="error"
isFilled
color="Icon/Feedback/Error"
size={20}
/>
{intl.formatMessage({ {intl.formatMessage({
id: "bookingWidget.validationError.destination", id: "bookingWidget.validationError.destination",
defaultMessage: "Enter destination or hotel", defaultMessage: "Enter destination or hotel",

View File

@@ -43,7 +43,7 @@
.optionsContainer { .optionsContainer {
display: grid; display: grid;
grid-template-columns: auto auto; grid-template-columns: auto auto;
column-gap: var(--Space-x2); column-gap: var(--Space-x3);
} }
} }
@@ -54,6 +54,9 @@
.showOnTablet { .showOnTablet {
display: flex; display: flex;
} }
.option {
min-height: 56px;
}
.skeletonContainer { .skeletonContainer {
padding: var(--Space-x2) 0; padding: var(--Space-x2) 0;
} }

View File

@@ -63,7 +63,7 @@
} }
@media screen and (max-width: 767px) { @media screen and (max-width: 767px) {
.voucherContainer { .voucherContainer {
padding: var(--Space-x2) 0 var(--Space-x4); padding: var(--Space-x15) 0 var(--Space-x4);
} }
.buttonContainer { .buttonContainer {
width: 100%; width: 100%;
@@ -73,7 +73,7 @@
@media screen and (max-width: 1366px) { @media screen and (max-width: 1366px) {
.inputContainer { .inputContainer {
display: grid; display: grid;
gap: var(--Space-x2); gap: var(--Space-x15);
} }
.rooms, .rooms,
@@ -111,7 +111,7 @@
.inputContainer { .inputContainer {
display: flex; display: flex;
flex: 2; flex: 2;
gap: var(--Space-x2); gap: var(--Space-x15);
} }
.voucherContainer { .voucherContainer {
border-radius: 0 0 var(--Corner-Radius-md) var(--Corner-Radius-md); border-radius: 0 0 var(--Corner-Radius-md) var(--Corner-Radius-md);
@@ -191,12 +191,18 @@
} }
@media screen and (min-width: 1367px) { @media screen and (min-width: 1367px) {
.inputContainer {
margin-left: calc(-1 * var(--Space-x15));
}
.input { .input {
gap: var(--Space-x2); gap: var(--Space-x15);
} }
.inputRow { .inputRow {
flex: 1; flex: 1;
} }
.voucherRow {
flex: 0 1;
}
.bookingCodeDisabled { .bookingCodeDisabled {
flex: none; flex: none;

View File

@@ -57,8 +57,7 @@ export default function DatePickerForm({
{ {
isOpen, isOpen,
onClose: () => { onClose: () => {
setIsOpen(false) close()
unlockScroll()
}, },
isDismissable: true, isDismissable: true,
}, },

View File

@@ -30,6 +30,7 @@ type ChildInfoSelectorProps = {
index: number index: number
roomIndex: number roomIndex: number
childrenInAdultsBed: number childrenInAdultsBed: number
scrollToRef?: React.Ref<HTMLDivElement>
} }
export default function ChildInfoSelector({ export default function ChildInfoSelector({
@@ -38,6 +39,7 @@ export default function ChildInfoSelector({
adults, adults,
index = 0, index = 0,
roomIndex = 0, roomIndex = 0,
scrollToRef,
}: ChildInfoSelectorProps) { }: ChildInfoSelectorProps) {
const ageSelectRef = useRef<HTMLDivElement>(null) const ageSelectRef = useRef<HTMLDivElement>(null)
const bedPrefSelectRef = useRef<HTMLDivElement>(null) const bedPrefSelectRef = useRef<HTMLDivElement>(null)
@@ -84,8 +86,11 @@ export default function ChildInfoSelector({
defaultMessage: "Child age is required", defaultMessage: "Child age is required",
}) })
const { setValue, formState } = useFormContext() const { setValue, formState, watch } = useFormContext()
const selectedAgeValue = watch(
`rooms.${roomIndex}.childrenInRoom.${index}.age`
)
function updateSelectedBed(bed: number) { function updateSelectedBed(bed: number) {
setValue(`rooms.${roomIndex}.childrenInRoom.${index}.bed`, bed) setValue(`rooms.${roomIndex}.childrenInRoom.${index}.bed`, bed)
} }
@@ -140,10 +145,16 @@ export default function ChildInfoSelector({
const ageError = roomErrors?.age const ageError = roomErrors?.age
const bedError = roomErrors?.bed const bedError = roomErrors?.bed
const isInvalid =
!!ageError ||
!!bedError ||
selectedAgeValue === undefined ||
selectedAgeValue === null ||
selectedAgeValue === -1
return ( return (
<> <>
<div key={index} className={styles.childInfoContainer}> <div key={index} ref={scrollToRef} className={styles.childInfoContainer}>
<div ref={ageSelectRef}> <div ref={ageSelectRef}>
<Select <Select
items={ageList} items={ageList}
@@ -152,9 +163,13 @@ export default function ChildInfoSelector({
onSelectionChange={(key) => { onSelectionChange={(key) => {
updateSelectedAge(key as number) updateSelectedAge(key as number)
}} }}
isRequired
popoverWidth={`${ageWidth}px`} popoverWidth={`${ageWidth}px`}
value={child.age ?? childDefaultValues.age} value={child.age ?? childDefaultValues.age}
isInvalid={!!ageError} isInvalid={
(!!ageError || selectedAgeValue === undefined) ??
childDefaultValues.age === -1
}
/> />
</div> </div>
<div ref={bedPrefSelectRef}> <div ref={bedPrefSelectRef}>
@@ -186,7 +201,7 @@ export default function ChildInfoSelector({
</Typography> </Typography>
) : null} ) : null}
{ageError || bedError ? ( {isInvalid ? (
<> <>
<Typography <Typography
variant="Body/Supporting text (caption)/smRegular" variant="Body/Supporting text (caption)/smRegular"

View File

@@ -1,6 +1,6 @@
"use client" "use client"
import { useRef } from "react" import { useCallback, useEffect, useRef } from "react"
import { useFormContext } from "react-hook-form" import { useFormContext } from "react-hook-form"
import { useIntl } from "react-intl" import { useIntl } from "react-intl"
@@ -18,20 +18,25 @@ type ChildSelectorProps = {
currentAdults: number currentAdults: number
currentChildren: Child[] currentChildren: Child[]
childrenInAdultsBed: number childrenInAdultsBed: number
containerRef?: React.RefObject<HTMLDivElement | null>
roomsDividerRef?: React.RefObject<HTMLDivElement | null>
} }
export default function ChildSelector({ export default function ChildSelector({
roomIndex = 0, roomIndex = 0,
currentAdults, currentAdults,
childrenInAdultsBed, childrenInAdultsBed,
currentChildren, currentChildren,
containerRef,
}: ChildSelectorProps) { }: ChildSelectorProps) {
const intl = useIntl() const intl = useIntl()
const childRefs = useRef<(HTMLDivElement | null)[]>([])
const previousChildCount = useRef(currentChildren.length)
const childrenLabel = intl.formatMessage({ const childrenLabel = intl.formatMessage({
id: "bookingwidget.dropdown.children", id: "bookingwidget.dropdown.children",
defaultMessage: "Children (012 years)", defaultMessage: "Children (012 years)",
}) })
const { setValue } = useFormContext() const { setValue } = useFormContext()
const agePickerRef = useRef<HTMLDivElement | null>(null)
function increaseChildrenCount(roomIndex: number) { function increaseChildrenCount(roomIndex: number) {
if (currentChildren.length < 5) { if (currentChildren.length < 5) {
@@ -53,6 +58,35 @@ export default function ChildSelector({
}) })
} }
} }
const scrollToAgePicker = useCallback(() => {
if (!containerRef?.current) return
const lastChildIndex = currentChildren.length - 1
const lastChild = childRefs.current[lastChildIndex]
if (!lastChild) return
const container = containerRef.current
const containerRect = container.getBoundingClientRect()
const targetRect = lastChild.getBoundingClientRect()
const targetOffset =
targetRect.top - containerRect.top + container.scrollTop
const centerOffset = containerRect.height / 2 - targetRect.height / 2
container.scrollTo({
top: targetOffset - centerOffset,
behavior: "smooth",
})
}, [containerRef, currentChildren.length])
useEffect(() => {
// Only scroll when a child was added
if (currentChildren.length > previousChildCount.current) {
scrollToAgePicker()
}
previousChildCount.current = currentChildren.length
}, [currentChildren.length, scrollToAgePicker])
return ( return (
<> <>
@@ -71,19 +105,13 @@ export default function ChildSelector({
decreaseChildrenCount(roomIndex) decreaseChildrenCount(roomIndex)
}} }}
handleOnIncrease={() => { handleOnIncrease={() => {
requestAnimationFrame(() => {
agePickerRef.current?.scrollIntoView({
behavior: "smooth",
block: "center",
})
})
increaseChildrenCount(roomIndex) increaseChildrenCount(roomIndex)
}} }}
disableDecrease={currentChildren.length == 0} disableDecrease={currentChildren.length == 0}
disableIncrease={currentChildren.length == 5} disableIncrease={currentChildren.length == 5}
/> />
</section> </section>
<span ref={agePickerRef} />
{currentChildren.map((child, index) => ( {currentChildren.map((child, index) => (
<ChildInfoSelector <ChildInfoSelector
roomIndex={roomIndex} roomIndex={roomIndex}
@@ -92,6 +120,9 @@ export default function ChildSelector({
adults={currentAdults} adults={currentAdults}
key={"child_" + index} key={"child_" + index}
childrenInAdultsBed={childrenInAdultsBed} childrenInAdultsBed={childrenInAdultsBed}
scrollToRef={(el) => {
childRefs.current[index] = el
}}
/> />
))} ))}
</> </>

View File

@@ -23,11 +23,13 @@ const MAX_ROOMS = 4
interface GuestsRoomsPickerDialogProps { interface GuestsRoomsPickerDialogProps {
rooms: TGuestsRoom[] rooms: TGuestsRoom[]
onClose: () => void onClose: () => void
containerRef?: React.RefObject<HTMLDivElement | null>
} }
export default function GuestsRoomsPickerDialog({ export default function GuestsRoomsPickerDialog({
rooms, rooms,
onClose, onClose,
containerRef,
}: GuestsRoomsPickerDialogProps) { }: GuestsRoomsPickerDialogProps) {
const intl = useIntl() const intl = useIntl()
const isDesktop = useIsDesktop() const isDesktop = useIsDesktop()
@@ -124,6 +126,7 @@ export default function GuestsRoomsPickerDialog({
room={room} room={room}
index={index} index={index}
onRemove={handleRemoveRoom} onRemove={handleRemoveRoom}
containerRef={containerRef}
/> />
))} ))}
{!isDesktop && ( {!isDesktop && (

View File

@@ -16,12 +16,15 @@ export function GuestsRoom({
room, room,
index, index,
onRemove, onRemove,
containerRef,
}: { }: {
room: GuestsRoom room: GuestsRoom
index: number index: number
onRemove: (index: number) => void onRemove: (index: number) => void
containerRef?: React.RefObject<HTMLDivElement | null>
}) { }) {
const intl = useIntl() const intl = useIntl()
const roomLabel = intl.formatMessage( const roomLabel = intl.formatMessage(
{ {
id: "booking.roomIndex", id: "booking.roomIndex",
@@ -48,6 +51,7 @@ export function GuestsRoom({
currentAdults={room.adults} currentAdults={room.adults}
currentChildren={room.childrenInRoom} currentChildren={room.childrenInRoom}
childrenInAdultsBed={childrenInAdultsBed} childrenInAdultsBed={childrenInAdultsBed}
containerRef={containerRef}
/> />
{index !== 0 && ( {index !== 0 && (
<Button <Button
@@ -64,6 +68,7 @@ export function GuestsRoom({
</Button> </Button>
)} )}
</section> </section>
<Divider color="Border/Divider/Subtle" /> <Divider color="Border/Divider/Subtle" />
</div> </div>
) )

View File

@@ -20,7 +20,12 @@ export default function ValidationError() {
variant="Body/Supporting text (caption)/smBold" variant="Body/Supporting text (caption)/smBold"
> >
<span> <span>
<MaterialIcon icon="error" color="Icon/Feedback/Error" size={20} /> <MaterialIcon
icon="error"
isFilled
color="Icon/Feedback/Error"
size={20}
/>
{errorMessage} {errorMessage}
</span> </span>
</Typography> </Typography>

View File

@@ -108,7 +108,6 @@
left: 0; left: 0;
transition: top 300ms ease; transition: top 300ms ease;
z-index: var(--booking-widget-z-index); z-index: var(--booking-widget-z-index);
overflow: scroll;
} }
@media screen and (max-width: 1366px) { @media screen and (max-width: 1366px) {
@@ -116,6 +115,7 @@
grid-area: content; grid-area: content;
overflow-y: scroll; overflow-y: scroll;
scroll-snap-type: y mandatory; scroll-snap-type: y mandatory;
padding-bottom: calc(120px + var(--Space-x2));
} }
.header { .header {

View File

@@ -105,7 +105,7 @@ export default function GuestsRoomsPickerForm({
}, []) }, [])
useEffect(() => { useEffect(() => {
if (isDesktop && rooms.length > 1) { if (isDesktop) {
updateHeight(containerConstraint) updateHeight(containerConstraint)
} }
}, [ }, [
@@ -138,7 +138,7 @@ export default function GuestsRoomsPickerForm({
setIsOpen(false) setIsOpen(false)
unlockScroll() unlockScroll()
}, },
isDismissable: !errors.rooms, isDismissable: true,
}, },
ref ref
) )
@@ -183,6 +183,7 @@ export default function GuestsRoomsPickerForm({
setIsOpen((prev) => !prev) setIsOpen((prev) => !prev)
unlockScroll() unlockScroll()
}} }}
containerRef={ref}
/> />
</div> </div>
</FocusScope> </FocusScope>

View File

@@ -12,6 +12,8 @@
bottom: 0; bottom: 0;
width: 100%; width: 100%;
z-index: 1; z-index: 1;
transform: translateY(0);
visibility: visible;
} }
@media screen and (max-width: 767px) { @media screen and (max-width: 767px) {
.backdrop { .backdrop {
@@ -41,9 +43,11 @@
padding: var(--Space-x3) var(--Space-x2); padding: var(--Space-x3) var(--Space-x2);
position: fixed; position: fixed;
left: 0; left: 0;
bottom: -100%; bottom: 0;
overflow-y: auto; overflow-y: auto;
transition: bottom 300ms ease; transform: translateY(100%);
transition: transform 300ms ease;
visibility: hidden;
&:has([data-searchlist-open="true"]) { &:has([data-searchlist-open="true"]) {
overflow-y: visible; overflow-y: visible;
@@ -83,6 +87,8 @@
position: static; position: static;
padding: 0; padding: 0;
overflow-y: visible; overflow-y: visible;
transform: none;
visibility: visible;
&.compactFormContainer { &.compactFormContainer {
box-shadow: none; box-shadow: none;
@@ -93,3 +99,11 @@
display: none; display: none;
} }
} }
@media screen and (min-width: 1367px) {
.formContainer {
&.compactFormContainer {
padding-left: var(--Space-x15);
}
}
}

View File

@@ -29,7 +29,7 @@
.link { .link {
display: flex; display: flex;
gap: var(--Space-x05); gap: var(--Space-x05);
align-items: center; align-items: baseline;
} }
.bookingCodeFilter { .bookingCodeFilter {