Skip to content

Commit

Permalink
fix(ld-notification): screen reader now announces notifications corre…
Browse files Browse the repository at this point in the history
…ctly
  • Loading branch information
borisdiakur committed Aug 5, 2021
1 parent fe7fdc0 commit ce7f2c1
Show file tree
Hide file tree
Showing 3 changed files with 60 additions and 33 deletions.
7 changes: 5 additions & 2 deletions src/liquid/components/ld-notification/ld-notification.css
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,9 @@
width: fit-content;
min-width: 19rem;
max-width: calc(90% - 1rem);
max-height: 80vh;
min-height: 2.5rem;
overflow-y: auto;
border-radius: var(--ld-br-m);
display: flex;
justify-content: space-between;
Expand Down Expand Up @@ -118,7 +121,7 @@
background-color: var(--ld-col-vy-default);
color: var(--ld-col-rblck-default);
}
.ld-notification__item--error {
.ld-notification__item--alert {
background-color: var(--ld-col-rr-default);
color: var(--ld-col-wht);
}
Expand All @@ -133,7 +136,7 @@
grid-auto-flow: column;
align-items: center;
gap: var(--ld-sp-12);
padding: var(--ld-sp-8) var(--ld-sp-12);
padding: var(--ld-sp-6) var(--ld-sp-12) var(--ld-sp-8);
}

