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

Manage focus for HUD components #11985

Merged
2 changes: 1 addition & 1 deletion CHANGELOG-WIP.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@
- Added `Craft.BaseElementIndex::getSourceLevel()`.

### Changed
- Improved the control panel accessibility. ([#10546](https://github.com/craftcms/cms/pull/10546), [#11534](https://github.com/craftcms/cms/pull/11534), [#11565](https://github.com/craftcms/cms/pull/11565), [#11578](https://github.com/craftcms/cms/pull/11578), [#11589](https://github.com/craftcms/cms/pull/11589), [#11604](https://github.com/craftcms/cms/pull/11604), [#11610](https://github.com/craftcms/cms/pull/11610), [#11611](https://github.com/craftcms/cms/pull/11611), [#11613](https://github.com/craftcms/cms/pull/11613), [#11636](https://github.com/craftcms/cms/pull/11636), [#11662](https://github.com/craftcms/cms/pull/11662)[#11703](https://github.com/craftcms/cms/pull/11703), [#11727](https://github.com/craftcms/cms/pull/11727), [#11763](https://github.com/craftcms/cms/pull/11763), [#11768](https://github.com/craftcms/cms/pull/11768), [#11775](https://github.com/craftcms/cms/pull/11775), [#11844](https://github.com/craftcms/cms/pull/11844), [#11905](https://github.com/craftcms/cms/pull/11905), [#11906](https://github.com/craftcms/cms/pull/11906), [#11911](https://github.com/craftcms/cms/pull/11911), [#11915](https://github.com/craftcms/cms/pull/11915), [#11926](https://github.com/craftcms/cms/discussions/11926), [#11942](https://github.com/craftcms/cms/pull/11942), [#11945](https://github.com/craftcms/cms/pull/11945), [#11952](https://github.com/craftcms/cms/pull/11952), [#11953](https://github.com/craftcms/cms/pull/11953))
- Improved the control panel accessibility. ([#10546](https://github.com/craftcms/cms/pull/10546), [#11534](https://github.com/craftcms/cms/pull/11534), [#11565](https://github.com/craftcms/cms/pull/11565), [#11578](https://github.com/craftcms/cms/pull/11578), [#11589](https://github.com/craftcms/cms/pull/11589), [#11604](https://github.com/craftcms/cms/pull/11604), [#11610](https://github.com/craftcms/cms/pull/11610), [#11611](https://github.com/craftcms/cms/pull/11611), [#11613](https://github.com/craftcms/cms/pull/11613), [#11636](https://github.com/craftcms/cms/pull/11636), [#11662](https://github.com/craftcms/cms/pull/11662)[#11703](https://github.com/craftcms/cms/pull/11703), [#11727](https://github.com/craftcms/cms/pull/11727), [#11763](https://github.com/craftcms/cms/pull/11763), [#11768](https://github.com/craftcms/cms/pull/11768), [#11775](https://github.com/craftcms/cms/pull/11775), [#11844](https://github.com/craftcms/cms/pull/11844), [#11905](https://github.com/craftcms/cms/pull/11905), [#11906](https://github.com/craftcms/cms/pull/11906), [#11911](https://github.com/craftcms/cms/pull/11911), [#11915](https://github.com/craftcms/cms/pull/11915), [#11926](https://github.com/craftcms/cms/discussions/11926), [#11942](https://github.com/craftcms/cms/pull/11942), [#11945](https://github.com/craftcms/cms/pull/11945), [#11952](https://github.com/craftcms/cms/pull/11952), [#11953](https://github.com/craftcms/cms/pull/11953), [#11985](https://github.com/craftcms/cms/pull/11985))
- Improved control panel moblile support. ([#11963](https://github.com/craftcms/cms/pull/11963))
- Element indexes now respect field layouts’ user conditions when determining which custom field columns to show. ([#11913](https://github.com/craftcms/cms/pull/11913))
- Element index footers now stick to the bottom of the window, and element action triggers are now inserted into the footer rather than replacing the contents of the page’s toolbar. ([#11844](https://github.com/craftcms/cms/pull/11844))
Expand Down
2 changes: 1 addition & 1 deletion src/web/assets/garnish/dist/garnish.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion src/web/assets/garnish/dist/garnish.js.map

Large diffs are not rendered by default.

31 changes: 31 additions & 0 deletions src/web/assets/garnish/src/Garnish.js
Original file line number Diff line number Diff line change
Expand Up @@ -410,6 +410,37 @@ Garnish = $.extend(Garnish, {
return $(container).find(':focusable').first();
},

/**
* Returns a collection of all keyboard focusable-elements inside a container
* @param {object} container
* @return {object} A collection of keyboard-focusable elements
*/
getKeyboardFocusableElements: function (container) {
const $focusable = $(container).find(':focusable');
const $keyboardFocusable = $focusable.filter((index, element) => {
return Garnish.isKeyboardFocusable(element);
});

return $keyboardFocusable;
},

/**
* Returns whether the element is focusable by keyboard (i.e. does not have tabindex of -1)
* @param {object} element
* @return {boolean}
*/
isKeyboardFocusable: function (element) {
let keyboardFocusable;

if (!$(element).is(':focusable') || $(element).attr('tabindex') === '-1') {
keyboardFocusable = false;
} else {
keyboardFocusable = true;
}

return keyboardFocusable;
},

/**
* Traps focus within a container, so when focus is tabbed out of it, it’s cycled back into it.
* @param {Object} container
Expand Down
67 changes: 67 additions & 0 deletions src/web/assets/garnish/src/HUD.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export default Base.extend(
$mainContainer: null,
$main: null,
$shade: null,
$nextFocusableElement: null,

showing: false,
orientation: null,
Expand Down Expand Up @@ -109,6 +110,43 @@ export default Base.extend(
'updateSizeAndPosition'
);
}

// When the menu is expanded, tabbing on the trigger should move focus into it
this.addListener(this.$trigger, 'keydown', (ev) => {
if (ev.keyCode === Garnish.TAB_KEY && !ev.shiftKey && this.showing) {
const $focusableElement = Garnish.getKeyboardFocusableElements(
this.$hud
).first();
if ($focusableElement.length) {
ev.preventDefault();
$focusableElement.focus();
}
}
});

// Add listener to manage focus
this.addListener(this.$hud, 'keydown', function (event) {
const {keyCode} = event;

if (keyCode !== Garnish.TAB_KEY) return;

const $focusableElements = Garnish.getKeyboardFocusableElements(
this.$hud
);
const index = $focusableElements.index(event.target);

if (index === 0 && event.shiftKey) {
event.preventDefault();
this.$trigger.focus();
} else if (
index === $focusableElements.length - 1 &&
!event.shiftKey &&
this.$nextFocusableElement
) {
event.preventDefault();
this.$nextFocusableElement.focus();
}
});
},

/**
Expand Down Expand Up @@ -187,6 +225,25 @@ export default Base.extend(
);
}

// Find the next focusable element in the DOM after the trigger.
// Shift-tabbing on it should take focus back into the container.
const $focusableElements = Garnish.$bod.find(':focusable');
const triggerIndex = $focusableElements.index(this.$trigger[0]);
if (triggerIndex !== -1 && $focusableElements.length > triggerIndex + 1) {
this.$nextFocusableElement = $focusableElements.eq(triggerIndex + 1);
this.addListener(this.$nextFocusableElement, 'keydown', (ev) => {
if (ev.keyCode === Garnish.TAB_KEY && ev.shiftKey) {
const $focusableElement = Garnish.getKeyboardFocusableElements(
this.$hud
).last();
if ($focusableElement.length) {
ev.preventDefault();
$focusableElement.focus();
}
}
});
}

this.onShow();
this.enable();

Expand Down Expand Up @@ -522,6 +579,16 @@ export default Base.extend(
this.showing = false;
delete Garnish.HUD.activeHUDs[this._namespace];
Garnish.uiLayerManager.removeLayer();

if (Garnish.focusIsInside(this.$hud)) {
this.$trigger.trigger('focus');
}

if (this.$nextFocusableElement) {
this.removeListener(this.$nextFocusableElement, 'keydown');
this.$nextFocusableElement = null;
}

this.onHide();
},

Expand Down