Skip to content

Commit 9ddd2cb

Browse files
committed
[#2379] Reordered notifications order + forced focus on 1st element
1 parent 6389a61 commit 9ddd2cb

File tree

10 files changed

+132
-21
lines changed

10 files changed

+132
-21
lines changed

src/open_inwoner/components/templates/components/Form/Error.html

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
<div class="notifications notifications__errors">
55
<div class="notification notification--warning">
66
{% icon icon="warning_amber" icon_position="before" outlined=True %}
7-
<div class="notification__content" tabindex="-1">
7+
<div class="notification__content" role="alert" tabindex="-1">
88
<p class="utrecht-paragraph utrecht-paragraph--oip utrecht-paragraph--oip-compact">{{ message.message }}</p>
99
</div>
1010
</div>

src/open_inwoner/components/templates/components/Notification/Notification.html

+8-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,14 @@
66
</div>
77
{% endif %}
88

9-
<div class="notification__content" tabindex="-1">
9+
<div class="notification__content"
10+
{% if type == "error" %}
11+
role="alert"
12+
{% elif type == "warning" %}
13+
role="alert"
14+
{% else %}
15+
role="status"
16+
{% endif %} tabindex="-1">
1017
{% if title %}<h2 class="utrecht-heading-2">{{ title }}</h2>{% endif %}
1118
{% if notification %}<p class="utrecht-paragraph">{{ notification }}</p>{% endif %}
1219
{% if action %}{% button href=action text=action_text %}{% endif %}

src/open_inwoner/components/templates/components/Notification/Notifications.html

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{% load notification_tags string_tags %}
22

3-
<div class="notifications">
3+
<section class="notifications">
44
{% for message in messages %}
55
{% with as_markdown=message.extra_tags|is_substring:"as_markdown" %}
66
{% with local_message=message.extra_tags|is_substring:"local_message" %}
@@ -14,4 +14,4 @@
1414
{% endwith %}
1515
{% endwith %}
1616
{% endfor %}
17-
</div>
17+
</section>

src/open_inwoner/conf/locale/nl/LC_MESSAGES/django.po

+2-2
Original file line numberDiff line numberDiff line change
@@ -6552,9 +6552,9 @@ msgstr "Zoekindex opnieuw opbouwen"
65526552
#: open_inwoner/search/tests/test_feedback.py:192
65536553
#: open_inwoner/search/views.py:158
65546554
msgid ""
6555-
"Thank you for your feedback. It will help us to improve our search engine"
6555+
"Thank you for your feedback, it will help us improve our search engine."
65566556
msgstr ""
6557-
"Dank u voor uw feedback, hiermee kunnen wij de omgeving verder verbeteren"
6557+
"Dank u voor uw feedback, hiermee kunnen wij de omgeving verder verbeteren."
65586558

65596559
#: open_inwoner/search/tests/test_logging.py:33 open_inwoner/search/views.py:63
65606560
#, python-brace-format

src/open_inwoner/js/components/index.js

+2
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import './header'
2222
import './map'
2323
import './message-file'
2424
import { Notification } from './notifications'
25+
import { NotificationsList } from './notifications/NotificationsList'
2526
import './plans'
2627
import './plan-preview'
2728
import './questionnaire'
@@ -55,6 +56,7 @@ const elementWrappers = [
5556
(elt) => new DisableContactFormButton(elt),
5657
],
5758
[Notification.selector, (elt) => new Notification(elt)],
59+
[NotificationsList.selector, (elt) => new NotificationsList(elt)],
5860
[AnchorMobile.selector, (elt) => new AnchorMobile(elt)],
5961
[StatusAccordion.selector, (elt) => new StatusAccordion(elt)],
6062
[FileInput.selector, (elt) => new FileInput(elt)],
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
/**
2+
* Surrounding Notifications class.
3+
* @class
4+
*/
5+
export class NotificationsList {
6+
static selector = '.notifications'
7+
constructor(notificationContents) {
8+
this.notificationContents = notificationContents
9+
}
10+
11+
scrollToFirstNotification() {
12+
if (this.notificationContents.length > 0) {
13+
// Scroll to the first notification, since there could be multiple
14+
this.notificationContents[0].scrollIntoView({
15+
block: 'center',
16+
behavior: 'smooth',
17+
})
18+
19+
// Add a delay before setting focus for screen readers
20+
setTimeout(() => {
21+
// Set focus on the first notification content
22+
this.notificationContents[0].focus()
23+
}, 100)
24+
}
25+
}
26+
}
27+
28+
// Start!
29+
30+
const notificationContents = document.querySelectorAll('.notification__content')
31+
32+
// Instantiate notifications section
33+
document
34+
.querySelectorAll(NotificationsList.selector)
35+
.forEach((notifications) => new NotificationsList(notifications))
36+
37+
// Focus only the first notification content
38+
const scrollManager = new NotificationsList(notificationContents)
39+
scrollManager.scrollToFirstNotification()

src/open_inwoner/js/components/notifications/index.js

+75-12
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,32 @@
1+
const typeOrder = ['error', 'warning', 'success', 'info']
2+
3+
/**
4+
* Helper function to determine the order index of a notification type.
5+
* @param {HTMLElement} notification - The notification element.
6+
* @returns {number} - Order index of the notification type.
7+
*/
8+
const getTypeOrderIndex = (notification) => {
9+
const type = getTypeFromNotification(notification)
10+
return typeOrder.indexOf(type)
11+
}
12+
13+
/**
14+
* Helper function to get the type of a notification.
15+
* @param {HTMLElement} notification - The notification element.
16+
* @returns {string} - Type of the notification.
17+
*/
18+
const getTypeFromNotification = (notification) => {
19+
let notificationType = ''
20+
notification.classList.forEach((cls) => {
21+
if (cls.startsWith('notification--')) {
22+
notificationType = cls.replace('notification--', '')
23+
}
24+
})
25+
return notificationType
26+
}
27+
128
/**
2-
* Notification class.
29+
* Single Notification class.
330
* @class
431
*/
532
export class Notification {
@@ -14,6 +41,7 @@ export class Notification {
1441
this.node = node
1542

1643
this.bindEvents()
44+
this.reorderNotifications()
1745
}
1846

1947
/**
@@ -28,18 +56,23 @@ export class Notification {
2856
* Scrolls to the notification content and sets focus.
2957
*/
3058
scrollToNotification() {
31-
const notificationContent = this.node.querySelector(
32-
'.notification__content'
59+
const notificationContents = Array.from(
60+
this.node.querySelectorAll('.notification__content')
3361
)
3462

35-
if (notificationContent) {
36-
// If errors are present, scroll and trigger the opened state
37-
notificationContent.scrollIntoView({
38-
block: 'center',
39-
behavior: 'smooth',
63+
if (notificationContents) {
64+
notificationContents.forEach((content) => {
65+
// If errors are present, scroll and trigger the opened state
66+
content.scrollIntoView({
67+
block: 'center',
68+
behavior: 'smooth',
69+
})
70+
71+
// Add a pause before setting focus for screen readers after DOM load
72+
setTimeout(() => {
73+
content.focus()
74+
}, 100)
4075
})
41-
// Set focus for screen readers
42-
notificationContent.focus()
4376
}
4477
}
4578

@@ -51,8 +84,6 @@ export class Notification {
5184
e.preventDefault()
5285
this.close()
5386
})
54-
55-
this.scrollToNotification()
5687
}
5788

5889
/**
@@ -61,6 +92,29 @@ export class Notification {
6192
close() {
6293
this.node.parentElement.removeChild(this.node)
6394
}
95+
96+
/**
97+
* Reorders notifications based on type.
98+
*/
99+
reorderNotifications() {
100+
// Get all notifications in the parent container
101+
const notificationsContainer = document.querySelector('.notifications')
102+
const notifications = Array.from(
103+
notificationsContainer.querySelectorAll(Notification.selector)
104+
)
105+
106+
// Sort notifications based on type order
107+
notifications.sort((a, b) => {
108+
const typeA = getTypeOrderIndex(a)
109+
const typeB = getTypeOrderIndex(b)
110+
return typeA - typeB
111+
})
112+
113+
// Re-append sorted notifications to parent container
114+
notifications.forEach((notification) =>
115+
notificationsContainer.appendChild(notification)
116+
)
117+
}
64118
}
65119

66120
// Start!
@@ -69,3 +123,12 @@ export class Notification {
69123
document
70124
.querySelectorAll(Notification.selector)
71125
.forEach((notification) => new Notification(notification))
126+
127+
// Scroll to the notifications after reordering
128+
setTimeout(() => {
129+
const firstNotification = document.querySelector(Notification.selector)
130+
if (firstNotification) {
131+
const instance = new Notification(firstNotification)
132+
instance.scrollToNotification()
133+
}
134+
}, 0)

src/open_inwoner/scss/components/Notification/_Notifications.scss

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
top: var(--spacing-extra-large);
44
display: flex;
55
flex-direction: column;
6-
gap: var(--spacing-extra-large);
6+
gap: var(--spacing-medium);
77
z-index: 1002;
88

99
/// Multiple errors.

src/open_inwoner/search/tests/test_feedback.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,6 @@ def test_feedback_form_not_displayed_after_submit(self):
189189
self.assertEqual(
190190
message.message,
191191
_(
192-
"Thank you for your feedback. It will help us to improve our search engine"
192+
"Thank you for your feedback, it will help us improve our search engine."
193193
),
194194
)

src/open_inwoner/search/views.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,7 @@ def form_valid(self, form):
155155
self.request,
156156
messages.SUCCESS,
157157
_(
158-
"Thank you for your feedback. It will help us to improve our search engine"
158+
"Thank you for your feedback, it will help us improve our search engine."
159159
),
160160
)
161161
redirect = furl(reverse("search:search"))

0 commit comments

Comments
 (0)