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

💄 [#2741] Add multiselect listbox mobile design #1398

Merged
merged 1 commit into from
Oct 28, 2024
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
Original file line number Diff line number Diff line change
Expand Up @@ -39,10 +39,10 @@
{% if text_icon %}
<span class="button__text-wrapper">
{% icon icon=text_icon outlined=icon_outlined %}
{% if not hide_text %}{{ text }}{% endif %}
{% if not hide_text %}<span class="button__inner-text">{{ text }}</span>{% endif %}
</span>
{% else %}
{% if not hide_text %}{{ text }}{% endif %}
{% if not hide_text %}<span class="button__inner-text">{{ text }}</span>{% endif %}
{% endif %}
</button>
{% endif %}
Original file line number Diff line number Diff line change
@@ -1,11 +1,33 @@
{% load i18n form_tags %}
{% load i18n form_tags button_tags %}

{# Wrapper for multiple filters #}
<div class="filter-bar" id="filterBar">
<form class="form" method="{{ method }}"
{% if no_action %}action=""{% else %}action="{% firstof form_action request.path %}"{% endif %}
{% if id %}id="{{ id }}"{% endif %}>
{# Note: each element inside the form is a flex column #}
{{ contents }}
</form>
<div class="filter-bar__backdrop" id="filterBarBackdrop">
{# Wrapper for multiple filters #}
<div class="filter-bar" id="filterBar">
<div class="filter-bar__mobile-controls">
{% button icon="close" text=_("Sluiten") hide_text=True icon_outlined=True transparent=True extra_classes="show-controls" %}
<div class="form__reset--mobile form__actions--fullwidth form__actions--reset">
<button class="button button--primary button--transparent" type="button" name="" value="" title="Wis alle filters" aria-label="Wis alle filters" id="resetAllFilters">
Wis alle filters
</button>
</div>
</div>
<div class="filter-bar__mobile-button">
<p class="utrecht-paragraph filter-bar__heading">Filters</p>
{% button icon="filter_alt" text=_("Filters") icon_outlined=True transparent=True extra_classes="show-modal" %}
<p class="utrecht-paragraph filter-bar__status-text">Status</p>
</div>
<form class="form filter-bar__form" method="{{ method }}"
{% if no_action %}action="" {% else %}action="{% firstof form_action request.path %}" {% endif %}
{% if id %}id="{{ id }}" {% endif %}>
{# Note: each element inside the form is a flex column #}
{{ contents }}

{# Mobile submit button updates on select #}
<div class="form__reload--mobile form__actions form__actions--fullwidth" id="filterFormActions">
<button class="button button--primary" type="submit" title="{% trans 'Toon resultaten' %}" aria-label="{% trans 'Toon resultaten' %}" id="filterCases">
{% trans 'Toon' %}<span class="filter-bar__frequency-sum" id="frequencySum">0</span><span id="resultText">{% trans 'resultaten' %}</span>
</button>
</div>
</form>
</div>
</div>
Original file line number Diff line number Diff line change
@@ -1,29 +1,31 @@
{% load i18n l10n form_tags icon_tags button_tags %}

<div class="filter-bar__multiselect-listbox multiselect-listbox" id="selectDropdownWrapper">
<button id="selectButton" type="button" class="button button__select" aria-haspopup="listbox" aria-expanded="false" aria-live="polite">
{% trans 'Status' %}:
{% icon icon="expand_more" icon_position="after" icon_outlined=True %}
</button>
<div id="listboxDropdown" class="multiselect-listbox__content" role="listbox" aria-labelledby="selectButton">
<div class="multiselect-listbox__scroll" role="presentation">
{% for status, frequency in statusfrequencies %}
<div class="checkbox" role="option">
<input type="checkbox" name="status" value="{{ status }}" id="id_status_{{ forloop.counter }}" class="checkbox__input">
<label class="checkbox__label" for="id_status_{{ forloop.counter }}">
<span class="ellipsis">{{ status }} </span><span class="frequency-counter">({{ frequency }})</span>
</label>
</div>
{% endfor %}
<div class="multiselect-listbox__popup">
<button id="selectButton" type="button" class="button button__select" aria-haspopup="listbox" aria-expanded="false" aria-live="polite">
{% trans 'Status' %}:
{% icon icon="expand_more" icon_position="after" icon_outlined=True %}
</button>
<div id="listboxDropdown" class="multiselect-listbox__content" role="listbox" aria-labelledby="selectButton">
<div class="multiselect-listbox__scroll" role="presentation">
{% for status, frequency in statusfrequencies %}
<div class="checkbox" role="option">
<input type="checkbox" name="status" value="{{ status }}" id="id_status_{{ forloop.counter }}" class="checkbox__input">
<label class="checkbox__label" for="id_status_{{ forloop.counter }}">
<span class="ellipsis">{{ status }} </span><span class="frequency-counter">({{ frequency }})</span>
</label>
</div>
{% endfor %}
</div>
{# Submit button updates on select #}
<div class="form__actions form__actions--fullwidth" id="multiselectFormActions">
<button class="button button--primary" type="submit" title="{% trans 'Toon resultaten' %}" aria-label="{% trans 'Toon resultaten' %}" id="filterCases">
{% trans 'Toon' %}<span class="filter-bar__frequency-sum" id="frequencySum">0</span><span id="resultText">{% trans 'resultaten' %}</span>
</button>
</div>
</div>
{# Submit button appears on select #}
<div class="form__actions form__actions--fullwidth" id="filterFormActions">
<button class="button button--primary" type="submit" title="{% trans 'Toon resultaten' %}" aria-label="{% trans 'Toon resultaten' %}" id="filterCases">
{% trans 'Toon' %}<span class="filter-bar__frequency-sum" id="frequencySum">0</span><span id="resultText">{% trans 'resultaten' %}</span>
</button>
<div class="form__reset--desktop form__actions--fullwidth form__actions--reset">
{% button bordered=False text=_("Wis alle filters") id="resetMultiSelectFilters" type="button" transparent=True primary=True %}
</div>
</div>
<div class="form__actions form__actions--fullwidth form__actions--reset">
{% button bordered=False text=_("Wis alle filters") id="resetFilters" type="button" transparent=True primary=True %}
</div>
</div>
102 changes: 102 additions & 0 deletions src/open_inwoner/js/components/FilterBar/filterbar.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
export class FilterBar {
static selector = '.filter-bar'

constructor(node) {
this.node = node
this.filterPopup = node.querySelector('.show-modal')
this.filterButton = node.querySelector('#selectButton')
this.backdrop = document.getElementById('filterBarBackdrop')
this.closeButton = node.querySelector('.show-controls')

// Check if elements are found
if (!this.filterPopup) {
console.error('Filter popup button not found!')
return
}

if (!this.filterButton) {
console.error('Select button not found!')
return
}

// Event listeners
this.filterPopup.addEventListener(
'click',
this.toggleOpenFilterPopup.bind(this)
)
this.closeButton.addEventListener(
'click',
this.closeFilterPopupDirect.bind(this) // Added a specific handler for direct close button click
)
document.addEventListener('click', this.closeFilterPopup.bind(this), false)
document.addEventListener(
'keydown',
this.closeFilterPopup.bind(this),
false
)
}

toggleOpenFilterPopup(event) {
event.preventDefault()

// Add 'show' class to the backdrop to make it visible
this.backdrop.classList.add('show')

// Toggle mobile filter class
setTimeout(() => {
this.node.classList.toggle('filter-bar--mobile')
const isExpanded =
this.filterPopup.getAttribute('aria-expanded') === 'true'
this.filterPopup.setAttribute('aria-expanded', !isExpanded)
}, 5)
}

closeFilterPopupDirect(event) {
// Remove 'show' class from the backdrop to hide it
this.backdrop.classList.remove('show')

// Remove mobile class and reset aria-expanded
this.node.classList.remove('filter-bar--mobile')
this.filterPopup.setAttribute('aria-expanded', 'false')
}

closeFilterPopup(event) {
// Close on clicking outside or pressing Escape
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would prefer to distinguish the two events and have two separate handlers: closeFilterPopupByEscape and closeFilterPopupByOutsideClick. That way, you can (a) avoid the disjunction in your handler and (b) simplify the logic. For example, in closeFilterPopupByEscape you can check if event.key != 'Escape': return; and then do the removal afterwards outside the conditional (following the never-nester approach). However, I'm not too familiar with the coding conventions in JS land, so leaving it to you.

if (
(event.type === 'keydown' && event.key === 'Escape') ||
(event.type === 'click' &&
!this.node.contains(event.target) &&
!this.filterPopup.contains(event.target) &&
!this.backdrop.contains(event.target))
) {
// Remove 'show' class from the backdrop to hide it
this.backdrop.classList.remove('show')

// Remove mobile class and reset aria-expanded
this.node.classList.remove('filter-bar--mobile')
this.filterPopup.setAttribute('aria-expanded', 'false')
}
}
}

// Reinitialize FilterBar after HTMX swap
htmx.on('htmx:afterSwap', function (e) {
if (e.detail && e.detail.target.id === 'cases-content') {
const filterBars = document.querySelectorAll(FilterBar.selector)
if (filterBars.length === 0) {
console.error('No filter bars found on the page after swap.')
} else {
filterBars.forEach((filterbar) => new FilterBar(filterbar))
}
}
})

// Initialize FilterBar on DOM load for the initial page load
document.addEventListener('DOMContentLoaded', () => {
const filterBars = document.querySelectorAll(FilterBar.selector)
if (filterBars.length === 0) {
console.error('No filter bars found on the page.')
} else {
filterBars.forEach((filterbar) => new FilterBar(filterbar))
}
})
1 change: 1 addition & 0 deletions src/open_inwoner/js/components/FilterBar/index.js
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
import './filterbar'
import './multiselect_listbox_checkbox'
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,11 @@ function initFilterBar() {
)
let sum = 0
let selectedFilters = []
let anyChecked = false

checkboxes.forEach((checkbox) => {
if (checkbox.checked) {
anyChecked = true // Mark that we have at least one checkbox checked
const label = checkbox.nextElementSibling
selectedFilters.push(label.textContent.trim())
const frequencyCounter = label.querySelector('.frequency-counter')
Expand All @@ -52,6 +54,7 @@ function initFilterBar() {
let closeIcon = document.createElement('span')
closeIcon.classList.add('material-icons', 'close-icon')
closeIcon.setAttribute('aria-hidden', 'true')
closeIcon.setAttribute('tabindex', '0') // Adding tabindex for keyboard focus
closeIcon.textContent = 'close'

// Add text and icons based on selected filters
Expand All @@ -76,12 +79,28 @@ function initFilterBar() {
selectButton.classList.add('active')
}

closeIcon.addEventListener('click', function (event) {
event.stopPropagation()
const handleClose = function () {
checkboxes.forEach((checkbox) => {
checkbox.checked = false
})
calculateAndDisplayCheckedSum() // Recalculate and update the button and sum
calculateAndDisplayCheckedSum() // Recalculate and update the button and sum, even after refresh
const filterBarForm = document.querySelector('#filterBar .form')
if (filterBarForm) {
filterBarForm.submit()
}
}

closeIcon.addEventListener('click', function (event) {
event.stopPropagation()
handleClose()
})

// Add accessibility functionality for close icon
closeIcon.addEventListener('keydown', function (event) {
if (event.key === 'Enter' || event.key === ' ') {
event.preventDefault()
handleClose()
}
})

selectButton.setAttribute('aria-live', 'polite')
Expand All @@ -96,6 +115,18 @@ function initFilterBar() {
if (resultTextElement) {
resultTextElement.textContent = sum === 1 ? 'resultaat' : 'resultaten'
}

// Handle visibility of resetMultiSelectFilters
const resetMultiSelectFilters = document.getElementById(
'resetMultiSelectFilters'
)
if (resetMultiSelectFilters) {
if (anyChecked) {
resetMultiSelectFilters.classList.remove('hide') // Show the button
} else {
resetMultiSelectFilters.classList.add('hide') // Hide the button
}
}
}

const initSelectBehavior = function () {
Expand Down Expand Up @@ -177,10 +208,35 @@ function initFilterBar() {
}
})

const resetFilters = document.getElementById('resetFilters')
if (resetFilters) {
resetFilters.addEventListener('click', function (e) {
const resetMultiSelectFilters = document.getElementById(
'resetMultiSelectFilters'
)
const resetAllFilters = document.getElementById('resetAllFilters')

if (resetMultiSelectFilters) {
resetMultiSelectFilters.addEventListener('click', function (e) {
e.preventDefault()

const checkboxes = document.querySelectorAll(
'.filter-bar .checkbox__input'
)
checkboxes.forEach((checkbox) => {
checkbox.checked = false
})

calculateAndDisplayCheckedSum()

const filterBarForm = document.querySelector('#filterBar .form')
if (filterBarForm) {
filterBarForm.submit()
}
})
}

if (resetAllFilters) {
resetAllFilters.addEventListener('click', function (e) {
e.preventDefault()

const checkboxes = document.querySelectorAll(
'.filter-bar .checkbox__input'
)
Expand Down Expand Up @@ -224,9 +280,6 @@ document.body.addEventListener('htmx:afterSwap', function () {

document.addEventListener('click', function (e) {
if (e.target && e.target.classList.contains('pagination__link')) {
scrollToTopOfWindow()
setTimeout(function () {
initFilterBar() // Reinitialize filter bar after swap
}, 20)
scrollToTopOfWindow() // Scroll up after clicking pagination
}
})
2 changes: 1 addition & 1 deletion src/open_inwoner/js/components/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import { CookieBanner } from './cookie-consent'
import './datepicker'
import { Dropdown } from './dropdown'
import './emoji-button'
import './FilterBar/multiselect_listbox_checkbox'
import './FilterBar'
import './form'
import './header'
import './map'
Expand Down
4 changes: 4 additions & 0 deletions src/open_inwoner/scss/components/Button/Button.scss
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,10 @@
display: flex;
}

&__inner-text {
font-family: var(--font-family-body);
}

> .link__text {
width: 100%;
}
Expand Down
2 changes: 2 additions & 0 deletions src/open_inwoner/scss/components/Cases/Cases.scss
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
.cases {
position: relative;

/// cards on cases list
.card {
&__body {
Expand Down
Loading
Loading