From 21dc56ebf13eddefdcdfd731d5ce1013566d57c5 Mon Sep 17 00:00:00 2001 From: Miro Yovchev <2827783+myovchev@users.noreply.github.com> Date: Tue, 3 Dec 2024 15:30:08 +0200 Subject: [PATCH] PRO-4431: Context menu dynamic focus trap [a11y] (#4815) --- CHANGELOG.md | 5 +++++ .../ui/ui/apos/components/AposContextMenu.vue | 11 ++++++++++- .../ui/ui/apos/components/AposToggle.vue | 6 +++++- .../ui/ui/apos/composables/AposFocusTrap.js | 10 ++++++++++ 4 files changed, 30 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 94a6b80207..2b5845787c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,11 @@ * a11y improvements for context menus. * Fixes broken widget preview URL when the image is overridden (module improve) and external build module is registered. +### Adds + +* Adds support for dynamic focus trap in Context menus (prop `dynamicFocus`). When set to `true`, the focusable elements are recalculated on each cycle step. +* Adds option to disable `tabindex` on `AposToggle` component. A new prop `disableFocus` can be set to `false` to disable the focus on the toggle button. It's enabled by default. + ## 4.10.0 (2024-11-20) ### Fixes diff --git a/modules/@apostrophecms/ui/ui/apos/components/AposContextMenu.vue b/modules/@apostrophecms/ui/ui/apos/components/AposContextMenu.vue index c2e072e2d3..a35c8a6b48 100644 --- a/modules/@apostrophecms/ui/ui/apos/components/AposContextMenu.vue +++ b/modules/@apostrophecms/ui/ui/apos/components/AposContextMenu.vue @@ -133,6 +133,14 @@ const props = defineProps({ trapFocus: { type: Boolean, default: true + }, + // When set to true, the elements to focus on will be re-queried + // on everu Tab key press. Use this with caution, as it's a performance + // hit. Only use this if you have a context menu with + // dynamically changing (e.g. AposToggle item enables another item) items. + dynamicFocus: { + type: Boolean, + default: false } }); @@ -159,7 +167,8 @@ const otherMenuOpened = ref(false); const { onTab, runTrap, hasRunningTrap, resetTrap } = useFocusTrap({ - withPriority: true + withPriority: true, + refreshOnCycle: props.dynamicFocus // If enabled, the dropdown gets closed when the focus leaves // the context menu. // triggerRef: dropdownButton, diff --git a/modules/@apostrophecms/ui/ui/apos/components/AposToggle.vue b/modules/@apostrophecms/ui/ui/apos/components/AposToggle.vue index ddbc27a0f4..0ca746c508 100644 --- a/modules/@apostrophecms/ui/ui/apos/components/AposToggle.vue +++ b/modules/@apostrophecms/ui/ui/apos/components/AposToggle.vue @@ -2,7 +2,7 @@
* | HTMLElement | boolean; @@ -33,6 +36,7 @@ import { */ export function useFocusTrap({ triggerRef, + refreshOnCycle = false, onExit = () => {}, retries = 3, withPriority = true @@ -47,6 +51,7 @@ export function useFocusTrap({ const shouldRun = ref(false); const isRunning = ref(false); const currentRetries = ref(0); + const rootRef = ref(null); const elementsToFocus = ref([]); const hasRunningTrap = computed(() => { return isRunning.value; @@ -113,6 +118,7 @@ export function useFocusTrap({ if (shouldRun.value) { focusElement(findChecked(firstElementToFocus, elements)); elementsToFocus.value = elements; + rootRef.value = unref(containerRef); } } @@ -154,6 +160,10 @@ export function useFocusTrap({ * @param {KeyboardEvent} event */ function cycle(event) { + if (refreshOnCycle && rootRef.value) { + const elements = [ ...unref(rootRef).querySelectorAll(selector) ]; + elementsToFocus.value = elements; + } const elements = unref(elementsToFocus); parentCycleElementsToFocus(event, elements, focus);