Skip to content

Commit

Permalink
fix(sts): add station search in STS menu (#1210)
Browse files Browse the repository at this point in the history
* fix(STS): add SearchBar to STS menu

* chore: fix css

* chore: improve CSS, add test

* chore: update italian translations

* chore: remove context provider code

* chore: extract SearchInput into component

* chore: fix submit btn css

* chore: readd scrollbar to seach suggestion container

* chore: fix permalink cy test
  • Loading branch information
danji90 authored Feb 7, 2025
1 parent efc5133 commit a7ffbb0
Show file tree
Hide file tree
Showing 16 changed files with 576 additions and 455 deletions.
4 changes: 3 additions & 1 deletion cypress/e2e/permalink.cy.js
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,9 @@ describe("permalink", () => {
),
);
cy.url().should("match", /lang=de/);
cy.url().should("match", /layers=&/);
cy.url().should("match", /[?&]layers=(&|$)/);
// cy.url().then((url) => console.log(url));

// eslint-disable-next-line prefer-regex-literals
cy.url().should("match", new RegExp("x=928460&y=5908948&z=8.5"));
});
Expand Down
1 change: 1 addition & 0 deletions src/WebComponent.scss
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
@import "./components/Menu/MenuItemHeader.scss";
@import "./components/Search/Search.scss";
@import "./components/Search/SearchToggle.scss";
@import "./components/Search/SearchInput.scss";
@import "./components/Share/Share.scss";
@import "./components/TopicMenu/TopicMenu.scss";
@import "./components/TopicsMenu/TopicsMenu.scss";
Expand Down
3 changes: 2 additions & 1 deletion src/components/MapControls/MapControls.js
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ function MapControls({
const dispatch = useDispatch();
const overlayWidth = useOverlayWidth();
const screenHeight = useSelector((state) => state.app.screenHeight);
const activeTopic = useSelector((state) => state.app.activeTopic);
const isSmallHeight = useMemo(() => {
return ["xs", "s"].includes(screenHeight);
}, [screenHeight]);
Expand Down Expand Up @@ -153,7 +154,7 @@ function MapControls({
className={`wkp-map-controls ${classes.mapControls}`}
data-testid="map-controls-wrapper"
>
{menuToggler && <MenuToggler />}
{menuToggler && (activeTopic?.menuToggler ?? <MenuToggler />)}
<Zoom
map={map}
zoomInChildren={<ZoomIn />}
Expand Down
13 changes: 10 additions & 3 deletions src/components/MapControls/MenuToggler/MenuToggler.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import React from "react";
import { useDispatch, useSelector } from "react-redux";
import { makeStyles } from "@mui/styles";
import { useTranslation } from "react-i18next";
import PropTypes from "prop-types";
import MapButton from "../../MapButton";
import { ReactComponent as MenuOpen } from "../../../img/sbb/040_hamburgermenu_102_36.svg";
import { ReactComponent as MenuClosed } from "../../../img/sbb/040_schliessen_104_36.svg";
Expand All @@ -11,21 +12,27 @@ const useStyles = makeStyles({
displayMenuToggler: { padding: "8px" },
});

function MenuToggler() {
function MenuToggler({ children, ...props }) {
const classes = useStyles();
const { t } = useTranslation();
const displayMenu = useSelector((state) => state.app.displayMenu);
const dispatch = useDispatch();

return (
<MapButton
className={`wkp-display-menu-toggler ${classes.displayMenuToggler}`}
onClick={() => dispatch(setDisplayMenu(!displayMenu))}
title={t("Menü")}
title={displayMenu ? t("Schliessen") : t("Menü")}
data-testid="map-controls-menu-toggler"
{...props}
>
{displayMenu ? <MenuClosed /> : <MenuOpen />}
{children ?? (displayMenu ? <MenuClosed /> : <MenuOpen />)}
</MapButton>
);
}

MenuToggler.propTypes = {
children: PropTypes.node,
};

export default MenuToggler;
241 changes: 4 additions & 237 deletions src/components/Search/Search.js
Original file line number Diff line number Diff line change
@@ -1,249 +1,16 @@
/* eslint-disable react/jsx-props-no-spreading */
import React, { useEffect, useState, useRef } from "react";
import { useSelector, useDispatch } from "react-redux";
import Autosuggest from "react-autosuggest";
import { FaSearch, FaAngleDown, FaAngleUp } from "react-icons/fa";
import { useTranslation } from "react-i18next";
import { IconButton, Typography } from "@mui/material";
import { setFeatureInfo, setSearchOpen } from "../../model/app/actions";
import useHasScreenSize from "../../utils/useHasScreenSize";
import React, { useRef } from "react";
import SearchInput from "./SearchInput";
import SearchToggle from "./SearchToggle";

import "./Search.scss";
import CloseButton from "../CloseButton";
import { trackEvent } from "../../utils/trackingUtils";

const mobileMapPadding = [50, 50, 50, 50];

function Search() {
const [suggestions, setSuggestions] = useState([]);
const [value, setValue] = useState("");
const map = useSelector((state) => state.app.map);
const featureInfo = useSelector((state) => state.app.featureInfo);
const searchService = useSelector((state) => state.app.searchService);
const activeTopic = useSelector((state) => state.app.activeTopic);
const isMobile = useHasScreenSize();
const searchContainerRef = useRef();
const { t } = useTranslation();
const dispatch = useDispatch();

useEffect(() => {
if (!searchService) {
return;
}
searchService.setUpsert((section, items, position) => {
setSuggestions((oldSuggestions) => {
const index = oldSuggestions.findIndex((s) => s.section === section);
const start = index === -1 ? position : index;
const deleteCount = index === -1 ? 0 : 1;
const newSuggestions = [...oldSuggestions];
newSuggestions.splice(start, deleteCount, { section, items });
return newSuggestions;
});
});
}, [searchService, setSuggestions]);

useEffect(
() => searchService && map && searchService.setMap(map),
[searchService, map],
);

if (!searchService || !Object.keys(searchService.searches || []).length) {
return null;
}

return (
<div className="wkp-search">
<div className="wkp-search" ref={searchContainerRef}>
<SearchToggle popupAnchor={searchContainerRef?.current}>
<Autosuggest
multiSection
shouldRenderSuggestions={(val) => val.trim().length > 2}
suggestions={suggestions}
onSuggestionsFetchRequested={(evt) => {
searchService.search(evt.value);
}}
onSuggestionsClearRequested={() => {
setSuggestions([]);
}}
onSuggestionHighlighted={({ suggestion }) =>
searchService.highlight(suggestion)
}
onSuggestionSelected={(e, thing) => {
const { suggestion } = thing;
trackEvent(
{
eventType: "action",
componentName: "search result",
label: searchService.value(suggestion),
variant: suggestion.section,
location: t(activeTopic?.name, { lng: "de" }),
eventName: e.type,
},
activeTopic,
);
dispatch(setFeatureInfo());
searchService.select(
suggestion,
isMobile ? mobileMapPadding : undefined,
);
dispatch(setSearchOpen(false));
}}
getSuggestionValue={(suggestion) => searchService.value(suggestion)}
renderSuggestion={(suggestion) => searchService.render(suggestion)}
renderSectionTitle={({ section }) => {
const count = searchService.countItems(section);
return (
count > 0 && (
<div
className="wkp-search-section-opener"
onClick={() => searchService.toggleSection(section)}
onKeyPress={() => searchService.toggleSection(section)}
role="button"
tabIndex={0}
>
<div className="wkp-search-section-header">
<Typography variant="h4" component="span">
{t(section)}:{" "}
</Typography>
<Typography variant="subtitle1" component="span">
{t("overallResult", { count })}
</Typography>
</div>
{searchService.sectionCollapsed(section) ? (
<FaAngleDown focusable={false} />
) : (
<FaAngleUp focusable={false} />
)}
</div>
)
);
}}
getSectionSuggestions={(result) => {
return (
result?.items?.map((i) => ({ ...i, section: result.section })) ||
[]
);
}}
inputProps={{
autoFocus: true,
tabIndex: 0,
"aria-label": t("Suchmaske"),
onChange: (e, { newValue }) => setValue(newValue),
onKeyUp: (e) => {
const { key } = e;
if (key === "Enter") {
trackEvent(
{
eventType: "action",
componentName: "search input",
label: t("Suche starten"),
location: t(activeTopic?.name, { lng: "de" }),
variant: "Suche starten",
},
activeTopic,
);
const filtered = suggestions.filter((s) => s.items.length > 0);
if (filtered.length > 0) {
const { items, section } = filtered[0];
dispatch(setSearchOpen(false));
searchService.select(
{ ...items[0], section },
isMobile ? mobileMapPadding : undefined,
);
}
} else if (key === "ArrowDown" || key === "ArrowUp") {
searchService.highlightSection(); // for improved accessibility
}
},
placeholder: searchService.getPlaceholder(t),
value,
}}
renderInputComponent={(inputProps) => {
return (
<div className="wkp-search-input" ref={searchContainerRef}>
<input {...inputProps} />
{value && (
<CloseButton
tabIndex={0}
title={t("Suchtext löschen")}
className="wkp-search-button wkp-search-button-clear"
onClick={() => {
setValue("");
searchService.clearHighlight();
searchService.clearSelect();
const searchFeatureInfos = searchService.clearPopup();

// We remove the stations feature infos from the current list of feature infos.
if (featureInfo?.length && searchFeatureInfos?.length) {
(searchFeatureInfos || []).forEach(
(searchFeatureInfo) => {
const index = featureInfo?.findIndex((info) => {
return info === searchFeatureInfo;
});
if (index > -1) {
featureInfo.splice(index, 1);
}
},
);
dispatch(setFeatureInfo([...featureInfo]));
}
}}
/>
)}
<IconButton
tabIndex={0}
aria-label={t("Suche starten")}
title={t("Suche starten")}
className="wkp-search-button wkp-search-button-submit"
onClick={() => {
trackEvent(
{
eventType: "action",
componentName: "search button",
label: t("Suche starten"),
location: t(activeTopic?.name, { lng: "de" }),
variant: "Suche starten",
},
activeTopic,
);

if (!value) {
// Hide the search input on small screen
dispatch(setSearchOpen(false));
}

if (searchService.selectItem) {
// Will zoom on the current selected feature
searchService.select(
searchService.selectItem,
isMobile ? mobileMapPadding : undefined,
);
}

// Launch a search
if (value) {
searchService.search(value).then((searchResults) => {
const result = searchResults.find(
(results) => results.items.length > 0,
);
if (result) {
const { items, section } = result;
dispatch(setSearchOpen(false));
searchService.select(
{ ...items[0], section },
isMobile ? mobileMapPadding : undefined,
);
}
});
}
}}
>
<FaSearch focusable={false} />
</IconButton>
</div>
);
}}
/>
<SearchInput />
</SearchToggle>
</div>
);
Expand Down
Loading

0 comments on commit a7ffbb0

Please sign in to comment.