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

Add live regions logic #687

Merged
merged 8 commits into from
Aug 6, 2024
Merged
Show file tree
Hide file tree
Changes from 7 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
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

[#707]
- **Description:** Card Validations
- **Products impact:**
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 (see <DocsInternalLink href="/colors/#usage" text="Colors" />).</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" />).</li>
</ul>
</DocsPageSection>

<DocsPageSection title="2. Initialize theme" anchor="#initialize-theme">
<p>
Until this section is better documented, 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 <DocsExternalLink href="https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/ARIA_Live_Regions" text="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. You could also observe the content of <code>&lt;div id="k-live-region"&gt;</code> in the browser console, but note that an announcement will be visible for just a very brief moment.</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#install-plugin" 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
28 changes: 28 additions & 0 deletions jest.conf/setup.js
Original file line number Diff line number Diff line change
Expand Up @@ -50,3 +50,31 @@ global.flushPromises = function flushPromises() {
setImmediate(resolve);
});
};

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

global.expect.extend({
// check that document.body.innerHTML includes html string

toBeInDom(received) {
const pass = removeWhitespaceFromHtml(document.body.innerHTML).includes(
removeWhitespaceFromHtml(received)
);
if (pass) {
return {
message: () => `expected ${received} not to be in the document body`,
pass: true,
};
} else {
return {
message: () => `expected ${received} to be in the document body`,
pass: false,
};
}
},
});
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();
MisRob marked this conversation as resolved.
Show resolved Hide resolved
document.removeEventListener('DOMContentLoaded', onDomReady);
};

if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', onDomReady);
AlexVelezLl marked this conversation as resolved.
Show resolved Hide resolved
} else {
onDomReady();
}
}

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

const { sendPoliteMessage, sendAssertiveMessage } = useKLiveRegion();

describe('useKLiveRegion', () => {
// this is take care of by KThemePlugin.js that is already registered
// in the global jest setup
it(`document body contains live regions upon the KDS plugin initialization`, () => {
expect(`
<div id="k-live-region" class="visuallyhidden">
<div aria-live="polite">
</div>
<div aria-live="assertive">
</div>
</div>
`).toBeInDom();
});

it(`'sendPoliteMessage' updates the live region with the message`, async () => {
expect(`
<div id="k-live-region" class="visuallyhidden">
<div aria-live="polite">
</div>
<div aria-live="assertive">
</div>
</div>
`).toBeInDom();

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

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

it(`'sendAssertiveMessage' updates the live region with the message`, async () => {
expect(`
<div id="k-live-region" class="visuallyhidden">
<div aria-live="polite">
</div>
<div aria-live="assertive">
</div>
</div>
`).toBeInDom();

sendAssertiveMessage('Assertive message');
await new Promise(resolve => setTimeout(resolve, 100));

expect(`
<div id="k-live-region" class="visuallyhidden">
<div aria-live="polite">
</div>
<div aria-live="assertive">
Assertive message
</div>
</div>
`).toBeInDom();

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