diff --git a/frontend/e2e/pages/map.ts b/frontend/e2e/pages/map.ts index 53587642..d9204da5 100644 --- a/frontend/e2e/pages/map.ts +++ b/frontend/e2e/pages/map.ts @@ -113,4 +113,6 @@ export const map_page = async (page: Page) => { const showResultsBtn = page.getByRole('button', { name: 'Show Results' }) await expect(showResultsBtn).toBeVisible() await showResultsBtn.click() + + await page.getByTitle('Close').click({ force: true }) } diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 9e9bd586..623fdf0f 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,6 +1,5 @@ -import { useEffect } from 'react' import { BrowserRouter } from 'react-router-dom' -import { useDispatch, useSelector } from 'react-redux' +import { useSelector } from 'react-redux' import { Box, CircularProgress, Stack } from '@mui/material' import Header from '@/components/Header' diff --git a/frontend/src/assets/svgs/fa-chevron-up.svg b/frontend/src/assets/svgs/fa-chevron-up.svg new file mode 100644 index 00000000..a80b778d --- /dev/null +++ b/frontend/src/assets/svgs/fa-chevron-up.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/src/components/DataLayersCheckboxGroup.css b/frontend/src/components/DataLayersCheckboxGroup.css new file mode 100644 index 00000000..2a399377 --- /dev/null +++ b/frontend/src/components/DataLayersCheckboxGroup.css @@ -0,0 +1,46 @@ +.data-layers-checkbox-group { + gap: 24px; +} + +.data-layers-checkbox-group.data-layers-checkbox-group--small { + gap: 32px; + scrollbar-width: none; +} + +.data-layers-top-section { + gap: 8px; + padding: 0 8px; +} + +.data-layers-checkbox-group--small .data-layers-top-section { + padding: 0; +} + +.data-layers-top-text, +.data-layers-top-link { + color: var(--typography-color-disabled); + font-size: 14px; + font-style: italic; +} + +.data-layers-top-link, +.data-layers-top-link:hover { + text-decoration: underline; +} + +.data-layers-type-text { + color: var(--typography-color-primary); + font-weight: 700; +} + +.available-layers-row { + display: flex; + justify-content: space-between; + align-items: center; + height: 28px; + flex: 0 0 28px; +} + +.available-layers-text { + color: var(--typography-color-primary); +} diff --git a/frontend/src/components/DataLayersCheckboxGroup.test.tsx b/frontend/src/components/DataLayersCheckboxGroup.test.tsx new file mode 100644 index 00000000..394cd955 --- /dev/null +++ b/frontend/src/components/DataLayersCheckboxGroup.test.tsx @@ -0,0 +1,114 @@ +import { screen } from '@testing-library/react' + +import { DataLayersCheckboxGroup } from './DataLayersCheckboxGroup' +import { render } from '@/test-utils' +import { useDataLayers, useHasDataLayersOn } from '@/features/map/map-slice' +import { DataLayer } from '@/interfaces/data-layers' +import { DATA_LAYER_GROUPS } from '@/constants/data-layers' + +interface State { + dataLayers?: DataLayer[] + hasDataLayersOn?: boolean +} + +describe('Test suite for DataLayersCheckboxGroup', () => { + function renderComponent( + isSmall = false, + className: string | undefined = undefined, + ) { + const state: State = {} + const TestComponent = () => { + Object.assign(state, { + dataLayers: useDataLayers(), + hasDataLayersOn: useHasDataLayersOn(), + }) + return + } + const { user } = render(, { + withStateProvider: true, + }) + return { user, state } + } + + it('should render large DataLayersCheckboxGroup', async () => { + const { user, state } = renderComponent(false, 'test-class') + screen.getByText(/^All data layers sourced from GeoBC/) + expect(state.dataLayers).toHaveLength(0) + expect(state.hasDataLayersOn).toBe(false) + + const div = screen.getByTestId('data-layers-checkbox-group') + expect(div).not.toHaveClass('data-layers-checkbox-group--small') + expect(div).toHaveClass('test-class') + + DATA_LAYER_GROUPS.forEach(({ name, layers }) => { + screen.getByRole('button', { name, pressed: true }) + layers.forEach((layer) => { + const cb = screen.getByLabelText(layer.name) + expect(cb).not.toBeChecked() + if (layer.url) { + expect(cb).toBeEnabled() + } else { + expect(cb).toBeDisabled() + } + }) + }) + + expect(screen.queryByText('Available Layers')).not.toBeInTheDocument() + expect(screen.queryByText('Reset')).not.toBeInTheDocument() + expect(screen.queryByText('Reset Layers')).not.toBeInTheDocument() + + const toggle = screen.getByRole('button', { + name: 'Aquifers and Water Wells', + pressed: true, + }) + // should be expanded by default + screen.getByText('Aquifers - All') + + await user.click(toggle) + + // Should be collapsed now + screen.getByRole('button', { + name: 'Aquifers and Water Wells', + pressed: false, + }) + expect(screen.queryByText('Aquifers - All')).not.toBeInTheDocument() + + const cb = screen.getByLabelText('FWA Wetlands') + expect(cb).not.toBeChecked() + + await user.click(cb) + expect(cb).toBeChecked() + expect(state.dataLayers).toHaveLength(1) + expect(state.hasDataLayersOn).toBe(true) + + const resetBtn = screen.getByRole('button', { name: 'Reset Layers' }) + await user.click(resetBtn) + + expect(state.dataLayers).toHaveLength(0) + expect(state.hasDataLayersOn).toBe(false) + expect(screen.queryByText('Reset Layers')).not.toBeInTheDocument() + }) + + it('should render small DataLayersCheckboxGroup', async () => { + const { user, state } = renderComponent(true) + screen.getByText(/^All data layers sourced from GeoBC/) + expect(state.dataLayers).toHaveLength(0) + + const div = screen.getByTestId('data-layers-checkbox-group') + expect(div).toHaveClass('data-layers-checkbox-group--small') + + const cb = screen.getByLabelText('Water Rights - Licences') + expect(cb).not.toBeChecked() + + await user.click(cb) + expect(cb).toBeChecked() + expect(state.dataLayers).toHaveLength(1) + expect(state.hasDataLayersOn).toBe(true) + + const resetLink = screen.getByRole('button', { name: 'Reset' }) + await user.click(resetLink) + + expect(state.dataLayers).toHaveLength(0) + expect(screen.queryByText('Reset Layers')).not.toBeInTheDocument() + }) +}) diff --git a/frontend/src/components/DataLayersCheckboxGroup.tsx b/frontend/src/components/DataLayersCheckboxGroup.tsx new file mode 100644 index 00000000..65130038 --- /dev/null +++ b/frontend/src/components/DataLayersCheckboxGroup.tsx @@ -0,0 +1,101 @@ +import React from 'react' +import { useDispatch } from 'react-redux' +import { Button, Stack, Typography } from '@mui/material' +import clsx from 'clsx' + +import { DATA_LAYER_GROUPS } from '@/constants/data-layers' +import { + resetDataLayers, + toggleDataLayer, + useHasDataLayersOn, +} from '@/features/map/map-slice' +import { DataLayer, DataLayerGroup } from '@/interfaces/data-layers' +import { DataLayersToggleGroup } from './DataLayersToggleGroup' + +import './DataLayersCheckboxGroup.css' + +interface Props { + isSmall?: boolean + className?: string +} + +export function DataLayersCheckboxGroup({ + isSmall = false, + className, +}: Readonly) { + const dispatch = useDispatch() + const isLarge = !isSmall + const hasDataLayers = useHasDataLayersOn() + + const onLayerToggle = (layer: DataLayer) => { + dispatch(toggleDataLayer(layer)) + } + + const onReset = () => { + dispatch(resetDataLayers()) + } + + const isGuidanceReady = false + + return ( + + + + All data layers sourced from GeoBC. + + {isGuidanceReady && ( + + Click here to read our guidance page about map layers. + + )} + + {isSmall && ( +
+ + Available Layers + + {hasDataLayers && ( + + )} +
+ )} + {DATA_LAYER_GROUPS.map((group: DataLayerGroup) => ( + + ))} + {isLarge && hasDataLayers && ( + + )} +
+ ) +} diff --git a/frontend/src/components/DataLayersToggleGroup.css b/frontend/src/components/DataLayersToggleGroup.css new file mode 100644 index 00000000..f375c883 --- /dev/null +++ b/frontend/src/components/DataLayersToggleGroup.css @@ -0,0 +1,82 @@ +button.data-layers-toggle-button { + justify-content: space-between; + color: var(--typography-color-primary); + font-size: 16px; + font-weight: 700; + padding: 0 8px; +} + +button.data-layers-toggle-button .MuiButton-endIcon { + margin: 0 4px +} + +.data-layers-toggle-group--small button.data-layers-toggle-button { + font-weight: 400; + padding: 0; +} + +button.data-layers-reset-button { + align-self: flex-start; + color: var(--typography-color-primary); + border-color: var(--surface-color-border-dark); +} + +button.data-layers-reset-link { + color: var(--typography-color-link); + font-size: 18px; + line-height: 28px; + padding: 4px; +} + +.data-layers-expand-arrow { + transform: rotate(0deg); + transition: transform 0.2s linear; +} + +.data-layers-expand-arrow--invert { + transform: rotate(180deg); +} + +.data-layers-checkbox-list { + gap: 2px; + padding-left: 4px; +} + +.data-layers-toggle-group--small .data-layers-checkbox-list { + gap: 8px; + padding-left: 0; +} + +label.data-layers-checkbox-item { + display: flex; + align-items: center; + gap: 8px; + margin: 0; + padding: 0; + min-height: 40px; + color: var(--typography-color-secondary); + font-size: 14px; + font-weight: 400; +} + +label.data-layers-checkbox-item:hover { + color: var(--typography-color-primary); +} + +label.data-layers-checkbox-item .MuiCheckbox-root { + padding: 8px; +} + +.data-layers-toggle-group--small label.data-layers-checkbox-item { + flex-direction: row-reverse; + padding: 0 16px; + color: var(--typography-color-primary); + background-color: var(--surface-color-brand-gray-20); + border: 1px solid var(--surface-color-border-medium); + border-radius: 4px; +} + +label.data-layers-checkbox-item .MuiFormControlLabel-label { + flex: 1; + text-align: left; +} diff --git a/frontend/src/components/DataLayersToggleGroup.tsx b/frontend/src/components/DataLayersToggleGroup.tsx new file mode 100644 index 00000000..340710ab --- /dev/null +++ b/frontend/src/components/DataLayersToggleGroup.tsx @@ -0,0 +1,80 @@ +import React, { useState } from 'react' +import { + Button, + Checkbox, + Collapse, + FormControlLabel, + FormGroup, + Stack, +} from '@mui/material' +import clsx from 'clsx' + +import { useIsDataLayerOn } from '@/features/map/map-slice' +import { DataLayer, DataLayerGroup } from '@/interfaces/data-layers' + +import DownArrow from '@/assets/svgs/fa-caret-down.svg?react' + +import './DataLayersToggleGroup.css' + +interface Props { + group: DataLayerGroup + onLayerToggle: (layer: DataLayer) => void + isSmall: boolean +} + +export function DataLayersToggleGroup({ + group, + onLayerToggle, + isSmall, +}: Readonly) { + const [expanded, setExpanded] = useState(true) + const isDataLayerChecked = useIsDataLayerOn() + + const onToggle = () => { + setExpanded(!expanded) + } + + return ( + + + + + {group.layers.map((layer: DataLayer) => ( + } + checked={isDataLayerChecked(layer)} + label={layer.name} + disabled={!layer.url} + className="data-layers-checkbox-item" + onChange={() => onLayerToggle(layer)} + /> + ))} + + + + ) +} diff --git a/frontend/src/components/DropdownButton.css b/frontend/src/components/DropdownButton.css index 29390afa..8996fc2a 100644 --- a/frontend/src/components/DropdownButton.css +++ b/frontend/src/components/DropdownButton.css @@ -4,6 +4,21 @@ border: 1px solid var(--surface-color-border-light); background: var(--surface-color-background-white); box-shadow: var(--box-shadow-small); + scrollbar-width: thin; + scrollbar-color: #4e4e4e transparent; +} + +.dropdown-button-menu .MuiPaper-root::-webkit-scrollbar { + width: 8px; +} + +.dropdown-button-menu .MuiPaper-root::-webkit-scrollbar-thumb { + background: #4e4e4e; + border-radius: 8px; +} + +.dropdown-button-menu .MuiPaper-root::-webkit-scrollbar-thumb:hover { + background: #555; } .dropdown-button-menu .MuiList-root { diff --git a/frontend/src/components/DropdownButton.tsx b/frontend/src/components/DropdownButton.tsx index ac305c97..ef7b3810 100644 --- a/frontend/src/components/DropdownButton.tsx +++ b/frontend/src/components/DropdownButton.tsx @@ -22,6 +22,7 @@ interface ButtonDropdownProps extends ButtonProps { openClassName?: string menuClassName?: string menuProps?: Partial + horizontalAlign?: 'left' | 'right' [key: string]: any } @@ -35,6 +36,7 @@ export function DropdownButton({ openClassName, menuClassName, menuProps, + horizontalAlign = 'left', ...rest }: Readonly) { const [anchorEl, setAnchorEl] = useState(null) @@ -77,11 +79,11 @@ export function DropdownButton({ ) { +export function FilterByCheckboxGroup({ + isSmall = false, + className, + ...props +}: Readonly) { const dispatch = useDispatch() const filters = useSelector(selectFilters) @@ -43,11 +44,36 @@ export function FilterByCheckboxGroup(props: Readonly) { const showResetButton = flatFilters.some((f) => f.on) return ( - <> - +
+ {isSmall && ( +
+ + Facility Type + + {showResetButton && ( + + )} +
+ )} + {flatFilters.map((filter: OmrrFilter) => ( } checked={filter.on} label={filter.label} @@ -56,16 +82,15 @@ export function FilterByCheckboxGroup(props: Readonly) { /> ))} - {showResetButton && ( + {!isSmall && showResetButton && ( )} - +
) } diff --git a/frontend/src/components/SearchByRadioGroup.tsx b/frontend/src/components/SearchByRadioGroup.tsx index 93bc62e8..641aabf7 100644 --- a/frontend/src/components/SearchByRadioGroup.tsx +++ b/frontend/src/components/SearchByRadioGroup.tsx @@ -9,6 +9,7 @@ import { SEARCH_BY_INACTIVE, } from '@/interfaces/types' import { setSearchBy, useSearchBy } from '@/features/omrr/omrr-slice' +import clsx from 'clsx' interface Props extends Omit { defaultValue?: string @@ -18,6 +19,7 @@ export function SearchByRadioGroup({ defaultValue = SEARCH_BY_ALL, onChange, row = true, + className, sx, ...rest }: Readonly) { @@ -37,6 +39,7 @@ export function SearchByRadioGroup({ return ( { state.selectedItem = undefined }, + toggleDataLayer: (state, action: PayloadAction) => { + const layer = action.payload + const newLayers = [...state.dataLayers] + // Layers are cloned - need to compare names + const index = newLayers.findIndex( + (dl) => dl === layer || dl.name === layer.name, + ) + if (index === -1) { + newLayers.push(layer) + } else { + newLayers.splice(index, 1) + } + state.dataLayers = newLayers + }, + resetDataLayers: (state) => { + state.dataLayers = [] + }, + setBottomDrawerContent: ( + state, + action: PayloadAction, + ) => { + state.bottomDrawerContentType = action.payload + state.isDrawerExpanded = Boolean(action.payload) + }, }, }) @@ -71,6 +104,9 @@ export const { setSidebarWidth, setSelectedItem, clearSelectedItem, + toggleDataLayer, + resetDataLayers, + setBottomDrawerContent, } = mapSlice.actions // Selectors @@ -89,3 +125,23 @@ export const useSidebarWidth = () => useSelector(selectSidebarWidth) const selectSelectedItem = (state: RootState) => state.map.selectedItem export const useSelectedItem = () => useSelector(selectSelectedItem) + +const selectDataLayers = (state: RootState) => state.map.dataLayers +export const useDataLayers = () => useSelector(selectDataLayers) +export const useIsDataLayerOn = () => { + const dataLayers = useSelector(selectDataLayers) + return useCallback( + (layer: DataLayer) => { + return Boolean( + dataLayers.find((dl) => dl === layer || dl.name === layer.name), + ) + }, + [dataLayers], + ) +} +export const useHasDataLayersOn = () => useSelector(selectDataLayers).length > 0 + +const selectBottomDrawerContentType = (state: RootState) => + state.map.bottomDrawerContentType +export const useBottomDrawerContentType = () => + useSelector(selectBottomDrawerContentType) diff --git a/frontend/src/index.css b/frontend/src/index.css index 9c35acea..fcdef8e4 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -42,6 +42,24 @@ html, body { flex: 1 1 100%; } +.thin-scrollbar { + scrollbar-width: thin; + scrollbar-color: #4e4e4e transparent; +} + +.thin-scrollbar::-webkit-scrollbar { + width: 8px; +} + +.thin-scrollbar::-webkit-scrollbar-thumb { + background: #4e4e4e; + border-radius: 8px; +} + +.thin-scrollbar::-webkit-scrollbar-thumb:hover { + background: #555; +} + button.MuiButton-sizeMedium { display: flex; align-items: center; diff --git a/frontend/src/interfaces/data-layers.ts b/frontend/src/interfaces/data-layers.ts new file mode 100644 index 00000000..259596f3 --- /dev/null +++ b/frontend/src/interfaces/data-layers.ts @@ -0,0 +1,10 @@ +import { WMSTileLayerProps } from 'react-leaflet/lib/WMSTileLayer' + +export interface DataLayer extends WMSTileLayerProps { + name: string +} + +export interface DataLayerGroup { + name: string + layers: DataLayer[] +} diff --git a/frontend/src/pages/authorizationList/AuthorizationList.css b/frontend/src/pages/authorizationList/AuthorizationList.css index 0bd0c784..0f125605 100644 --- a/frontend/src/pages/authorizationList/AuthorizationList.css +++ b/frontend/src/pages/authorizationList/AuthorizationList.css @@ -23,6 +23,11 @@ } .list-search-checkbox-group { + margin-bottom: 16px; +} + +.list-search-checkbox-group .filter-by-checkbox-form-group { + flex-direction: row; gap: 12px 0; } diff --git a/frontend/src/pages/authorizationList/ListSearchSection.tsx b/frontend/src/pages/authorizationList/ListSearchSection.tsx index 1e16e117..d3c7306d 100644 --- a/frontend/src/pages/authorizationList/ListSearchSection.tsx +++ b/frontend/src/pages/authorizationList/ListSearchSection.tsx @@ -61,10 +61,7 @@ export function ListSearchSection() { }, }} > - + )} diff --git a/frontend/src/pages/dashboard/LearnMoreCard.tsx b/frontend/src/pages/dashboard/LearnMoreCard.tsx index 5e0ce963..e2ae65eb 100644 --- a/frontend/src/pages/dashboard/LearnMoreCard.tsx +++ b/frontend/src/pages/dashboard/LearnMoreCard.tsx @@ -70,7 +70,7 @@ export function LearnMoreCard({ icon, actions, divider = true, -}: Props) { +}: Readonly) { const titleElement = ( {title} diff --git a/frontend/src/pages/map/MapView.test.tsx b/frontend/src/pages/map/MapView.test.tsx index 5c11d3ee..259a2ace 100644 --- a/frontend/src/pages/map/MapView.test.tsx +++ b/frontend/src/pages/map/MapView.test.tsx @@ -2,7 +2,8 @@ import React from 'react' import { screen } from '@testing-library/react' import { render } from '@/test-utils' -import { initialState } from '@/features/omrr/omrr-slice' +import { initialState as initialOmrrState } from '@/features/omrr/omrr-slice' +import { initialState as initialMapState } from '@/features/map/map-slice' import { mockOmrrData } from '@/mocks/mock-omrr-data' import { themeBreakpointValues } from '@/theme' import MapView from './MapView' @@ -15,16 +16,14 @@ describe('Test suite for MapView', () => { withRouter: true, initialState: { omrr: { - ...initialState, + ...initialOmrrState, allResults: mockOmrrData, searchByFilteredResults: mockOmrrData, filteredResults: mockOmrrData, status: 'succeeded', }, map: { - isMyLocationVisible: false, - isDrawerExpanded: false, - sidebarWidth: 0, + ...initialMapState, }, }, }) @@ -44,18 +43,17 @@ describe('Test suite for MapView', () => { }) it('should render the MapView with no markers on a small screen', async () => { - render(, { + const { user } = render(, { screenWidth: themeBreakpointValues.sm - 10, withStateProvider: true, initialState: { omrr: { - ...initialState, + ...initialOmrrState, status: 'succeeded', }, map: { + ...initialMapState, isMyLocationVisible: true, - isDrawerExpanded: false, - sidebarWidth: 0, }, }, }) @@ -65,9 +63,49 @@ describe('Test suite for MapView', () => { const markers = screen.queryAllByAltText('Authorization marker') expect(markers).toHaveLength(0) - screen.getByTitle('Show the data layers') + const dataLayers = screen.getByTitle('Show the data layers') const findMeControl = screen.getByTitle('Show my location on the map') expect(findMeControl).toHaveClass('map-control-button--active') await screen.findByTitle('My location marker') + + expect(dataLayers).toBeEnabled() + expect(dataLayers).not.toHaveClass('map-control-button--active') + await user.click(dataLayers) + + expect(dataLayers).toHaveClass('map-control-button--active') + await screen.findByText('Data Layers') + screen.getByText('Available Layers') + const closeBtn = screen.getByTitle('Close') + await user.click(closeBtn) + expect(screen.queryByText('Available Layers')).not.toBeInTheDocument() + + const searchBy = screen.getByRole('button', { name: 'Search By' }) + expect(searchBy).not.toHaveClass('map-button--active') + await user.click(searchBy) + + expect(searchBy).toHaveClass('map-button--active') + const activeCb = screen.getByLabelText('Active') + expect(activeCb).toBeChecked() + const inactiveCb = screen.getByLabelText('Inactive') + expect(inactiveCb).not.toBeChecked() + + const filterBy = screen.getByRole('button', { + name: 'Filter by Facility Type', + }) + expect(filterBy).not.toHaveClass('map-button--active') + await user.click(filterBy) + + expect(filterBy).toHaveClass('map-button--active') + screen.getByText('Facility Type') + const opCertCb = screen.getByLabelText('Operational Certificate') + + expect(screen.queryByText('Reset')).not.toBeInTheDocument() + expect(screen.queryByText('Reset Filters')).not.toBeInTheDocument() + + await user.click(opCertCb) + + const resetLink = screen.getByRole('button', { name: 'Reset' }) + await user.click(resetLink) + expect(screen.queryByText('Reset')).not.toBeInTheDocument() }) }) diff --git a/frontend/src/pages/map/MapView.tsx b/frontend/src/pages/map/MapView.tsx index 4d818bbe..5077e6db 100644 --- a/frontend/src/pages/map/MapView.tsx +++ b/frontend/src/pages/map/MapView.tsx @@ -9,6 +9,7 @@ import { MapDrawer } from './drawer/MapDrawer' import { AuthorizationMarkers } from './layers/AuthorizationMarkers' import { MyLocationMarker } from './layers/MyLocationMarker' import { MapControls } from './layers/MapControls' +import { MapDataLayers } from './layers/MapDataLayers' import { MapZoom } from './layers/MapZoom' import 'leaflet/dist/leaflet.css' @@ -39,6 +40,7 @@ function MapView() { attribution='© OpenStreetMap contributors' url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png" /> + diff --git a/frontend/src/pages/map/drawer/MapBottomDrawer.css b/frontend/src/pages/map/drawer/MapBottomDrawer.css index 3f5b86cb..33b79c32 100644 --- a/frontend/src/pages/map/drawer/MapBottomDrawer.css +++ b/frontend/src/pages/map/drawer/MapBottomDrawer.css @@ -18,6 +18,10 @@ height: 320px; } +.map-bottom-drawer--expanded.map-bottom-drawer--searchBy { + height: 160px; +} + .map-bottom-drawer.map-bottom-drawer--full-height { height: calc(100% - var(--header-height)); } @@ -47,9 +51,15 @@ button.map-bottom-drawer-button { color: var(--typography-color-primary); } -svg.map-bottom-drawer-icon { - width: 28px; - height: 28px; +.map-bottom-drawer-icon { + width: 20px; + height: 20px; + transform: rotate(0deg); + transition: transform 0.2s linear; +} + +.map-bottom-drawer-icon--invert { + transform: rotate(180deg); } .map-bottom-close-icon { @@ -74,6 +84,12 @@ svg.map-bottom-drawer-icon { justify-content: flex-start; align-items: stretch; overflow: hidden; - padding: 24px 16px 40px; + padding: 24px 16px 0 16px; gap: 24px; +} + +.map-bottom-drawer-container .data-layers-checkbox-group, +.map-bottom-drawer-container .filter-by-checkbox-group { + overflow: hidden auto; + padding-bottom: 40px; } \ No newline at end of file diff --git a/frontend/src/pages/map/drawer/MapBottomDrawer.tsx b/frontend/src/pages/map/drawer/MapBottomDrawer.tsx index fb04d4f2..f936283b 100644 --- a/frontend/src/pages/map/drawer/MapBottomDrawer.tsx +++ b/frontend/src/pages/map/drawer/MapBottomDrawer.tsx @@ -1,16 +1,38 @@ -import { ReactNode, useEffect, useState } from 'react' +import { useEffect, useState } from 'react' import clsx from 'clsx' import { IconButton } from '@mui/material' -import { KeyboardArrowDown, KeyboardArrowUp } from '@mui/icons-material' +import { DataLayersCheckboxGroup } from '@/components/DataLayersCheckboxGroup' +import { BottomDrawerContentEnum } from '@/constants/constants' import { useLastSearchTime } from '@/features/omrr/omrr-slice' +import { useBottomDrawerContentType } from '@/features/map/map-slice' +import { SearchResultsList } from './SearchResultsList' +import ChevonUpIcon from '@/assets/svgs/fa-chevron-up.svg?react' import CloseIcon from '@/assets/svgs/close.svg?react' import './MapBottomDrawer.css' +import { SearchByRadioGroup } from '@/components/SearchByRadioGroup' +import { FilterByCheckboxGroup } from '@/components/FilterByCheckboxGroup' + +function getTitle(contentType: BottomDrawerContentEnum | undefined): string { + switch (contentType) { + case BottomDrawerContentEnum.dataLayers: + return 'Data Layers' + case BottomDrawerContentEnum.polygonSearch: + return 'Polygon Search' + case BottomDrawerContentEnum.pointSearch: + return 'Point Search' + case BottomDrawerContentEnum.searchBy: + return 'Search By' + case BottomDrawerContentEnum.filterBy: + return 'Filter by Facility Type' + default: + return 'Search Results' + } +} interface Props { - children?: ReactNode isExpanded: boolean setExpanded: (expanded: boolean) => void } @@ -21,15 +43,20 @@ interface Props { * When isExpanded is false - it is hidden. * It can be toggled to be full height. */ -export function MapBottomDrawer({ - children, - isExpanded, - setExpanded, -}: Readonly) { +export function MapBottomDrawer({ isExpanded, setExpanded }: Readonly) { const [fullHeight, setFullHeight] = useState(false) const lastSearchTime = useLastSearchTime() + const contentType = useBottomDrawerContentType() + const isSearchResultsVisible = !contentType + const isDataLayersVisible = contentType === BottomDrawerContentEnum.dataLayers + // const isPointSearchVisible = + // contentType === BottomDrawerContentEnum.pointSearch + // const isPolygonSearchVisible = + // contentType === BottomDrawerContentEnum.polygonSearch + const isSearchByVisible = contentType === BottomDrawerContentEnum.searchBy + const isFilterByVisible = contentType === BottomDrawerContentEnum.filterBy - // Expand when the search changes + // Expand when the search changes ??? useEffect(() => {}, [lastSearchTime]) const onClose = () => { @@ -37,16 +64,13 @@ export function MapBottomDrawer({ setFullHeight(false) } - // Single selected item or search results - const title = 'Search Results' - // Title will change for other modes: - // 'Polygon Search', 'Point Search', 'Search By', 'Filter by Facility Type' return (
@@ -56,14 +80,15 @@ export function MapBottomDrawer({ className="map-bottom-drawer-button" title={fullHeight ? 'Collapse' : 'Expand'} > - {fullHeight ? ( - - ) : ( - - )} + -
{title}
+
{getTitle(contentType)}
- {isExpanded && children} + {isExpanded && ( + <> + {isDataLayersVisible && } + {isSearchByVisible && } + {isFilterByVisible && } + {/* Add more contents like point/polygon search */} + {isSearchResultsVisible && ( + + )} + + )}
) diff --git a/frontend/src/pages/map/drawer/MapDrawer.tsx b/frontend/src/pages/map/drawer/MapDrawer.tsx index 11817a62..4b1e46dd 100644 --- a/frontend/src/pages/map/drawer/MapDrawer.tsx +++ b/frontend/src/pages/map/drawer/MapDrawer.tsx @@ -2,11 +2,14 @@ import { useDispatch } from 'react-redux' import { useTheme } from '@mui/material/styles' import useMediaQuery from '@mui/material/useMediaQuery' -import { setDrawerExpanded } from '@/features/map/map-slice' +import { + setBottomDrawerContent, + setDrawerExpanded, +} from '@/features/map/map-slice' import { useMapDrawerState } from '../hooks/useMapDrawerState' import { MapBottomDrawer } from './MapBottomDrawer' import { MapSidebar } from './MapSidebar' -import { SearchResultsList } from './SearchResultsList' +import { useCalculateSidebarWidth } from '../hooks/useCalculateSidebarWidth' import './MapDrawer.css' @@ -16,6 +19,9 @@ export function MapDrawer() { // If the screen width is small or medium - shown drawer below const isSmallScreen = useMediaQuery(theme.breakpoints.down('lg')) const { isVisible, isDrawerExpanded } = useMapDrawerState(isSmallScreen) + // This hook calculates the sidebar width based on screen size + // and sets the sidebarWidth state which other components use + useCalculateSidebarWidth() if (!isVisible) { return null @@ -23,23 +29,14 @@ export function MapDrawer() { const setExpanded = (expanded: boolean) => { dispatch(setDrawerExpanded(expanded)) + if (isSmallScreen && !expanded) { + dispatch(setBottomDrawerContent(undefined)) + } } - return ( - <> - {isSmallScreen ? ( - - - {/* Add more contents like point/polygon search */} - - ) : ( - - - - )} - + return isSmallScreen ? ( + + ) : ( + ) } diff --git a/frontend/src/pages/map/drawer/MapSidebar.tsx b/frontend/src/pages/map/drawer/MapSidebar.tsx index e59a6acc..19598091 100644 --- a/frontend/src/pages/map/drawer/MapSidebar.tsx +++ b/frontend/src/pages/map/drawer/MapSidebar.tsx @@ -1,9 +1,9 @@ -import { MouseEvent, ReactNode } from 'react' +import { MouseEvent } from 'react' import clsx from 'clsx' import { IconButton } from '@mui/material' -import { useSelectedItem } from '@/features/map/map-slice' -import { useCalculateSidebarWidth } from '../hooks/useCalculateSidebarWidth' +import { useSelectedItem, useSidebarWidth } from '@/features/map/map-slice' +import { SearchResultsList } from './SearchResultsList' import { SidebarToggleButton } from './SidebarToggleButton' import { ZoomToButton } from './ZoomToButton' @@ -12,20 +12,13 @@ import CloseIcon from '@/assets/svgs/close.svg?react' import './MapSidebar.css' interface Props { - children?: ReactNode isExpanded: boolean setExpanded: (expanded: boolean, ev: MouseEvent) => void } -export function MapSidebar({ - children, - isExpanded, - setExpanded, -}: Readonly) { +export function MapSidebar({ isExpanded, setExpanded }: Readonly) { const selectedItem = useSelectedItem() - // This hook calculates the sidebar width based on screen size - // It also updates the sidebarWidth state which other components use - const expandedWidth = useCalculateSidebarWidth() + const expandedWidth = useSidebarWidth() const onClose = (ev: MouseEvent) => { setExpanded(false, ev) @@ -59,7 +52,7 @@ export function MapSidebar({ - {children} + {isExpanded ? 'Hide Results' : 'Show Results'} diff --git a/frontend/src/pages/map/drawer/SearchResultsList.css b/frontend/src/pages/map/drawer/SearchResultsList.css index badb51ca..32bd4b0f 100644 --- a/frontend/src/pages/map/drawer/SearchResultsList.css +++ b/frontend/src/pages/map/drawer/SearchResultsList.css @@ -5,24 +5,9 @@ justify-content: flex-start; align-items: stretch; gap: 24px; - padding: 0; + padding: 0 0 24px 0; margin: 0; overflow: auto; - scrollbar-width: thin; - scrollbar-color: #4e4e4e transparent; -} - -.search-results-list::-webkit-scrollbar { - width: 8px; -} - -.search-results-list::-webkit-scrollbar-thumb { - background: #4e4e4e; - border-radius: 8px; -} - -.search-results-list::-webkit-scrollbar-thumb:hover { - background: #555; } .search-results-list--no-scrollbars { diff --git a/frontend/src/pages/map/drawer/SearchResultsList.tsx b/frontend/src/pages/map/drawer/SearchResultsList.tsx index 8b81d503..81728589 100644 --- a/frontend/src/pages/map/drawer/SearchResultsList.tsx +++ b/frontend/src/pages/map/drawer/SearchResultsList.tsx @@ -34,7 +34,10 @@ export function SearchResultsList({ const itemRenderer = (item: OmrrData) => ( diff --git a/frontend/src/pages/map/hooks/useMapDrawerState.tsx b/frontend/src/pages/map/hooks/useMapDrawerState.tsx index d97cb5e9..bb20a0d3 100644 --- a/frontend/src/pages/map/hooks/useMapDrawerState.tsx +++ b/frontend/src/pages/map/hooks/useMapDrawerState.tsx @@ -8,6 +8,7 @@ import { import { clearSelectedItem, setDrawerExpanded, + useBottomDrawerContentType, useDrawerExpanded, useSelectedItem, } from '@/features/map/map-slice' @@ -27,19 +28,18 @@ export function useMapDrawerState(isSmallScreen: boolean) { const isDrawerExpanded = useDrawerExpanded() const selectedItem = useSelectedItem() const lastSearchTime = useLastSearchTime() + const contentType = useBottomDrawerContentType() const [visible, setVisible] = useState(false) // Keep track of whether the drawer has been expanded for the first time const initialExpansionRef = useRef(false) + const hasSelectedItem = Boolean(selectedItem) + const hasContentType = Boolean(contentType) && isSmallScreen useEffect(() => { - // Show the drawer whenever there are filtered results - // or a single item is selected - setVisible(!allResultsShowing || Boolean(selectedItem)) - // For small screens also show the drawer when other displays like - // point/polygon tool, search by, or filter by is active - // if (isSmallScreen) { - // } - }, [allResultsShowing, selectedItem]) + // Show the drawer whenever there are filtered results, a single item is selected + // or on small screens when the point/polygon tools or search by/filter by are active + setVisible(!allResultsShowing || hasSelectedItem || hasContentType) + }, [allResultsShowing, hasSelectedItem, hasContentType]) // When the drawer is made visible, expand it // On small screens always expand when the search results change diff --git a/frontend/src/pages/map/layers/DataLayersControl.tsx b/frontend/src/pages/map/layers/DataLayersControl.tsx index 26d88f91..9b92d35d 100644 --- a/frontend/src/pages/map/layers/DataLayersControl.tsx +++ b/frontend/src/pages/map/layers/DataLayersControl.tsx @@ -1,24 +1,35 @@ +import { useDispatch } from 'react-redux' import { IconButton } from '@mui/material' -import { useTheme } from '@mui/material/styles' -import useMediaQuery from '@mui/material/useMediaQuery' +import clsx from 'clsx' + +import { + setBottomDrawerContent, + useBottomDrawerContentType, +} from '@/features/map/map-slice' +import { BottomDrawerContentEnum } from '@/constants/constants' import LayersIcon from '@/assets/svgs/fa-layers.svg?react' export function DataLayersControl() { - const theme = useTheme() - const isLarge = useMediaQuery(theme.breakpoints.up('lg')) + const dispatch = useDispatch() + const isActive = + useBottomDrawerContentType() === BottomDrawerContentEnum.dataLayers - if (isLarge) { - return null + const onClick = () => { + dispatch( + setBottomDrawerContent( + isActive ? undefined : BottomDrawerContentEnum.dataLayers, + ), + ) } - const onClick = () => {} - return ( diff --git a/frontend/src/pages/map/layers/FindMeControl.tsx b/frontend/src/pages/map/layers/FindMeControl.tsx index 05e4757f..1cd7b0d1 100644 --- a/frontend/src/pages/map/layers/FindMeControl.tsx +++ b/frontend/src/pages/map/layers/FindMeControl.tsx @@ -1,7 +1,5 @@ import { useDispatch } from 'react-redux' import { IconButton } from '@mui/material' -import { useTheme } from '@mui/material/styles' -import useMediaQuery from '@mui/material/useMediaQuery' import clsx from 'clsx' import { @@ -14,12 +12,6 @@ import GpsIcon from '@/assets/svgs/fa-gps.svg?react' export function FindMeControl() { const dispatch = useDispatch() const isMarkerVisible = useMyLocationVisible() - const theme = useTheme() - const isLarge = useMediaQuery(theme.breakpoints.up('lg')) - - if (isLarge) { - return null - } const onClick = () => { dispatch(setMyLocationVisible(!isMarkerVisible)) diff --git a/frontend/src/pages/map/layers/MapControls.tsx b/frontend/src/pages/map/layers/MapControls.tsx index c57d2e7c..76e9ce90 100644 --- a/frontend/src/pages/map/layers/MapControls.tsx +++ b/frontend/src/pages/map/layers/MapControls.tsx @@ -2,8 +2,21 @@ import { useMemo } from 'react' import { useTheme } from '@mui/material/styles' import useMediaQuery from '@mui/material/useMediaQuery' -import { MAP_BOTTOM_DRAWER_HEIGHT } from '@/constants/constants' -import { useDrawerExpanded, useSidebarWidth } from '@/features/map/map-slice' +import { + BottomDrawerContentEnum, + MAP_BOTTOM_DRAWER_HEIGHT, + MAP_BOTTOM_DRAWER_HEIGHT_SEARCH_BY, + MAP_CONTROLS_BOTTOM_LG, + MAP_CONTROLS_BOTTOM_SM, + MAP_CONTROLS_RIGHT_LG, + MAP_CONTROLS_RIGHT_SM, + MAP_CONTROLS_RIGHT_XL, +} from '@/constants/constants' +import { + useBottomDrawerContentType, + useDrawerExpanded, + useSidebarWidth, +} from '@/features/map/map-slice' import { DataLayersControl } from './DataLayersControl' import { FindMeControl } from './FindMeControl' import { ZoomInOutControl } from './ZoomInOutControl' @@ -14,28 +27,53 @@ import './MapControls.css' export function MapControls() { const theme = useTheme() const isSmall = useMediaQuery(theme.breakpoints.down('sm')) - const isMedium = useMediaQuery(theme.breakpoints.down('md')) + const isMedium = useMediaQuery(theme.breakpoints.down('lg')) + const isLarge = useMediaQuery(theme.breakpoints.down('xl')) const sidebarWidth = useSidebarWidth() const isExpanded = useDrawerExpanded() + const contentType = useBottomDrawerContentType() + const isSearchBy = contentType === BottomDrawerContentEnum.searchBy - const style = useMemo(() => { - if (sidebarWidth > 0 && isExpanded) { - // Sidebar is expanded - shift controls left - return { - marginRight: `${sidebarWidth + 10}px`, - } - } else if (isMedium && !isSmall && isExpanded) { - // medium devices only - shift the controls up 320px - return { - marginBottom: `${MAP_BOTTOM_DRAWER_HEIGHT + 10}px`, - } + // Shift the controls based on screen size and whether the sidebar or bottom drawer is expanded + let right = MAP_CONTROLS_RIGHT_XL + if (isSmall) { + right = MAP_CONTROLS_RIGHT_SM + } else if (isLarge) { + right = MAP_CONTROLS_RIGHT_LG + } + let bottom = MAP_CONTROLS_BOTTOM_LG + if (isSmall) { + bottom = MAP_CONTROLS_BOTTOM_SM + } + + if (sidebarWidth > 0 && isExpanded) { + // Sidebar is expanded - shift controls left + right = sidebarWidth + right + } else if (isMedium && !isSmall && isExpanded) { + // medium devices only - shift the controls up bottom drawer height + let height = MAP_BOTTOM_DRAWER_HEIGHT + if (isSearchBy) { + height = MAP_BOTTOM_DRAWER_HEIGHT_SEARCH_BY + bottom = height + bottom } - }, [sidebarWidth, isExpanded, isSmall, isMedium]) + } + + const style = useMemo( + () => ({ + marginRight: `${right}px`, + marginBottom: `${bottom}px`, + }), + [right, bottom], + ) return ( - - + {isMedium && ( + <> + + + + )} ) diff --git a/frontend/src/pages/map/layers/MapDataLayers.tsx b/frontend/src/pages/map/layers/MapDataLayers.tsx new file mode 100644 index 00000000..9c7f3921 --- /dev/null +++ b/frontend/src/pages/map/layers/MapDataLayers.tsx @@ -0,0 +1,34 @@ +import { WMSTileLayer } from 'react-leaflet' + +import { useDataLayers } from '@/features/map/map-slice' +import { DataLayer } from '@/interfaces/data-layers' + +export function MapDataLayers() { + const dataLayers = useDataLayers() + + if (!dataLayers || dataLayers.length === 0) { + return null + } + + return dataLayers.map((layer: DataLayer) => { + const { + name, + url, + layers, + styles, + format = 'image/png', + transparent = true, + } = layer + const key = `DataLayer-${name}` + return ( + + ) + }) +} diff --git a/frontend/src/pages/map/search/DataLayersButton.tsx b/frontend/src/pages/map/search/DataLayersButton.tsx index c36586cf..d5b306f6 100644 --- a/frontend/src/pages/map/search/DataLayersButton.tsx +++ b/frontend/src/pages/map/search/DataLayersButton.tsx @@ -1,5 +1,6 @@ import DropdownButton from '@/components/DropdownButton' import { useDrawerExpanded } from '@/features/map/map-slice' +import { DataLayersCheckboxGroup } from '@/components/DataLayersCheckboxGroup' import layersIcon from '@/assets/svgs/fa-layers.svg' @@ -13,9 +14,11 @@ export function DataLayersButton() { variant="contained" size="large" color="secondary" - className="map-button map-button--large data-layers-button" + className="map-button map-button--large" + menuClassName="data-layers-button-menu" startIcon={Data layers icon} - disabled + dropdownContent={} + horizontalAlign="right" > Data Layers diff --git a/frontend/src/pages/map/search/FilterByButton.tsx b/frontend/src/pages/map/search/FilterByButton.tsx index 08d06b06..6cd701bb 100644 --- a/frontend/src/pages/map/search/FilterByButton.tsx +++ b/frontend/src/pages/map/search/FilterByButton.tsx @@ -1,28 +1,63 @@ import clsx from 'clsx' -import { useTheme } from '@mui/material/styles' -import useMediaQuery from '@mui/material/useMediaQuery' import DropdownButton from '@/components/DropdownButton' import { FilterByCheckboxGroup } from '@/components/FilterByCheckboxGroup' +import { Button } from '@mui/material' +import { useDispatch } from 'react-redux' +import { + setBottomDrawerContent, + useBottomDrawerContentType, +} from '@/features/map/map-slice' +import { BottomDrawerContentEnum } from '@/constants/constants' -export function FilterByButton() { - const theme = useTheme() - const isLarge = useMediaQuery(theme.breakpoints.up('lg')) +interface Props { + isLarge: boolean +} + +export function FilterByButton({ isLarge }: Readonly) { + const dispatch = useDispatch() + // Bottom drawer only + const isActive = + useBottomDrawerContentType() === BottomDrawerContentEnum.filterBy + + const onClick = () => { + dispatch( + setBottomDrawerContent( + isActive ? undefined : BottomDrawerContentEnum.filterBy, + ), + ) + } + + if (isLarge) { + return ( + + } + > + Filter by Facility Type + + ) + } return ( - - } - showArrow={isLarge} + className={clsx( + 'map-button map-button--medium', + isActive && 'map-button--active', + )} + onClick={onClick} > Filter by Facility Type - + ) } diff --git a/frontend/src/pages/map/search/MapSearch.css b/frontend/src/pages/map/search/MapSearch.css index da630732..d3d68fc8 100644 --- a/frontend/src/pages/map/search/MapSearch.css +++ b/frontend/src/pages/map/search/MapSearch.css @@ -74,7 +74,6 @@ button.map-button--active:hover { border-color: var(--surface-color-primary-active-border); } - .data-layers-icon, .find-me-icon { color: var(--typography-color-secondary); } @@ -82,3 +81,9 @@ button.map-button--active:hover { .polygon-search-icon, .point-search-icon { color: var(--typography-color-primary); } + +.dropdown-button-menu.data-layers-button-menu .MuiPaper-root { + /* Fit remaining vertical space */ + max-height: calc(100vh - 265px); + max-width: 413px; +} \ No newline at end of file diff --git a/frontend/src/pages/map/search/MapSearch.tsx b/frontend/src/pages/map/search/MapSearch.tsx index 760549f8..9332e502 100644 --- a/frontend/src/pages/map/search/MapSearch.tsx +++ b/frontend/src/pages/map/search/MapSearch.tsx @@ -13,20 +13,26 @@ import { SearchAutocomplete } from './SearchAutocomplete' import { SearchButton } from './SearchButton' import './MapSearch.css' +import { + MAP_CONTROLS_RIGHT_LG, + MAP_CONTROLS_RIGHT_SM, + MAP_CONTROLS_RIGHT_XL, +} from '@/constants/constants' const styles = { marginTop: { md: '40px', }, left: { - xl: '72px', - lg: '48px', - md: '72px', xs: '24px', + md: '72px', + lg: '48px', + xl: '72px', }, right: { - xl: '72px', - lg: '48px', + xs: `${MAP_CONTROLS_RIGHT_SM}px`, + lg: `${MAP_CONTROLS_RIGHT_LG}px`, + xl: `${MAP_CONTROLS_RIGHT_XL}px`, }, flexWrap: { md: 'wrap', @@ -54,8 +60,8 @@ export function MapSearch() { {isLarge || isSmall ? null :
} - - + + ) diff --git a/frontend/src/pages/map/search/SearchAutocomplete.test.tsx b/frontend/src/pages/map/search/SearchAutocomplete.test.tsx index 13bb0097..328eb68f 100644 --- a/frontend/src/pages/map/search/SearchAutocomplete.test.tsx +++ b/frontend/src/pages/map/search/SearchAutocomplete.test.tsx @@ -2,7 +2,8 @@ import React from 'react' import { screen } from '@testing-library/react' import { render } from '@/test-utils' -import { initialState } from '@/features/omrr/omrr-slice' +import { initialState as initialOmrrState } from '@/features/omrr/omrr-slice' +import { initialState as initialMapState } from '@/features/map/map-slice' import { mockOmrrData } from '@/mocks/mock-omrr-data' import { SEARCH_BY_ALL } from '@/interfaces/types' import { SearchAutocomplete } from './SearchAutocomplete' @@ -13,7 +14,7 @@ describe('Test suite for SearchAutocomplete', () => { withStateProvider: true, initialState: { omrr: { - ...initialState, + ...initialOmrrState, // Use all results searchBy: SEARCH_BY_ALL, allResults: mockOmrrData, @@ -22,9 +23,7 @@ describe('Test suite for SearchAutocomplete', () => { status: 'succeeded', }, map: { - isDrawerExpanded: false, - isMyLocationVisible: false, - sidebarWidth: 0, + ...initialMapState, }, }, }) diff --git a/frontend/src/pages/map/search/SearchByButton.tsx b/frontend/src/pages/map/search/SearchByButton.tsx index 7fc53cae..6bb83236 100644 --- a/frontend/src/pages/map/search/SearchByButton.tsx +++ b/frontend/src/pages/map/search/SearchByButton.tsx @@ -1,16 +1,40 @@ +import { useDispatch } from 'react-redux' import clsx from 'clsx' -import { useTheme } from '@mui/material/styles' -import useMediaQuery from '@mui/material/useMediaQuery' +import { Button } from '@mui/material' -import { useSearchBy } from '@/features/omrr/omrr-slice' import DropdownButton from '@/components/DropdownButton' import { SearchByRadioGroup } from '@/components/SearchByRadioGroup' +import { BottomDrawerContentEnum } from '@/constants/constants' +import { useSearchBy } from '@/features/omrr/omrr-slice' +import { + setBottomDrawerContent, + useBottomDrawerContentType, +} from '@/features/map/map-slice' + +interface Props { + isLarge: boolean +} -export function SearchByButton() { +export function SearchByButton({ isLarge }: Readonly) { + const dispatch = useDispatch() const searchBy = useSearchBy() - const theme = useTheme() - const isLarge = useMediaQuery(theme.breakpoints.up('lg')) + // Bottom drawer only + const isActive = + useBottomDrawerContentType() === BottomDrawerContentEnum.searchBy + const onClick = () => { + dispatch( + setBottomDrawerContent( + isActive ? undefined : BottomDrawerContentEnum.searchBy, + ), + ) + } + + const label = ( + + Search By: {searchBy} + + ) const content = ( ) + if (isLarge) { + return ( + + {label} + + ) + } + return ( - - - Search By: {searchBy} - - + {label} + ) }