Merged in fix/SW-2451-destinations-mobile-search (pull request #1995)

fix: SW-2451 Fix Search field hidden behind list

* fix: SW-2451 Fix Search field hidden behind list

(cherry picked from commit 4e8f02ffd7dc94ec0469fc8c572aab39542d459e)

* fix: SW-2451 Optimized code

* fix: SW-2451 Added forced focus & optimised code

* Fix: SW-2451 Optimised code

* fix: SW-2451 Removed untranslated error message

* fix: SW-2451 Optimised code


Approved-by: Erik Tiekstra
This commit is contained in:
Hrishikesh Vaipurkar
2025-05-13 07:00:32 +00:00
parent 45f61bd81e
commit 5a351991e1
7 changed files with 197 additions and 31 deletions

View File

@@ -1,5 +1,3 @@
"use client"
import { zodResolver } from "@hookform/resolvers/zod"
import * as Sentry from "@sentry/nextjs"
import { useRouter } from "next/navigation"
@@ -10,29 +8,38 @@ import { z } from "zod"
import { Search } from "@/components/Forms/BookingWidget/FormContent/Search"
import { toast } from "@/components/TempDesignSystem/Toasts"
const jumpToSchema = z.object({
destinationSearch: z.string().min(1, "Please enter a search term"),
const destinationSearchFormSchema = z.object({
destinationSearch: z.string().min(1),
})
type JumpToSchema = z.infer<typeof jumpToSchema>
type DestinationSearchFormSchema = z.infer<typeof destinationSearchFormSchema>
export function JumpTo() {
type DestinationSearchFormProps = {
isMobile?: boolean
}
export function DestinationSearchForm({
isMobile,
}: DestinationSearchFormProps) {
const router = useRouter()
const intl = useIntl()
const methods = useForm<JumpToSchema>({
const methods = useForm<DestinationSearchFormSchema>({
defaultValues: {
destinationSearch: "",
},
shouldFocusError: false,
mode: "onSubmit",
resolver: zodResolver(jumpToSchema),
resolver: zodResolver(destinationSearchFormSchema),
reValidateMode: "onSubmit",
})
return (
<FormProvider {...methods}>
<Search
autoFocus={isMobile}
alwaysShowResults={isMobile}
includeTypes={["cities", "hotels", "countries"]}
variant="rounded"
variant={isMobile ? "default" : "rounded"}
handlePressEnter={() => {
void 0
}}
@@ -51,13 +58,11 @@ export function JumpTo() {
{ locationName: item.name }
)
)
return
}
router.push(item.url)
}}
withSearchButton
withSearchButton={!isMobile}
/>
</FormProvider>
)

View File

@@ -0,0 +1,90 @@
.trigger {
background: var(--Base-Surface-Primary-light-Normal);
border: 1px solid var(--Border-Default);
border-radius: var(--Corner-radius-rounded);
display: flex;
justify-content: space-between;
padding: var(--Space-x15) var(--Space-x15) var(--Space-x15) var(--Space-x3);
width: 100%;
}
.whereTo {
display: block;
color: var(--Base-Text-Accent);
text-align: left;
}
.icon {
background: var(--Base-Button-Primary-Fill-Normal);
width: 40px;
height: 40px;
display: flex;
align-items: center;
justify-content: center;
border-radius: var(--Corner-radius-Rounded);
color: var(--Base-Text-Inverted);
}
.modalOverlay {
position: fixed;
inset: 0;
background-color: var(--Overlay-40);
&[data-entering] {
animation: overlay-fade 200ms;
}
&[data-exiting] {
animation: overlay-fade 150ms reverse ease-in;
}
}
.modal {
position: fixed;
bottom: 0;
left: 0;
right: 0;
height: calc(100dvh - 20px);
border-radius: var(--Corner-radius-md) var(--Corner-radius-md) 0 0;
background-color: var(--Surface-Primary-Default);
box-shadow: 0px 0px 14px 6px rgba(0, 0, 0, 0.1);
&[data-entering] {
animation: modal-anim 200ms;
}
&[data-exiting] {
animation: modal-anim 150ms reverse ease-in;
}
}
.modalDialog {
display: grid;
gap: var(--Space-x15);
padding: var(--Space-x15) var(--Space-x2) var(--Space-x7);
}
.close {
margin-right: -10px;
justify-self: flex-end;
}
@keyframes overlay-fade {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes modal-anim {
from {
transform: translateY(100%);
}
to {
transform: translateY(0);
}
}

View File

@@ -0,0 +1,80 @@
"use client"
import {
Button,
Dialog,
DialogTrigger,
Modal,
ModalOverlay,
} from "react-aria-components"
import { useIntl } from "react-intl"
import { useMediaQuery } from "usehooks-ts"
import { IconButton } from "@scandic-hotels/design-system/IconButton"
import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
import { Typography } from "@scandic-hotels/design-system/Typography"
import { DestinationSearchForm } from "./Form"
import styles from "./destinationSearch.module.css"
export function DestinationSearch() {
const intl = useIntl()
const displayInModal = useMediaQuery("(max-width: 767px)")
return (
<>
<div hidden={!displayInModal}>
<DialogTrigger>
<Button className={styles.trigger}>
<span>
<Typography variant="Body/Supporting text (caption)/smBold">
<span className={styles.whereTo}>
{intl.formatMessage({
defaultMessage: "Where to?",
})}
</span>
</Typography>
<Typography variant="Body/Paragraph/mdRegular">
<span>
{intl.formatMessage({
defaultMessage: "Hotels & Destinations",
})}
</span>
</Typography>
</span>
<span className={styles.icon}>
<MaterialIcon color="CurrentColor" icon="search" size={24} />
</span>
</Button>
<ModalOverlay className={styles.modalOverlay} isDismissable={true}>
<Modal className={styles.modal}>
<Dialog className={styles.modalDialog}>
{({ close }) => (
<>
<IconButton
onPress={close}
theme="Black"
style="Muted"
className={styles.close}
>
<MaterialIcon
color="CurrentColor"
icon="close"
size={24}
/>
</IconButton>
<DestinationSearchForm isMobile={true} />
</>
)}
</Dialog>
</Modal>
</ModalOverlay>
</DialogTrigger>
</div>
<div hidden={displayInModal}>
<DestinationSearchForm />
</div>
</>
)
}

View File

@@ -1,15 +0,0 @@
.searchContainer {
display: flex;
justify-content: center;
align-items: center;
margin: 0 auto;
width: 100%;
border-radius: var(--Corner-radius-rounded);
border: 1px solid var(--Border-Default);
background: var(--Surface-Primary-Default);
padding: var(--Space-x15) var(--Space-x15) var(--Space-x15) var(--Space-x3);
& > * {
flex: 1;
}
}

View File

@@ -49,7 +49,7 @@
text-align: center;
}
.jumpToContainer {
.destinationSearchContainer {
width: 100%;
@media screen and (min-width: 768px) {

View File

@@ -8,8 +8,8 @@ import Blocks from "@/components/Blocks"
import SkeletonShimmer from "@/components/SkeletonShimmer"
import TrackingSDK from "@/components/TrackingSDK"
import { DestinationSearch } from "./DestinationSearch"
import HotelsSection from "./HotelsSection"
import { JumpTo } from "./JumpTo"
import OverviewMapContainer from "./OverviewMapContainer"
import styles from "./destinationOverviewPage.module.css"
@@ -29,8 +29,8 @@ export default async function DestinationOverviewPage() {
<Typography variant="Title/lg">
<h1 className={styles.heading}>{destinationOverviewPage.heading}</h1>
</Typography>
<div className={styles.jumpToContainer}>
<JumpTo />
<div className={styles.destinationSearchContainer}>
<DestinationSearch />
</div>
</div>
<div className={styles.mapContainer}>

View File

@@ -21,6 +21,8 @@ import SearchList from "./SearchList"
import styles from "./search.module.css"
interface SearchProps {
autoFocus?: boolean
alwaysShowResults?: boolean
className?: string
handlePressEnter: () => void
inputName: string
@@ -32,6 +34,8 @@ interface SearchProps {
}
export function Search({
autoFocus,
alwaysShowResults,
handlePressEnter,
inputName: SEARCH_TERM_NAME,
onSelect,
@@ -97,6 +101,7 @@ export function Search({
itemToString={(value) => (value ? value.name : "")}
onSelect={handleOnSelect}
defaultHighlightedIndex={0}
isOpen={alwaysShowResults}
>
{({
getInputProps,
@@ -149,6 +154,7 @@ export function Search({
},
type: "search",
})}
autoFocus={autoFocus}
/>
</div>
</div>