outsystems-ui
Version:
OutSystems UI Framework
300 lines (259 loc) • 11.9 kB
text/typescript
// eslint-disable-next-line @typescript-eslint/no-unused-vars
namespace OSFramework.OSUI.Behaviors {
// FocusTrap type
export type FocusTrapParams = {
canContainOtherPatterns?: boolean;
focusBottomCallback?: GlobalCallbacks.Generic;
focusTargetElement: HTMLElement;
focusTopCallback?: GlobalCallbacks.Generic;
};
/**
* Class that represents the focus trap.
*
* @export
* @class FocusTrap
*/
export class FocusTrap {
private _canTargetContainOtherPatts = false;
private _firstFocusableElement: HTMLElement;
private _focusBottomCallback: GlobalCallbacks.Generic;
private _focusBottomHandlerCallback: GlobalCallbacks.Generic;
private _focusTopCallback: GlobalCallbacks.Generic;
private _focusTopHandlerCallback: GlobalCallbacks.Generic;
private _focusableElements: HTMLElement[];
private _hasBeenPassThoughFirstOne = false;
private _lastFocusableElement: HTMLElement;
private _predictableBottomElement: HTMLElement;
private _predictableTopElement: HTMLElement;
private _targetElement: HTMLElement;
/**
* Creates an instance of FocusTrap.
*
* @param {FocusTrapParams} opts
* @memberof FocusTrap
*/
constructor(opts: FocusTrapParams) {
this._focusableElements = [];
// Store the focus target element
this._targetElement = opts.focusTargetElement;
// Set the callbacks to focusable elements
this._focusBottomCallback = opts.focusBottomCallback;
this._focusTopCallback = opts.focusTopCallback;
// Set the indicator that will reflect if the target element is capable to have other patterns inside
this._canTargetContainOtherPatts = opts.canContainOtherPatterns || false;
// Create the elements needed!
this._buildPredictableElements();
}
// Method to create elements
private _buildPredictableElements(): void {
// Create the focusable elements
this._predictableTopElement = document.createElement(GlobalEnum.HTMLElement.Span);
this._predictableBottomElement = document.createElement(GlobalEnum.HTMLElement.Span);
// Add the created elements to DOM
this._targetElement.prepend(this._predictableTopElement);
this._targetElement.append(this._predictableBottomElement);
// Set the default properties of focusable elements
this._setFocusableProperties();
// Set focusable elements
this._setFocusableElements();
}
// Handler for bottom on top element
private _focusBottomHandler(): void {
this._focusHandler(this._predictableBottomElement, this._focusBottomCallback);
}
// Method that will handle the focus behaviour
private _focusHandler(focusableElement: HTMLElement, callback: GlobalCallbacks.Generic): void {
// Ensure the list of focusable elements is updated!
this._setFocusableElements();
// Ensure callback has been not defined in order to manage the focus trap.
if (callback === undefined) {
if (focusableElement === this._predictableTopElement && this._hasBeenPassThoughFirstOne === false) {
this._firstFocusableElement.focus();
this._hasBeenPassThoughFirstOne = true;
} else {
this._lastFocusableElement.focus();
}
if (focusableElement === this._predictableBottomElement) {
this._firstFocusableElement.focus();
this._hasBeenPassThoughFirstOne = true;
}
} else {
// If a callback has been defined, the focus will be managed on the context of that callback (pattern)
callback();
}
}
// Handler for focus on top element
private _focusTopHandler(): void {
this._focusHandler(this._predictableTopElement, this._focusTopCallback);
}
// Method that removes the added event listeners
private _removeEventListeners(): void {
this._predictableBottomElement.removeEventListener(
GlobalEnum.HTMLEvent.Focus,
this._focusBottomHandlerCallback
);
this._predictableTopElement.removeEventListener(GlobalEnum.HTMLEvent.Focus, this._focusTopHandlerCallback);
}
private _setCallbacks(): void {
this._focusBottomHandlerCallback = this._focusBottomHandler.bind(this);
this._focusTopHandlerCallback = this._focusTopHandler.bind(this);
}
// Method to add the event listeners to the created elements
private _setEventListeners(): void {
this._predictableBottomElement.addEventListener(
GlobalEnum.HTMLEvent.Focus,
this._focusBottomHandlerCallback
);
this._predictableTopElement.addEventListener(GlobalEnum.HTMLEvent.Focus, this._focusTopHandlerCallback);
}
// Method to set the focusable elements to be used
private _setFocusableElements(includeTabIndexHidden = false): void {
/**
* NOTE: (MORE INFO @ROU-10963)
* We can find 2 sections below that must be explained:
* - TO BE REMOVED
* - After the missing part of the code is implemented (mentioned at the bullet point below), code inside this section should be removed!
* - TO KEEP
* - With this implementation we ensure that the focusable elements are not part of the inner patterns
* - However we still need to ensure focusable elements inside child patterns should be managed through their own FocusTrap
* - For now code is not prepared to it, but it should be considered in the future, that's why we keep this implementation commented
*/
/* TO BE REMOVED • START ••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••• */
// eslint-disable-next-line @typescript-eslint/no-unused-vars
includeTabIndexHidden = false;
this._focusableElements = Helper.Dom.GetFocusableElements(this._targetElement, false);
// Check if predicted elements exist at the _focusableElements
for (const predictedElement of this._focusableElements.filter(
(item) => item === this._predictableTopElement || item === this._predictableBottomElement
)) {
// If so, remove them from the array collection of _focusableElements
this._focusableElements.splice(this._focusableElements.indexOf(predictedElement), 1);
}
/* TO BE REMOVED • END ••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••• */
/* TO KEEP •••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••
// Ensure the list of focusable elements is empty
this._focusableElements.length = 0;
// Get all the possible focusable
const possibleFocusableElements = Helper.Dom.GetFocusableElements(
this._targetElement,
includeTabIndexHidden
);
// Ensure we get the proper list of focusable elements
for (const element of possibleFocusableElements) {
// Get the closest data block from the element
const closestDataBlock = element.closest(GlobalEnum.DataBlocksTag.DataBlock);
// Check if the element is a child of the given targetElement
if (closestDataBlock === this._targetElement || closestDataBlock.contains(this._targetElement)) {
// Check if element is not a predictable element
if (element !== this._predictableTopElement && element !== this._predictableBottomElement) {
this._focusableElements.push(element);
}
// If the targetElement is a "special" element, we need to ensure that the focusable elements are not part of the inner patterns
// Example of having an OverflowMenu inside the Sidebar and also get all the focusable elements from it as a part of the ones at the sideBar context
} else if (this._canTargetContainOtherPatts) {
// Ensure the element we bring from the inner pattern is the default tabindex element for it!
if (Helper.Dom.Attribute.Has(element, Constants.FocusableTabIndexDefault)) {
this._focusableElements.push(element);
}
}
}
••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••• */
// Remove the first element from array, because of predictable top element added for trapping
this._firstFocusableElement = this._focusableElements[0];
// Remove the last element from array, because of predictable bottom element added for trapping
this._lastFocusableElement = this._focusableElements[this._focusableElements.length - 1];
}
// Method to add properties to HTML elements
private _setFocusableProperties(): void {
// Set CSS classes
Helper.Dom.Styles.AddClass(this._predictableBottomElement, GlobalEnum.FocusTrapClasses.FocusTrapBottom);
Helper.Dom.Styles.AddClass(this._predictableBottomElement, Constants.AccessibilityHideElementClass);
Helper.Dom.Styles.AddClass(this._predictableTopElement, GlobalEnum.FocusTrapClasses.FocusTrapTop);
Helper.Dom.Styles.AddClass(this._predictableTopElement, Constants.AccessibilityHideElementClass);
// Hide by default the focusable elements
this.disableForA11y();
// Add event listeneres to focusable elements
this._setEventListeners();
}
// Method that unsets all the callback defined
private _unsetCallbacks(): void {
this._focusBottomCallback = undefined;
this._focusTopCallback = undefined;
this._focusBottomHandlerCallback = undefined;
this._focusTopHandlerCallback = undefined;
}
/**
* Set element disabled for A11Y
*
* @memberof OSFramework.Behaviors.FocusTrap
*/
public disableForA11y(): void {
this._hasBeenPassThoughFirstOne = false;
// Unset A11Y properties from bottom & top focusable element
Helper.A11Y.TabIndexFalse(this._predictableBottomElement);
Helper.A11Y.TabIndexFalse(this._predictableTopElement);
// Set A11Y AriaHidden as true
Helper.A11Y.AriaHiddenTrue(this._predictableBottomElement);
Helper.A11Y.AriaHiddenTrue(this._predictableTopElement);
}
/**
* Method to remove the event listeners and unset the callbacks
*
* @memberof OSFramework.Behaviors.FocusTrap
*/
public dispose(): void {
// remove events added
this._removeEventListeners();
// unset defined callback
this._unsetCallbacks();
// ensure we also remove the html elements
this._predictableTopElement.remove();
this._predictableBottomElement.remove();
}
/**
* Set element enabled for A11Y
*
* @memberof OSFramework.Behaviors.FocusTrap
*/
public enableForA11y(): void {
// Set A11Y properties to bottom & top focusable element
Helper.A11Y.TabIndexTrue(this._predictableBottomElement);
Helper.A11Y.TabIndexTrue(this._predictableTopElement);
// Set A11Y AriaHidden as false
Helper.A11Y.AriaHiddenFalse(this._predictableBottomElement);
Helper.A11Y.AriaHiddenFalse(this._predictableTopElement);
// Ensure the list of focusable elements is updated, predictable elements starts with TabIndex Hidden
this._setFocusableElements(true);
}
/**
* Getter that allows to obtain the bottomElement reference
*
* @readonly
* @type {HTMLElement}
* @memberof OSFramework.Behaviors.FocusTrap
*/
public get bottomElement(): HTMLElement {
return this._predictableBottomElement;
}
/**
* Getter that allows to obtain the bottomElement reference
*
* @readonly
* @type {HTMLElement}
* @memberof OSFramework.Behaviors.FocusTrap
*/
public get topElement(): HTMLElement {
return this._predictableTopElement;
}
/**
* Getter that allows to obtain the list of focusable elements
*
* @readonly
* @type {HTMLElement}
* @memberof OSFramework.Behaviors.FocusTrap
*/
public get focusableElements(): HTMLElement[] {
return this._focusableElements;
}
}
}