Skip to content

Commit

Permalink
PRO-4431: Context menu dynamic focus trap [a11y] (#4815)
Browse files Browse the repository at this point in the history
  • Loading branch information
myovchev authored Dec 3, 2024
1 parent 219b5c3 commit 21dc56e
Show file tree
Hide file tree
Showing 4 changed files with 30 additions and 2 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
11 changes: 10 additions & 1 deletion modules/@apostrophecms/ui/ui/apos/components/AposContextMenu.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
});
Expand All @@ -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,
Expand Down
6 changes: 5 additions & 1 deletion modules/@apostrophecms/ui/ui/apos/components/AposToggle.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
<div class="apos-toggle__container">
<div
class="apos-toggle__slider"
tabindex="0"
:tabindex="disableFocus ? null : '0'"
:class="{'apos-toggle__slider--activated': !modelValue}"
@click="$emit('toggle')"
@keydown.stop.space="$emit('toggle')"
Expand All @@ -18,6 +18,10 @@ export default {
modelValue: {
type: Boolean,
required: true
},
disableFocus: {
type: Boolean,
default: false
}
},
emits: [ 'toggle' ],
Expand Down
10 changes: 10 additions & 0 deletions modules/@apostrophecms/ui/ui/apos/composables/AposFocusTrap.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import {
* Options:
* - `retries`: Number of retries to focus (trap) the first element in the given
* container. Default is 3.
* - `refreshOnCycle`: If true, the elements to focus will be refreshed (query)
* on each cycle. Default is false.
* - `withPriority`: If true, 'data-apos-focus-priority' attribute will be used
* to find the first element to focus. Default is true.
* - `triggerRef`: (optional) A ref to the element that will trigger the focus trap.
Expand All @@ -18,6 +20,7 @@ import {
*
* @param {{
* retries?: number;
* refreshOnCycle?: boolean;
* withPriority?: boolean;
* triggerRef?: import('vue').Ref<HTMLElement | import('vue').ComponentPublicInstance>
* | HTMLElement | boolean;
Expand All @@ -33,6 +36,7 @@ import {
*/
export function useFocusTrap({
triggerRef,
refreshOnCycle = false,
onExit = () => {},
retries = 3,
withPriority = true
Expand All @@ -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;
Expand Down Expand Up @@ -113,6 +118,7 @@ export function useFocusTrap({
if (shouldRun.value) {
focusElement(findChecked(firstElementToFocus, elements));
elementsToFocus.value = elements;
rootRef.value = unref(containerRef);
}
}

Expand Down Expand Up @@ -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);

Expand Down

0 comments on commit 21dc56e

Please sign in to comment.