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);
|
background-color: var(--Background-Primary);
|
||||||
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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 {
|
.bookingCodeLabel {
|
||||||
@@ -20,21 +29,9 @@
|
|||||||
color: var(--Text-Secondary);
|
color: var(--Text-Secondary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.colorSecondary {
|
.input {
|
||||||
color: var(--Text-Secondary);
|
background-color: var(--Surface-Primary-Hover);
|
||||||
}
|
color: var(--Text-Interactive-Focus);
|
||||||
|
|
||||||
.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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.bookingCodeRemember,
|
.bookingCodeRemember,
|
||||||
@@ -48,29 +45,10 @@
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.bookingCodeTooltip {
|
|
||||||
max-width: 560px;
|
|
||||||
margin-top: var(--Space-x2);
|
|
||||||
color: var(--Text-Secondary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.bookingCodeRememberVisible label {
|
.bookingCodeRememberVisible label {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.removeButton {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media screen and (max-width: 767px) {
|
|
||||||
.hideOnMobile {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
.removeButton {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media screen and (min-width: 768px) {
|
@media screen and (min-width: 768px) {
|
||||||
.bookingCode {
|
.bookingCode {
|
||||||
height: auto;
|
height: auto;
|
||||||
@@ -82,9 +60,6 @@
|
|||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
border-radius: var(--Space-x15);
|
border-radius: var(--Space-x15);
|
||||||
}
|
}
|
||||||
.error {
|
|
||||||
color: var(--Text-Default);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@media screen and (min-width: 768px) and (max-width: 1366px) {
|
@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) {
|
@media screen and (min-width: 1367px) {
|
||||||
.container:hover {
|
.container:hover {
|
||||||
background-color: var(--Surface-Primary-Hover);
|
background-color: var(--Surface-Primary-Hover);
|
||||||
border-radius: var(--Corner-radius-md);
|
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 {
|
.bookingCodeRememberVisible {
|
||||||
padding: var(--Space-x2);
|
padding: var(--Space-x2);
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: calc(100% + var(--Space-x3));
|
top: calc(100% + var(--Space-x3));
|
||||||
left: calc(0% - var(--Space-x05));
|
left: calc(0% - var(--Space-x05));
|
||||||
width: 360px;
|
width: 320px;
|
||||||
box-shadow: var(--popup-box-shadow);
|
box-shadow: var(--popup-box-shadow);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,24 +1,20 @@
|
|||||||
import { useCallback, useEffect, useRef, useState } from "react"
|
import { useCallback, useEffect, useRef, useState } from "react"
|
||||||
import { Dialog, DialogTrigger, Popover } from "react-aria-components"
|
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 { useIntl } from "react-intl"
|
||||||
import { useMediaQuery } from "usehooks-ts"
|
import { useMediaQuery } from "usehooks-ts"
|
||||||
|
|
||||||
import { Button } from "@scandic-hotels/design-system/Button"
|
import { Button } from "@scandic-hotels/design-system/Button"
|
||||||
import Checkbox from "@scandic-hotels/design-system/Form/Checkbox"
|
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 Switch from "@scandic-hotels/design-system/Switch"
|
||||||
import { Typography } from "@scandic-hotels/design-system/Typography"
|
import { Typography } from "@scandic-hotels/design-system/Typography"
|
||||||
import { SEARCH_TYPE_REDEMPTION } from "@scandic-hotels/trpc/constants/booking"
|
import { SEARCH_TYPE_REDEMPTION } from "@scandic-hotels/trpc/constants/booking"
|
||||||
|
|
||||||
import { useBookingFlowConfig } from "../../../../../bookingFlowConfig/bookingFlowConfigContext"
|
|
||||||
import BookingFlowInput from "../../../../BookingFlowInput"
|
import BookingFlowInput from "../../../../BookingFlowInput"
|
||||||
import { getErrorMessage } from "../../../../BookingFlowInput/errors"
|
|
||||||
import { Input as BookingWidgetInput } from "../Input"
|
import { Input as BookingWidgetInput } from "../Input"
|
||||||
import { RemoveExtraRooms } from "../RemoveExtraRooms/RemoveExtraRooms"
|
import { BookingCodeError } from "./BookingCodeError"
|
||||||
import { isMultiRoomError } from "../utils"
|
import CodeRulesModal from "./CodeRulesModal"
|
||||||
|
import { RememberCode } from "./RememberCode"
|
||||||
|
|
||||||
import styles from "./booking-code.module.css"
|
import styles from "./booking-code.module.css"
|
||||||
|
|
||||||
@@ -106,6 +102,21 @@ export default function BookingCode() {
|
|||||||
setShowRemember(true)
|
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(() => {
|
useEffect(() => {
|
||||||
setIsTablet(checkIsTablet)
|
setIsTablet(checkIsTablet)
|
||||||
}, [checkIsTablet])
|
}, [checkIsTablet])
|
||||||
@@ -142,27 +153,48 @@ export default function BookingCode() {
|
|||||||
<div
|
<div
|
||||||
className={styles.container}
|
className={styles.container}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
onFocus={showRememberCheck}
|
|
||||||
onBlur={(e) => closeIfOutside(e.nativeEvent.relatedTarget as HTMLElement)}
|
onBlur={(e) => closeIfOutside(e.nativeEvent.relatedTarget as HTMLElement)}
|
||||||
>
|
>
|
||||||
<div className={styles.bookingCode}>
|
<div className={styles.bookingCode}>
|
||||||
<div className={styles.bookingCodeLabel}>
|
<span className={styles.bookingCodeLabel}>
|
||||||
<Typography variant="Body/Supporting text (caption)/smBold">
|
<Typography variant="Body/Supporting text (caption)/smBold">
|
||||||
<span>{codeVoucher}</span>
|
<label htmlFor="booking-code" id="bookingCodeLabel">
|
||||||
|
{codeVoucher}
|
||||||
|
</label>
|
||||||
</Typography>
|
</Typography>
|
||||||
<CodeRulesModal />
|
<CodeRulesModal />
|
||||||
</div>
|
</span>
|
||||||
|
<span className={styles.inputWrapper}>
|
||||||
<BookingWidgetInput
|
<BookingWidgetInput
|
||||||
className="input"
|
className={styles.input}
|
||||||
type="search"
|
type="text"
|
||||||
placeholder={addCode}
|
placeholder={addCode}
|
||||||
|
aria-labelledby="bookingCodeLabel"
|
||||||
name="bookingCode.value"
|
name="bookingCode.value"
|
||||||
id="booking-code"
|
id="booking-code"
|
||||||
onChange={(event) => updateBookingCodeFormValue(event.target.value)}
|
onChange={(event) => {
|
||||||
|
updateBookingCodeFormValue(event.target.value)
|
||||||
|
if (!!bookingCode?.value) {
|
||||||
|
showRememberCheck()
|
||||||
|
} else {
|
||||||
|
hideRememberCheck()
|
||||||
|
resetRememberCheck()
|
||||||
|
}
|
||||||
|
}}
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
value={bookingCode?.value}
|
value={bookingCode?.value}
|
||||||
|
onFocus={() => {
|
||||||
|
if (!!bookingCode?.value) {
|
||||||
|
showRememberCheck()
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onBlur={(e) =>
|
||||||
|
closeIfOutside(e.nativeEvent.relatedTarget as HTMLElement)
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{isDesktop ? (
|
{isDesktop ? (
|
||||||
<div
|
<div
|
||||||
className={
|
className={
|
||||||
@@ -174,7 +206,7 @@ export default function BookingCode() {
|
|||||||
{codeError?.message ? (
|
{codeError?.message ? (
|
||||||
<BookingCodeError codeError={codeError} isDesktop />
|
<BookingCodeError codeError={codeError} isDesktop />
|
||||||
) : (
|
) : (
|
||||||
<CodeRemember
|
<RememberCode
|
||||||
bookingCodeValue={bookingCode?.value}
|
bookingCodeValue={bookingCode?.value}
|
||||||
onApplyClick={() => setShowRemember(false)}
|
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({
|
function TabletBookingCode({
|
||||||
bookingCode,
|
bookingCode,
|
||||||
updateValue,
|
updateValue,
|
||||||
@@ -340,6 +272,7 @@ function TabletBookingCode({
|
|||||||
document.body.style.overflow = "clip !important"
|
document.body.style.overflow = "clip !important"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isOpen && !bookingCode?.value) {
|
if (!isOpen && !bookingCode?.value) {
|
||||||
setValue("bookingCode.flag", false, { shouldDirty: true })
|
setValue("bookingCode.flag", false, { shouldDirty: true })
|
||||||
setIsOpen(isOpen)
|
setIsOpen(isOpen)
|
||||||
@@ -367,8 +300,8 @@ function TabletBookingCode({
|
|||||||
},
|
},
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<Typography variant="Body/Supporting text (caption)/smBold">
|
<Typography variant="Body/Supporting text (caption)/smRegular">
|
||||||
<span className={styles.colorSecondary}>{codeVoucher}</span>
|
<span>{codeVoucher}</span>
|
||||||
</Typography>
|
</Typography>
|
||||||
</Checkbox>
|
</Checkbox>
|
||||||
</Button>
|
</Button>
|
||||||
@@ -395,7 +328,7 @@ function TabletBookingCode({
|
|||||||
{codeError?.message ? (
|
{codeError?.message ? (
|
||||||
<BookingCodeError codeError={codeError} />
|
<BookingCodeError codeError={codeError} />
|
||||||
) : (
|
) : (
|
||||||
<CodeRemember
|
<RememberCode
|
||||||
bookingCodeValue={bookingCode?.value}
|
bookingCodeValue={bookingCode?.value}
|
||||||
onApplyClick={close}
|
onApplyClick={close}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -36,6 +36,14 @@ export default function RewardNight() {
|
|||||||
const reward = getRewardMessage(config, intl)
|
const reward = getRewardMessage(config, intl)
|
||||||
const rewardNightTooltip = getRewardNightTooltipMessage(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 redemptionErr = errors[SEARCH_TYPE_REDEMPTION]
|
||||||
const isDesktop = useMediaQuery("(min-width: 767px)")
|
const isDesktop = useMediaQuery("(min-width: 767px)")
|
||||||
|
|
||||||
@@ -45,6 +53,7 @@ export default function RewardNight() {
|
|||||||
if (value && getValues("bookingCode.value")) {
|
if (value && getValues("bookingCode.value")) {
|
||||||
setValue("bookingCode.flag", false)
|
setValue("bookingCode.flag", false)
|
||||||
setValue("bookingCode.value", "", { shouldValidate: true })
|
setValue("bookingCode.value", "", { shouldValidate: true })
|
||||||
|
setValue("bookingCode.remember", false)
|
||||||
// Hide the notification popup after 5 seconds by re-triggering validation
|
// Hide the notification popup after 5 seconds by re-triggering validation
|
||||||
// This is kept consistent with location search field error notification timeout
|
// This is kept consistent with location search field error notification timeout
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
@@ -83,6 +92,7 @@ export default function RewardNight() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div ref={ref} onBlur={(e) => closeOnBlur(e.nativeEvent)}>
|
<div ref={ref} onBlur={(e) => closeOnBlur(e.nativeEvent)}>
|
||||||
|
<div className={styles.rewardNightLabel}>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
hideError
|
hideError
|
||||||
name={SEARCH_TYPE_REDEMPTION}
|
name={SEARCH_TYPE_REDEMPTION}
|
||||||
@@ -92,16 +102,23 @@ export default function RewardNight() {
|
|||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className={styles.rewardNightLabel}>
|
|
||||||
<Typography
|
<Typography
|
||||||
variant="Body/Supporting text (caption)/smRegular"
|
variant="Body/Supporting text (caption)/smRegular"
|
||||||
className={styles.label}
|
className={styles.label}
|
||||||
>
|
>
|
||||||
<span>{reward}</span>
|
<span>{reward}</span>
|
||||||
</Typography>
|
</Typography>
|
||||||
|
</Checkbox>
|
||||||
<Modal
|
<Modal
|
||||||
trigger={
|
trigger={
|
||||||
<IconButton variant="Muted" emphasis size="sm" iconName="info" />
|
<IconButton
|
||||||
|
className={styles.infoButton}
|
||||||
|
variant="Muted"
|
||||||
|
emphasis
|
||||||
|
size="sm"
|
||||||
|
iconName="info"
|
||||||
|
aria-label={rewardLabel}
|
||||||
|
/>
|
||||||
}
|
}
|
||||||
title={reward}
|
title={reward}
|
||||||
>
|
>
|
||||||
@@ -113,7 +130,7 @@ export default function RewardNight() {
|
|||||||
</Typography>
|
</Typography>
|
||||||
</Modal>
|
</Modal>
|
||||||
</div>
|
</div>
|
||||||
</Checkbox>
|
|
||||||
{redemptionErr && (
|
{redemptionErr && (
|
||||||
<div className={styles.errorContainer}>
|
<div className={styles.errorContainer}>
|
||||||
<Typography
|
<Typography
|
||||||
|
|||||||
@@ -26,6 +26,12 @@
|
|||||||
max-width: 560px;
|
max-width: 560px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.infoButton {
|
||||||
|
align-self: center;
|
||||||
|
color: var(
|
||||||
|
--Icon-Interactive-Placeholder
|
||||||
|
) !important; /* Override IconButton default color */
|
||||||
|
}
|
||||||
@media screen and (max-width: 767px) {
|
@media screen and (max-width: 767px) {
|
||||||
.hideOnMobile {
|
.hideOnMobile {
|
||||||
display: none;
|
display: none;
|
||||||
|
|||||||
@@ -310,7 +310,10 @@ function SearchListError({
|
|||||||
<MaterialIcon icon="error" color="Icon/Interactive/Accent" />
|
<MaterialIcon icon="error" color="Icon/Interactive/Accent" />
|
||||||
{caption}
|
{caption}
|
||||||
</Caption>
|
</Caption>
|
||||||
<Typography variant="Body/Paragraph/mdRegular">
|
<Typography
|
||||||
|
className={styles.errorBody}
|
||||||
|
variant="Body/Paragraph/mdRegular"
|
||||||
|
>
|
||||||
<p>{body}</p>
|
<p>{body}</p>
|
||||||
</Typography>
|
</Typography>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|||||||
@@ -36,3 +36,7 @@
|
|||||||
.textPlaceholderColor {
|
.textPlaceholderColor {
|
||||||
color: var(--UI-Text-Placeholder);
|
color: var(--UI-Text-Placeholder);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.errorBody {
|
||||||
|
color: var(--UI-Text-High-contrast);
|
||||||
|
}
|
||||||
|
|||||||
@@ -19,10 +19,6 @@
|
|||||||
&:has(input:active, input:focus, input:focus-within) {
|
&:has(input:active, input:focus, input:focus-within) {
|
||||||
background-color: var(--Surface-Primary-Hover);
|
background-color: var(--Surface-Primary-Hover);
|
||||||
}
|
}
|
||||||
|
|
||||||
&:has(input:active, input:focus, input:focus-within) {
|
|
||||||
border: 2px solid var(--Border-Interactive-Focus);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.label {
|
.label {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { useIntl } from "react-intl"
|
import { useIntl } from "react-intl"
|
||||||
|
|
||||||
import Caption from "@scandic-hotels/design-system/Caption"
|
|
||||||
import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
|
import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
|
||||||
|
import { Typography } from "@scandic-hotels/design-system/Typography"
|
||||||
|
|
||||||
import styles from "./validationError.module.css"
|
import styles from "./validationError.module.css"
|
||||||
|
|
||||||
@@ -10,20 +10,34 @@ export default function ValidationError() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.container}>
|
<div className={styles.container}>
|
||||||
<Caption className={styles.title} color="red" type="bold">
|
<Typography
|
||||||
<MaterialIcon icon="dangerous" color="Icon/Feedback/Error" size={20} />
|
className={styles.title}
|
||||||
|
variant="Body/Supporting text (caption)/smBold"
|
||||||
|
>
|
||||||
|
<span>
|
||||||
|
<MaterialIcon
|
||||||
|
icon="error_circle_rounded"
|
||||||
|
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",
|
||||||
})}
|
})}
|
||||||
</Caption>
|
</span>
|
||||||
<Caption className={styles.message} type="regular">
|
</Typography>
|
||||||
|
<Typography
|
||||||
|
className={styles.message}
|
||||||
|
variant="Body/Supporting text (caption)/smRegular"
|
||||||
|
>
|
||||||
|
<span>
|
||||||
{intl.formatMessage({
|
{intl.formatMessage({
|
||||||
id: "bookingWidget.validationError.destinationDesc",
|
id: "bookingWidget.validationError.destinationDesc",
|
||||||
defaultMessage:
|
defaultMessage:
|
||||||
"A destination or hotel name is needed to be able to search for a hotel room.",
|
"A destination or hotel name is needed to be able to search for a hotel room.",
|
||||||
})}
|
})}
|
||||||
</Caption>
|
</span>
|
||||||
|
</Typography>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,9 +20,11 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: var(--Space-x1);
|
gap: var(--Space-x1);
|
||||||
|
color: var(--UI-Text-Error);
|
||||||
}
|
}
|
||||||
|
|
||||||
.message {
|
.message {
|
||||||
|
color: var(--UI-Text-High-contrast);
|
||||||
text-wrap: auto;
|
text-wrap: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -12,11 +12,14 @@
|
|||||||
.rooms,
|
.rooms,
|
||||||
.when {
|
.when {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
border: 2px solid transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
.buttonContainer {
|
.buttonContainer {
|
||||||
|
align-self: center;
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: var(--Space-x1);
|
gap: var(--Space-x1);
|
||||||
|
border: 2px solid transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
.showOnTablet {
|
.showOnTablet {
|
||||||
@@ -30,15 +33,41 @@
|
|||||||
.label {
|
.label {
|
||||||
color: var(--Text-Accent-Primary);
|
color: var(--Text-Accent-Primary);
|
||||||
}
|
}
|
||||||
.when:has([data-datepicker-open="true"]) .label,
|
.when:has([data-datepicker-open="true"], [data-pressed="true"]) .label,
|
||||||
.rooms:has([data-pressed="true"]) .label {
|
.rooms:has([data-rooms-open="true"], [data-pressed="true"]) .label {
|
||||||
color: var(--Text-Interactive-Focus);
|
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) {
|
@media screen and (max-width: 767px) {
|
||||||
.voucherContainer {
|
.voucherContainer {
|
||||||
padding: var(--Space-x2) 0 var(--Space-x4);
|
padding: var(--Space-x2) 0 var(--Space-x4);
|
||||||
}
|
}
|
||||||
|
.buttonContainer {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media screen and (max-width: 1366px) {
|
@media screen and (max-width: 1366px) {
|
||||||
@@ -83,16 +112,16 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex: 2;
|
flex: 2;
|
||||||
gap: var(--Space-x2);
|
gap: var(--Space-x2);
|
||||||
margin-left: calc(-1 * var(--Space-x15));
|
|
||||||
}
|
}
|
||||||
.voucherContainer {
|
.voucherContainer {
|
||||||
flex: 1;
|
border-radius: 0 0 var(--Corner-radius-md) var(--Corner-radius-md);
|
||||||
}
|
}
|
||||||
|
|
||||||
.rooms,
|
.rooms,
|
||||||
.when,
|
.when,
|
||||||
.where {
|
.where {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
border-radius: var(--Corner-radius-md);
|
||||||
}
|
}
|
||||||
|
|
||||||
.inputContainer input[type="text"] {
|
.inputContainer input[type="text"] {
|
||||||
@@ -103,18 +132,6 @@
|
|||||||
.rooms,
|
.rooms,
|
||||||
.when {
|
.when {
|
||||||
padding: var(--Space-x1) var(--Space-x15);
|
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 {
|
.where {
|
||||||
@@ -137,13 +154,18 @@
|
|||||||
.input {
|
.input {
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
.inputRow {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: var(--Space-x2);
|
||||||
|
width: 100%;
|
||||||
|
padding: var(--Space-x2);
|
||||||
|
}
|
||||||
|
|
||||||
.inputContainer {
|
.inputContainer {
|
||||||
padding: var(--Space-x2) var(--Space-x2) var(--Space-x2)
|
|
||||||
var(--Layout-Tablet-Margin-Margin-min);
|
|
||||||
flex-basis: 80%;
|
flex-basis: 80%;
|
||||||
}
|
}
|
||||||
.buttonContainer {
|
.buttonContainer {
|
||||||
padding-right: var(--Layout-Tablet-Margin-Margin-min);
|
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
.input .buttonContainer .button {
|
.input .buttonContainer .button {
|
||||||
@@ -153,12 +175,11 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.voucherRow {
|
.voucherRow {
|
||||||
|
flex: 1;
|
||||||
display: flex;
|
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);
|
border-bottom: 1px solid var(--Primary-Light-On-Surface-Divider-subtle);
|
||||||
padding: var(--Space-x2) var(--Layout-Tablet-Margin-Margin-min);
|
padding-left: var(--Space-x2);
|
||||||
border-radius: 0 0 var(--Corner-radius-lg) var(--Corner-radius-lg);
|
|
||||||
margin-left: calc(var(--Space-x15) * -1);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.showOnTablet {
|
.showOnTablet {
|
||||||
@@ -173,6 +194,9 @@
|
|||||||
.input {
|
.input {
|
||||||
gap: var(--Space-x2);
|
gap: var(--Space-x2);
|
||||||
}
|
}
|
||||||
|
.inputRow {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
.bookingCodeDisabled {
|
.bookingCodeDisabled {
|
||||||
flex: none;
|
flex: none;
|
||||||
|
|||||||
@@ -62,6 +62,7 @@ export default function FormContent({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.input}>
|
<div className={styles.input}>
|
||||||
|
<div className={styles.inputRow}>
|
||||||
<div className={styles.inputContainer}>
|
<div className={styles.inputContainer}>
|
||||||
<div className={styles.where}>
|
<div className={styles.where}>
|
||||||
<Search
|
<Search
|
||||||
@@ -78,7 +79,7 @@ export default function FormContent({
|
|||||||
variant="Body/Supporting text (caption)/smBold"
|
variant="Body/Supporting text (caption)/smBold"
|
||||||
className={styles.label}
|
className={styles.label}
|
||||||
>
|
>
|
||||||
<label htmlFor="date">
|
<label id="bookingWidgetDatePickerLabel">
|
||||||
{nights > 0
|
{nights > 0
|
||||||
? intl.formatMessage(
|
? intl.formatMessage(
|
||||||
{
|
{
|
||||||
@@ -94,7 +95,7 @@ export default function FormContent({
|
|||||||
})}
|
})}
|
||||||
</label>
|
</label>
|
||||||
</Typography>
|
</Typography>
|
||||||
<DatePicker name="date" />
|
<DatePicker ariaLabelledBy="bookingWidgetDatePickerLabel" />
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.rooms}>
|
<div className={styles.rooms}>
|
||||||
<Typography
|
<Typography
|
||||||
@@ -121,6 +122,7 @@ export default function FormContent({
|
|||||||
iconName="search"
|
iconName="search"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
<div
|
<div
|
||||||
className={cx(
|
className={cx(
|
||||||
styles.voucherContainer,
|
styles.voucherContainer,
|
||||||
|
|||||||
@@ -28,8 +28,7 @@
|
|||||||
|
|
||||||
@media screen and (min-width: 1367px) {
|
@media screen and (min-width: 1367px) {
|
||||||
.default {
|
.default {
|
||||||
padding: var(--Space-x15) var(--Space-x2) var(--Space-x15)
|
padding: var(--Space-x15) var(--Space-x2) var(--Space-x15) var(--Space-x1);
|
||||||
var(--Space-x1);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.full {
|
.full {
|
||||||
@@ -43,8 +42,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.compact {
|
.compact {
|
||||||
padding: var(--Space-x15) var(--Space-x2) var(--Space-x15)
|
padding: var(--Space-x15) var(--Space-x2) var(--Space-x15) var(--Space-x15);
|
||||||
var(--Space-x1);
|
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,10 +6,9 @@ import { useIntl } from "react-intl"
|
|||||||
|
|
||||||
import { Lang } from "@scandic-hotels/common/constants/language"
|
import { Lang } from "@scandic-hotels/common/constants/language"
|
||||||
import { dt } from "@scandic-hotels/common/dt"
|
import { dt } from "@scandic-hotels/common/dt"
|
||||||
import Caption from "@scandic-hotels/design-system/Caption"
|
import { Button } from "@scandic-hotels/design-system/Button"
|
||||||
import { Divider } from "@scandic-hotels/design-system/Divider"
|
import { Divider } from "@scandic-hotels/design-system/Divider"
|
||||||
import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
|
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 { Typography } from "@scandic-hotels/design-system/Typography"
|
||||||
|
|
||||||
import useLang from "../../../../hooks/useLang"
|
import useLang from "../../../../hooks/useLang"
|
||||||
@@ -112,20 +111,11 @@ export default function DatePickerRangeDesktop({
|
|||||||
color="Border/Divider/Subtle"
|
color="Border/Divider/Subtle"
|
||||||
/>
|
/>
|
||||||
<footer className={props.className}>
|
<footer className={props.className}>
|
||||||
<Button
|
<Button variant="Tertiary" onPress={close} size="sm">
|
||||||
intent="tertiary"
|
|
||||||
onPress={close}
|
|
||||||
size="small"
|
|
||||||
theme="base"
|
|
||||||
>
|
|
||||||
<Caption color="white" type="bold" asChild>
|
|
||||||
<span>
|
|
||||||
{intl.formatMessage({
|
{intl.formatMessage({
|
||||||
id: "datePicker.selectDates",
|
id: "datePicker.selectDates",
|
||||||
defaultMessage: "Select dates",
|
defaultMessage: "Select dates",
|
||||||
})}
|
})}
|
||||||
</span>
|
|
||||||
</Caption>
|
|
||||||
</Button>
|
</Button>
|
||||||
</footer>
|
</footer>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ import { useIntl } from "react-intl"
|
|||||||
|
|
||||||
import { Lang } from "@scandic-hotels/common/constants/language"
|
import { Lang } from "@scandic-hotels/common/constants/language"
|
||||||
import { dt } from "@scandic-hotels/common/dt"
|
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 { 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 { Typography } from "@scandic-hotels/design-system/Typography"
|
||||||
|
|
||||||
import useLang from "../../../../hooks/useLang"
|
import useLang from "../../../../hooks/useLang"
|
||||||
@@ -131,19 +131,15 @@ export default function DatePickerRangeMobile({
|
|||||||
<footer className={styles.footer}>
|
<footer className={styles.footer}>
|
||||||
<Button
|
<Button
|
||||||
className={styles.button}
|
className={styles.button}
|
||||||
intent="tertiary"
|
variant="Primary"
|
||||||
|
color="Primary"
|
||||||
|
size="md"
|
||||||
onPress={close}
|
onPress={close}
|
||||||
size="large"
|
|
||||||
theme="base"
|
|
||||||
>
|
>
|
||||||
<Typography variant="Body/Paragraph/mdBold">
|
|
||||||
<span>
|
|
||||||
{intl.formatMessage({
|
{intl.formatMessage({
|
||||||
id: "datePicker.selectDates",
|
id: "datePicker.selectDates",
|
||||||
defaultMessage: "Select dates",
|
defaultMessage: "Select dates",
|
||||||
})}
|
})}
|
||||||
</span>
|
|
||||||
</Typography>
|
|
||||||
</Button>
|
</Button>
|
||||||
</footer>
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ div.months {
|
|||||||
|
|
||||||
.captionLabel {
|
.captionLabel {
|
||||||
text-transform: capitalize;
|
text-transform: capitalize;
|
||||||
|
color: var(--Text-Default);
|
||||||
}
|
}
|
||||||
|
|
||||||
td.day,
|
td.day,
|
||||||
|
|||||||
@@ -24,7 +24,7 @@
|
|||||||
align-self: flex-end;
|
align-self: flex-end;
|
||||||
background-color: var(--Main-Grey-White);
|
background-color: var(--Main-Grey-White);
|
||||||
grid-area: header;
|
grid-area: header;
|
||||||
padding: var(--Space-x3) var(--Space-x2);
|
padding: 0 var(--Space-x2) 0;
|
||||||
position: sticky;
|
position: sticky;
|
||||||
top: 0;
|
top: 0;
|
||||||
z-index: 10;
|
z-index: 10;
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
.btn {
|
.triggerButton {
|
||||||
background: none;
|
background: none;
|
||||||
border: none;
|
border: none;
|
||||||
|
color: var(--Text-Default);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
outline: none;
|
outline: none;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
@@ -12,55 +13,63 @@
|
|||||||
bottom: 0;
|
bottom: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
padding: 20px var(--Space-x15) 0;
|
padding: 20px var(--Space-x15) 0;
|
||||||
|
border-radius: var(--Corner-radius-lg);
|
||||||
}
|
}
|
||||||
|
|
||||||
.body {
|
.datePicker[data-datepicker-open="true"] {
|
||||||
color: var(--Text-Default);
|
|
||||||
}
|
|
||||||
|
|
||||||
.hideWrapper {
|
|
||||||
background-color: var(--Main-Grey-White);
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.container[data-datepicker-open="true"] .hideWrapper {
|
|
||||||
display: block;
|
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) {
|
@media screen and (max-width: 1366px) {
|
||||||
.container {
|
.datePicker {
|
||||||
z-index: 10001;
|
|
||||||
height: 24px;
|
height: 24px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.hideWrapper {
|
.datePicker[data-datepicker-open="true"] {
|
||||||
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 {
|
|
||||||
border-radius: var(--Corner-radius-lg) var(--Corner-radius-lg) 0 0;
|
border-radius: var(--Corner-radius-lg) var(--Corner-radius-lg) 0 0;
|
||||||
top: calc(max(var(--sitewide-alert-sticky-height), 20px));
|
top: calc(max(var(--sitewide-alert-sticky-height), 20px));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media screen and (min-width: 1367px) {
|
@media screen and (min-width: 1367px) {
|
||||||
.hideWrapper {
|
.datePicker {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pickerContainer {
|
||||||
|
position: absolute;
|
||||||
|
display: grid;
|
||||||
border-radius: var(--Corner-radius-lg);
|
border-radius: var(--Corner-radius-lg);
|
||||||
box-shadow: var(--popup-box-shadow);
|
box-shadow: var(--popup-box-shadow);
|
||||||
padding: var(--Space-x2) var(--Space-x3);
|
padding: var(--Space-x2) var(--Space-x3);
|
||||||
position: absolute;
|
max-width: calc(100vw - 20px);
|
||||||
/**
|
max-height: 440px;
|
||||||
BookingWidget padding +
|
top: calc(100% + 36px);
|
||||||
border-width +
|
left: auto;
|
||||||
wanted space below booking widget
|
right: auto;
|
||||||
*/
|
bottom: auto;
|
||||||
top: calc(100% + var(--Space-x1) + 1px + var(--Space-x4));
|
overflow: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
.triggerButton {
|
||||||
|
display: block;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
border-radius: var(--Corner-radius-md);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,14 @@
|
|||||||
"use client"
|
"use client"
|
||||||
import { useCallback, useEffect, useRef, useState } from "react"
|
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 { useFormContext, useWatch } from "react-hook-form"
|
||||||
import { useIntl } from "react-intl"
|
import { useIntl } from "react-intl"
|
||||||
|
import { useMediaQuery } from "usehooks-ts"
|
||||||
|
|
||||||
import { longDateFormat } from "@scandic-hotels/common/constants/dateFormats"
|
import { longDateFormat } from "@scandic-hotels/common/constants/dateFormats"
|
||||||
import { dt } from "@scandic-hotels/common/dt"
|
import { dt } from "@scandic-hotels/common/dt"
|
||||||
|
import { useScrollLock } from "@scandic-hotels/common/hooks/useScrollLock"
|
||||||
import { Typography } from "@scandic-hotels/design-system/Typography"
|
import { Typography } from "@scandic-hotels/design-system/Typography"
|
||||||
|
|
||||||
import useLang from "../../../hooks/useLang"
|
import useLang from "../../../hooks/useLang"
|
||||||
@@ -16,17 +20,24 @@ import styles from "./date-picker.module.css"
|
|||||||
import type { DateRange } from "react-day-picker"
|
import type { DateRange } from "react-day-picker"
|
||||||
|
|
||||||
type DatePickerFormProps = {
|
type DatePickerFormProps = {
|
||||||
|
ariaLabelledBy?: string
|
||||||
name?: string
|
name?: string
|
||||||
}
|
}
|
||||||
export default function DatePickerForm({ name = "date" }: DatePickerFormProps) {
|
export default function DatePickerForm({
|
||||||
|
ariaLabelledBy,
|
||||||
|
name = "date",
|
||||||
|
}: DatePickerFormProps) {
|
||||||
const lang = useLang()
|
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 [isOpen, setIsOpen] = useState(false)
|
||||||
const selectedDate = useWatch({ name })
|
const selectedDate = useWatch({ name })
|
||||||
const { register, setValue } = useFormContext()
|
const { setValue } = useFormContext()
|
||||||
const ref = useRef<HTMLDivElement | null>(null)
|
const ref = useRef<HTMLDivElement | null>(null)
|
||||||
|
|
||||||
const close = useCallback(() => {
|
const close = useCallback(() => {
|
||||||
if (!selectedDate.toDate) {
|
if (!selectedDate.toDate) {
|
||||||
setValue(
|
setValue(
|
||||||
@@ -38,13 +49,21 @@ export default function DatePickerForm({ name = "date" }: DatePickerFormProps) {
|
|||||||
{ shouldDirty: true }
|
{ shouldDirty: true }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
setIsOpen(false)
|
setIsOpen(false)
|
||||||
}, [name, setValue, selectedDate])
|
unlockScroll()
|
||||||
|
}, [name, setValue, selectedDate, unlockScroll])
|
||||||
|
|
||||||
function showOnFocus() {
|
const { overlayProps, underlayProps } = useOverlay(
|
||||||
setIsOpen(true)
|
{
|
||||||
}
|
isOpen,
|
||||||
|
onClose: () => {
|
||||||
|
setIsOpen(false)
|
||||||
|
unlockScroll()
|
||||||
|
},
|
||||||
|
isDismissable: true,
|
||||||
|
},
|
||||||
|
ref
|
||||||
|
)
|
||||||
|
|
||||||
function handleSelectDate(
|
function handleSelectDate(
|
||||||
_nextRange: DateRange | undefined,
|
_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(() => {
|
useEffect(() => {
|
||||||
function handleClickOutside(evt: Event) {
|
setIsDesktop(checkIsDesktop)
|
||||||
if (isOpen) {
|
}, [checkIsDesktop])
|
||||||
const target = evt.target as HTMLElement
|
|
||||||
closeIfOutside(target)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
document.body.addEventListener("click", handleClickOutside)
|
|
||||||
return () => {
|
|
||||||
document.body.removeEventListener("click", handleClickOutside)
|
|
||||||
}
|
|
||||||
}, [closeIfOutside, isOpen])
|
|
||||||
|
|
||||||
const selectedFromDate = dt(selectedDate.fromDate)
|
const selectedFromDate = dt(selectedDate.fromDate)
|
||||||
.locale(lang)
|
.locale(lang)
|
||||||
@@ -134,39 +128,25 @@ export default function DatePickerForm({ name = "date" }: DatePickerFormProps) {
|
|||||||
? dt(selectedDate.toDate).locale(lang).format(longDateFormat[lang])
|
? dt(selectedDate.toDate).locale(lang).format(longDateFormat[lang])
|
||||||
: ""
|
: ""
|
||||||
|
|
||||||
return (
|
return isDesktop ? (
|
||||||
<div
|
<div className={styles.datePicker}>
|
||||||
className={styles.container}
|
<Trigger
|
||||||
onBlur={(e) => {
|
ariaLabelledBy={ariaLabelledBy}
|
||||||
closeOnBlur(e.nativeEvent)
|
onPress={() => {
|
||||||
|
setIsOpen((prev) => !prev)
|
||||||
}}
|
}}
|
||||||
data-datepicker-open={isOpen}
|
selectedFromDate={selectedFromDate}
|
||||||
|
selectedToDate={selectedToDate}
|
||||||
|
/>
|
||||||
|
{isOpen && (
|
||||||
|
<div {...underlayProps}>
|
||||||
|
<FocusScope contain restoreFocus autoFocus>
|
||||||
|
<div
|
||||||
|
{...overlayProps}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
|
className={styles.pickerContainer}
|
||||||
|
data-datepicker-open={isOpen}
|
||||||
>
|
>
|
||||||
<button
|
|
||||||
className={styles.btn}
|
|
||||||
onFocus={showOnFocus}
|
|
||||||
onClick={() => setIsOpen(true)}
|
|
||||||
type="button"
|
|
||||||
>
|
|
||||||
<Typography variant="Body/Paragraph/mdRegular" className={styles.body}>
|
|
||||||
<span>
|
|
||||||
{intl.formatMessage(
|
|
||||||
{
|
|
||||||
id: "booking.selectedDateRange",
|
|
||||||
defaultMessage: "{selectedFromDate} – {selectedToDate}",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
selectedFromDate,
|
|
||||||
selectedToDate,
|
|
||||||
}
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
</Typography>
|
|
||||||
</button>
|
|
||||||
<input {...register("date.fromDate")} type="hidden" />
|
|
||||||
<input {...register("date.toDate")} type="hidden" />
|
|
||||||
<div aria-modal className={styles.hideWrapper} role="dialog">
|
|
||||||
<DatePickerRangeDesktop
|
<DatePickerRangeDesktop
|
||||||
close={close}
|
close={close}
|
||||||
handleOnSelect={handleSelectDate}
|
handleOnSelect={handleSelectDate}
|
||||||
@@ -178,7 +158,35 @@ export default function DatePickerForm({ name = "date" }: DatePickerFormProps) {
|
|||||||
: undefined,
|
: undefined,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
|
</FocusScope>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className={styles.datePicker}>
|
||||||
|
<Trigger
|
||||||
|
ariaLabelledBy={ariaLabelledBy}
|
||||||
|
onPress={() => {
|
||||||
|
setIsOpen((prev) => !prev)
|
||||||
|
if (!isOpen) {
|
||||||
|
lockScroll()
|
||||||
|
} else {
|
||||||
|
unlockScroll()
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
selectedFromDate={selectedFromDate}
|
||||||
|
selectedToDate={selectedToDate}
|
||||||
|
/>
|
||||||
{isOpen && (
|
{isOpen && (
|
||||||
|
<div {...underlayProps}>
|
||||||
|
<FocusScope contain restoreFocus autoFocus>
|
||||||
|
<div
|
||||||
|
{...overlayProps}
|
||||||
|
ref={ref}
|
||||||
|
className={styles.pickerContainer}
|
||||||
|
data-datepicker-open={isOpen}
|
||||||
|
>
|
||||||
<DatePickerRangeMobile
|
<DatePickerRangeMobile
|
||||||
close={close}
|
close={close}
|
||||||
handleOnSelect={handleSelectDate}
|
handleOnSelect={handleSelectDate}
|
||||||
@@ -190,8 +198,52 @@ export default function DatePickerForm({ name = "date" }: DatePickerFormProps) {
|
|||||||
: undefined,
|
: undefined,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
</FocusScope>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<>
|
||||||
|
<Typography variant="Body/Paragraph/mdRegular">
|
||||||
|
<ButtonRAC
|
||||||
|
className={styles.triggerButton}
|
||||||
|
onPress={onPress}
|
||||||
|
type="button"
|
||||||
|
aria-labelledby={ariaLabelledBy}
|
||||||
|
>
|
||||||
|
{triggerText}
|
||||||
|
</ButtonRAC>
|
||||||
|
</Typography>
|
||||||
|
<input {...register("date.fromDate")} type="hidden" />
|
||||||
|
<input {...register("date.toDate")} type="hidden" />
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
|
import { useEffect, useRef, useState } from "react"
|
||||||
import { useFormContext } from "react-hook-form"
|
import { useFormContext } from "react-hook-form"
|
||||||
import { useIntl } from "react-intl"
|
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 { 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 { ChildBedMapEnum } from "@scandic-hotels/trpc/enums/childBedMapEnum"
|
||||||
|
|
||||||
import styles from "./child-selector.module.css"
|
import styles from "./child-selector.module.css"
|
||||||
@@ -38,6 +39,34 @@ export default function ChildInfoSelector({
|
|||||||
index = 0,
|
index = 0,
|
||||||
roomIndex = 0,
|
roomIndex = 0,
|
||||||
}: ChildInfoSelectorProps) {
|
}: ChildInfoSelectorProps) {
|
||||||
|
const ageSelectRef = useRef<HTMLDivElement>(null)
|
||||||
|
const bedPrefSelectRef = useRef<HTMLDivElement>(null)
|
||||||
|
const [ageWidth, setAgeWidth] = useState<number | undefined>(undefined)
|
||||||
|
const [bedWidth, setBedWidth] = useState<number | undefined>(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 ageFieldName = `rooms.${roomIndex}.childrenInRoom.${index}.age`
|
||||||
const bedFieldName = `rooms.${roomIndex}.childrenInRoom.${index}.bed`
|
const bedFieldName = `rooms.${roomIndex}.childrenInRoom.${index}.bed`
|
||||||
const intl = useIntl()
|
const intl = useIntl()
|
||||||
@@ -49,10 +78,12 @@ export default function ChildInfoSelector({
|
|||||||
id: "booking.bedPreference",
|
id: "booking.bedPreference",
|
||||||
defaultMessage: "Bed preference",
|
defaultMessage: "Bed preference",
|
||||||
})
|
})
|
||||||
|
|
||||||
const errorMessage = intl.formatMessage({
|
const errorMessage = intl.formatMessage({
|
||||||
id: "bookingWidget.child.ageRequiredError",
|
id: "bookingWidget.child.ageRequiredError",
|
||||||
defaultMessage: "Child age is required",
|
defaultMessage: "Child age is required",
|
||||||
})
|
})
|
||||||
|
|
||||||
const { setValue, formState } = useFormContext()
|
const { setValue, formState } = useFormContext()
|
||||||
|
|
||||||
function updateSelectedBed(bed: number) {
|
function updateSelectedBed(bed: number) {
|
||||||
@@ -113,50 +144,62 @@ export default function ChildInfoSelector({
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div key={index} className={styles.childInfoContainer}>
|
<div key={index} className={styles.childInfoContainer}>
|
||||||
<div>
|
<div ref={ageSelectRef}>
|
||||||
<DeprecatedSelect
|
<Select
|
||||||
required={true}
|
isRequired
|
||||||
items={ageList}
|
items={ageList}
|
||||||
|
name={ageFieldName}
|
||||||
label={ageLabel}
|
label={ageLabel}
|
||||||
aria-label={ageLabel}
|
onSelectionChange={(key) => {
|
||||||
value={child.age ?? childDefaultValues.age}
|
|
||||||
onSelect={(key) => {
|
|
||||||
updateSelectedAge(key as number)
|
updateSelectedAge(key as number)
|
||||||
}}
|
}}
|
||||||
maxHeight={180}
|
popoverWidth={`${ageWidth}px`}
|
||||||
name={ageFieldName}
|
value={child.age ?? childDefaultValues.age}
|
||||||
isNestedInModal={true}
|
isInvalid={!!ageError}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div ref={bedPrefSelectRef}>
|
||||||
{child.age >= 0 ? (
|
{child.age >= 0 ? (
|
||||||
<DeprecatedSelect
|
<Select
|
||||||
|
isRequired
|
||||||
items={getAvailableBeds(child.age)}
|
items={getAvailableBeds(child.age)}
|
||||||
|
name={bedFieldName}
|
||||||
label={bedLabel}
|
label={bedLabel}
|
||||||
aria-label={bedLabel}
|
aria-label={bedLabel}
|
||||||
value={child.bed ?? childDefaultValues.bed}
|
value={child.bed ?? childDefaultValues.bed}
|
||||||
onSelect={(key) => {
|
onSelectionChange={(key) => {
|
||||||
updateSelectedBed(key as number)
|
updateSelectedBed(key as number)
|
||||||
}}
|
}}
|
||||||
name={bedFieldName}
|
popoverWidth={`${bedWidth}px`}
|
||||||
isNestedInModal={true}
|
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{roomErrors && roomErrors.message ? (
|
{roomErrors && roomErrors.message ? (
|
||||||
<Caption color="red" className={styles.error}>
|
<Typography
|
||||||
|
variant="Body/Supporting text (caption)/smRegular"
|
||||||
|
className={styles.error}
|
||||||
|
>
|
||||||
|
<span>
|
||||||
<MaterialIcon icon="error" color="Icon/Interactive/Accent" />
|
<MaterialIcon icon="error" color="Icon/Interactive/Accent" />
|
||||||
{roomErrors.message}
|
{roomErrors.message}
|
||||||
</Caption>
|
</span>
|
||||||
|
</Typography>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{ageError || bedError ? (
|
{ageError || bedError ? (
|
||||||
<Caption color="red" className={styles.error}>
|
<>
|
||||||
<MaterialIcon icon="error" color="Icon/Interactive/Accent" />
|
<Typography
|
||||||
|
variant="Body/Supporting text (caption)/smRegular"
|
||||||
|
className={styles.error}
|
||||||
|
>
|
||||||
|
<span>
|
||||||
|
<MaterialIcon icon="error" color="Icon/Feedback/Error" />
|
||||||
{errorMessage}
|
{errorMessage}
|
||||||
</Caption>
|
</span>
|
||||||
|
</Typography>
|
||||||
|
</>
|
||||||
) : null}
|
) : null}
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -15,6 +15,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.error {
|
.error {
|
||||||
|
color: var(--Text-Interactive-Error);
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: var(--Space-x1);
|
gap: var(--Space-x1);
|
||||||
|
|||||||
@@ -6,11 +6,11 @@ import { useIntl } from "react-intl"
|
|||||||
|
|
||||||
import { Button } from "@scandic-hotels/design-system/Button"
|
import { Button } from "@scandic-hotels/design-system/Button"
|
||||||
import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
|
import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
|
||||||
import { Tooltip } from "@scandic-hotels/design-system/Tooltip"
|
|
||||||
import { Typography } from "@scandic-hotels/design-system/Typography"
|
import { Typography } from "@scandic-hotels/design-system/Typography"
|
||||||
import { SEARCH_TYPE_REDEMPTION } from "@scandic-hotels/trpc/constants/booking"
|
import { SEARCH_TYPE_REDEMPTION } from "@scandic-hotels/trpc/constants/booking"
|
||||||
|
|
||||||
import { useBookingFlowConfig } from "../../../bookingFlowConfig/bookingFlowConfigContext"
|
import { useBookingFlowConfig } from "../../../bookingFlowConfig/bookingFlowConfigContext"
|
||||||
|
import { useIsDesktop } from "../../../hooks/useBreakpoint"
|
||||||
import { GuestsRoom } from "./GuestsRoom"
|
import { GuestsRoom } from "./GuestsRoom"
|
||||||
|
|
||||||
import styles from "./guests-rooms-picker.module.css"
|
import styles from "./guests-rooms-picker.module.css"
|
||||||
@@ -30,6 +30,7 @@ export default function GuestsRoomsPickerDialog({
|
|||||||
onClose,
|
onClose,
|
||||||
}: GuestsRoomsPickerDialogProps) {
|
}: GuestsRoomsPickerDialogProps) {
|
||||||
const intl = useIntl()
|
const intl = useIntl()
|
||||||
|
const isDesktop = useIsDesktop()
|
||||||
const config = useBookingFlowConfig()
|
const config = useBookingFlowConfig()
|
||||||
const { getFieldState, trigger, setValue, getValues } =
|
const { getFieldState, trigger, setValue, getValues } =
|
||||||
useFormContext<BookingWidgetSchema>()
|
useFormContext<BookingWidgetSchema>()
|
||||||
@@ -61,6 +62,11 @@ export default function GuestsRoomsPickerDialog({
|
|||||||
defaultMessage:
|
defaultMessage:
|
||||||
"Multi-room booking is not available with this booking code.",
|
"Multi-room booking is not available with this booking code.",
|
||||||
})
|
})
|
||||||
|
const isInvalid =
|
||||||
|
getFieldState("rooms").invalid ||
|
||||||
|
roomsValue.some((room) =>
|
||||||
|
room.childrenInRoom.some((child) => child.age === undefined)
|
||||||
|
)
|
||||||
|
|
||||||
const handleClose = useCallback(async () => {
|
const handleClose = useCallback(async () => {
|
||||||
const isValid = await trigger("rooms")
|
const isValid = await trigger("rooms")
|
||||||
@@ -97,13 +103,11 @@ export default function GuestsRoomsPickerDialog({
|
|||||||
if (fieldState.invalid) trigger("rooms")
|
if (fieldState.invalid) trigger("rooms")
|
||||||
}, [roomsValue, getFieldState, trigger])
|
}, [roomsValue, getFieldState, trigger])
|
||||||
|
|
||||||
const isInvalid =
|
|
||||||
getFieldState("rooms").invalid ||
|
|
||||||
roomsValue.some((room) =>
|
|
||||||
room.childrenInRoom.some((child) => child.age === undefined)
|
|
||||||
)
|
|
||||||
const canAddRooms = rooms.length < MAX_ROOMS
|
const canAddRooms = rooms.length < MAX_ROOMS
|
||||||
|
|
||||||
|
const addRoomButtonDisabled =
|
||||||
|
!!addRoomDisabledTextForSpecialRate || !canAddRooms
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<section className={styles.contentWrapper}>
|
<section className={styles.contentWrapper}>
|
||||||
@@ -122,20 +126,24 @@ export default function GuestsRoomsPickerDialog({
|
|||||||
onRemove={handleRemoveRoom}
|
onRemove={handleRemoveRoom}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
{!isDesktop && (
|
||||||
{addRoomDisabledTextForSpecialRate ? (
|
<>
|
||||||
<div className={styles.addRoomMobileContainer}>
|
<div className={styles.addRoomBtnContainer}>
|
||||||
<Button
|
<Button
|
||||||
|
className={styles.addRoomBtn}
|
||||||
variant="Text"
|
variant="Text"
|
||||||
color="Primary"
|
|
||||||
wrapping
|
wrapping
|
||||||
|
color="Primary"
|
||||||
onPress={handleAddRoom}
|
onPress={handleAddRoom}
|
||||||
isDisabled
|
isDisabled={addRoomButtonDisabled}
|
||||||
size="sm"
|
size="sm"
|
||||||
>
|
>
|
||||||
<MaterialIcon icon="add" color="CurrentColor" />
|
<MaterialIcon icon="add" color="CurrentColor" />
|
||||||
{addRoomLabel}
|
{addRoomLabel}
|
||||||
</Button>
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{addRoomDisabledTextForSpecialRate && (
|
||||||
<div className={styles.errorContainer}>
|
<div className={styles.errorContainer}>
|
||||||
<Typography
|
<Typography
|
||||||
className={styles.error}
|
className={styles.error}
|
||||||
@@ -152,84 +160,55 @@ export default function GuestsRoomsPickerDialog({
|
|||||||
</span>
|
</span>
|
||||||
</Typography>
|
</Typography>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)}
|
||||||
) : (
|
</>
|
||||||
canAddRooms && (
|
|
||||||
<div className={styles.addRoomMobileContainer}>
|
|
||||||
<Button
|
|
||||||
className={styles.addRoomBtn}
|
|
||||||
variant="Text"
|
|
||||||
wrapping
|
|
||||||
color="Primary"
|
|
||||||
onPress={handleAddRoom}
|
|
||||||
size="sm"
|
|
||||||
>
|
|
||||||
<MaterialIcon icon="add" color="CurrentColor" />
|
|
||||||
{addRoomLabel}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
<footer className={styles.footer}>
|
<footer className={styles.footer}>
|
||||||
{addRoomDisabledTextForSpecialRate ? (
|
<div className={styles.footerButtons}>
|
||||||
<div className={styles.hideOnMobile}>
|
{isDesktop && (
|
||||||
<Tooltip
|
|
||||||
text={addRoomDisabledTextForSpecialRate}
|
|
||||||
position="bottom"
|
|
||||||
arrow="left"
|
|
||||||
>
|
|
||||||
<Button
|
<Button
|
||||||
variant="Text"
|
variant="Text"
|
||||||
wrapping
|
wrapping
|
||||||
color="Primary"
|
color="Primary"
|
||||||
isDisabled
|
isDisabled={addRoomButtonDisabled}
|
||||||
size="sm"
|
size="sm"
|
||||||
onPress={handleAddRoom}
|
onPress={handleAddRoom}
|
||||||
>
|
>
|
||||||
<MaterialIcon icon="add_circle" color="CurrentColor" />
|
<MaterialIcon icon="add_circle" color="CurrentColor" />
|
||||||
{addRoomLabel}
|
{addRoomLabel}
|
||||||
</Button>
|
</Button>
|
||||||
</Tooltip>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
canAddRooms && (
|
|
||||||
<div className={styles.hideOnMobile}>
|
|
||||||
<Button
|
|
||||||
className={styles.addRoomBtn}
|
|
||||||
variant="Text"
|
|
||||||
wrapping
|
|
||||||
color="Primary"
|
|
||||||
size="sm"
|
|
||||||
onPress={handleAddRoom}
|
|
||||||
>
|
|
||||||
<MaterialIcon icon="add_circle" color="CurrentColor" />
|
|
||||||
{addRoomLabel}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
)}
|
)}
|
||||||
<Button
|
<Button
|
||||||
onPress={handleClose}
|
onPress={handleClose}
|
||||||
isDisabled={isInvalid}
|
isDisabled={isInvalid}
|
||||||
className={styles.hideOnDesktop}
|
className={styles.doneButton}
|
||||||
variant="Tertiary"
|
variant={isDesktop ? "Tertiary" : "Primary"}
|
||||||
color="Primary"
|
color="Primary"
|
||||||
size="sm"
|
size={isDesktop ? "sm" : "md"}
|
||||||
>
|
>
|
||||||
{doneLabel}
|
{doneLabel}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
</div>
|
||||||
onPress={handleClose}
|
|
||||||
isDisabled={isInvalid}
|
{/* DESKTOP INLINE ERROR MESSAGE */}
|
||||||
className={styles.hideOnMobile}
|
{addRoomDisabledTextForSpecialRate && isDesktop && (
|
||||||
variant="Tertiary"
|
<Typography
|
||||||
color="Primary"
|
className={styles.error}
|
||||||
size="sm"
|
variant="Body/Supporting text (caption)/smRegular"
|
||||||
>
|
>
|
||||||
{doneLabel}
|
<div className={styles.errorContainer}>
|
||||||
</Button>
|
<MaterialIcon
|
||||||
|
icon="error"
|
||||||
|
size={20}
|
||||||
|
color="Icon/Feedback/Error"
|
||||||
|
isFilled
|
||||||
|
/>
|
||||||
|
{addRoomDisabledTextForSpecialRate}
|
||||||
|
</div>
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
</footer>
|
</footer>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
import { useIntl } from "react-intl"
|
||||||
|
|
||||||
|
import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
|
||||||
|
import { Typography } from "@scandic-hotels/design-system/Typography"
|
||||||
|
|
||||||
|
import styles from "./validationError.module.css"
|
||||||
|
|
||||||
|
export default function ValidationError() {
|
||||||
|
const intl = useIntl()
|
||||||
|
|
||||||
|
const errorMessage = intl.formatMessage({
|
||||||
|
id: "bookingWidget.child.ageRequiredError",
|
||||||
|
defaultMessage: "Child age is required",
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.container}>
|
||||||
|
<Typography
|
||||||
|
className={styles.title}
|
||||||
|
variant="Body/Supporting text (caption)/smBold"
|
||||||
|
>
|
||||||
|
<span>
|
||||||
|
<MaterialIcon
|
||||||
|
icon="error_circle_rounded"
|
||||||
|
color="Icon/Feedback/Error"
|
||||||
|
size={20}
|
||||||
|
/>
|
||||||
|
{errorMessage}
|
||||||
|
</span>
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
.container {
|
||||||
|
position: absolute;
|
||||||
|
top: calc(100% + var(--Space-x2));
|
||||||
|
background: var(--Surface-Primary-Default);
|
||||||
|
border-radius: var(--Corner-radius-lg);
|
||||||
|
box-shadow: 0px 0px 14px 6px rgba(0, 0, 0, 0.1);
|
||||||
|
padding: var(--Space-x15);
|
||||||
|
max-width: min(100vw, calc(360px - var(--Space-x2)));
|
||||||
|
width: 100%;
|
||||||
|
left: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
padding: var(--Space-x15);
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: var(--Space-x05);
|
||||||
|
z-index: var(--dialog-z-index);
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--Space-x1);
|
||||||
|
color: var(--UI-Text-Error);
|
||||||
|
}
|
||||||
|
|
||||||
|
.message {
|
||||||
|
text-wrap: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (min-width: 1367px) {
|
||||||
|
.container {
|
||||||
|
top: calc(100% + var(--Space-x1) + var(--Space-x2));
|
||||||
|
left: calc(var(--Space-x1) * -1);
|
||||||
|
padding: var(--Space-x2);
|
||||||
|
max-width: 360px;
|
||||||
|
width: fit-content;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,30 +1,17 @@
|
|||||||
.triggerDesktop {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.errorContainer {
|
.errorContainer {
|
||||||
display: flex;
|
display: flex;
|
||||||
padding: var(--Space-x2);
|
padding: var(--Space-x15);
|
||||||
|
border: 1px solid var(--Border-Default);
|
||||||
|
border-radius: var(--Corner-radius-md);
|
||||||
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.error {
|
.error {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: var(--Space-x1);
|
gap: var(--Space-x1);
|
||||||
color: var(--UI-Text-Error);
|
color: var(--Text-Feedback-Error);
|
||||||
}
|
text-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
.pickerContainerMobile {
|
|
||||||
--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;
|
|
||||||
bottom: 0;
|
|
||||||
left: 0;
|
|
||||||
position: fixed;
|
|
||||||
right: 0;
|
|
||||||
top: calc(max(var(--sitewide-alert-sticky-height), 20px));
|
|
||||||
transition: top 300ms ease;
|
|
||||||
z-index: 100;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.contentWrapper {
|
.contentWrapper {
|
||||||
@@ -35,10 +22,6 @@
|
|||||||
grid-template-rows: var(--header-height) calc(100dvh - var(--header-height));
|
grid-template-rows: var(--header-height) calc(100dvh - var(--header-height));
|
||||||
}
|
}
|
||||||
|
|
||||||
.pickerContainerDesktop {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.roomContainer {
|
.roomContainer {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: var(--Space-x2);
|
gap: var(--Space-x2);
|
||||||
@@ -69,6 +52,7 @@
|
|||||||
bottom: 0;
|
bottom: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
padding: 20px var(--Space-x15) 0;
|
padding: 20px var(--Space-x15) 0;
|
||||||
|
border-radius: var(--Corner-radius-lg);
|
||||||
}
|
}
|
||||||
|
|
||||||
.guestsAndRooms {
|
.guestsAndRooms {
|
||||||
@@ -77,10 +61,18 @@
|
|||||||
|
|
||||||
.footer {
|
.footer {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: column;
|
||||||
gap: var(--Space-x1);
|
gap: var(--Space-x2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.addRoomBtnContainer {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
.footerButtons {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
.roomContainer {
|
.roomContainer {
|
||||||
padding: var(--Space-x2);
|
padding: var(--Space-x2);
|
||||||
}
|
}
|
||||||
@@ -97,15 +89,27 @@
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.contentWrapper
|
.contentWrapper .addRoomBtn:is(:focus, :focus-visible, :focus-within),
|
||||||
.addRoomMobileContainer
|
.footer .addRoomBtn:is(:focus, :focus-visible, :focus-within),
|
||||||
.addRoomBtn:is(:focus, :focus-visible, :focus-within),
|
|
||||||
.footer .hideOnMobile .addRoomBtn:is(:focus, :focus-visible, :focus-within),
|
|
||||||
.roomActionsButton:is(:focus, :focus-visible, :focus-within) {
|
.roomActionsButton:is(:focus, :focus-visible, :focus-within) {
|
||||||
outline: var(--Border-Interactive-Focus) auto 1px;
|
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.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;
|
||||||
|
top: calc(max(var(--sitewide-alert-sticky-height), 20px));
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
transition: top 300ms ease;
|
||||||
|
z-index: var(--booking-widget-z-index);
|
||||||
|
overflow: scroll;
|
||||||
|
}
|
||||||
|
|
||||||
@media screen and (max-width: 1366px) {
|
@media screen and (max-width: 1366px) {
|
||||||
.contentContainer {
|
.contentContainer {
|
||||||
grid-area: content;
|
grid-area: content;
|
||||||
@@ -140,43 +144,52 @@
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.footer .hideOnMobile {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.addRoomMobileContainer {
|
.addRoomMobileContainer {
|
||||||
display: grid;
|
display: grid;
|
||||||
padding-bottom: calc(var(--sticky-button-height) + 20px);
|
padding-bottom: var(--Space-x3);
|
||||||
|
}
|
||||||
|
.errorContainer {
|
||||||
|
margin: var(--Space-x2);
|
||||||
}
|
}
|
||||||
|
|
||||||
.addRoomMobileContainer button {
|
.addRoomMobileContainer button {
|
||||||
width: 150px;
|
width: 150px;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.addRoomMobileContainer .addRoomMobileDisabledText {
|
|
||||||
padding: var(--Space-x1) var(--Space-x2);
|
|
||||||
background-color: var(--Background-Primary);
|
|
||||||
margin: 0 var(--Space-x2);
|
|
||||||
border-radius: var(--Corner-radius-md);
|
|
||||||
display: flex;
|
|
||||||
gap: var(--Space-x1);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@media screen and (min-width: 1367px) {
|
@media screen and (min-width: 1367px) {
|
||||||
.container {
|
.container {
|
||||||
height: 24px;
|
height: 24px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.pickerContainerMobile {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.contentWrapper {
|
.contentWrapper {
|
||||||
grid-template-rows: auto;
|
grid-template-rows: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.footerButtons {
|
||||||
|
max-height: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.doneButton {
|
||||||
|
min-width: 125px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pickerContainer {
|
||||||
|
position: absolute;
|
||||||
|
display: grid;
|
||||||
|
bottom: auto;
|
||||||
|
left: auto;
|
||||||
|
right: auto;
|
||||||
|
border-radius: var(--Corner-radius-lg);
|
||||||
|
box-shadow: var(--popup-box-shadow);
|
||||||
|
min-width: 360px;
|
||||||
|
max-width: calc(100vw - 20px);
|
||||||
|
padding: var(--Space-x2) var(--Space-x3);
|
||||||
|
top: calc(100% + 36px);
|
||||||
|
max-height: none;
|
||||||
|
overflow-y: auto;
|
||||||
|
overflow-x: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
.roomContainer {
|
.roomContainer {
|
||||||
padding: var(--Space-x2) 0 0 0;
|
padding: var(--Space-x2) 0 0 0;
|
||||||
}
|
}
|
||||||
@@ -193,34 +206,13 @@
|
|||||||
overflow-y: visible;
|
overflow-y: visible;
|
||||||
}
|
}
|
||||||
|
|
||||||
.triggerMobile {
|
.trigger > span {
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.triggerDesktop {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.triggerDesktop > span {
|
|
||||||
display: block;
|
display: block;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
|
|
||||||
.pickerContainerDesktop {
|
.pickerContainer:focus-visible {
|
||||||
--header-height: 72px;
|
|
||||||
--sticky-button-height: 140px;
|
|
||||||
|
|
||||||
background-color: var(--Main-Grey-White);
|
|
||||||
display: grid;
|
|
||||||
border-radius: var(--Corner-radius-lg);
|
|
||||||
box-shadow: var(--popup-box-shadow);
|
|
||||||
max-width: calc(100vw - 20px);
|
|
||||||
padding: var(--Space-x2) var(--Space-x3);
|
|
||||||
width: 360px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pickerContainerDesktop:focus-visible {
|
|
||||||
outline: none;
|
outline: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -229,18 +221,13 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.footer {
|
.footer {
|
||||||
grid-template-columns: auto auto;
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
padding-top: var(--Space-x2);
|
padding-top: var(--Space-x2);
|
||||||
|
height: fit-content;
|
||||||
}
|
}
|
||||||
|
|
||||||
.footer button {
|
.footer button {
|
||||||
margin-left: auto;
|
|
||||||
width: auto;
|
width: auto;
|
||||||
min-width: 125px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.footer .hideOnDesktop,
|
|
||||||
.addRoomMobileContainer {
|
|
||||||
display: none;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,19 +1,16 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import { useCallback, useEffect, useId, useState } from "react"
|
import { useCallback, useEffect, useRef, useState } from "react"
|
||||||
import {
|
import { FocusScope, useOverlay } from "react-aria"
|
||||||
Button,
|
import { Button } from "react-aria-components"
|
||||||
Dialog,
|
|
||||||
DialogTrigger,
|
|
||||||
Modal,
|
|
||||||
Popover,
|
|
||||||
} from "react-aria-components"
|
|
||||||
import { useFormContext, useWatch } from "react-hook-form"
|
import { useFormContext, useWatch } from "react-hook-form"
|
||||||
import { useIntl } from "react-intl"
|
import { useIntl } from "react-intl"
|
||||||
import { useMediaQuery } from "usehooks-ts"
|
import { useMediaQuery } from "usehooks-ts"
|
||||||
|
|
||||||
|
import { useScrollLock } from "@scandic-hotels/common/hooks/useScrollLock"
|
||||||
import { Typography } from "@scandic-hotels/design-system/Typography"
|
import { Typography } from "@scandic-hotels/design-system/Typography"
|
||||||
|
|
||||||
|
import ValidationError from "./ValidationError/index"
|
||||||
import PickerForm from "./Form"
|
import PickerForm from "./Form"
|
||||||
|
|
||||||
import styles from "./guests-rooms-picker.module.css"
|
import styles from "./guests-rooms-picker.module.css"
|
||||||
@@ -26,107 +23,175 @@ export default function GuestsRoomsPickerForm({
|
|||||||
}: {
|
}: {
|
||||||
ariaLabelledBy?: string
|
ariaLabelledBy?: string
|
||||||
}) {
|
}) {
|
||||||
const { trigger } = useFormContext<BookingWidgetSchema>()
|
|
||||||
const rooms = useWatch<BookingWidgetSchema, "rooms">({ name: "rooms" })
|
const rooms = useWatch<BookingWidgetSchema, "rooms">({ name: "rooms" })
|
||||||
|
|
||||||
const popoverId = useId()
|
|
||||||
const checkIsDesktop = useMediaQuery("(min-width: 1367px)")
|
const checkIsDesktop = useMediaQuery("(min-width: 1367px)")
|
||||||
const [isDesktop, setIsDesktop] = useState(true)
|
const [isDesktop, setIsDesktop] = useState(true)
|
||||||
const [isOpen, setIsOpen] = useState(false)
|
const [isOpen, setIsOpen] = useState(false)
|
||||||
const [containerHeight, setContainerHeight] = useState(0)
|
const [containerConstraint, setContainerConstraint] = useState(0)
|
||||||
const childCount = rooms[0]?.childrenInRoom.length ?? 0 // ToDo Update for multiroom later
|
const [showErrorModal, setShowErrorModal] = useState(false)
|
||||||
|
|
||||||
//isOpen is the 'old state', so isOpen === true means "The modal is open and WILL be closed".
|
const ref = useRef<HTMLDivElement | null>(null)
|
||||||
async function setOverflowClip(isOpen: boolean) {
|
// const childCount = rooms[0]?.childrenInRoom.length ?? 0 // ToDo Update for multiroom later
|
||||||
const bodyElement = document.body
|
const {
|
||||||
if (bodyElement) {
|
clearErrors,
|
||||||
if (isOpen) {
|
formState: { errors },
|
||||||
bodyElement.style.overflow = "visible"
|
} = useFormContext()
|
||||||
} else {
|
|
||||||
// !important needed to override 'overflow: hidden' set by react-aria.
|
const [scrollPosition, setScrollPosition] = useState(0)
|
||||||
// 'overflow: hidden' does not work in combination with other sticky positioned elements, which clip does.
|
const roomError = errors["rooms"]
|
||||||
bodyElement.style.overflow = "clip !important"
|
const { lockScroll, unlockScroll } = useScrollLock({
|
||||||
}
|
autoLock: false,
|
||||||
}
|
})
|
||||||
if (!isOpen) {
|
useEffect(() => {
|
||||||
const state = await trigger("rooms")
|
if (roomError) {
|
||||||
if (state) {
|
setShowErrorModal(true)
|
||||||
setIsOpen(isOpen)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}, [roomError])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
clearErrors("rooms")
|
||||||
|
}, [clearErrors])
|
||||||
|
|
||||||
|
const timeoutRef = useRef<NodeJS.Timeout | null>(null)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!roomError) return
|
||||||
|
if (timeoutRef.current) return
|
||||||
|
if (roomError) {
|
||||||
|
timeoutRef.current = setTimeout(() => {
|
||||||
|
setShowErrorModal(false)
|
||||||
|
// magic number originates from animation
|
||||||
|
// 5000ms delay + 120ms exectuion
|
||||||
|
timeoutRef.current = null
|
||||||
|
}, 5120)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return () => {}
|
||||||
|
}, [clearErrors, roomError])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setIsDesktop(checkIsDesktop)
|
setIsDesktop(checkIsDesktop)
|
||||||
}, [checkIsDesktop])
|
}, [checkIsDesktop])
|
||||||
|
|
||||||
const updateHeight = useCallback(() => {
|
const updateHeight = useCallback((containerConstraint: number) => {
|
||||||
// Get available space for picker to show without going beyond screen
|
// Get available space for picker to show without going beyond screen
|
||||||
const bookingWidget = document.getElementById("booking-widget")
|
const bookingWidget = document.getElementById("booking-widget")
|
||||||
|
const popoverElement = document.getElementById("guestsPopover")
|
||||||
const maxHeight =
|
const maxHeight =
|
||||||
window.innerHeight -
|
window.innerHeight -
|
||||||
(bookingWidget?.getBoundingClientRect().bottom ?? 0) -
|
(bookingWidget?.getBoundingClientRect().bottom ?? 0) -
|
||||||
50
|
50
|
||||||
const innerContainerHeight = document
|
const innerContainerHeight = popoverElement?.getBoundingClientRect().height
|
||||||
.getElementsByClassName(popoverId)[0]
|
|
||||||
?.getBoundingClientRect().height
|
const shouldAdjustHeight = Boolean(
|
||||||
if (
|
// height should be constrained
|
||||||
maxHeight != containerHeight &&
|
maxHeight != containerConstraint &&
|
||||||
innerContainerHeight &&
|
innerContainerHeight &&
|
||||||
maxHeight <= innerContainerHeight
|
maxHeight <= innerContainerHeight
|
||||||
) {
|
)
|
||||||
setContainerHeight(maxHeight)
|
const hasExcessVerticalSpace = Boolean(
|
||||||
} else if (
|
// no need to constrain height
|
||||||
containerHeight &&
|
containerConstraint &&
|
||||||
innerContainerHeight &&
|
innerContainerHeight &&
|
||||||
maxHeight > innerContainerHeight
|
Math.floor(maxHeight) > Math.floor(innerContainerHeight)
|
||||||
) {
|
)
|
||||||
setContainerHeight(0)
|
if (shouldAdjustHeight) {
|
||||||
|
// avoid clipping if there's only one room
|
||||||
|
setContainerConstraint(Math.max(200, maxHeight))
|
||||||
|
} else if (hasExcessVerticalSpace) {
|
||||||
|
setContainerConstraint(0)
|
||||||
}
|
}
|
||||||
}, [containerHeight, popoverId])
|
}, [])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isDesktop && rooms.length > 0) {
|
if (isDesktop && rooms.length > 1) {
|
||||||
updateHeight()
|
updateHeight(containerConstraint)
|
||||||
}
|
}
|
||||||
}, [childCount, isDesktop, updateHeight, rooms])
|
}, [
|
||||||
|
isOpen,
|
||||||
|
scrollPosition,
|
||||||
|
isDesktop,
|
||||||
|
updateHeight,
|
||||||
|
containerConstraint,
|
||||||
|
rooms,
|
||||||
|
])
|
||||||
|
|
||||||
return isDesktop ? (
|
useEffect(() => {
|
||||||
<DialogTrigger onOpenChange={setOverflowClip} isOpen={isOpen}>
|
if (isOpen && isDesktop) {
|
||||||
|
const handleScroll = () => {
|
||||||
|
setScrollPosition(window.scrollY)
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener("scroll", handleScroll)
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener("scroll", handleScroll)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [isOpen, isDesktop, rooms])
|
||||||
|
|
||||||
|
const { overlayProps, underlayProps } = useOverlay(
|
||||||
|
{
|
||||||
|
isOpen,
|
||||||
|
onClose: () => {
|
||||||
|
setIsOpen(false)
|
||||||
|
unlockScroll()
|
||||||
|
},
|
||||||
|
isDismissable: !errors.rooms,
|
||||||
|
},
|
||||||
|
ref
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className={styles.container}>
|
||||||
<Trigger
|
<Trigger
|
||||||
rooms={rooms}
|
rooms={rooms}
|
||||||
className={styles.triggerDesktop}
|
className={styles.trigger}
|
||||||
triggerFn={() => {
|
triggerFn={() => {
|
||||||
setIsOpen(true)
|
setIsOpen((prev) => !prev)
|
||||||
}}
|
if (!isDesktop && !isOpen) {
|
||||||
/>
|
lockScroll()
|
||||||
<Popover
|
} else {
|
||||||
className={popoverId}
|
unlockScroll()
|
||||||
placement="bottom start"
|
}
|
||||||
offset={36}
|
|
||||||
style={containerHeight ? { overflow: "auto" } : undefined}
|
|
||||||
>
|
|
||||||
<Dialog className={styles.pickerContainerDesktop}>
|
|
||||||
{({ close }) => <PickerForm rooms={rooms} onClose={close} />}
|
|
||||||
</Dialog>
|
|
||||||
</Popover>
|
|
||||||
</DialogTrigger>
|
|
||||||
) : (
|
|
||||||
<DialogTrigger onOpenChange={setOverflowClip} isOpen={isOpen}>
|
|
||||||
<Trigger
|
|
||||||
rooms={rooms}
|
|
||||||
className={styles.triggerMobile}
|
|
||||||
triggerFn={() => {
|
|
||||||
setIsOpen(true)
|
|
||||||
}}
|
}}
|
||||||
ariaLabelledBy={ariaLabelledBy}
|
ariaLabelledBy={ariaLabelledBy}
|
||||||
/>
|
/>
|
||||||
<Modal>
|
|
||||||
<Dialog className={styles.pickerContainerMobile}>
|
{isOpen && (
|
||||||
{({ close }) => <PickerForm rooms={rooms} onClose={close} />}
|
<div {...underlayProps}>
|
||||||
</Dialog>
|
<FocusScope contain restoreFocus autoFocus>
|
||||||
</Modal>
|
<div
|
||||||
</DialogTrigger>
|
id="guestsPopover"
|
||||||
|
{...overlayProps}
|
||||||
|
ref={ref}
|
||||||
|
className={styles.pickerContainer}
|
||||||
|
data-pressed={isOpen}
|
||||||
|
data-rooms-open={isOpen}
|
||||||
|
style={
|
||||||
|
isDesktop
|
||||||
|
? containerConstraint > 0
|
||||||
|
? { maxHeight: containerConstraint }
|
||||||
|
: { maxHeight: "none" }
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<PickerForm
|
||||||
|
rooms={rooms}
|
||||||
|
onClose={() => {
|
||||||
|
setIsOpen((prev) => !prev)
|
||||||
|
unlockScroll()
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</FocusScope>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{showErrorModal && !isOpen && errors.rooms && <ValidationError />}
|
||||||
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -24,7 +24,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* Make sure Date Picker is placed on top of other sticky/fixed components */
|
/* Make sure Date Picker is placed on top of other sticky/fixed components */
|
||||||
&:has([data-datepicker-open="true"]) {
|
&:has([data-datepicker-open="true"]),
|
||||||
|
&:has([data-rooms-open="true"]) {
|
||||||
z-index: var(--booking-widget-open-z-index);
|
z-index: var(--booking-widget-open-z-index);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -37,7 +38,7 @@
|
|||||||
gap: var(--Space-x3);
|
gap: var(--Space-x3);
|
||||||
height: calc(100dvh - max(var(--sitewide-alert-sticky-height), 20px));
|
height: calc(100dvh - max(var(--sitewide-alert-sticky-height), 20px));
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: var(--Space-x3) var(--Space-x2) var(--Space-x7);
|
padding: var(--Space-x3) var(--Space-x2);
|
||||||
position: fixed;
|
position: fixed;
|
||||||
left: 0;
|
left: 0;
|
||||||
bottom: -100%;
|
bottom: -100%;
|
||||||
@@ -85,7 +86,6 @@
|
|||||||
|
|
||||||
&.compactFormContainer {
|
&.compactFormContainer {
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
padding-left: var(--Space-x15);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -64,6 +64,7 @@
|
|||||||
"libphonenumber-js": "^1.12.15",
|
"libphonenumber-js": "^1.12.15",
|
||||||
"motion": "^12.10.0",
|
"motion": "^12.10.0",
|
||||||
"nuqs": "2.4.3",
|
"nuqs": "2.4.3",
|
||||||
|
"react-aria": "^3.39.0",
|
||||||
"react-aria-components": "1.8.0",
|
"react-aria-components": "1.8.0",
|
||||||
"react-day-picker": "^9.6.7",
|
"react-day-picker": "^9.6.7",
|
||||||
"react-hook-form": "^7.56.2",
|
"react-hook-form": "^7.56.2",
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
&[data-disabled] {
|
&[data-disabled] {
|
||||||
cursor: unset;
|
cursor: not-allowed;
|
||||||
}
|
}
|
||||||
|
|
||||||
&[data-pending] {
|
&[data-pending] {
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ const CheckboxComponent = forwardRef<
|
|||||||
hideError,
|
hideError,
|
||||||
topAlign = false,
|
topAlign = false,
|
||||||
errorCodeMessages,
|
errorCodeMessages,
|
||||||
|
disabled = false,
|
||||||
},
|
},
|
||||||
ref
|
ref
|
||||||
) {
|
) {
|
||||||
@@ -49,7 +50,7 @@ const CheckboxComponent = forwardRef<
|
|||||||
onChange={field.onChange}
|
onChange={field.onChange}
|
||||||
data-testid={name}
|
data-testid={name}
|
||||||
name={name}
|
name={name}
|
||||||
isDisabled={registerOptions?.disabled}
|
isDisabled={registerOptions?.disabled || disabled}
|
||||||
excludeFromTabOrder
|
excludeFromTabOrder
|
||||||
>
|
>
|
||||||
{({ isSelected }) => (
|
{({ isSelected }) => (
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ export function Select({
|
|||||||
isDisabled,
|
isDisabled,
|
||||||
icon,
|
icon,
|
||||||
itemIcon,
|
itemIcon,
|
||||||
|
popoverWidth,
|
||||||
...props
|
...props
|
||||||
}: SelectProps | SelectFilterProps) {
|
}: SelectProps | SelectFilterProps) {
|
||||||
const [isOpen, setIsOpen] = useState(false)
|
const [isOpen, setIsOpen] = useState(false)
|
||||||
@@ -88,7 +89,11 @@ export function Select({
|
|||||||
/>
|
/>
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<Popover className={styles.popover} shouldFlip={false}>
|
<Popover
|
||||||
|
className={styles.popover}
|
||||||
|
style={popoverWidth ? { minWidth: popoverWidth } : undefined}
|
||||||
|
shouldFlip={false}
|
||||||
|
>
|
||||||
<ListBox className={styles.listBox}>
|
<ListBox className={styles.listBox}>
|
||||||
{items.map((item, idx) => (
|
{items.map((item, idx) => (
|
||||||
<SelectItem
|
<SelectItem
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ export interface SelectProps extends ComponentProps<typeof Select> {
|
|||||||
label: string
|
label: string
|
||||||
onSelectionChange?: (key: Key | null) => void
|
onSelectionChange?: (key: Key | null) => void
|
||||||
enableFiltering?: false
|
enableFiltering?: false
|
||||||
|
popoverWidth?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SelectItemProps extends ComponentProps<typeof ListBoxItem> {
|
export interface SelectItemProps extends ComponentProps<typeof ListBoxItem> {
|
||||||
@@ -35,4 +36,5 @@ export interface SelectFilterProps extends ComponentProps<typeof ComboBox> {
|
|||||||
label: string
|
label: string
|
||||||
onSelectionChange?: (key: Key | null) => void
|
onSelectionChange?: (key: Key | null) => void
|
||||||
enableFiltering: true
|
enableFiltering: true
|
||||||
|
popoverWidth?: string
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6053,6 +6053,7 @@ __metadata:
|
|||||||
libphonenumber-js: "npm:^1.12.15"
|
libphonenumber-js: "npm:^1.12.15"
|
||||||
motion: "npm:^12.10.0"
|
motion: "npm:^12.10.0"
|
||||||
nuqs: "npm:2.4.3"
|
nuqs: "npm:2.4.3"
|
||||||
|
react-aria: "npm:^3.39.0"
|
||||||
react-aria-components: "npm:1.8.0"
|
react-aria-components: "npm:1.8.0"
|
||||||
react-day-picker: "npm:^9.6.7"
|
react-day-picker: "npm:^9.6.7"
|
||||||
react-hook-form: "npm:^7.56.2"
|
react-hook-form: "npm:^7.56.2"
|
||||||
|
|||||||
Reference in New Issue
Block a user