Merged in feat/SW-1738-mobile-tablet-design-filtering (pull request #1472)

feat/SW-1738 mobile tablet design filtering

* feat(SW-1738): add top bar

* feat(SW-1738): set list type

* feat(SW-1738): mobile design for filter modal

* feat(SW-1738): change button component

* feat(SW-1738): hide divider

* feat(SW-1738): set list type

* feat(SW-1738): remove background color from checkbox wrapper in mobile

* feat(SW-1738): remove props from filter and sort component


Approved-by: Erik Tiekstra
Approved-by: Matilda Landström
This commit is contained in:
Fredrik Thorsson
2025-03-05 12:32:53 +00:00
parent 76c20df8e8
commit 4ab586660a
15 changed files with 157 additions and 77 deletions

View File

@@ -26,13 +26,10 @@ export default function CityListing() {
threshold: 300, threshold: 300,
elementRef: scrollRef, elementRef: scrollRef,
}) })
const { activeCities, filters, sortItems, isLoading } = const { activeCities, isLoading } = useDestinationDataStore((state) => ({
useDestinationDataStore((state) => ({ activeCities: state.activeCities,
activeCities: state.activeCities, isLoading: state.isLoading,
filters: state.allFilters, }))
sortItems: state.sortItems,
isLoading: state.isLoading,
}))
const [allCitiesVisible, setAllCitiesVisible] = useState( const [allCitiesVisible, setAllCitiesVisible] = useState(
activeCities.length <= 5 activeCities.length <= 5
) )
@@ -57,11 +54,7 @@ export default function CityListing() {
{ count: activeCities.length } { count: activeCities.length }
)} )}
</Subtitle> </Subtitle>
<DestinationFilterAndSort <DestinationFilterAndSort listType="city" />
filters={filters}
sortItems={sortItems}
listType="city"
/>
</div> </div>
{activeCities.length === 0 ? ( {activeCities.length === 0 ? (
<Alert <Alert

View File

@@ -18,13 +18,10 @@
} }
@media screen and (max-width: 949px) { @media screen and (max-width: 949px) {
.hotelListWrapper {
overflow-y: scroll;
}
.hotelList { .hotelList {
flex-direction: row; flex-direction: row;
align-items: end; align-items: end;
overflow-x: scroll;
} }
.header { .header {

View File

@@ -25,13 +25,10 @@ export default function HotelList() {
const map = useMap() const map = useMap()
const coreLib = useMapsLibrary("core") const coreLib = useMapsLibrary("core")
const [visibleHotels, setVisibleHotels] = useState<HotelDataWithUrl[]>([]) const [visibleHotels, setVisibleHotels] = useState<HotelDataWithUrl[]>([])
const { filters, sortItems, activeHotels, isLoading } = const { activeHotels, isLoading } = useDestinationDataStore((state) => ({
useDestinationDataStore((state) => ({ activeHotels: state.activeHotels,
filters: state.allFilters, isLoading: state.isLoading,
sortItems: state.sortItems, }))
activeHotels: state.activeHotels,
isLoading: state.isLoading,
}))
const debouncedUpdateVisibleHotels = useMemo( const debouncedUpdateVisibleHotels = useMemo(
() => () =>
@@ -67,11 +64,7 @@ export default function HotelList() {
{ count: visibleHotels.length } { count: visibleHotels.length }
)} )}
</Body> </Body>
<DestinationFilterAndSort <DestinationFilterAndSort listType="hotel" />
filters={filters}
sortItems={sortItems}
listType="hotel"
/>
</div> </div>
{activeHotels.length === 0 ? ( {activeHotels.length === 0 ? (
<Alert <Alert

View File

@@ -65,7 +65,7 @@
justify-self: stretch; justify-self: stretch;
} }
@media screen and (max-width: 950px) { @media screen and (max-width: 949px) {
.hotelListItem { .hotelListItem {
width: 360px; width: 360px;
min-height: 150px; min-height: 150px;
@@ -76,6 +76,11 @@
height: 100%; height: 100%;
} }
.tripAdvisor {
top: 12px;
left: 12px;
}
.content { .content {
padding: var(--Spacing-x-one-and-half); padding: var(--Spacing-x-one-and-half);
gap: var(--Spacing-x1); gap: var(--Spacing-x1);

View File

@@ -18,13 +18,10 @@
} }
@media screen and (max-width: 949px) { @media screen and (max-width: 949px) {
.cityListWrapper {
overflow-x: scroll;
}
.cityList { .cityList {
flex-direction: row; flex-direction: row;
align-items: end; align-items: end;
overflow-x: scroll;
} }
.header { .header {

View File

@@ -17,13 +17,10 @@ import { AlertTypeEnum } from "@/types/enums/alert"
export default function CityList() { export default function CityList() {
const intl = useIntl() const intl = useIntl()
const { filters, sortItems, activeCities, isLoading } = const { activeCities, isLoading } = useDestinationDataStore((state) => ({
useDestinationDataStore((state) => ({ activeCities: state.activeCities,
filters: state.allFilters, isLoading: state.isLoading,
sortItems: state.sortItems, }))
activeCities: state.activeCities,
isLoading: state.isLoading,
}))
return isLoading ? ( return isLoading ? (
<CityListSkeleton /> <CityListSkeleton />
@@ -36,11 +33,7 @@ export default function CityList() {
{ count: activeCities.length } { count: activeCities.length }
)} )}
</Body> </Body>
<DestinationFilterAndSort <DestinationFilterAndSort listType="city" />
filters={filters}
sortItems={sortItems}
listType="city"
/>
</div> </div>
{activeCities.length === 0 ? ( {activeCities.length === 0 ? (
<Alert <Alert

View File

@@ -26,13 +26,10 @@ export default function HotelListing() {
threshold: 300, threshold: 300,
elementRef: scrollRef, elementRef: scrollRef,
}) })
const { activeHotels, filters, sortItems, isLoading } = const { activeHotels, isLoading } = useDestinationDataStore((state) => ({
useDestinationDataStore((state) => ({ activeHotels: state.activeHotels,
activeHotels: state.activeHotels, isLoading: state.isLoading,
filters: state.allFilters, }))
sortItems: state.sortItems,
isLoading: state.isLoading,
}))
const [allHotelsVisible, setAllHotelsVisible] = useState( const [allHotelsVisible, setAllHotelsVisible] = useState(
activeHotels.length <= 5 activeHotels.length <= 5
) )
@@ -57,11 +54,7 @@ export default function HotelListing() {
{ count: activeHotels.length } { count: activeHotels.length }
)} )}
</Subtitle> </Subtitle>
<DestinationFilterAndSort <DestinationFilterAndSort listType="hotel" />
filters={filters}
sortItems={sortItems}
listType="hotel"
/>
</div> </div>
{activeHotels.length === 0 ? ( {activeHotels.length === 0 ? (
<Alert <Alert

View File

@@ -40,9 +40,7 @@
} }
.closeButton { .closeButton {
pointer-events: initial; display: none !important;
box-shadow: var(--button-box-shadow);
gap: var(--Spacing-x-half);
} }
.zoomButton { .zoomButton {
@@ -65,4 +63,11 @@
display: flex; display: flex;
flex-direction: row-reverse; flex-direction: row-reverse;
} }
.closeButton {
display: flex !important;
pointer-events: initial;
box-shadow: var(--button-box-shadow);
gap: var(--Spacing-x-half);
}
} }

View File

@@ -10,7 +10,11 @@ import {
useState, useState,
} from "react" } from "react"
import { Dialog, Modal } from "react-aria-components" import { Dialog, Modal } from "react-aria-components"
import { useIntl } from "react-intl"
import DestinationFilterAndSort from "@/components/DestinationFilterAndSort"
import { ChevronLeftSmallIcon } from "@/components/Icons"
import Button from "@/components/TempDesignSystem/Button"
import { debounce } from "@/utils/debounce" import { debounce } from "@/utils/debounce"
import DynamicMap from "./DynamicMap" import DynamicMap from "./DynamicMap"
@@ -49,6 +53,8 @@ export default function Map({
const [mapHeight, setMapHeight] = useState("0px") const [mapHeight, setMapHeight] = useState("0px")
const [scrollHeightWhenOpened, setScrollHeightWhenOpened] = useState(0) const [scrollHeightWhenOpened, setScrollHeightWhenOpened] = useState(0)
const intl = useIntl()
const markers = getHotelMapMarkers(hotels) const markers = getHotelMapMarkers(hotels)
const geoJson = mapMarkerDataToGeoJson(markers) const geoJson = mapMarkerDataToGeoJson(markers)
const defaultCoordinates = defaultLocation const defaultCoordinates = defaultLocation
@@ -122,6 +128,20 @@ export default function Map({
} }
aria-label={"Mapview"} aria-label={"Mapview"}
> >
<div className={styles.mobileNavigation}>
<Button
intent="text"
theme="base"
variant="icon"
onClick={handleClose}
>
<ChevronLeftSmallIcon color="baseTextHighcontrast" />
{intl.formatMessage({ id: "Back" })}
</Button>
<DestinationFilterAndSort
listType={pageType === "city" ? "hotel" : "city"}
/>
</div>
<aside className={styles.sidebar}>{children}</aside> <aside className={styles.sidebar}>{children}</aside>
<DynamicMap <DynamicMap
markers={markers} markers={markers}

View File

@@ -34,7 +34,22 @@
gap: var(--Spacing-x-half); gap: var(--Spacing-x-half);
} }
.mobileNavigation {
display: none;
}
@media screen and (max-width: 949px) { @media screen and (max-width: 949px) {
.dialog {
flex-direction: column;
}
.mobileNavigation {
display: flex;
padding: var(--Spacing-x2);
justify-content: space-between;
background-color: var(--Surface-Primary-OnSurface-Default);
}
.sidebar { .sidebar {
position: absolute; position: absolute;
max-width: none; max-width: none;

View File

@@ -29,3 +29,13 @@
border-color: var(--UI-Input-Controls-Fill-Selected); border-color: var(--UI-Input-Controls-Fill-Selected);
background-color: var(--UI-Input-Controls-Fill-Selected); background-color: var(--UI-Input-Controls-Fill-Selected);
} }
@media screen and (max-width: 767px) {
.checkboxWrapper:hover {
background-color: transparent;
}
.checkboxWrapper[data-selected] {
background-color: transparent;
}
}

View File

@@ -25,3 +25,13 @@
gap: var(--Spacing-x1) var(--Spacing-x2); gap: var(--Spacing-x1) var(--Spacing-x2);
margin: var(--Spacing-x3) 0; margin: var(--Spacing-x3) 0;
} }
@media screen and (max-width: 767px) {
.list {
grid-template-columns: 1fr;
}
.list label {
padding-left: 0;
}
}

View File

@@ -41,7 +41,7 @@ export default function Filter({ filters }: FilterProps) {
</Subtitle> </Subtitle>
<ul className={styles.list}> <ul className={styles.list}>
{facilityFilters.map((filter) => ( {facilityFilters.map((filter) => (
<li key={`filter-${filter.slug}`} className={styles.item}> <li key={`filter-${filter.slug}`}>
<Checkbox <Checkbox
name={filter.name} name={filter.name}
value={filter.slug} value={filter.slug}
@@ -59,7 +59,7 @@ export default function Filter({ filters }: FilterProps) {
</Subtitle> </Subtitle>
<ul className={styles.list}> <ul className={styles.list}>
{surroundingsFilters.map((filter) => ( {surroundingsFilters.map((filter) => (
<li key={`filter-${filter.slug}`} className={styles.item}> <li key={`filter-${filter.slug}`}>
<Checkbox <Checkbox
name={filter.name} name={filter.name}
value={filter.slug} value={filter.slug}

View File

@@ -8,6 +8,7 @@
align-items: center; align-items: center;
z-index: var(--default-modal-overlay-z-index); z-index: var(--default-modal-overlay-z-index);
} }
.dialog { .dialog {
width: min(80dvw, 960px); width: min(80dvw, 960px);
border-radius: var(--Corner-radius-Large); border-radius: var(--Corner-radius-Large);
@@ -65,3 +66,53 @@
padding: var(--Spacing-x2) var(--Spacing-x4); padding: var(--Spacing-x2) var(--Spacing-x4);
border-top: 1px solid var(--Base-Border-Subtle); border-top: 1px solid var(--Base-Border-Subtle);
} }
.close {
background: none;
border: none;
cursor: pointer;
padding: 0;
}
@media screen and (max-width: 767px) {
.overlay {
height: var(--visual-viewport-height);
}
.dialog {
display: flex;
flex-direction: column;
height: 100dvh;
width: 100vw;
border-radius: 0;
}
.header {
display: flex;
justify-content: flex-end;
border-bottom: none;
padding: var(--Spacing-x3) var(--Spacing-x2);
}
.title,
.divider {
display: none;
}
.content {
height: 100%;
padding: 0 var(--Spacing-x2) var(--Spacing-x3);
overflow-y: scroll;
}
.alertWrapper:not(:empty) {
padding: var(--Spacing-x3) var(--Spacing-x2) 0;
}
.footer {
flex-direction: column-reverse;
gap: var(--Spacing-x3);
padding: var(--Spacing-x3) var(--Spacing-x2);
margin-top: auto;
}
}

View File

@@ -23,26 +23,20 @@ import Sort from "./Sort"
import styles from "./destinationFilterAndSort.module.css" import styles from "./destinationFilterAndSort.module.css"
import type {
CategorizedFilters,
SortItem,
} from "@/types/components/destinationFilterAndSort"
import { AlertTypeEnum } from "@/types/enums/alert" import { AlertTypeEnum } from "@/types/enums/alert"
interface HotelFilterAndSortProps { interface HotelFilterAndSortProps {
filters: CategorizedFilters
sortItems: SortItem[]
listType: "city" | "hotel" listType: "city" | "hotel"
} }
export default function DestinationFilterAndSort({ export default function DestinationFilterAndSort({
filters,
sortItems,
listType, listType,
}: HotelFilterAndSortProps) { }: HotelFilterAndSortProps) {
const intl = useIntl() const intl = useIntl()
const router = useRouter() const router = useRouter()
const { const {
filters,
sortItems,
pendingFilters, pendingFilters,
pendingSort, pendingSort,
defaultSort, defaultSort,
@@ -53,6 +47,8 @@ export default function DestinationFilterAndSort({
resetPendingValues, resetPendingValues,
setIsLoading, setIsLoading,
} = useDestinationDataStore((state) => ({ } = useDestinationDataStore((state) => ({
filters: state.allFilters,
sortItems: state.sortItems,
pendingFilters: state.pendingFilters, pendingFilters: state.pendingFilters,
pendingSort: state.pendingSort, pendingSort: state.pendingSort,
basePath: state.basePathnameWithoutFilters, basePath: state.basePathnameWithoutFilters,
@@ -142,13 +138,17 @@ export default function DestinationFilterAndSort({
> >
<h3>{intl.formatMessage({ id: "Filter and sort" })}</h3> <h3>{intl.formatMessage({ id: "Filter and sort" })}</h3>
</Subtitle> </Subtitle>
<Button onClick={close} variant="icon" intent="tertiary"> <button
onClick={close}
type="button"
className={styles.close}
>
<CloseLargeIcon /> <CloseLargeIcon />
</Button> </button>
</header> </header>
<div className={styles.content}> <div className={styles.content}>
<Sort sortItems={sortItems} /> <Sort sortItems={sortItems} />
<Divider color="subtle" /> <Divider color="subtle" className={styles.divider} />
<Filter filters={filters} /> <Filter filters={filters} />
</div> </div>
{pendingCount === 0 && ( {pendingCount === 0 && (
@@ -171,7 +171,7 @@ export default function DestinationFilterAndSort({
{intl.formatMessage({ id: "Clear all filters" })} {intl.formatMessage({ id: "Clear all filters" })}
</Button> </Button>
<Button <Button
intent="tertiary" intent="primary"
size="large" size="large"
theme="base" theme="base"
disabled={pendingCount === 0} disabled={pendingCount === 0}
@@ -179,9 +179,7 @@ export default function DestinationFilterAndSort({
> >
{intl.formatMessage( {intl.formatMessage(
{ id: "See results ({ count })" }, { id: "See results ({ count })" },
{ { count: pendingCount }
count: pendingCount,
}
)} )}
</Button> </Button>
</footer> </footer>