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
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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 (
|
||||
<div className={styles.errorContainer}>
|
||||
<Typography
|
||||
className={styles.error}
|
||||
variant="Body/Supporting text (caption)/smRegular"
|
||||
>
|
||||
<span>
|
||||
<MaterialIcon
|
||||
size={20}
|
||||
icon="error"
|
||||
color="Icon/Feedback/Error"
|
||||
isFilled={!isDesktop}
|
||||
/>
|
||||
{getErrorMessage(intl, config.variant, codeError.message)}
|
||||
</span>
|
||||
</Typography>
|
||||
{isMultiroomError ? (
|
||||
<div className={styles.removeButton}>
|
||||
<RemoveExtraRooms />
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -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 */
|
||||
}
|
||||
@@ -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 (
|
||||
<Modal
|
||||
trigger={
|
||||
<IconButton
|
||||
variant="Muted"
|
||||
emphasis
|
||||
size="sm"
|
||||
aria-label={infoButtonAriaLabel}
|
||||
iconName="info"
|
||||
className={styles.infoButton}
|
||||
/>
|
||||
}
|
||||
title={codeVoucher}
|
||||
>
|
||||
<Typography variant="Body/Paragraph/mdRegular">
|
||||
<p className={styles.bookingCodeTooltip}>{bookingCodeTooltipText}</p>
|
||||
</Typography>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
@@ -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 (
|
||||
<>
|
||||
<Checkbox name="bookingCode.remember" disabled={!bookingCodeValue}>
|
||||
<Typography variant="Body/Supporting text (caption)/smRegular">
|
||||
<span>{checkBoxLabel}</span>
|
||||
</Typography>
|
||||
</Checkbox>
|
||||
|
||||
<Button
|
||||
size="sm"
|
||||
className={styles.applyButton}
|
||||
variant="Secondary"
|
||||
wrapping
|
||||
color="Primary"
|
||||
type="button"
|
||||
isDisabled={!bookingCodeValue}
|
||||
onPress={onApplyClick}
|
||||
>
|
||||
{intl.formatMessage({
|
||||
id: "common.apply",
|
||||
defaultMessage: "Apply",
|
||||
})}
|
||||
</Button>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
.applyButton {
|
||||
min-width: 100px;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 767px) {
|
||||
.applyButton {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
<div
|
||||
className={styles.container}
|
||||
ref={ref}
|
||||
onFocus={showRememberCheck}
|
||||
onBlur={(e) => closeIfOutside(e.nativeEvent.relatedTarget as HTMLElement)}
|
||||
>
|
||||
<div className={styles.bookingCode}>
|
||||
<div className={styles.bookingCodeLabel}>
|
||||
<span className={styles.bookingCodeLabel}>
|
||||
<Typography variant="Body/Supporting text (caption)/smBold">
|
||||
<span>{codeVoucher}</span>
|
||||
<label htmlFor="booking-code" id="bookingCodeLabel">
|
||||
{codeVoucher}
|
||||
</label>
|
||||
</Typography>
|
||||
<CodeRulesModal />
|
||||
</div>
|
||||
<BookingWidgetInput
|
||||
className="input"
|
||||
type="search"
|
||||
placeholder={addCode}
|
||||
name="bookingCode.value"
|
||||
id="booking-code"
|
||||
onChange={(event) => updateBookingCodeFormValue(event.target.value)}
|
||||
autoComplete="off"
|
||||
value={bookingCode?.value}
|
||||
/>
|
||||
</span>
|
||||
<span className={styles.inputWrapper}>
|
||||
<BookingWidgetInput
|
||||
className={styles.input}
|
||||
type="text"
|
||||
placeholder={addCode}
|
||||
aria-labelledby="bookingCodeLabel"
|
||||
name="bookingCode.value"
|
||||
id="booking-code"
|
||||
onChange={(event) => {
|
||||
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)
|
||||
}
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{isDesktop ? (
|
||||
<div
|
||||
className={
|
||||
@@ -174,7 +206,7 @@ export default function BookingCode() {
|
||||
{codeError?.message ? (
|
||||
<BookingCodeError codeError={codeError} isDesktop />
|
||||
) : (
|
||||
<CodeRemember
|
||||
<RememberCode
|
||||
bookingCodeValue={bookingCode?.value}
|
||||
onApplyClick={() => 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 (
|
||||
<Modal
|
||||
trigger={
|
||||
<IconButton variant="Muted" size="sm" emphasis iconName="info" />
|
||||
}
|
||||
title={codeVoucher}
|
||||
>
|
||||
<Typography variant="Body/Paragraph/mdRegular">
|
||||
<p className={styles.bookingCodeTooltip}>{bookingCodeTooltipText}</p>
|
||||
</Typography>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
function CodeRemember({ bookingCodeValue, onApplyClick }: CodeRememberProps) {
|
||||
const intl = useIntl()
|
||||
|
||||
return (
|
||||
<>
|
||||
<Checkbox name="bookingCode.remember">
|
||||
<Typography variant="Body/Supporting text (caption)/smRegular">
|
||||
<span>
|
||||
{intl.formatMessage({
|
||||
id: "bookingWidget.bookingCode.remember",
|
||||
defaultMessage: "Remember code",
|
||||
})}
|
||||
</span>
|
||||
</Typography>
|
||||
</Checkbox>
|
||||
{bookingCodeValue ? (
|
||||
<Button
|
||||
size="sm"
|
||||
className={styles.hideOnMobile}
|
||||
variant="Tertiary"
|
||||
type="button"
|
||||
onClick={onApplyClick}
|
||||
>
|
||||
{intl.formatMessage({
|
||||
id: "common.apply",
|
||||
defaultMessage: "Apply",
|
||||
})}
|
||||
</Button>
|
||||
) : null}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
function BookingCodeError({
|
||||
codeError,
|
||||
isDesktop = false,
|
||||
}: {
|
||||
codeError: FieldError
|
||||
isDesktop?: boolean
|
||||
}) {
|
||||
const intl = useIntl()
|
||||
const isMultiroomError = isMultiRoomError(codeError.message)
|
||||
const config = useBookingFlowConfig()
|
||||
|
||||
return (
|
||||
<div className={styles.errorContainer}>
|
||||
<Typography
|
||||
className={styles.error}
|
||||
variant="Body/Supporting text (caption)/smRegular"
|
||||
>
|
||||
<span>
|
||||
<MaterialIcon
|
||||
size={20}
|
||||
icon="error"
|
||||
color="Icon/Feedback/Error"
|
||||
isFilled={!isDesktop}
|
||||
/>
|
||||
{getErrorMessage(intl, config.variant, codeError.message)}
|
||||
</span>
|
||||
</Typography>
|
||||
{isMultiroomError ? (
|
||||
<div className={styles.removeButton}>
|
||||
<RemoveExtraRooms />
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
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({
|
||||
},
|
||||
})}
|
||||
>
|
||||
<Typography variant="Body/Supporting text (caption)/smBold">
|
||||
<span className={styles.colorSecondary}>{codeVoucher}</span>
|
||||
<Typography variant="Body/Supporting text (caption)/smRegular">
|
||||
<span>{codeVoucher}</span>
|
||||
</Typography>
|
||||
</Checkbox>
|
||||
</Button>
|
||||
@@ -395,7 +328,7 @@ function TabletBookingCode({
|
||||
{codeError?.message ? (
|
||||
<BookingCodeError codeError={codeError} />
|
||||
) : (
|
||||
<CodeRemember
|
||||
<RememberCode
|
||||
bookingCodeValue={bookingCode?.value}
|
||||
onApplyClick={close}
|
||||
/>
|
||||
|
||||
@@ -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 (
|
||||
<div ref={ref} onBlur={(e) => closeOnBlur(e.nativeEvent)}>
|
||||
<Checkbox
|
||||
hideError
|
||||
name={SEARCH_TYPE_REDEMPTION}
|
||||
registerOptions={{
|
||||
onChange: (e) => {
|
||||
validateRedemption(e.target.value)
|
||||
},
|
||||
}}
|
||||
>
|
||||
<div className={styles.rewardNightLabel}>
|
||||
<div className={styles.rewardNightLabel}>
|
||||
<Checkbox
|
||||
hideError
|
||||
name={SEARCH_TYPE_REDEMPTION}
|
||||
registerOptions={{
|
||||
onChange: (e) => {
|
||||
validateRedemption(e.target.value)
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
variant="Body/Supporting text (caption)/smRegular"
|
||||
className={styles.label}
|
||||
>
|
||||
<span>{reward}</span>
|
||||
</Typography>
|
||||
<Modal
|
||||
trigger={
|
||||
<IconButton variant="Muted" emphasis size="sm" iconName="info" />
|
||||
}
|
||||
title={reward}
|
||||
</Checkbox>
|
||||
<Modal
|
||||
trigger={
|
||||
<IconButton
|
||||
className={styles.infoButton}
|
||||
variant="Muted"
|
||||
emphasis
|
||||
size="sm"
|
||||
iconName="info"
|
||||
aria-label={rewardLabel}
|
||||
/>
|
||||
}
|
||||
title={reward}
|
||||
>
|
||||
<Typography
|
||||
variant="Body/Paragraph/mdRegular"
|
||||
className={styles.rewardNightTooltip}
|
||||
>
|
||||
<Typography
|
||||
variant="Body/Paragraph/mdRegular"
|
||||
className={styles.rewardNightTooltip}
|
||||
>
|
||||
<span>{rewardNightTooltip}</span>
|
||||
</Typography>
|
||||
</Modal>
|
||||
</div>
|
||||
</Checkbox>
|
||||
<span>{rewardNightTooltip}</span>
|
||||
</Typography>
|
||||
</Modal>
|
||||
</div>
|
||||
|
||||
{redemptionErr && (
|
||||
<div className={styles.errorContainer}>
|
||||
<Typography
|
||||
|
||||
@@ -26,6 +26,12 @@
|
||||
max-width: 560px;
|
||||
}
|
||||
|
||||
.infoButton {
|
||||
align-self: center;
|
||||
color: var(
|
||||
--Icon-Interactive-Placeholder
|
||||
) !important; /* Override IconButton default color */
|
||||
}
|
||||
@media screen and (max-width: 767px) {
|
||||
.hideOnMobile {
|
||||
display: none;
|
||||
|
||||
@@ -310,7 +310,10 @@ function SearchListError({
|
||||
<MaterialIcon icon="error" color="Icon/Interactive/Accent" />
|
||||
{caption}
|
||||
</Caption>
|
||||
<Typography variant="Body/Paragraph/mdRegular">
|
||||
<Typography
|
||||
className={styles.errorBody}
|
||||
variant="Body/Paragraph/mdRegular"
|
||||
>
|
||||
<p>{body}</p>
|
||||
</Typography>
|
||||
</Dialog>
|
||||
|
||||
@@ -36,3 +36,7 @@
|
||||
.textPlaceholderColor {
|
||||
color: var(--UI-Text-Placeholder);
|
||||
}
|
||||
|
||||
.errorBody {
|
||||
color: var(--UI-Text-High-contrast);
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 (
|
||||
<div className={styles.container}>
|
||||
<Caption className={styles.title} color="red" type="bold">
|
||||
<MaterialIcon icon="dangerous" color="Icon/Feedback/Error" size={20} />
|
||||
{intl.formatMessage({
|
||||
id: "bookingWidget.validationError.destination",
|
||||
defaultMessage: "Enter destination or hotel",
|
||||
})}
|
||||
</Caption>
|
||||
<Caption className={styles.message} type="regular">
|
||||
{intl.formatMessage({
|
||||
id: "bookingWidget.validationError.destinationDesc",
|
||||
defaultMessage:
|
||||
"A destination or hotel name is needed to be able to search for a hotel room.",
|
||||
})}
|
||||
</Caption>
|
||||
<Typography
|
||||
className={styles.title}
|
||||
variant="Body/Supporting text (caption)/smBold"
|
||||
>
|
||||
<span>
|
||||
<MaterialIcon
|
||||
icon="error_circle_rounded"
|
||||
color="Icon/Feedback/Error"
|
||||
size={20}
|
||||
/>
|
||||
{intl.formatMessage({
|
||||
id: "bookingWidget.validationError.destination",
|
||||
defaultMessage: "Enter destination or hotel",
|
||||
})}
|
||||
</span>
|
||||
</Typography>
|
||||
<Typography
|
||||
className={styles.message}
|
||||
variant="Body/Supporting text (caption)/smRegular"
|
||||
>
|
||||
<span>
|
||||
{intl.formatMessage({
|
||||
id: "bookingWidget.validationError.destinationDesc",
|
||||
defaultMessage:
|
||||
"A destination or hotel name is needed to be able to search for a hotel room.",
|
||||
})}
|
||||
</span>
|
||||
</Typography>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -62,64 +62,66 @@ export default function FormContent({
|
||||
|
||||
return (
|
||||
<div className={styles.input}>
|
||||
<div className={styles.inputContainer}>
|
||||
<div className={styles.where}>
|
||||
<Search
|
||||
handlePressEnter={onSubmit}
|
||||
selectOnBlur={true}
|
||||
inputName="search"
|
||||
includeTypes={["cities", "hotels"]}
|
||||
autoFocus={focusWidget}
|
||||
<div className={styles.inputRow}>
|
||||
<div className={styles.inputContainer}>
|
||||
<div className={styles.where}>
|
||||
<Search
|
||||
handlePressEnter={onSubmit}
|
||||
selectOnBlur={true}
|
||||
inputName="search"
|
||||
includeTypes={["cities", "hotels"]}
|
||||
autoFocus={focusWidget}
|
||||
/>
|
||||
{errors.search && <ValidationError />}
|
||||
</div>
|
||||
<div className={styles.when}>
|
||||
<Typography
|
||||
variant="Body/Supporting text (caption)/smBold"
|
||||
className={styles.label}
|
||||
>
|
||||
<label id="bookingWidgetDatePickerLabel">
|
||||
{nights > 0
|
||||
? intl.formatMessage(
|
||||
{
|
||||
id: "booking.numberOfNights",
|
||||
defaultMessage:
|
||||
"{totalNights, plural, one {# night} other {# nights}}",
|
||||
},
|
||||
{ totalNights: nights }
|
||||
)
|
||||
: intl.formatMessage({
|
||||
id: "bookingWidget.label.checkIn",
|
||||
defaultMessage: "Check in",
|
||||
})}
|
||||
</label>
|
||||
</Typography>
|
||||
<DatePicker ariaLabelledBy="bookingWidgetDatePickerLabel" />
|
||||
</div>
|
||||
<div className={styles.rooms}>
|
||||
<Typography
|
||||
variant="Body/Supporting text (caption)/smBold"
|
||||
className={styles.label}
|
||||
>
|
||||
<label id="rooms-and-guests">
|
||||
{intl.formatMessage({
|
||||
id: "bookingWidget.label.roomsAndGuests",
|
||||
defaultMessage: "Rooms & Guests",
|
||||
})}
|
||||
</label>
|
||||
</Typography>
|
||||
<GuestsRoomsPickerForm ariaLabelledBy="rooms-and-guests" />
|
||||
</div>
|
||||
</div>
|
||||
<div className={cx(styles.buttonContainer, styles.showOnTablet)}>
|
||||
<IconButton
|
||||
size="xl"
|
||||
variant="Filled"
|
||||
form={formId}
|
||||
type="submit"
|
||||
isDisabled={isSearching}
|
||||
iconName="search"
|
||||
/>
|
||||
{errors.search && <ValidationError />}
|
||||
</div>
|
||||
<div className={styles.when}>
|
||||
<Typography
|
||||
variant="Body/Supporting text (caption)/smBold"
|
||||
className={styles.label}
|
||||
>
|
||||
<label htmlFor="date">
|
||||
{nights > 0
|
||||
? intl.formatMessage(
|
||||
{
|
||||
id: "booking.numberOfNights",
|
||||
defaultMessage:
|
||||
"{totalNights, plural, one {# night} other {# nights}}",
|
||||
},
|
||||
{ totalNights: nights }
|
||||
)
|
||||
: intl.formatMessage({
|
||||
id: "bookingWidget.label.checkIn",
|
||||
defaultMessage: "Check in",
|
||||
})}
|
||||
</label>
|
||||
</Typography>
|
||||
<DatePicker name="date" />
|
||||
</div>
|
||||
<div className={styles.rooms}>
|
||||
<Typography
|
||||
variant="Body/Supporting text (caption)/smBold"
|
||||
className={styles.label}
|
||||
>
|
||||
<label id="rooms-and-guests">
|
||||
{intl.formatMessage({
|
||||
id: "bookingWidget.label.roomsAndGuests",
|
||||
defaultMessage: "Rooms & Guests",
|
||||
})}
|
||||
</label>
|
||||
</Typography>
|
||||
<GuestsRoomsPickerForm ariaLabelledBy="rooms-and-guests" />
|
||||
</div>
|
||||
</div>
|
||||
<div className={cx(styles.buttonContainer, styles.showOnTablet)}>
|
||||
<IconButton
|
||||
size="xl"
|
||||
variant="Filled"
|
||||
form={formId}
|
||||
type="submit"
|
||||
isDisabled={isSearching}
|
||||
iconName="search"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className={cx(
|
||||
|
||||
Reference in New Issue
Block a user