Skip to content

Commit

Permalink
Add live regions logic
Browse files Browse the repository at this point in the history
  • Loading branch information
MisRob committed Jul 23, 2024
1 parent 6a6865f commit d1344de
Show file tree
Hide file tree
Showing 9 changed files with 399 additions and 10 deletions.
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,17 @@ Changelog is rather internal in nature. See release notes for the public overvie

## Upcoming version 5.x.x (`develop` branch)

- [#687]
- **Description:** Adds logic that inserts ARIA live assertive and polite regions to an application's document body during KDS initialization and documents this on the new "Installation" page. Relatedly adds `useKLiveRegion` composable with public methods for updating the live regions with assertive and polite messages.
- **Products impact:** new API
- **Addresses:** https://github.com/learningequality/kolibri-design-system/issues/668
- **Components:** `useKLiveRegion`
- **Breaking:** No
- **Impacts a11y:** Yes. It will fix several places utilizing live regions that don't work in our applications at all. Furthemore, it follows the recommended practices that will fix major a11y issues with live regions we're having.
- **Guidance:** Find all polite and live regions (or roles) in an application. Remove them and instead use `useKLiveRegion.sendPoliteMessage` and `useKLiveRegion.sendAssertiveMessage` to update the live regions that KDS inserted to document body during installation.

[#687]: https://github.com/learningequality/kolibri-design-system/pull/687

[#625]
- **Description:** Initial implementation of `KCard` component
- **Products impact:** New Component
Expand Down
5 changes: 5 additions & 0 deletions docs/assets/main.scss
Original file line number Diff line number Diff line change
Expand Up @@ -83,3 +83,8 @@ dt {
dd {
margin-left: 20px;
}

em {
font-style: normal;
font-weight: bold;
}
40 changes: 40 additions & 0 deletions docs/pages/installation.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<template>

<DocsPageTemplate>

<DocsPageSection title="1. Install the plugin" anchor="#install-plugin">
<p>Kolibri Design System (KDS) is a Vue plugin. Register it in your application alongside scripts that are needed for its full functioning:</p>

<!-- eslint-disable -->
<DocsShowCode language="javascript">
import KThemePlugin from 'kolibri-design-system/lib/KThemePlugin';
import trackInputModality from 'kolibri-design-system/lib/styles/trackInputModality';
import trackMediaType from 'kolibri-design-system/lib/styles/trackMediaType';

Vue.use(KThemePlugin);

trackInputModality();
trackMediaType();
</DocsShowCode>
<!-- eslint-enable -->

This ensures the following:

<ul>
<li>Installs <code>$themeBrand</code>, <code>$themeTokens</code> <code>$themePalette</code>, and <code>$computedClass</code> helpers on all Vue instances.</li>
<li>Provides <code>$coreOutline</code>, <code>$inputModality</code>, <code>$mediaType</code>, and <code>$isPrint</code> computed properties as well as <code>$print</code> method to all Vue instances.</li>
<li>Globally registers all KDS Vue components.</li>
<li>Inserts assertive and polite ARIA live regions to your application's document body (see <DocsInternalLink href="/usekliveregion" text="useKLiveRegion" /> to understand how to utilize them).</li>
</ul>
</DocsPageSection>

<DocsPageSection title="2. Initialize theme" anchor="#initialize-theme">
<p>
Until this section is better documented (TODO link GH issue), refer to <DocsExternalLink href="https://github.com/learningequality/kolibri/blob/develop/kolibri/core/assets/src/styles/initializeTheme.js" text="Kolibri's initializeTheme.js" />.
</p>
</DocsPageSection>

</DocsPageTemplate>

</template>

10 changes: 0 additions & 10 deletions docs/pages/kimg.vue
Original file line number Diff line number Diff line change
Expand Up @@ -308,13 +308,3 @@
export default {};
</script>


<style lang="scss" scoped>
em {
font-style: normal;
font-weight: bold;
}
</style>
123 changes: 123 additions & 0 deletions docs/pages/usekliveregion.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
<template>

<DocsPageTemplate apiDocs>

<DocsPageSection title="Overview" anchor="#overview">
<p>A composable that offers <code>sendPoliteMessage</code> and <code>sendAssertiveMessage</code> functions that send polite and assertive messages to their corresponding ARIA live regions.</p>
</DocsPageSection>

<DocsPageSection title="When to use live regions" anchor="#usage">
<p>Before sending messages to live regions, always research carefully if you really need it for the task ahead. Live regions can be <DocsExternalLink href="https://www.sarasoueidan.com/blog/accessible-notifications-with-aria-live-regions-part-2/#avoid-live-regions-if-you-can" text="buggy and inconsistent" />. There are often better alternatives, such as utilizing <DocsExternalLink href="https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes" text="WAI-ARIA attributes" />. A good rule of thumb is to use live regions only when there's no other way.</p>
</DocsPageSection>

<DocsPageSection title="Usage" anchor="#usage">
<p>Since polite and assertive regions are inserted to an application's document body automatically <DocsInternalLink href="/installation#install-plugin" text="during the KDS installation process" />, the only thing you need to do to deliver messages is to import and call <code>sendPoliteMessage</code> or <code>sendAssertiveMessage</code> from any place in your application.</p>

<p>These two methods are also used internally from some KDS components to provide a11y out of the box. Always check that you don't send messages to announce updates that are already being announced from KDS to prevent from duplicate announcements.</p>

<h3>Polite message</h3>

<p>Sending a polite message updates the text content of <code>aria-live="polite"</code> region. <em>Use it to send messages that can wait to be announced until the user is idle. This message should typically be the most commonly used.</em></p>

<p>Send polite messages with <code>sendPoliteMessage(message)</code>:</p>

<!-- eslint-disable -->
<!-- prevent prettier from changing indentation -->
<DocsShowCode language="javascript">
import useKLiveRegion from 'kolibri-design-system/lib/composables/useKLiveRegion';

export default {
setup() {
const { sendPoliteMessage } = useKLiveRegion();
sendPoliteMessage('Polite message');
}
};
</DocsShowCode>
<!-- eslint-enable -->

<h3>Assertive message</h3>

<p>Sending an assertive message updates the text content of <code>aria-live="assertive"</code> region. <em>It should be used with caution because it disrupts the user's flow. Use it only to send messages that require immediate attention, such as errors.</em></p>

<p>Send assertive messages with <code>sendAssertiveMessage(message)</code>:</p>

<!-- eslint-disable -->
<!-- prevent prettier from changing indentation -->
<DocsShowCode language="javascript">
import useKLiveRegion from 'kolibri-design-system/lib/composables/useKLiveRegion';

export default {
setup() {
const { sendAssertiveMessage } = useKLiveRegion();
sendPoliteMessage('Assertive message');
}
};
</DocsShowCode>
<!-- eslint-enable -->
</DocsPageSection>

<DocsPageSection title="Demo" anchor="#demo">
<p>Send messages below and turn on your screen reader or observe the content of <code>&lt;div id="k-live-region"&gt;</code> in the browser console.</p>

<DocsShow language="html">
<KTextbox label="Polite message" :value="politeMessageInput" @input="updatePoliteMessage" />
<KButton @click="sendPoliteMessage(politeMessageInput)">
Send
</KButton>
</DocsShow>

<DocsShow language="html">
<KTextbox label="Assertive message" :value="assertiveMessageInput" @input="updateAssertiveMessage" />
<KButton @click="sendAssertiveMessage(assertiveMessageInput)">
Send
</KButton>
</DocsShow>
</DocsPageSection>

<DocsPageSection title="Related" anchor="#related">
<ul>
<li>
<DocsInternalLink href="/installation#initialize-dom" text="KDS installation step" /> that attaches live regions to an application's DOM
</li>
</ul>
</DocsPageSection>
</DocsPageTemplate>

</template>


<script>
import { ref } from '@vue/composition-api';
import useKLiveRegion from '../../lib/composables/useKLiveRegion';
export default {
setup() {
const { _mountLiveRegion, sendPoliteMessage, sendAssertiveMessage } = useKLiveRegion();
const politeMessageInput = ref('Polite hello');
const updatePoliteMessage = message => {
politeMessageInput.value = message;
};
const assertiveMessageInput = ref('I cannot wait');
const updateAssertiveMessage = message => {
assertiveMessageInput.value = message;
};
return {
_mountLiveRegion,
updatePoliteMessage,
politeMessageInput,
updateAssertiveMessage,
assertiveMessageInput,
sendPoliteMessage,
sendAssertiveMessage,
};
},
mounted() {
this._mountLiveRegion(this.$root.$el);
},
};
</script>
19 changes: 19 additions & 0 deletions docs/tableOfContents.js
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,10 @@ export default [
path: '/',
title: 'Home',
}),
new Page({
path: '/installation',
title: 'Installation',
}),
new Page({
path: '/principles',
title: 'Design principles',
Expand Down Expand Up @@ -180,6 +184,21 @@ export default [
isCode: true,
keywords: [...compositionRelatedKeywords, 'responsive', 'window', 'breakpoint'],
}),
new Page({
path: '/usekliveregion',
title: 'useKLiveRegion',
isCode: true,
keywords: [
...compositionRelatedKeywords,
'a11y',
'live',
'region',
'aria',
'polite',
'assertive',
'message',
],
}),
new Page({
path: '/usekshow',
title: 'useKShow',
Expand Down
22 changes: 22 additions & 0 deletions lib/KThemePlugin.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { isNuxtServerSideRendering } from '../lib/utils';
import computedClass from './styles/computedClass';

import KBreadcrumbs from './KBreadcrumbs';
Expand Down Expand Up @@ -39,13 +40,34 @@ import KCard from './KCard';
import { themeTokens, themeBrand, themePalette, themeOutlineStyle } from './styles/theme';
import globalThemeState from './styles/globalThemeState';

import useKLiveRegion from './composables/useKLiveRegion';

const { _mountLiveRegion } = useKLiveRegion();

require('./grids/globalStyles.js'); // global grid styles

/**
* Install Kolibri theme helpers on all Vue instances.
* Also, set up global state, listeners, and styles.
*/
export default function KThemePlugin(Vue) {
// Note that if DOM live regions need to be demostrated
// on the KDS website, and therefore attached to the DOM,
// just call _mountLiveRegion() in the relevant documentation
// page's 'mounted' (see 'docs/pages/usekliveregio.vue' for an example)
if (!isNuxtServerSideRendering) {
const onDomReady = () => {
_mountLiveRegion();
document.removeEventListener('DOMContentLoaded', onDomReady);
};

if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', onDomReady);
} else {
onDomReady();
}
}

Vue.mixin({
/* eslint-disable kolibri/vue-no-unused-properties */
computed: {
Expand Down
91 changes: 91 additions & 0 deletions lib/composables/useKLiveRegion/__tests__/index.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import useKLiveRegion from '../index.js';

const { _mountLiveRegion, sendPoliteMessage, sendAssertiveMessage } = useKLiveRegion();
function removeWhitespaceFromHtml(htmlString) {
// https://stackoverflow.com/a/33108909
return htmlString.replace(/>\s+|\s+</g, function(m) {
return m.trim();
});
}

// check that document.body.innerHTML includes html string
function assertDocumentBodyIncludes(htmlString) {
expect(
removeWhitespaceFromHtml(document.body.innerHTML).includes(removeWhitespaceFromHtml(htmlString))
).toBeTruthy();
}

describe('useKLiveRegion', () => {
it(`'_mountLiveRegion' inserts the live regions to a given element`, () => {
expect(document.body.innerHTML).toEqual('');

_mountLiveRegion();

assertDocumentBodyIncludes(`
<div id="k-live-region" class="visuallyhidden">
<div aria-live="polite">
</div>
<div aria-live="assertive">
</div>
</div>
`);
});

it(`'sendPoliteMessage' updates the live region with the message`, async () => {
_mountLiveRegion();
await new Promise(resolve => setTimeout(resolve, 100));

assertDocumentBodyIncludes(`
<div id="k-live-region" class="visuallyhidden">
<div aria-live="polite">
</div>
<div aria-live="assertive">
</div>
</div>
`);

sendPoliteMessage('Polite message');
await new Promise(resolve => setTimeout(resolve, 100));
assertDocumentBodyIncludes(`
<div id="k-live-region" class="visuallyhidden">
<div aria-live="polite">
Polite message
</div>
<div aria-live="assertive">
</div>
</div>
`);

// cleanup
sendPoliteMessage('');
});

it(`'sendAssertiveMessage' updates the live region with the message`, async () => {
_mountLiveRegion();
await new Promise(resolve => setTimeout(resolve, 100));

assertDocumentBodyIncludes(`
<div id="k-live-region" class="visuallyhidden">
<div aria-live="polite">
</div>
<div aria-live="assertive">
</div>
</div>
`);

sendAssertiveMessage('Assertive message');
await new Promise(resolve => setTimeout(resolve, 100));
assertDocumentBodyIncludes(`
<div id="k-live-region" class="visuallyhidden">
<div aria-live="polite">
</div>
<div aria-live="assertive">
Assertive message
</div>
</div>
`);

// cleanup
sendAssertiveMessage('');
});
});
Loading

0 comments on commit d1344de

Please sign in to comment.