From 799760440e069ecd1be8d1a7cbaccaa73442f97c Mon Sep 17 00:00:00 2001 From: Hajime Yamasaki Vukelic Date: Mon, 21 Aug 2023 15:01:38 +0200 Subject: [PATCH] Make the header toolbar dropdown menu accessible - Correctly indicate the roles and states of the dropdown menu - Use a vanilla JavaScript implementation of the dropdown - Introduce new folders `client/{js,styles}/custom-elements` - Improve accessibility of the tooltips for the #create-* buttons --- CHANGES.rst | 4 + .../web/client/js/custom_elements/TipBase.js | 123 ++++++++++++++++ .../custom_elements/ind_bypass_block_links.js | 3 +- .../web/client/js/custom_elements/ind_menu.js | 57 +++++++ .../client/js/custom_elements/ind_menu.scss | 49 ++++++ .../js/custom_elements/ind_with_toggletip.js | 54 +++++++ .../js/custom_elements/ind_with_tooltip.js | 50 +++++++ .../web/client/js/custom_elements/tips.scss | 95 ++++++++++++ indico/web/client/js/index.js | 2 + indico/web/client/js/utils/domstate.js | 14 ++ indico/web/client/js/widgets/dynamic-tips.js | 23 +++ indico/web/client/styles/base/_reset.scss | 23 ++- .../client/styles/design_system/_button.scss | 65 +++++--- .../styles/design_system/_button_group.scss | 8 +- .../client/styles/design_system/_dialog.scss | 80 ++++++++++ .../styles/design_system/_form_controls.scss | 5 +- .../styles/design_system/_headings.scss | 27 ++++ .../client/styles/design_system/_index.scss | 5 + .../client/styles/design_system/_layout.scss | 69 ++++++++- .../client/styles/design_system/_link.scss | 34 ++++- .../client/styles/design_system/_popup.scss | 1 + .../client/styles/design_system/_regions.scss | 21 +++ .../client/styles/design_system/_tags.scss | 22 +++ .../client/styles/design_system/_text.scss | 139 ++++++++++++++++++ .../client/styles/design_system/_utils.scss | 4 + .../styles/design_system/variables.scss | 57 ++++++- indico/web/client/styles/partials/_main.scss | 58 ++++++-- indico/web/templates/header.html | 133 +++++++++-------- 28 files changed, 1098 insertions(+), 127 deletions(-) create mode 100644 indico/web/client/js/custom_elements/TipBase.js create mode 100644 indico/web/client/js/custom_elements/ind_menu.js create mode 100644 indico/web/client/js/custom_elements/ind_menu.scss create mode 100644 indico/web/client/js/custom_elements/ind_with_toggletip.js create mode 100644 indico/web/client/js/custom_elements/ind_with_tooltip.js create mode 100644 indico/web/client/js/custom_elements/tips.scss create mode 100644 indico/web/client/js/utils/domstate.js create mode 100644 indico/web/client/js/widgets/dynamic-tips.js create mode 100644 indico/web/client/styles/design_system/_dialog.scss create mode 100644 indico/web/client/styles/design_system/_headings.scss create mode 100644 indico/web/client/styles/design_system/_regions.scss create mode 100644 indico/web/client/styles/design_system/_tags.scss create mode 100644 indico/web/client/styles/design_system/_text.scss diff --git a/CHANGES.rst b/CHANGES.rst index 396ff9ea82c..1fb0a610f8c 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -48,6 +48,8 @@ Improvements :user:`kewisch`) - Allow editors to edit their review comments on editables (:pr:`6008`) - Auto-linking of patterns in minutes (e.g. issue trackers, Github repos...) (:pr:`5998`) +- Allow event managers to delete editables from contributions (:pr:`5778`) +- Support custom user-defined stylesheets with text metric overrides (:pr:`5895`) Bugfixes ^^^^^^^^ @@ -75,6 +77,7 @@ Accessibility thanks :user:`foxbunny`) - Prevent icons from being announced to screen readers as random characters (:issue:`5985`, :pr:`5986`, thanks :user:`foxbunny`) +- Make dropdown menu fully accessible (:issue:`5896`, :pr:`5897`, thanks :user:`foxbunny`) Internal Changes ^^^^^^^^^^^^^^^^ @@ -90,6 +93,7 @@ Internal Changes the removal of registration form fields (:pr:`5924`) - Add a tool ``bin/managemnent/icons_generate.py`` to generate CSS for icomoon icons based on ``selection.json`` (:pr:`5986`, thanks :user:`foxbunny`) +- Add ```` custom element for managing drop-down menus (:issue:`5896`, :pr:`5897`, thanks :user:`foxbunny`) ---- diff --git a/indico/web/client/js/custom_elements/TipBase.js b/indico/web/client/js/custom_elements/TipBase.js new file mode 100644 index 00000000000..17d5e2333b7 --- /dev/null +++ b/indico/web/client/js/custom_elements/TipBase.js @@ -0,0 +1,123 @@ +// This file is part of Indico. +// Copyright (C) 2002 - 2023 CERN +// +// Indico is free software; you can redistribute it and/or +// modify it under the terms of the MIT License; see the +// LICENSE file for more details. + +import {domReady} from 'indico/utils/domstate'; +import './tips.scss'; + +let viewportWidth = document.documentElement.clientWidth; +let viewportHeight = document.documentElement.clientHeight; + +window.addEventListener('DOMContentLoaded', () => requestIdleCallback(updateClientGeometry)); +window.addEventListener('resize', updateClientGeometry); +window.addEventListener('orientationchange', updateClientGeometry); + +function updateClientGeometry() { + viewportWidth = document.documentElement.clientWidth; + viewportHeight = document.documentElement.clientHeight; +} + +export class TipBase extends HTMLElement { + constructor() { + super(); + this.updatePosition = this.updatePosition.bind(this); + this.show = this.show.bind(this); + this.hide = this.hide.bind(this); + this.dismiss = this.dismiss.bind(this); + } + + connectedCallback() { + domReady.then(() => { + this.$tip = this.querySelector('[data-tip-content]'); + console.assert(!!this.$tip, 'Must contain a *[data-tip-content] element'); + this.setup(); + }); + } + + disconnectedCallback() { + window.removeEventListener('keydown', this.dismiss); + } + + get shown() { + return this.hasAttribute('shown'); + } + + set shown(isShown) { + if (this.shown === isShown) { + return; + } + this.toggleAttribute('shown', isShown); + this.dispatchEvent(new Event('toggle')); + } + + getTipCSS() { + const vw = viewportWidth; + const vh = viewportHeight; + const referenceRect = this.getBoundingClientRect(); + const tooltipRect = this.$tip.getBoundingClientRect(); + + let top = 'auto'; + let bottom = 'auto'; + const refCenter = `${referenceRect.x + referenceRect.width / 2}px`; + let arrowBorder; + + console.log(referenceRect); + + // Place above or below? + if (tooltipRect.height < referenceRect.top) { + bottom = `${vh - referenceRect.top}px`; + arrowBorder = 'var(--tooltip-surface-color) transparent transparent'; + } else { + top = `${referenceRect.bottom}px`; + arrowBorder = 'transparent transparent var(--tooltip-surface-color)'; + } + + // Ideal left coordinate (CSS will adjust as needed) + const idealLeft = referenceRect.left + (referenceRect.width - tooltipRect.width) / 2; + // NB: the clamp() formula assumes the tooltip content will always fit the viewport, which is ensured using CSS + const left = `clamp(0.5em, ${idealLeft}px, ${vw - tooltipRect.width}px - 0.5em)`; + + return { + top, + bottom, + left, + 'ref-center': refCenter, + 'arrow-borders': arrowBorder, + }; + } + + updatePosition() { + this.style = ''; + const css = this.getTipCSS(); + for (const key in css) { + this.style.setProperty(`--${key}`, css[key]); + } + } + + dismiss(evt) { + if (!this.shown || evt.code !== 'Escape') { + return; + } + evt.preventDefault(); + this.shown = false; + } + + setup() { + window.addEventListener('keydown', this.dismiss); + this.$tip.addEventListener('click', evt => { + evt.preventDefault(); + }); + this.addEventListener('toggle', () => { + if (this.shown) { + window.addEventListener('resize', this.updatePosition); + window.addEventListener('scroll', this.updatePosition, {passive: true}); + } else { + window.removeEventListener('resize', this.updatePosition); + window.removeEventListener('scroll', this.updatePosition); + } + }); + } +} diff --git a/indico/web/client/js/custom_elements/ind_bypass_block_links.js b/indico/web/client/js/custom_elements/ind_bypass_block_links.js index 4c9645c87e5..1be9430bfba 100644 --- a/indico/web/client/js/custom_elements/ind_bypass_block_links.js +++ b/indico/web/client/js/custom_elements/ind_bypass_block_links.js @@ -5,13 +5,14 @@ // modify it under the terms of the MIT License; see the // LICENSE file for more details. +import {domReady} from 'indico/utils/domstate'; import './ind_bypass_block_links.scss'; customElements.define( 'ind-bypass-block-links', class extends HTMLElement { connectedCallback() { - window.addEventListener('DOMContentLoaded', () => { + domReady.then(() => { const bypassBlockTargets = document.querySelectorAll('[id][data-bypass-target]'); for (const target of bypassBlockTargets) { this.append( diff --git a/indico/web/client/js/custom_elements/ind_menu.js b/indico/web/client/js/custom_elements/ind_menu.js new file mode 100644 index 00000000000..2abb61dc119 --- /dev/null +++ b/indico/web/client/js/custom_elements/ind_menu.js @@ -0,0 +1,57 @@ +// This file is part of Indico. +// Copyright (C) 2002 - 2023 CERN +// +// Indico is free software; you can redistribute it and/or +// modify it under the terms of the MIT License; see the +// LICENSE file for more details. + +import './ind_menu.scss'; +import {domReady} from 'indico/utils/domstate'; + +let lastId = 0; // Track the assigned IDs to give each element a unique ID + +customElements.define( + 'ind-menu', + class extends HTMLElement { + connectedCallback() { + domReady.then(() => { + const trigger = this.querySelector('button'); + const list = this.querySelector('menu'); + + console.assert( + trigger.nextElementSibling === list, + 'The element must come after + + {{ loop(item.items) }} + + + + {% endif %} + {% endfor %} + {% endblock %} - - - - - + + +