Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[#8572] projects: add address search, zoom style and autocomplete component #5911

Merged
merged 1 commit into from
Jan 14, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions changelog/8527.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
### Changed
- new style for mailings
- removed HTML from mailings when using a text-only client

### Added
- mechanism for adding new attachments to all mailings
12 changes: 7 additions & 5 deletions changelog/8572.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
### Changed
- new style for mailings
- removed HTML from mailings when using a text-only client

### Added
- mechanism for adding new attachments to all mailings
- AutoComplete component with accessible aria combobox pattern
- useDebounce to allow for debounced callbacks that are only called after a certain amount of time has passed
- useCombobox to handle keyboard navigation and selection
- ProjectsMapSearch component to allow for searching by address on the map

### Changed
- moved logic out of MultiSelect into useCombobox hook
4 changes: 2 additions & 2 deletions meinberlin/assets/scss/components_user_facing/_alert.scss
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -50,4 +50,4 @@

.alert--small {
padding: 0.5em;
}
}
5 changes: 5 additions & 0 deletions meinberlin/assets/scss/components_user_facing/_leaflet.scss
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,8 @@
font-size: $font-size-sm;
}
}

.leaflet-control.leaflet-bar.leaflet-control-zoom {
border: 2px solid $black;
border-radius: 0;
}
45 changes: 39 additions & 6 deletions meinberlin/assets/scss/components_user_facing/_multi-select.scss
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
16 changes: 11 additions & 5 deletions meinberlin/assets/scss/components_user_facing/_projects-list.scss
Original file line number Diff line number Diff line change
Expand Up @@ -44,19 +44,26 @@
}

.projects-list__wrapper--combined {
height: 600px;

@media screen and (min-width: $breakpoint-tablet) {
height: 1100px;
display: flex;
position: relative;
}

.projects-list__list {
display: none;

@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;
}
}

Expand All @@ -65,7 +72,6 @@
flex: 1;
}

.projects-list__map,
.modul-geomap,
.geomap-main,
.geomap-container,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
.projects-map {
height: 100%;
margin: 0 -12px;
overflow: hidden;

@media screen and (min-width: $breakpoint-tablet) {
margin: 0;
Expand Down Expand Up @@ -57,3 +58,6 @@
}
}

.projects-map__search.leaflet-control {
z-index: 1001;
}
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
.projects-map-search {
form {
margin-bottom: 0;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
.rbt-menu .dropdown-item {
color: $text-base;
text-decoration: none;
display: block;
}

.rbt-input-multi .rbt-input-wrapper {
Expand Down
3 changes: 3 additions & 0 deletions meinberlin/assets/scss/style_user_facing.scss
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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";
Expand Down
5 changes: 5 additions & 0 deletions meinberlin/assets/scss/styles_user_facing/_base.scss
Original file line number Diff line number Diff line change
@@ -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;
}
2 changes: 1 addition & 1 deletion meinberlin/config/settings/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [
Expand Down
123 changes: 123 additions & 0 deletions meinberlin/react/contrib/forms/AutoComplete.jsx
Original file line number Diff line number Diff line change
@@ -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 (
<div className="form-group multi-select multi-select--autocomplete">
<p id={labelId} className={classNames('label', hideLabel && 'aural')}>
{label}
</p>
<div className="multi-select__input-wrapper form-control">
{before && <div className="multi-select__before">{before}</div>}
{comboboxProps.isMultiple && (
<div className="multi-select__selected">
{activeItems.map((choice) => choice.name).join(', ')}
</div>
)}
<input
type="text"
value={text}
onChange={onChangeHandler}
placeholder={placeholder}
className={comboboxClasses}
{...comboboxAttrs}
/>
{after && <div className="multi-select__after">{after}</div>}
</div>
{filteredChoices.length > 0 && (
<ul className={classes} {...listboxAttrs}>
{
filteredChoices.map((choice) => {
const { active, focused, ...attrs } = getChoicesAttr(choice)
const liClasses = classNames(
liClassName,
'multi-select__option',
active && 'multi-select__option--active',
focused && 'multi-select__option--focus'
)

return (
// No keyboard event is needed here. Keyboard management happens
// in the combobox element, where the focus is kept at all times.
// see https://www.w3.org/WAI/ARIA/apg/patterns/combobox/
// eslint-disable-next-line jsx-a11y/click-events-have-key-events
<li
key={choice.value}
className={liClasses}
{...attrs}
>
<span>{choice.name}</span>
{active && <i className="bicon bicon-check" aria-hidden="true" />}
</li>
)
})
}
</ul>
)}
</div>
)
}
Loading