From 8694adbad2d08cf681309ad85a98beb3a32bfb17 Mon Sep 17 00:00:00 2001 From: MisRob Date: Wed, 10 Jul 2024 21:52:00 +0200 Subject: [PATCH 1/6] Add live regions logic --- CHANGELOG.md | 11 ++ docs/assets/main.scss | 5 + docs/pages/installation.vue | 40 ++++++ docs/pages/kimg.vue | 10 -- docs/pages/usekliveregion.vue | 123 ++++++++++++++++++ docs/tableOfContents.js | 19 +++ lib/KThemePlugin.js | 22 ++++ .../useKLiveRegion/__tests__/index.spec.js | 84 ++++++++++++ lib/composables/useKLiveRegion/index.js | 88 +++++++++++++ 9 files changed, 392 insertions(+), 10 deletions(-) create mode 100644 docs/pages/installation.vue create mode 100644 docs/pages/usekliveregion.vue create mode 100644 lib/composables/useKLiveRegion/__tests__/index.spec.js create mode 100644 lib/composables/useKLiveRegion/index.js diff --git a/CHANGELOG.md b/CHANGELOG.md index b632bd80b..3677d547c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/docs/assets/main.scss b/docs/assets/main.scss index 541c1a37e..60f0408ae 100644 --- a/docs/assets/main.scss +++ b/docs/assets/main.scss @@ -83,3 +83,8 @@ dt { dd { margin-left: 20px; } + +em { + font-style: normal; + font-weight: bold; +} diff --git a/docs/pages/installation.vue b/docs/pages/installation.vue new file mode 100644 index 000000000..059fada0e --- /dev/null +++ b/docs/pages/installation.vue @@ -0,0 +1,40 @@ + + diff --git a/docs/pages/kimg.vue b/docs/pages/kimg.vue index 51e3698d0..5d983a46c 100644 --- a/docs/pages/kimg.vue +++ b/docs/pages/kimg.vue @@ -308,13 +308,3 @@ export default {}; - - - diff --git a/docs/pages/usekliveregion.vue b/docs/pages/usekliveregion.vue new file mode 100644 index 000000000..744f2703c --- /dev/null +++ b/docs/pages/usekliveregion.vue @@ -0,0 +1,123 @@ + + + + diff --git a/docs/tableOfContents.js b/docs/tableOfContents.js index bad622b79..3166c255b 100644 --- a/docs/tableOfContents.js +++ b/docs/tableOfContents.js @@ -68,6 +68,10 @@ export default [ path: '/', title: 'Home', }), + new Page({ + path: '/installation', + title: 'Installation', + }), new Page({ path: '/principles', title: 'Design principles', @@ -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', diff --git a/lib/KThemePlugin.js b/lib/KThemePlugin.js index 8757ecdc1..8f7e377a4 100644 --- a/lib/KThemePlugin.js +++ b/lib/KThemePlugin.js @@ -1,3 +1,4 @@ +import { isNuxtServerSideRendering } from '../lib/utils'; import computedClass from './styles/computedClass'; import KBreadcrumbs from './KBreadcrumbs'; @@ -39,6 +40,10 @@ 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 /** @@ -46,6 +51,23 @@ require('./grids/globalStyles.js'); // global grid styles * 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: { diff --git a/lib/composables/useKLiveRegion/__tests__/index.spec.js b/lib/composables/useKLiveRegion/__tests__/index.spec.js new file mode 100644 index 000000000..1eb23264f --- /dev/null +++ b/lib/composables/useKLiveRegion/__tests__/index.spec.js @@ -0,0 +1,84 @@ +import useKLiveRegion from '../index.js'; + +const { sendPoliteMessage, sendAssertiveMessage } = useKLiveRegion(); +function removeWhitespaceFromHtml(htmlString) { + // https://stackoverflow.com/a/33108909 + return htmlString.replace(/>\s+|\s+ { + // 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`, () => { + assertDocumentBodyIncludes(` +
+
+
+
+
+
+ `); + }); + + it(`'sendPoliteMessage' updates the live region with the message`, async () => { + assertDocumentBodyIncludes(` +
+
+
+
+
+
+ `); + + sendPoliteMessage('Polite message'); + await new Promise(resolve => setTimeout(resolve, 100)); + assertDocumentBodyIncludes(` +
+
+ Polite message +
+
+
+
+ `); + + // cleanup + sendPoliteMessage(''); + }); + + it(`'sendAssertiveMessage' updates the live region with the message`, async () => { + assertDocumentBodyIncludes(` +
+
+
+
+
+
+ `); + + sendAssertiveMessage('Assertive message'); + await new Promise(resolve => setTimeout(resolve, 100)); + + assertDocumentBodyIncludes(` +
+
+
+
+ Assertive message +
+
+ `); + + // cleanup + sendAssertiveMessage(''); + }); +}); diff --git a/lib/composables/useKLiveRegion/index.js b/lib/composables/useKLiveRegion/index.js new file mode 100644 index 000000000..3b0f89ee0 --- /dev/null +++ b/lib/composables/useKLiveRegion/index.js @@ -0,0 +1,88 @@ +const LIVE_REGION_WRAPPER_ID = 'k-live-region'; +const POLITE_LIVE_REGION_SELECTOR = `#${LIVE_REGION_WRAPPER_ID} [aria-live="polite"]`; +const ASSERTIVE_LIVE_REGION_SELECTOR = `#${LIVE_REGION_WRAPPER_ID} [aria-live="assertive"]`; + +/** + * Exposes public 'sendPoliteMessage' and 'sendAssertiveMessage' + * functions that can be used to update polite/assertive + * aria-live regions. + * + * Also provides private '_mountLiveRegion'. + */ +export default function useKLiveRegion() { + /** + * Inserts assertive and polite live regions + * to an application's document body. + * For private use in KDS (see KThemePlugin.js) + */ + function _mountLiveRegion() { + const politeRegionEl = document.querySelector(POLITE_LIVE_REGION_SELECTOR); + const assertiveRegionEl = document.querySelector(ASSERTIVE_LIVE_REGION_SELECTOR); + + // Already mounted, so don't mount again, + // just make sure elements are available in window + if (politeRegionEl && assertiveRegionEl) { + window.politeRegionEl = politeRegionEl; + window.assertiveRegionEl = assertiveRegionEl; + return; + } + + const newWrapperEl = document.createElement('div'); + newWrapperEl.id = LIVE_REGION_WRAPPER_ID; + newWrapperEl.className = 'visuallyhidden'; + + const newPoliteRegionEl = document.createElement('div'); + newPoliteRegionEl.setAttribute('aria-live', 'polite'); + + const newAssertiveRegionEl = document.createElement('div'); + newAssertiveRegionEl.setAttribute('aria-live', 'assertive'); + + newWrapperEl.appendChild(newPoliteRegionEl); + newWrapperEl.appendChild(newAssertiveRegionEl); + + document.body.insertBefore(newWrapperEl, document.body.firstChild); + + // Save for later use so that we don't need to query + // every time we call the 'sendPoliteMessage'/'sendAssertiveMessage'. + // Storing these two elements in the variables in this file, + // even in the highest scope above the 'useKLiveRegion' function, + // is insufficient since this file can be on some occasions + // loaded repeatedly while live regions are already in the DOM. + window.politeRegionEl = newPoliteRegionEl; + window.assertiveRegionEl = newAssertiveRegionEl; + } + + function _sendMessage(message, el) { + try { + el.textContent = message; + } catch (error) { + console.error('[useKLiveRegion] Could not send the message:', error); + } finally { + // empty the live region + // recommended in https://www.sarasoueidan.com/blog/accessible-notifications-with-aria-live-regions-part-2/ + setTimeout(() => { + el.textContent = ''; + }, 350); + } + } + + /** + * Updates the polite aria live region with the provided message. + */ + function sendPoliteMessage(message) { + _sendMessage(message, window.politeRegionEl); + } + + /** + * Updates the assertive aria live region with the provided message. + */ + function sendAssertiveMessage(message) { + _sendMessage(message, window.assertiveRegionEl); + } + + return { + _mountLiveRegion, + sendPoliteMessage, + sendAssertiveMessage, + }; +} From 3525ca3dff3751703a4cb2499935ba2450209be7 Mon Sep 17 00:00:00 2001 From: Michaela Robosova Date: Thu, 1 Aug 2024 15:52:36 +0200 Subject: [PATCH 2/6] Link MDN docs on ARIA live regions --- docs/pages/usekliveregion.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/pages/usekliveregion.vue b/docs/pages/usekliveregion.vue index 744f2703c..5aa392b5b 100644 --- a/docs/pages/usekliveregion.vue +++ b/docs/pages/usekliveregion.vue @@ -3,7 +3,7 @@ -

A composable that offers sendPoliteMessage and sendAssertiveMessage functions that send polite and assertive messages to their corresponding ARIA live regions.

+

A composable that offers sendPoliteMessage and sendAssertiveMessage functions that send polite and assertive messages to their corresponding .

From 2551590683cb0b462e07e6ca0e5079cd4e3a3bac Mon Sep 17 00:00:00 2001 From: Michaela Robosova Date: Thu, 1 Aug 2024 15:54:26 +0200 Subject: [PATCH 3/6] Fix link target --- docs/pages/usekliveregion.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/pages/usekliveregion.vue b/docs/pages/usekliveregion.vue index 5aa392b5b..1a4c1feb1 100644 --- a/docs/pages/usekliveregion.vue +++ b/docs/pages/usekliveregion.vue @@ -77,7 +77,7 @@
  • - that attaches live regions to an application's DOM + that attaches live regions to an application's DOM
From e3f7ebb38c961869d4789c3f1e615abe1a60e98d Mon Sep 17 00:00:00 2001 From: Michaela Robosova Date: Thu, 1 Aug 2024 15:58:29 +0200 Subject: [PATCH 4/6] Add cross-links --- docs/pages/installation.vue | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/pages/installation.vue b/docs/pages/installation.vue index 059fada0e..2dea90431 100644 --- a/docs/pages/installation.vue +++ b/docs/pages/installation.vue @@ -21,16 +21,16 @@ This ensures the following:
    -
  • Installs $themeBrand, $themeTokens $themePalette, and $computedClass helpers on all Vue instances.
  • +
  • Installs $themeBrand, $themeTokens $themePalette, and $computedClass helpers on all Vue instances (see ).
  • Provides $coreOutline, $inputModality, $mediaType, and $isPrint computed properties as well as $print method to all Vue instances.
  • Globally registers all KDS Vue components.
  • -
  • Inserts assertive and polite ARIA live regions to your application's document body (see to understand how to utilize them).
  • +
  • Inserts assertive and polite ARIA live regions to your application's document body (see ).

- Until this section is better documented (TODO link GH issue), refer to . + Until this section is better documented, refer to .

From 2d355b408841b26f0e17dbe7aa8c0456c3baf2f8 Mon Sep 17 00:00:00 2001 From: Michaela Robosova Date: Thu, 1 Aug 2024 17:26:10 +0200 Subject: [PATCH 5/6] Convert utility function to the custom matcher --- jest.conf/setup.js | 28 ++++++++++++++++ .../useKLiveRegion/__tests__/index.spec.js | 33 ++++++------------- 2 files changed, 38 insertions(+), 23 deletions(-) diff --git a/jest.conf/setup.js b/jest.conf/setup.js index b41acc4eb..99e5b0200 100644 --- a/jest.conf/setup.js +++ b/jest.conf/setup.js @@ -50,3 +50,31 @@ global.flushPromises = function flushPromises() { setImmediate(resolve); }); }; + +function removeWhitespaceFromHtml(htmlString) { + // https://stackoverflow.com/a/33108909 + return htmlString.replace(/>\s+|\s+ `expected ${received} not to be in the document body`, + pass: true, + }; + } else { + return { + message: () => `expected ${received} to be in the document body`, + pass: false, + }; + } + }, +}); diff --git a/lib/composables/useKLiveRegion/__tests__/index.spec.js b/lib/composables/useKLiveRegion/__tests__/index.spec.js index 1eb23264f..708000ab5 100644 --- a/lib/composables/useKLiveRegion/__tests__/index.spec.js +++ b/lib/composables/useKLiveRegion/__tests__/index.spec.js @@ -1,47 +1,34 @@ import useKLiveRegion from '../index.js'; const { sendPoliteMessage, sendAssertiveMessage } = useKLiveRegion(); -function removeWhitespaceFromHtml(htmlString) { - // https://stackoverflow.com/a/33108909 - return htmlString.replace(/>\s+|\s+ { // 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`, () => { - assertDocumentBodyIncludes(` + expect(`
- `); + `).toBeInDom(); }); it(`'sendPoliteMessage' updates the live region with the message`, async () => { - assertDocumentBodyIncludes(` + expect(`
- `); + `).toBeInDom(); sendPoliteMessage('Polite message'); await new Promise(resolve => setTimeout(resolve, 100)); - assertDocumentBodyIncludes(` + expect(`
Polite message @@ -49,26 +36,26 @@ describe('useKLiveRegion', () => {
- `); + `).toBeInDom(); // cleanup sendPoliteMessage(''); }); it(`'sendAssertiveMessage' updates the live region with the message`, async () => { - assertDocumentBodyIncludes(` + expect(`
- `); + `).toBeInDom(); sendAssertiveMessage('Assertive message'); await new Promise(resolve => setTimeout(resolve, 100)); - assertDocumentBodyIncludes(` + expect(`
@@ -76,7 +63,7 @@ describe('useKLiveRegion', () => { Assertive message
- `); + `).toBeInDom(); // cleanup sendAssertiveMessage(''); From 0c12eff3823e6d9791b7ab9522717edae02d49d0 Mon Sep 17 00:00:00 2001 From: Michaela Robosova Date: Thu, 1 Aug 2024 17:35:22 +0200 Subject: [PATCH 6/6] Update docs --- docs/pages/usekliveregion.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/pages/usekliveregion.vue b/docs/pages/usekliveregion.vue index 1a4c1feb1..be46710a3 100644 --- a/docs/pages/usekliveregion.vue +++ b/docs/pages/usekliveregion.vue @@ -57,7 +57,7 @@ -

Send messages below and turn on your screen reader or observe the content of <div id="k-live-region"> in the browser console.

+

Send messages below and turn on your screen reader. You could also observe the content of <div id="k-live-region"> in the browser console, but note that an announcement will be visible for just a very brief moment.