Skip to content

Commit b15706d

Browse files
authored
Merge pull request #972 from OutSystems/ROU-10963
ROU-10963: Fixed tab navigation inside sidebar.
2 parents e5cb64b + c9f9cb8 commit b15706d

File tree

15 files changed

+96
-16
lines changed

15 files changed

+96
-16
lines changed

.vscode/settings.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"diffEditor.ignoreTrimWhitespace": true,
3-
"files.autoSave": "afterDelay",
3+
"files.autoSave": "off",
44
"editor.minimap.maxColumn": 170,
55
"editor.wordWrapColumn": 170,
66
"editor.formatOnSave": true,

src/scripts/OSFramework/OSUI/Behaviors/FocusTrap.ts

Lines changed: 36 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
namespace OSFramework.OSUI.Behaviors {
33
// FocusTrap type
44
export type FocusTrapParams = {
5+
canContainOtherPatterns?: boolean;
56
focusBottomCallback?: GlobalCallbacks.Generic;
67
focusTargetElement: HTMLElement;
78
focusTopCallback?: GlobalCallbacks.Generic;
@@ -14,6 +15,7 @@ namespace OSFramework.OSUI.Behaviors {
1415
* @class FocusTrap
1516
*/
1617
export class FocusTrap {
18+
private _canTargetContainOtherPatts = false;
1719
private _firstFocusableElement: HTMLElement;
1820
private _focusBottomCallback: GlobalCallbacks.Generic;
1921
private _focusTopCallback: GlobalCallbacks.Generic;
@@ -31,13 +33,18 @@ namespace OSFramework.OSUI.Behaviors {
3133
* @memberof FocusTrap
3234
*/
3335
constructor(opts: FocusTrapParams) {
36+
this._focusableElements = [];
37+
3438
// Store the focus target element
3539
this._targetElement = opts.focusTargetElement;
3640

3741
// Set the callbacks to focusable elements
3842
this._focusBottomCallback = opts.focusBottomCallback;
3943
this._focusTopCallback = opts.focusTopCallback;
4044

45+
// Set the indicator that will reflect if the target element is capable to have other patterns inside
46+
this._canTargetContainOtherPatts = opts.canContainOtherPatterns || false;
47+
4148
// Create the elements needed!
4249
this._buildPredictableElements();
4350
}
@@ -115,15 +122,34 @@ namespace OSFramework.OSUI.Behaviors {
115122
}
116123

117124
// Method to set the focusable elements to be used
118-
private _setFocusableElements(): void {
119-
this._focusableElements = Helper.Dom.GetFocusableElements(this._targetElement);
120-
121-
// Check if predicted elements exist at the _focusableElements
122-
for (const predictedElement of this._focusableElements.filter(
123-
(item) => item === this._predictableTopElement || item === this._predictableBottomElement
124-
)) {
125-
// If so, remove them from the array collection of _focusableElements
126-
this._focusableElements.splice(this._focusableElements.indexOf(predictedElement), 1);
125+
private _setFocusableElements(includeTabIndexHidden = false): void {
126+
// Ensure the list of focusable elements is empty
127+
this._focusableElements.length = 0;
128+
129+
// Get all the possible focusable
130+
const possibleFocusableElements = Helper.Dom.GetFocusableElements(
131+
this._targetElement,
132+
includeTabIndexHidden
133+
);
134+
135+
// Ensure we get the proper list of focusable elements
136+
for (const element of possibleFocusableElements) {
137+
// Get the closest data block from the element
138+
const closestDataBlock = element.closest(GlobalEnum.DataBlocksTag.DataBlock);
139+
// Check if the element is a child of the given targetElement
140+
if (closestDataBlock === this._targetElement || closestDataBlock.contains(this._targetElement)) {
141+
// Check if element is not a predictable element
142+
if (element !== this._predictableTopElement && element !== this._predictableBottomElement) {
143+
this._focusableElements.push(element);
144+
}
145+
// If the targetElement is a "special" element, we need to ensure that the focusable elements are not part of the inner patterns
146+
// 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
147+
} else if (this._canTargetContainOtherPatts) {
148+
// Ensure the element we bring from the inner pattern is the default tabindex element for it!
149+
if (Helper.Dom.Attribute.Has(element, Constants.FocusableTabIndexDefault)) {
150+
this._focusableElements.push(element);
151+
}
152+
}
127153
}
128154

129155
// Remove the first element from array, because of predictable top element added for trapping
@@ -204,7 +230,7 @@ namespace OSFramework.OSUI.Behaviors {
204230
Helper.A11Y.AriaHiddenFalse(this._predictableTopElement);
205231

206232
// Ensure the list of focusable elements is updated, predictable elements starts with TabIndex Hidden
207-
this._setFocusableElements();
233+
this._setFocusableElements(true);
208234
}
209235

210236
/**

src/scripts/OSFramework/OSUI/Constants.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,14 @@ namespace OSFramework.OSUI.Constants {
8686

8787
/* Store focusable elements when doing a focus trap inside an element*/
8888
export const FocusableElems =
89-
'a[href]:not([disabled]),[tabindex="0"], button:not([disabled]), textarea:not([disabled]), input:not([disabled]), select:not([disabled])';
89+
'a[href]:not([disabled]), [tabindex="0"], button:not([disabled]), textarea:not([disabled]), input:not([disabled]), select:not([disabled])';
90+
91+
/* Attribute used to flag the default tabindex element under the pattern context */
92+
export const FocusableTabIndexDefault = 'default-tabindex-element';
93+
94+
/* Store all the hidden elements under the context of a hidden container (ex: sidebar when it's closed) in order to turn them
95+
visible for A11Y when hidden container turns into visible */
96+
export const FocusableTabIndexHidden = '[tabindex="-1"][default-tabindex-element]';
9097

9198
/* Attribute used to flag some elements to be ignored by the Focus Trap behaviour */
9299
export const FocusTrapIgnoreAttr = 'ignore-focus-trap';

src/scripts/OSFramework/OSUI/Helper/Dom.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -441,12 +441,16 @@ namespace OSFramework.OSUI.Helper {
441441
/**
442442
* Method to get the list of focusable elements
443443
*
444-
* @readonly
445444
* @static
446-
* @memberof OSFramework.Helper.Dom
445+
* @param {HTMLElement} element Element to be queried.
446+
* @param {boolean} [includeTabIndexHidden=false] Include elements with tabindex -1
447+
* @return {*} {HTMLElement[]} List of focusable elements
448+
* @memberof Dom
447449
*/
448-
public static GetFocusableElements(element: HTMLElement): HTMLElement[] {
449-
const _focusableElems = element.querySelectorAll(Constants.FocusableElems);
450+
public static GetFocusableElements(element: HTMLElement, includeTabIndexHidden = false): HTMLElement[] {
451+
const _focusableElems = element.querySelectorAll(
452+
`${Constants.FocusableElems}${includeTabIndexHidden ? ', ' + Constants.FocusableTabIndexHidden : ''}`
453+
);
450454
// Remove any element that has the focus-trap-ignore attribute
451455
const _filteredElements = Array.from(_focusableElems).filter(
452456
(element) => element.getAttribute(Constants.FocusTrapIgnoreAttr) !== 'true'

src/scripts/OSFramework/OSUI/Pattern/AccordionItem/AccordionItem.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -353,12 +353,18 @@ namespace OSFramework.OSUI.Patterns.AccordionItem {
353353
Helper.Dom.Styles.AddClass(this.selfElement, Enum.CssClass.PatternDisabled);
354354
Helper.Dom.Styles.RemoveClass(this._elementWithEvents, Enum.CssClass.PatternClickArea);
355355
Helper.A11Y.AriaDisabledTrue(this.selfElement);
356+
Helper.Dom.Attribute.Remove(this._elementWithEvents, Constants.FocusableTabIndexDefault);
356357
this._removeEvents();
357358
this.unsetCallbacks();
358359
} else {
359360
Helper.Dom.Styles.RemoveClass(this.selfElement, Enum.CssClass.PatternDisabled);
360361
Helper.Dom.Styles.AddClass(this._elementWithEvents, Enum.CssClass.PatternClickArea);
361362
Helper.A11Y.AriaDisabledFalse(this.selfElement);
363+
Helper.Dom.Attribute.Set(
364+
this._elementWithEvents,
365+
Constants.FocusableTabIndexDefault,
366+
Constants.EmptyString
367+
);
362368
this.setCallbacks();
363369
this._addEvents();
364370
}

src/scripts/OSFramework/OSUI/Pattern/BottomSheet/BottomSheet.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ namespace OSFramework.OSUI.Patterns.BottomSheet {
7777
private _handleFocusBehavior(): void {
7878
const opts = {
7979
focusTargetElement: this._parentSelf,
80+
canContainOtherPatterns: true,
8081
} as Behaviors.FocusTrapParams;
8182

8283
this._focusTrapInstance = new Behaviors.FocusTrap(opts);

src/scripts/OSFramework/OSUI/Pattern/FlipContent/FlipContent.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,8 @@ namespace OSFramework.OSUI.Patterns.FlipContent {
133133
Helper.A11Y.TabIndexTrue(this.selfElement);
134134
Helper.A11Y.RoleButton(this.selfElement);
135135
Helper.A11Y.AriaLivePolite(this.selfElement);
136+
// Set the attr that will be used to define the default tabindex element
137+
Helper.Dom.Attribute.Set(this.selfElement, Constants.FocusableTabIndexDefault, Constants.EmptyString);
136138
}
137139
}
138140

src/scripts/OSFramework/OSUI/Pattern/OverflowMenu/OverflowMenu.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,8 @@ namespace OSFramework.OSUI.Patterns.OverflowMenu {
140140
Helper.A11Y.RoleMenu(this._balloonElem);
141141
this.setTriggerAriaLabel(Enum.AriaLabel.Trigger);
142142
Helper.Dom.Attribute.Set(this._triggerElem, Constants.FocusTrapIgnoreAttr, true);
143+
// Set the attr that will be used to define the default tabindex element
144+
Helper.Dom.Attribute.Set(this._triggerElem, Constants.FocusableTabIndexDefault, Constants.EmptyString);
143145
}
144146

145147
Helper.A11Y.AriaExpanded(this._triggerElem, this.isOpen.toString());

src/scripts/OSFramework/OSUI/Pattern/Progress/AbstractProgress.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,9 @@ namespace OSFramework.OSUI.Patterns.Progress {
2727
private _setAccessibilityProps(): void {
2828
Helper.Dom.Attribute.Set(this.selfElement, Constants.A11YAttributes.TabIndex, '0');
2929

30+
// Set the attr that will be used to define the default tabindex element
31+
Helper.Dom.Attribute.Set(this.selfElement, Constants.FocusableTabIndexDefault, Constants.EmptyString);
32+
3033
Helper.Dom.Attribute.Set(
3134
this.selfElement,
3235
Constants.A11YAttributes.Role.AttrName,

src/scripts/OSFramework/OSUI/Pattern/Rating/Rating.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,7 @@ namespace OSFramework.OSUI.Patterns.Rating {
148148
];
149149
if (_lastChosen) {
150150
_lastChosen.ariaChecked = Constants.A11YAttributes.States.False;
151+
Helper.Dom.Attribute.Remove(_lastChosen, Constants.FocusableTabIndexDefault);
151152
}
152153

153154
// If it is, then get the input:checked value
@@ -356,6 +357,8 @@ namespace OSFramework.OSUI.Patterns.Rating {
356357
if (this.configs.RatingScale === 1) {
357358
ratingItems[1].checked = true;
358359
ratingItems[1].ariaChecked = Constants.A11YAttributes.States.True;
360+
// Set the attr that will be used to define the default tabindex element
361+
Helper.Dom.Attribute.Set(ratingItems[1], Constants.FocusableTabIndexDefault, Constants.EmptyString);
359362
return;
360363
}
361364

@@ -386,6 +389,8 @@ namespace OSFramework.OSUI.Patterns.Rating {
386389
// Set the itemas as :checked
387390
ratingItems[newValue].checked = true;
388391
ratingItems[newValue].ariaChecked = Constants.A11YAttributes.States.True;
392+
// Set the attr that will be used to define the default tabindex element
393+
Helper.Dom.Attribute.Set(ratingItems[newValue], Constants.FocusableTabIndexDefault, Constants.EmptyString);
389394

390395
// If is-half add the appropriate class, otherwise just declare the this.isHalfValue, to complete the if statement
391396
if (this._isHalfValue) {

0 commit comments

Comments
 (0)