.ld-notification__btn-dismiss {
Expand Down
28 changes: 11 additions & 17 deletions src/liquid/components/ld-notification/ld-notification.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import '../../components' // type definitions for type checks and intelliSense
import { Component, h, Host, Listen, Prop, State, Watch } from '@stencil/core'

type Notification = {
type: 'info' | 'warn' | 'error'
type: 'info' | 'warn' | 'alert'
content: string
timeout?: number
}
Expand All @@ -24,9 +24,9 @@ export class LdNotification {
@State() queue: Notification[] = []
@State() queueDismissed: Notification[] = []

@State() dismissTimeout: number
@State() dismissTimeout: NodeJS.Timeout | null

@State() fadeoutTimeouts: number[] = []
@State() fadeoutTimeouts: (NodeJS.Timeout | null)[] = []

@State() currentNotification?: Notification

Expand All @@ -35,10 +35,10 @@ export class LdNotification {
clearTimeout(this.dismissTimeout)

if (!this.currentNotification) return
if (this.currentNotification.type === 'error') return
if (this.currentNotification.type === 'alert') return
if (this.currentNotification.timeout === 0) return

this.dismissTimeout = window.setTimeout(() => {
this.dismissTimeout = setTimeout(() => {
this.handleNotificationDismiss()
}, this.currentNotification.timeout || DEFAULT_NOTIFICATION_TIMEOUT)
}
Expand All @@ -60,14 +60,14 @@ export class LdNotification {
if (inQueue) return

// Insert by relevance, whith error notifications being more relevant than non-error notifications.
if (newNotification.type === 'error') {
if (newNotification.type === 'alert') {
this.queue = [...this.queue, newNotification]
this.currentNotification = newNotification
return
}

const firstErrorNotificationIndex = this.queue.findIndex(
(notification) => notification.type === 'error'
(notification) => notification.type === 'alert'
)
if (firstErrorNotificationIndex === -1) {
this.queue = [...this.queue, newNotification]
Expand All @@ -92,7 +92,7 @@ export class LdNotification {
this.currentNotification = this.queue[this.queue.length - 1]

this.fadeoutTimeouts.push(
window.setTimeout(() => {
setTimeout(() => {
this.queueDismissed = this.queueDismissed.slice(0, -1)
}, FADE_TRANSITION_DURATION)
)
Expand All @@ -108,7 +108,7 @@ export class LdNotification {
this.currentNotification = undefined
this.fadeoutTimeouts.forEach((timeoutId) => clearTimeout(timeoutId))
this.fadeoutTimeouts.push(
window.setTimeout(() => {
setTimeout(() => {
this.queueDismissed = []
}, FADE_TRANSITION_DURATION)
)
Expand Down Expand Up @@ -150,13 +150,7 @@ export class LdNotification {
cl += ` ld-notification--${this.placement}`

return (
<Host
class={cl}
role="region"
aria-label="Notifications"
aria-live="polite"
aria-relevant="additions"
>
<Host class={cl} role="region" aria-label="Notifications">
{this.queue.map((notification, index) => (
<div
class={`ld-notification__item ld-notification__item--${notification.type}`}
Expand All @@ -165,7 +159,7 @@ export class LdNotification {
<div
class="ld-notification__item-content"
innerHTML={notification.content}
role={notification.type === 'error' ? 'alert' : 'status'}
role={notification.type === 'alert' ? 'alert' : 'status'}
></div>
<button
class="ld-notification__btn-dismiss"
Expand Down
58 changes: 44 additions & 14 deletions src/liquid/components/ld-notification/readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,22 +17,42 @@ Use the `ld-notification` component in your application to display popup notific

## How it works

Add the component to your application, preferably close after the opening `<body>` tag. The component is invisible as long as no new notifications are triggered. It listens to three [custom events](https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent) on the `window`: the `ldNotification` the `ldNotificationDismiss` and the `ldNotificationClear` event. As soon as one of those events reaches the `window`, the component either queues and displays new notifications or removes queued notifications from its queue. The content and type of each notification is set via the `event.detail` property. Here is an example on how you can trigger a notification containing an error message:
Add the component to your application, preferably close after the opening `<body>` tag. The component is invisible as long as no new notifications are triggered. It listens to three [custom events](https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent) on the `window`: the `ldNotification` the `ldNotificationDismiss` and the `ldNotificationClear` event. As soon as one of those events reaches the `window`, the component either queues and displays new notifications or removes queued notifications from its queue. The content and type of each notification is set via the `event.detail` property. Here is an example on how you can trigger a notification containing an alert message:

```js
dispatchEvent(new CustomEvent('ldNotification', {
detail: {
content: 'Something went wrong.',
type: 'error',
type: 'alert',
}
}))
```

> **Note**: You should not use more than one `ld-notification` component in your app. It wouldn't make much sense to have multiple components managing notification queues and displaying them independently, would it?
## Accessibility

Each notification item that appears on the screen has either the ARIA role `status` or `alert`, so that assistive technology should announce the notification content to the user.

Keep in mind that focus is not explicitly changed when a notification appears. This means that users with visual disabilities may have problems navigating to a notification. This is especially the case for notifications which time out. And even more for notifications containing interaction elements, such as confirmation buttons etc. Thus, we recommend you avoid using notifications for critical information that users need to act on immediately. In summary, notifications may be difficult for users with low vision or low dexterity to access because they

- Disappear automatically
- Can’t be easily accessed with the keyboard
- Might appear outside the proximity of the user’s current focus

### Notifications with interactive content

Make sure that users can accomplish the interaction in the notification another way, since an interaction element within a notification may be difficult to access for some users.

If you really do want to include an interaction element within a notification, [make sure the notification doesn't time out](components/ld-notification/#preventing-a-timeout) so that the user has enough time to navigate to and interact with the notification.

## Notification hierarchy

Notifications of type `'alert'` take precedence of notifications of type `'info'` and `'warn'`, which means that if a notification of type `'info'` or `'warn'` is fired after a notification of type `'alert'` and the notification of type `'alert'` has not been dismissed yet, the potentially less important notifications gets placed behind the notification of type `'alert'`. Other than that most recent notifications take precedence of older notifications, pushing the older ones back in the queue and resetting and pausing their timeouts.

## Notification timeout

While notifications with type `'error'` do not time out, notifications of type `'info'` and `'warn'` have a default timeout of **six seconds** after which they disappear automatically. You can customize this timeout by attaching a timeout value of your choice to the appropriate property on the event detail object:
While notifications with type `'alert'` do not time out, notifications of type `'info'` and `'warn'` have a default timeout of **six seconds** after which they disappear automatically. You can customize this timeout by attaching a timeout value of your choice to the appropriate property on the event detail object:

```js
dispatchEvent(new CustomEvent('ldNotification', {
Expand All @@ -44,6 +64,8 @@ dispatchEvent(new CustomEvent('ldNotification', {
}))
```

### Preventing a timeout

If you want to prevent a notification of type `'info'` and `'warn'` from timing out, use the timeout value `0`:

```js
Expand All @@ -56,6 +78,10 @@ dispatchEvent(new CustomEvent('ldNotification', {
}))
```

### Timeout handlint for queued notifications

If a notification gets queued behind another notification, its timeout is reset to its initial value and on pause.

## Notification content

The examples above used simple text as content for the notification. But you can also use an HTML string containing links and other components:
Expand All @@ -69,19 +95,23 @@ dispatchEvent(new CustomEvent('ldNotification', {
}))
```

## Redundant notifications handling
### Redundant notifications handling

If a notification event is triggered containing the same content and type as another notification which already is queued for notification display, the event is ignored. If you still need to trigger another notification with the same content, you can append a zero-space character to your content.

## Dismissing current notificaiton
## Dismissing notificaitons

While the user can dismiss the currently displayed notification by pressing the cross button on the notification, there also exist ways to programmatically dismiss notifications.

### Dismissing current notificaiton

You can dismiss the current notification programmatically by dispatching the `ldNotificationDismiss` event on the `window`:

```js
dispatchEvent(new CustomEvent('ldNotificationDismiss'))
```

## Clearing all notifications
### Clearing all notifications

You can dismiss all notifications programmatically by dispatching the `ldNotificationClear` event on the `window`:

Expand Down Expand Up @@ -138,10 +168,10 @@ The examples below illustrate how you can trigger notifications using different
<ld-button type="submit">Submit</ld-button>
</form>

<form class="notification-form" id="form-error">
<form class="notification-form" id="form-alert">
<ld-label>
Error message
<ld-input id="input-error" value="Ooops."></ld-input>
alert message
<ld-input id="input-alert" value="Ooops."></ld-input>
</ld-label>
<ld-button type="submit">Submit</ld-button>
</form>
Expand Down Expand Up @@ -205,14 +235,14 @@ formInfoNoTimeout.addEventListener('submit', ev => {
}))
})

const formError = document.getElementById('form-error')
const inputError = document.getElementById('input-error')
formError.addEventListener('submit', ev => {
const formAlert = document.getElementById('form-alert')
const inputAlert = document.getElementById('input-alert')
formAlert.addEventListener('submit', ev => {
ev.preventDefault()
dispatchEvent(new CustomEvent('ldNotification', {
detail: {
content: inputError.value || '',
type: 'error',
content: inputAlert.value || '',
type: 'alert',
}
}))
})
Expand Down

0 comments on commit ce7f2c1

Please sign in to comment.