From 08305717a37330c843749356e5d30e127e671095 Mon Sep 17 00:00:00 2001 From: Philipp Veller Date: Fri, 3 Jan 2025 11:02:15 +0100 Subject: [PATCH] projects: add address search, zoom style and autocomplete component --- .../scss/components_user_facing/_alert.scss | 4 +- .../scss/components_user_facing/_leaflet.scss | 5 + .../components_user_facing/_multi-select.scss | 45 +++- .../_projects-list.scss | 16 +- .../components_user_facing/_projects_map.scss | 4 + .../_projects_map_info.scss | 33 +++ .../_projects_map_search.scss | 5 + .../components_user_facing/_typeahead.scss | 1 + meinberlin/assets/scss/style_user_facing.scss | 3 + .../assets/scss/styles_user_facing/_base.scss | 5 + meinberlin/config/settings/base.py | 2 +- .../react/contrib/forms/AutoComplete.jsx | 123 +++++++++ .../react/contrib/forms/MultiSelect.jsx | 171 ++---------- .../forms/__tests__/AutoComplete.jest.jsx | 218 +++++++++++++++ .../forms/__tests__/MultiSelect.jest.jsx | 252 +++++++----------- .../forms/__tests__/useCombobox.jest.js | 233 ++++++++++++++++ meinberlin/react/contrib/forms/useCombobox.js | 177 ++++++++++++ meinberlin/react/contrib/useDebounce.js | 32 +++ meinberlin/react/projects/ProjectMarker.jsx | 2 +- meinberlin/react/projects/ProjectsMap.jsx | 13 +- meinberlin/react/projects/ProjectsMapInfo.jsx | 37 +++ .../react/projects/ProjectsMapSearch.jsx | 98 +++++++ package.json | 1 + 23 files changed, 1158 insertions(+), 322 deletions(-) create mode 100644 meinberlin/assets/scss/components_user_facing/_projects_map_info.scss create mode 100644 meinberlin/assets/scss/components_user_facing/_projects_map_search.scss create mode 100644 meinberlin/assets/scss/styles_user_facing/_base.scss create mode 100644 meinberlin/react/contrib/forms/AutoComplete.jsx create mode 100644 meinberlin/react/contrib/forms/__tests__/AutoComplete.jest.jsx create mode 100644 meinberlin/react/contrib/forms/__tests__/useCombobox.jest.js create mode 100644 meinberlin/react/contrib/forms/useCombobox.js create mode 100644 meinberlin/react/contrib/useDebounce.js create mode 100644 meinberlin/react/projects/ProjectsMapInfo.jsx create mode 100644 meinberlin/react/projects/ProjectsMapSearch.jsx diff --git a/meinberlin/assets/scss/components_user_facing/_alert.scss b/meinberlin/assets/scss/components_user_facing/_alert.scss index f91e51ea74..77a1772067 100644 --- a/meinberlin/assets/scss/components_user_facing/_alert.scss +++ b/meinberlin/assets/scss/components_user_facing/_alert.scss @@ -4,7 +4,7 @@ background-color: $message-light-blue; } -.alert__content { +.a4-alert__content { padding: 1.125rem 2rem 1.125rem 1.125rem; margin: 0 auto; max-width: 81rem; @@ -50,4 +50,4 @@ .alert--small { padding: 0.5em; -} \ No newline at end of file +} diff --git a/meinberlin/assets/scss/components_user_facing/_leaflet.scss b/meinberlin/assets/scss/components_user_facing/_leaflet.scss index c38dad63a5..1713286faa 100644 --- a/meinberlin/assets/scss/components_user_facing/_leaflet.scss +++ b/meinberlin/assets/scss/components_user_facing/_leaflet.scss @@ -32,3 +32,8 @@ font-size: $font-size-sm; } } + +.leaflet-control.leaflet-bar.leaflet-control-zoom { + border: 2px solid $black; + border-radius: 0; +} diff --git a/meinberlin/assets/scss/components_user_facing/_multi-select.scss b/meinberlin/assets/scss/components_user_facing/_multi-select.scss index 0003bd949f..fe3233816b 100644 --- a/meinberlin/assets/scss/components_user_facing/_multi-select.scss +++ b/meinberlin/assets/scss/components_user_facing/_multi-select.scss @@ -4,18 +4,51 @@ .form-label { font-weight: bold; } + + &:not(.multi-select--autocomplete) { + .multi-select__combobox { + background-image: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA1MTIgNTEyIj48cGF0aCBkPSJNMjMzLjQgNDA2LjZjMTIuNSAxMi41IDMyLjggMTIuNSA0NS4zIDBsMTkyLTE5MmMxMi41LTEyLjUgMTIuNS0zMi44IDAtNDUuM3MtMzIuOC0xMi41LTQ1LjMgMEwyNTYgMzM4LjcgODYuNiAxNjkuNGMtMTIuNS0xMi41LTMyLjgtMTIuNS00NS4zIDBzLTEyLjUgMzIuOCAwIDQ1LjNsMTkyIDE5MnoiLz48L3N2Zz4=") !important; + background-repeat: no-repeat; + background-position: right 0.5em center; + background-size: 1em; + padding-right: 2em; + } + } } -.multi-select__combobox { + +input.multi-select__combobox { + border: 0; line-height: normal; - background-image: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA1MTIgNTEyIj48cGF0aCBkPSJNMjMzLjQgNDA2LjZjMTIuNSAxMi41IDMyLjggMTIuNSA0NS4zIDBsMTkyLTE5MmMxMi41LTEyLjUgMTIuNS0zMi44IDAtNDUuM3MtMzIuOC0xMi41LTQ1LjMgMEwyNTYgMzM4LjcgODYuNiAxNjkuNGMtMTIuNS0xMi41LTMyLjgtMTIuNS00NS4zIDBzLTEyLjUgMzIuOCAwIDQ1LjNsMTkyIDE5MnoiLz48L3N2Zz4=") !important; - background-repeat: no-repeat; - background-position: right 0.5em center; - background-size: 1em; - padding-right: 2em; text-overflow: ellipsis; white-space: nowrap; overflow: hidden; + min-height: 0; + padding: 0; + + &:focus { + outline: 0; + border: 0; + box-shadow: none; + } +} + +.multi-select__input-wrapper { + display: flex; + align-items: center; +} + +.multi-select__input-wrapper:has(input:focus) { + outline: 2px solid Highlight; + outline: 5px auto -webkit-focus-ring-color; +} + +.multi-select__before { + margin-right: 1em; +} + +.multi-select__after { + margin-left: 1em; } .multi-select__container, diff --git a/meinberlin/assets/scss/components_user_facing/_projects-list.scss b/meinberlin/assets/scss/components_user_facing/_projects-list.scss index 1758622804..a7c2bc1e19 100644 --- a/meinberlin/assets/scss/components_user_facing/_projects-list.scss +++ b/meinberlin/assets/scss/components_user_facing/_projects-list.scss @@ -44,11 +44,9 @@ } .projects-list__wrapper--combined { - height: 600px; - @media screen and (min-width: $breakpoint-tablet) { - height: 1100px; display: flex; + position: relative; } .projects-list__list { @@ -56,7 +54,16 @@ @media screen and (min-width: $breakpoint-tablet) { display: block; - overflow: auto; + } + } + + .projects-list__map { + height: 600px; + + @media screen and (min-width: $breakpoint-tablet) { + position: sticky; + top: 65px; + height: 1100px; } } @@ -65,7 +72,6 @@ flex: 1; } - .projects-list__map, .modul-geomap, .geomap-main, .geomap-container, diff --git a/meinberlin/assets/scss/components_user_facing/_projects_map.scss b/meinberlin/assets/scss/components_user_facing/_projects_map.scss index ee2bad4baf..b9433de336 100644 --- a/meinberlin/assets/scss/components_user_facing/_projects_map.scss +++ b/meinberlin/assets/scss/components_user_facing/_projects_map.scss @@ -1,6 +1,7 @@ .projects-map { height: 100%; margin: 0 -12px; + overflow: hidden; @media screen and (min-width: $breakpoint-tablet) { margin: 0; @@ -57,3 +58,6 @@ } } +.projects-map__search.leaflet-control { + z-index: 1001; +} diff --git a/meinberlin/assets/scss/components_user_facing/_projects_map_info.scss b/meinberlin/assets/scss/components_user_facing/_projects_map_info.scss new file mode 100644 index 0000000000..ac03070675 --- /dev/null +++ b/meinberlin/assets/scss/components_user_facing/_projects_map_info.scss @@ -0,0 +1,33 @@ +.projects-map-info { + font-size: $font-size-base; +} + +.projects-map-info__wrapper { + margin: 10px; + bottom: 2em; + display: none; + + .alert { + margin: 0; + } + + .container, + .a4-alert__content { + margin: 0; + width: auto; + } + + @media screen and (min-width: $breakpoint-tablet) { + display: block; + } +} + +.projects-map-info--mobile { + @media screen and (min-width: $breakpoint-tablet) { + display: none; + } + + .alert { + margin: 0; + } +} diff --git a/meinberlin/assets/scss/components_user_facing/_projects_map_search.scss b/meinberlin/assets/scss/components_user_facing/_projects_map_search.scss new file mode 100644 index 0000000000..e325652ee0 --- /dev/null +++ b/meinberlin/assets/scss/components_user_facing/_projects_map_search.scss @@ -0,0 +1,5 @@ +.projects-map-search { + form { + margin-bottom: 0; + } +} diff --git a/meinberlin/assets/scss/components_user_facing/_typeahead.scss b/meinberlin/assets/scss/components_user_facing/_typeahead.scss index 3319018444..4ca5238d68 100644 --- a/meinberlin/assets/scss/components_user_facing/_typeahead.scss +++ b/meinberlin/assets/scss/components_user_facing/_typeahead.scss @@ -13,6 +13,7 @@ .rbt-menu .dropdown-item { color: $text-base; text-decoration: none; + display: block; } .rbt-input-multi .rbt-input-wrapper { diff --git a/meinberlin/assets/scss/style_user_facing.scss b/meinberlin/assets/scss/style_user_facing.scss index 1a54af1cb9..dcb5f868d2 100644 --- a/meinberlin/assets/scss/style_user_facing.scss +++ b/meinberlin/assets/scss/style_user_facing.scss @@ -19,6 +19,7 @@ @import "styles_user_facing/form"; @import "styles_user_facing/utility"; +@import "styles_user_facing/base"; @import "components_user_facing/accordion-list"; @import "components_user_facing/alert"; @@ -52,6 +53,8 @@ @import "components_user_facing/icon_switch"; @import "components_user_facing/projects-list"; @import "components_user_facing/projects_map"; +@import "components_user_facing/projects_map_search"; +@import "components_user_facing/projects_map_info"; @import "components_user_facing/leaflet"; @import "components_user_facing/project-tile"; @import "components_user_facing/status-bar"; diff --git a/meinberlin/assets/scss/styles_user_facing/_base.scss b/meinberlin/assets/scss/styles_user_facing/_base.scss new file mode 100644 index 0000000000..9b43d7939f --- /dev/null +++ b/meinberlin/assets/scss/styles_user_facing/_base.scss @@ -0,0 +1,5 @@ +// Using overflow: hidden (as used in the styleguide) causes position: sticky +// to not work as expected. See https://www.terluinwebdesign.nl/en/blog/position-sticky-not-working-try-overflow-clip-not-overflow-hidden/ +body { + overflow: clip; +} diff --git a/meinberlin/config/settings/base.py b/meinberlin/config/settings/base.py index 465999ea1b..018747a95b 100644 --- a/meinberlin/config/settings/base.py +++ b/meinberlin/config/settings/base.py @@ -497,7 +497,7 @@ A4_MAP_BASEURL = "https://basemap.berlin.de/gdz_basemapde_vektor/styles/bm_web_col.json" A4_OPENMAPTILES_TOKEN = "9aVUrssbx7PKNUKo3WtXY6MqETI6Q336u5D142QS" A4_MAPBOX_TOKEN = "" - +A4_MAP_ATTRIBUTION = "© 2024 basemap.de / BKG | Datenquellen: © GeoBasis-DE" A4_PROJECT_TOPICS = "meinberlin.apps.contrib.enums.TopicEnum" A4_BLUEPRINT_TYPES = [ diff --git a/meinberlin/react/contrib/forms/AutoComplete.jsx b/meinberlin/react/contrib/forms/AutoComplete.jsx new file mode 100644 index 0000000000..b2581d163e --- /dev/null +++ b/meinberlin/react/contrib/forms/AutoComplete.jsx @@ -0,0 +1,123 @@ +import React, { useState } from 'react' +import { classNames } from '../helpers' +import useCombobox from './useCombobox' + +/* + * Returns the item at the given index in an array, wrapping around + * if index is out of bounds. + */ +export const getLoopedIndex = (array, index) => { + const length = array.length + const wrappedIndex = ((index % length) + length) % length + return array[wrappedIndex] +} + +const defaultFilterFn = (choice, text) => choice.name.toLowerCase().includes(text.toLowerCase()) + +/* + Choice formatting looks like: + { name: 'Swedish', value: 'sv' }, + { name: 'English', value: 'en' } + Values need to be unique! + */ +export const AutoComplete = ({ + label, + className, + liClassName, + comboboxClassName, + choices, + hideLabel, + onChangeInput, + filterFn, + placeholder, + before, + after, + ...comboboxProps +}) => { + const { + opened, + labelId, + activeItems, + listboxAttrs, + comboboxAttrs, + getChoicesAttr + } = useCombobox({ + choices, + ...comboboxProps, + isAutoComplete: true + }) + const [text, setText] = useState('') + + const classes = classNames( + 'form-control input__element multi-select__container', + opened && 'multi-select__container--opened', + className + ) + const comboboxClasses = classNames( + 'form-control multi-select__combobox', + comboboxClassName + ) + + const actualFilterFn = filterFn || defaultFilterFn + const filteredChoices = text !== '' ? choices.filter(choice => actualFilterFn(choice, text)) : choices + + const onChangeHandler = (e) => { + setText(e.target.value) + onChangeInput?.(e.target.value) + } + + return ( +
+

+ {label} +

+
+ {before &&
{before}
} + {comboboxProps.isMultiple && ( +
+ {activeItems.map((choice) => choice.name).join(', ')} +
+ )} + + {after &&
{after}
} +
+ {filteredChoices.length > 0 && ( +