From 376f38836cce6f17dbdff7f2e45ac8d94516428c Mon Sep 17 00:00:00 2001 From: "ala'n (Alexey Stsefanovich)" Date: Fri, 8 Nov 2024 05:06:57 +0100 Subject: [PATCH 01/14] feat(esl-utils): add extended `handleFocusFlow` keyboard based focus manager --- src/modules/esl-utils/dom/focus.ts | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/src/modules/esl-utils/dom/focus.ts b/src/modules/esl-utils/dom/focus.ts index e78d63e4a..36162f812 100644 --- a/src/modules/esl-utils/dom/focus.ts +++ b/src/modules/esl-utils/dom/focus.ts @@ -18,6 +18,28 @@ export const handleFocusChain = (e: KeyboardEvent, first: HTMLElement | undefine } }; +export type FocusFlowType = 'none' | 'loop' | 'chain'; + +export const handleFocusFlow = ( + e: KeyboardEvent, + $focusables: HTMLElement[], + $fallback: HTMLElement, + type: FocusFlowType = 'loop' +): boolean | undefined => { + if (!type || type === 'none') return; + + const $first = $focusables[0]; + const $last = $focusables[$focusables.length - 1]; + + if (type === 'loop') return handleFocusChain(e, $first, $last); + + if (type === 'chain' && $last && $fallback) { + if (e.target !== (e.shiftKey ? $first : $last)) return; + $fallback.focus(); + e.preventDefault(); + } +}; + /** * TODO: add visibility check * Gets keyboard-focusable elements within a specified root element From c954d72dad67ec726b6d42012b03796bc8143896 Mon Sep 17 00:00:00 2001 From: "ala'n (Alexey Stsefanovich)" Date: Fri, 8 Nov 2024 05:09:44 +0100 Subject: [PATCH 02/14] feat(esl-toggleable): add out of the box `ESLToggleable` focus manager Focus control based on new focusBehaviour property and utilizes keyboard events to handle flow. New `$focusables` property available for `ESLToggleable` out of the box. --- .../core/esl-toggleable.shape.ts | 8 ++++++++ .../esl-toggleable/core/esl-toggleable.ts | 20 ++++++++++++++++++- 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/src/modules/esl-toggleable/core/esl-toggleable.shape.ts b/src/modules/esl-toggleable/core/esl-toggleable.shape.ts index a4397b8ca..b8a1aab99 100644 --- a/src/modules/esl-toggleable/core/esl-toggleable.shape.ts +++ b/src/modules/esl-toggleable/core/esl-toggleable.shape.ts @@ -19,6 +19,14 @@ export interface ESLToggleableTagShape /** Open toggleable marker. Can be used to define initial state */ 'open'?: boolean; + /** + * Define focus behaviour + * - 'none' - no focus management + * - 'chain' - focus on the first focusable element first and return focus to the activator after the last focusable element + * - 'loop' - focus on the first focusable element and loop through the focusable elements + */ + 'focus-behaviour'?: 'none' | 'chain' | 'loop'; + /** Define Toggleable group meta information to organize groups */ 'group'?: string; diff --git a/src/modules/esl-toggleable/core/esl-toggleable.ts b/src/modules/esl-toggleable/core/esl-toggleable.ts index b5f29d652..ab890c0c9 100644 --- a/src/modules/esl-toggleable/core/esl-toggleable.ts +++ b/src/modules/esl-toggleable/core/esl-toggleable.ts @@ -1,5 +1,5 @@ import {ExportNs} from '../../esl-utils/environment/export-ns'; -import {ESC, SYSTEM_KEYS} from '../../esl-utils/dom/keys'; +import {SYSTEM_KEYS, ESC, TAB} from '../../esl-utils/dom/keys'; import {CSSClassUtils} from '../../esl-utils/dom/class'; import {prop, attr, jsonAttr, listen} from '../../esl-utils/decorators'; import {defined, copyDefinedKeys} from '../../esl-utils/misc/object'; @@ -9,7 +9,9 @@ import {hasHover} from '../../esl-utils/environment/device-detector'; import {DelayedTask} from '../../esl-utils/async/delayed-task'; import {ESLBaseElement} from '../../esl-base-element/core'; import {findParent, isMatches} from '../../esl-utils/dom/traversing'; +import {getKeyboardFocusableElements, handleFocusFlow} from '../../esl-utils/dom/focus'; +import type {FocusFlowType} from '../../esl-utils/dom/focus'; import type {DelegatedEvent} from '../../esl-event-listener/core/types'; /** Default Toggleable action params type definition */ @@ -101,6 +103,14 @@ export class ESLToggleable extends ESLBaseElement { */ @attr({defaultValue: '*'}) public containerActiveClassTarget: string; + /** + * Focus behaviour. Awailable values: + * - 'none' - no focus management + * - 'chain' - focus on the first focusable element first and return focus to the activator after the last focusable element + * - 'loop' - focus on the first focusable element and loop through the focusable elements + */ + @attr({defaultValue: 'none'}) public focusBehaviour: FocusFlowType; + /** Toggleable group meta information to organize groups */ @attr({name: 'group'}) public groupName: string; /** Selector to mark inner close triggers */ @@ -302,6 +312,11 @@ export class ESLToggleable extends ESLBaseElement { el ? activators.set(this, el) : activators.delete(this); } + /** List of all focusable elements inside instance */ + public get $focusables(): HTMLElement[] { + return getKeyboardFocusableElements(this) as HTMLElement[]; + } + /** Returns the element to apply a11y attributes */ protected get $a11yTarget(): HTMLElement | null { const target = this.getAttribute('a11y-target'); @@ -353,6 +368,9 @@ export class ESLToggleable extends ESLBaseElement { if (this.closeOnEsc && e.key === ESC) { this.hide({initiator: 'keyboard', event: e}); } + if (this.focusBehaviour !== 'none' && e.key === TAB) { + handleFocusFlow(e, this.$focusables, this.activator || this, this.focusBehaviour); + } } @listen({auto: false, event: 'mouseenter'}) From 6ef1f2e23b821fcccefb219330bcead8f7c57a4b Mon Sep 17 00:00:00 2001 From: "ala'n (Alexey Stsefanovich)" Date: Fri, 8 Nov 2024 05:16:02 +0100 Subject: [PATCH 03/14] feat(esl-tooltip): get rid from inner `hasFocusLoop` and custom focus manager, now utilizes ESLToggleable features BREAKING-CHANGE: `hasFocusLoop` no longer available use `focusBehaviour` instead --- src/modules/esl-share/core/esl-share-popup.ts | 2 - src/modules/esl-tooltip/core/esl-tooltip.ts | 43 ++++--------------- 2 files changed, 9 insertions(+), 36 deletions(-) diff --git a/src/modules/esl-share/core/esl-share-popup.ts b/src/modules/esl-share/core/esl-share-popup.ts index 8bb885f82..fb24dc7ee 100644 --- a/src/modules/esl-share/core/esl-share-popup.ts +++ b/src/modules/esl-share/core/esl-share-popup.ts @@ -53,8 +53,6 @@ export class ESLSharePopup extends ESLTooltip { return ESLSharePopup.create(); } - @prop(true) public override hasFocusLoop: boolean; - /** Hashstring with a list of buttons already rendered in the popup */ protected _list: string = ''; diff --git a/src/modules/esl-tooltip/core/esl-tooltip.ts b/src/modules/esl-tooltip/core/esl-tooltip.ts index fd19f44a9..8013f8234 100644 --- a/src/modules/esl-tooltip/core/esl-tooltip.ts +++ b/src/modules/esl-tooltip/core/esl-tooltip.ts @@ -1,11 +1,10 @@ import {ExportNs} from '../../esl-utils/environment/export-ns'; import {ESLPopup} from '../../esl-popup/core'; -import {memoize, attr, boolAttr, listen, prop} from '../../esl-utils/decorators'; -import {TAB} from '../../esl-utils/dom/keys'; -import {getKeyboardFocusableElements, handleFocusChain} from '../../esl-utils/dom/focus'; +import {memoize, attr, boolAttr} from '../../esl-utils/decorators'; import type {ESLPopupActionParams} from '../../esl-popup/core'; import type {PositionType} from '../../esl-popup/core/esl-popup-position'; +import type {FocusFlowType} from '../../esl-utils/dom/focus'; export interface ESLTooltipActionParams extends ESLPopupActionParams { /** text to be shown */ @@ -33,7 +32,13 @@ export class ESLTooltip extends ESLPopup { autofocus: true }; - @prop(false) public hasFocusLoop: boolean; + /** + * Focus behaviour. Awailable values: + * - 'none' - no focus management + * - 'chain' (default) - focus on the first focusable element first and return focus to the activator after the last focusable element + * - 'loop' - focus on the first focusable element and loop through the focusable elements + */ + @attr({defaultValue: 'chain'}) public override focusBehaviour: FocusFlowType; /** * Tooltip position relative to the trigger. @@ -53,19 +58,6 @@ export class ESLTooltip extends ESLPopup { return document.createElement('esl-tooltip'); } - /** List of all focusable elements inside instance */ - public get $focusables(): HTMLElement[] { - return getKeyboardFocusableElements(this) as HTMLElement[]; - } - - /** First and last focusable elements inside instance */ - public get $boundaryFocusable(): {$first: HTMLElement | undefined, $last: HTMLElement | undefined} { - const {$focusables} = this; - const $first = $focusables[0]; - const $last = $focusables.pop(); - return {$first, $last}; - } - /** Active state marker */ public static get open(): boolean { return this.sharedInstance.open; @@ -113,23 +105,6 @@ export class ESLTooltip extends ESLPopup { super.onHide(params); this.parentNode === document.body && document.body.removeChild(this); } - - @listen({inherit: true}) - protected override _onKeyboardEvent(e: KeyboardEvent): void { - super._onKeyboardEvent(e); - if (e.key === TAB) this._onTabKey(e); - } - - /** Actions on TAB keypressed */ - protected _onTabKey(e: KeyboardEvent): void { - if (!this.activator) return; - const {$first, $last} = this.$boundaryFocusable; - if (this.hasFocusLoop) return handleFocusChain(e, $first, $last) as void; - if (!$last || e.target === (e.shiftKey ? $first : $last)) { - this.activator.focus(); - e.preventDefault(); - } - } } declare global { From b5260b937840fbd5a6023d7d0ed1557f86c00e8a Mon Sep 17 00:00:00 2001 From: "ala'n (Alexey Stsefanovich)" Date: Fri, 8 Nov 2024 05:18:46 +0100 Subject: [PATCH 04/14] feat(esl-share): separate `ESLSharePopup` implementation from `ESLTooltip` BREAKING-CHANGE: `ESLSharePopup` no longer inherits `ESLTooltip`, `ESLPopup` now direct base for `ESLSharePopup` --- .../esl-share/core/esl-share-popup.shape.ts | 4 +- src/modules/esl-share/core/esl-share-popup.ts | 55 +++++++++++++++++-- 2 files changed, 52 insertions(+), 7 deletions(-) diff --git a/src/modules/esl-share/core/esl-share-popup.shape.ts b/src/modules/esl-share/core/esl-share-popup.shape.ts index a14606a3d..fa4de44b9 100644 --- a/src/modules/esl-share/core/esl-share-popup.shape.ts +++ b/src/modules/esl-share/core/esl-share-popup.shape.ts @@ -1,11 +1,11 @@ -import type {ESLTooltipTagShape} from '../../esl-tooltip/core/esl-tooltip.shape'; +import type {ESLPopupTagShape} from '../../esl-popup/core/esl-popup.shape'; import type {ESLSharePopup} from './esl-share-popup'; /** * Tag declaration interface of ESL Share Popup element * Used for TSX declaration */ -export interface ESLSharePopupTagShape extends ESLTooltipTagShape { +export interface ESLSharePopupTagShape extends ESLPopupTagShape { /** Allowed children */ children?: any; } diff --git a/src/modules/esl-share/core/esl-share-popup.ts b/src/modules/esl-share/core/esl-share-popup.ts index fb24dc7ee..8273823e4 100644 --- a/src/modules/esl-share/core/esl-share-popup.ts +++ b/src/modules/esl-share/core/esl-share-popup.ts @@ -1,11 +1,13 @@ import {ExportNs} from '../../esl-utils/environment/export-ns'; -import {ESLTooltip} from '../../esl-tooltip/core/esl-tooltip'; -import {bind, listen, memoize, prop} from '../../esl-utils/decorators'; +import {ESLPopup} from '../../esl-popup/core/esl-popup'; +import {attr, bind, boolAttr, listen, memoize} from '../../esl-utils/decorators'; import {ESLShareButton} from './esl-share-button'; import {ESLShareConfig} from './esl-share-config'; import type {ESLTooltipActionParams} from '../../esl-tooltip/core/esl-tooltip'; import type {ESLShareButtonConfig} from './esl-share-config'; +import type {FocusFlowType} from '../../esl-utils/dom/focus'; +import type {PositionType} from '../../esl-popup/core/esl-popup-position'; export type {ESLSharePopupTagShape} from './esl-share-popup.shape'; @@ -28,12 +30,13 @@ export interface ESLSharePopupActionParams extends ESLTooltipActionParams { * - forwards the sharing attributes from the host share {@link ESLShare} component */ @ExportNs('SharePopup') -export class ESLSharePopup extends ESLTooltip { +export class ESLSharePopup extends ESLPopup { static override is = 'esl-share-popup'; /** Default params to pass into the share popup */ static override DEFAULT_PARAMS: ESLSharePopupActionParams = { - ...ESLTooltip.DEFAULT_PARAMS, + ...ESLPopup.DEFAULT_PARAMS, + autofocus: true, position: 'top', hideDelay: 300 }; @@ -49,22 +52,64 @@ export class ESLSharePopup extends ESLTooltip { /** Shared instance of ESLSharePopup */ @memoize() - public static override get sharedInstance(): ESLSharePopup { + public static get sharedInstance(): ESLSharePopup { return ESLSharePopup.create(); } + /** + * Focus behaviour. Awailable values: + * - 'none' - no focus management + * - 'chain' (default) - focus on the first focusable element first and return focus to the activator after the last focusable element + * - 'loop' - focus on the first focusable element and loop through the focusable elements + */ + @attr({defaultValue: 'chain'}) public override focusBehaviour: FocusFlowType; + + /** + * Popup position relative to the trigger. + * Currently supported: 'top', 'bottom', 'left', 'right' position types ('top' by default) + */ + @attr({defaultValue: 'top'}) public override position: PositionType; + + /** Popup behavior if it does not fit in the window ('fit' by default) */ + @attr({defaultValue: 'fit'}) public override behavior: string; + + /** Disable arrow at Tooltip */ + @boolAttr() public disableArrow: boolean; + /** Hashstring with a list of buttons already rendered in the popup */ protected _list: string = ''; + public override connectedCallback(): void { + super.connectedCallback(); + this.classList.add(ESLPopup.is); + this.classList.toggle('disable-arrow', this.disableArrow); + this.tabIndex = 0; + } + + /** Sets initial state of the Tooltip */ + protected override setInitialState(): void {} + public override onShow(params: ESLTooltipActionParams): void { + if (params.disableArrow) { + this.disableArrow = params.disableArrow; + } if (params.list) { const buttonsList = ESLShareConfig.instance.get(params.list); this.appendButtonsFromList(buttonsList); } this.forwardAttributes(); + this.dir = params.dir || ''; + this.lang = params.lang || ''; + this.parentNode !== document.body && document.body.appendChild(this); super.onShow(params); } + /** Actions to execute on Tooltip hiding. */ + public override onHide(params: ESLTooltipActionParams): void { + super.onHide(params); + this.parentNode === document.body && document.body.removeChild(this); + } + /** Checks that the button list from the config was already rendered in the popup. */ protected isEqual(config: ESLShareButtonConfig[]): boolean { return stringifyButtonsList(config) === this._list; From ea8dd9407cbecf20cf18c8df7bfb11253fe34e61 Mon Sep 17 00:00:00 2001 From: "ala'n (Alexey Stsefanovich)" Date: Fri, 8 Nov 2024 14:30:45 +0100 Subject: [PATCH 05/14] fix(esl-share): fix inner ESLToggleableActionParams instances type --- src/modules/esl-share/core/esl-share-popup.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/modules/esl-share/core/esl-share-popup.ts b/src/modules/esl-share/core/esl-share-popup.ts index 8273823e4..7e4587200 100644 --- a/src/modules/esl-share/core/esl-share-popup.ts +++ b/src/modules/esl-share/core/esl-share-popup.ts @@ -4,7 +4,7 @@ import {attr, bind, boolAttr, listen, memoize} from '../../esl-utils/decorators' import {ESLShareButton} from './esl-share-button'; import {ESLShareConfig} from './esl-share-config'; -import type {ESLTooltipActionParams} from '../../esl-tooltip/core/esl-tooltip'; +import type {ESLPopupActionParams} from '../../esl-popup/core/esl-popup'; import type {ESLShareButtonConfig} from './esl-share-config'; import type {FocusFlowType} from '../../esl-utils/dom/focus'; import type {PositionType} from '../../esl-popup/core/esl-popup-position'; @@ -15,7 +15,7 @@ function stringifyButtonsList(btns: ESLShareButtonConfig[]): string { return btns.map((btn) => btn.name).join(','); } -export interface ESLSharePopupActionParams extends ESLTooltipActionParams { +export interface ESLSharePopupActionParams extends ESLPopupActionParams { /** list of social networks or groups of them to display */ list?: string; } @@ -89,7 +89,7 @@ export class ESLSharePopup extends ESLPopup { /** Sets initial state of the Tooltip */ protected override setInitialState(): void {} - public override onShow(params: ESLTooltipActionParams): void { + public override onShow(params: ESLSharePopupActionParams): void { if (params.disableArrow) { this.disableArrow = params.disableArrow; } @@ -105,7 +105,7 @@ export class ESLSharePopup extends ESLPopup { } /** Actions to execute on Tooltip hiding. */ - public override onHide(params: ESLTooltipActionParams): void { + public override onHide(params: ESLSharePopupActionParams): void { super.onHide(params); this.parentNode === document.body && document.body.removeChild(this); } From 2b8a0c7affa2b5af5b6baa8efd64932e735ca5d9 Mon Sep 17 00:00:00 2001 From: "ala'n (Alexey Stsefanovich)" Date: Thu, 14 Nov 2024 02:39:36 +0100 Subject: [PATCH 06/14] feat(esl-toggleable): update focusBehaviour option to smoothly support boundary focus actions across different options --- .../esl-toggleable/core/esl-toggleable.ts | 75 ++++++++++++++++--- 1 file changed, 65 insertions(+), 10 deletions(-) diff --git a/src/modules/esl-toggleable/core/esl-toggleable.ts b/src/modules/esl-toggleable/core/esl-toggleable.ts index ab890c0c9..261457e48 100644 --- a/src/modules/esl-toggleable/core/esl-toggleable.ts +++ b/src/modules/esl-toggleable/core/esl-toggleable.ts @@ -7,6 +7,7 @@ import {parseBoolean, toBooleanAttribute} from '../../esl-utils/misc/format'; import {sequentialUID} from '../../esl-utils/misc/uid'; import {hasHover} from '../../esl-utils/environment/device-detector'; import {DelayedTask} from '../../esl-utils/async/delayed-task'; +import {afterNextRender} from '../../esl-utils/async/raf'; import {ESLBaseElement} from '../../esl-base-element/core'; import {findParent, isMatches} from '../../esl-utils/dom/traversing'; import {getKeyboardFocusableElements, handleFocusFlow} from '../../esl-utils/dom/focus'; @@ -103,14 +104,6 @@ export class ESLToggleable extends ESLBaseElement { */ @attr({defaultValue: '*'}) public containerActiveClassTarget: string; - /** - * Focus behaviour. Awailable values: - * - 'none' - no focus management - * - 'chain' - focus on the first focusable element first and return focus to the activator after the last focusable element - * - 'loop' - focus on the first focusable element and loop through the focusable elements - */ - @attr({defaultValue: 'none'}) public focusBehaviour: FocusFlowType; - /** Toggleable group meta information to organize groups */ @attr({name: 'group'}) public groupName: string; /** Selector to mark inner close triggers */ @@ -123,6 +116,14 @@ export class ESLToggleable extends ESLBaseElement { /** Close the Toggleable on a click/tap outside */ @attr({parser: parseBoolean, serializer: toBooleanAttribute}) public closeOnOutsideAction: boolean; + /** + * Focus behaviour. Awailable values: + * - 'none' - no focus management + * - 'chain' - focus on the first focusable element first and return focus to the activator after the last focusable element + * - 'loop' - focus on the first focusable element and loop through the focusable elements + */ + @attr({defaultValue: 'none'}) public focusBehaviour: FocusFlowType; + /** Initial params to pass to show/hide action on the start */ @jsonAttr({defaultValue: {force: true, initiator: 'init'}}) public initialParams: ESLToggleableActionParams; @@ -199,6 +200,32 @@ export class ESLToggleable extends ESLBaseElement { track ? this.$$on(this._onMouseLeave) : this.$$off(this._onMouseLeave); } + /** Focus the first focusable element or the element itself if it's focusable */ + public override focus(options?: FocusOptions): void { + if (this.hasAttribute('tabindex')) { + super.focus(options); + } else { + const focusable = this.$focusables[0]; + focusable && focusable.focus(options); + } + } + + /** + * Delegate focus to the last activator (or move it out if there is no activator) + * if the focused element is inside the Toggleable. + * @param deep - if true, the inner focused element will be handled as well + */ + public override blur(deep = false): void { + if (!this.hasFocus) return; + if (this.activator) { + this.activator.focus(); + } else if (deep) { + (document.activeElement! as HTMLElement).blur(); + } else { + super.blur(); + } + } + /** Function to merge the result action params */ protected mergeDefaultParams(params?: ESLToggleableActionParams): ESLToggleableActionParams { const type = this.constructor as typeof ESLToggleable; @@ -259,7 +286,7 @@ export class ESLToggleable extends ESLBaseElement { * Inner state and 'open' attribute are not affected and updated before `onShow` execution. * Adds CSS classes, update a11y and fire {@link ESLToggleable.REFRESH_EVENT} event by default. */ - protected onShow(params: ESLToggleableActionParams): void { + protected onShow(params: ESLToggleableActionParams): void | Promise { this.open = true; CSSClassUtils.add(this, this.activeClass); CSSClassUtils.add(document.body, this.bodyClass, this); @@ -270,6 +297,11 @@ export class ESLToggleable extends ESLBaseElement { this.updateA11y(); this.$$fire(this.REFRESH_EVENT); // To notify other components about content change + + // Focus on the first focusable element + if (this.focusBehaviour !== 'none') { + queueMicrotask(() => afterNextRender(() => this.focus({preventScroll: true}))); + } } /** @@ -285,7 +317,7 @@ export class ESLToggleable extends ESLBaseElement { * Inner state and 'open' attribute are not affected and updated before `onShow` execution. * Removes CSS classes and update a11y by default. */ - protected onHide(params: ESLToggleableActionParams): void { + protected onHide(params: ESLToggleableActionParams): void | Promise { this.open = false; CSSClassUtils.remove(this, this.activeClass); CSSClassUtils.remove(document.body, this.bodyClass, this); @@ -294,6 +326,9 @@ export class ESLToggleable extends ESLBaseElement { $container && CSSClassUtils.remove($container, this.containerActiveClass, this); } this.updateA11y(); + + // Blur if the toggleable has focus + queueMicrotask(() => afterNextRender(() => this.blur(true))); } /** Active state marker */ @@ -312,6 +347,11 @@ export class ESLToggleable extends ESLBaseElement { el ? activators.set(this, el) : activators.delete(this); } + /** If the togleable or its content has focus */ + public get hasFocus(): boolean { + return this === document.activeElement || this.contains(document.activeElement); + } + /** List of all focusable elements inside instance */ public get $focusables(): HTMLElement[] { return getKeyboardFocusableElements(this) as HTMLElement[]; @@ -367,12 +407,27 @@ export class ESLToggleable extends ESLBaseElement { protected _onKeyboardEvent(e: KeyboardEvent): void { if (this.closeOnEsc && e.key === ESC) { this.hide({initiator: 'keyboard', event: e}); + e.stopPropagation(); } if (this.focusBehaviour !== 'none' && e.key === TAB) { handleFocusFlow(e, this.$focusables, this.activator || this, this.focusBehaviour); } } + @listen('focusout') + protected _onFocusOut(e: FocusEvent): void { + if (!this.open) return; + if (this.focusBehaviour === 'chain') { + afterNextRender(() => { + if (this.hasFocus) return; + this.hide({initiator: 'focusout', event: e}); + }); + } + if (this.focusBehaviour === 'loop') { + this.focus({preventScroll: true}); + } + } + @listen({auto: false, event: 'mouseenter'}) protected _onMouseEnter(e: MouseEvent): void { const baseParams: ESLToggleableActionParams = { From 11b10eaf8514b5c27f257d470464ce720e2c0765 Mon Sep 17 00:00:00 2001 From: "ala'n (Alexey Stsefanovich)" Date: Thu, 14 Nov 2024 02:41:26 +0100 Subject: [PATCH 07/14] feat(esl-popup): get rid from all focus management code BREAKING-CHANGE: 'autofocus' no longer available for popup, use 'focus-behaviour' instead --- src/modules/esl-popup/core/esl-popup.ts | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/src/modules/esl-popup/core/esl-popup.ts b/src/modules/esl-popup/core/esl-popup.ts index 32576f0e1..cfafc072a 100644 --- a/src/modules/esl-popup/core/esl-popup.ts +++ b/src/modules/esl-popup/core/esl-popup.ts @@ -4,7 +4,7 @@ import {bind, memoize, ready, attr, boolAttr, jsonAttr, listen, decorate} from ' import {ESLTraversingQuery} from '../../esl-traversing-query/core'; import {afterNextRender, rafDecorator} from '../../esl-utils/async/raf'; import {ESLToggleable} from '../../esl-toggleable/core'; -import {isElement, isRelativeNode, isRTL, Rect, getListScrollParents, getViewportRect} from '../../esl-utils/dom'; +import {isElement, isRelativeNode, isRTL, Rect, getListScrollParents, getViewportRect, type FocusFlowType} from '../../esl-utils/dom'; import {parseBoolean, parseNumber, toBooleanAttribute} from '../../esl-utils/misc/format'; import {copyDefinedKeys} from '../../esl-utils/misc/object'; import {ESLIntersectionTarget, ESLIntersectionEvent} from '../../esl-event-listener/core/targets/intersection.target'; @@ -44,8 +44,6 @@ export interface ESLPopupActionParams extends ESLToggleableActionParams { container?: string; /** Container element that defines bounds of popups visibility (is not taken into account if the container attr is set on popup) */ containerEl?: HTMLElement; - /** Autofocus on popup/activator */ - autofocus?: boolean; /** Extra class to add to popup on activation */ extraClass?: string; @@ -111,6 +109,15 @@ export class ESLPopup extends ESLToggleable { @attr({parser: parseBoolean, serializer: toBooleanAttribute, defaultValue: true}) public override closeOnOutsideAction: boolean; + /** + * Focus behaviour. Awailable values: + * - 'none' - no focus management + * - 'chain' (default) - focus on the first focusable element first and return focus to the activator after the last focusable element + * - 'loop' - focus on the first focusable element and loop through the focusable elements + */ + @attr({defaultValue: 'none'}) + public override focusBehaviour: FocusFlowType; + public $placeholder: ESLPopupPlaceholder | null; protected _extraClass?: string; @@ -227,9 +234,6 @@ export class ESLPopup extends ESLToggleable { // running as a separate task solves the problem with incorrect positioning on the first showing if (wasOpened) this.afterOnShow(params); else afterNextRender(() => this.afterOnShow(params)); - - // Autofocus logic - afterNextRender(() => params.autofocus && this.focus({preventScroll: true})); } /** @@ -241,7 +245,6 @@ export class ESLPopup extends ESLToggleable { this.beforeOnHide(params); super.onHide(params); this.afterOnHide(params); - params.autofocus && this.activator?.focus({preventScroll: true}); } /** From 04d6a63819049242230841365d21a1f2eb4510e7 Mon Sep 17 00:00:00 2001 From: "ala'n (Alexey Stsefanovich)" Date: Thu, 14 Nov 2024 02:42:51 +0100 Subject: [PATCH 08/14] fix(esl-share): simplify code and remove overrides (according to esl-popup base state) --- src/modules/esl-share/core/esl-share-popup.ts | 22 +------------------ 1 file changed, 1 insertion(+), 21 deletions(-) diff --git a/src/modules/esl-share/core/esl-share-popup.ts b/src/modules/esl-share/core/esl-share-popup.ts index 7e4587200..9fddfe0d5 100644 --- a/src/modules/esl-share/core/esl-share-popup.ts +++ b/src/modules/esl-share/core/esl-share-popup.ts @@ -1,13 +1,11 @@ import {ExportNs} from '../../esl-utils/environment/export-ns'; import {ESLPopup} from '../../esl-popup/core/esl-popup'; -import {attr, bind, boolAttr, listen, memoize} from '../../esl-utils/decorators'; +import {bind, boolAttr, listen, memoize} from '../../esl-utils/decorators'; import {ESLShareButton} from './esl-share-button'; import {ESLShareConfig} from './esl-share-config'; import type {ESLPopupActionParams} from '../../esl-popup/core/esl-popup'; import type {ESLShareButtonConfig} from './esl-share-config'; -import type {FocusFlowType} from '../../esl-utils/dom/focus'; -import type {PositionType} from '../../esl-popup/core/esl-popup-position'; export type {ESLSharePopupTagShape} from './esl-share-popup.shape'; @@ -36,7 +34,6 @@ export class ESLSharePopup extends ESLPopup { /** Default params to pass into the share popup */ static override DEFAULT_PARAMS: ESLSharePopupActionParams = { ...ESLPopup.DEFAULT_PARAMS, - autofocus: true, position: 'top', hideDelay: 300 }; @@ -56,23 +53,6 @@ export class ESLSharePopup extends ESLPopup { return ESLSharePopup.create(); } - /** - * Focus behaviour. Awailable values: - * - 'none' - no focus management - * - 'chain' (default) - focus on the first focusable element first and return focus to the activator after the last focusable element - * - 'loop' - focus on the first focusable element and loop through the focusable elements - */ - @attr({defaultValue: 'chain'}) public override focusBehaviour: FocusFlowType; - - /** - * Popup position relative to the trigger. - * Currently supported: 'top', 'bottom', 'left', 'right' position types ('top' by default) - */ - @attr({defaultValue: 'top'}) public override position: PositionType; - - /** Popup behavior if it does not fit in the window ('fit' by default) */ - @attr({defaultValue: 'fit'}) public override behavior: string; - /** Disable arrow at Tooltip */ @boolAttr() public disableArrow: boolean; From 699ac7ff8ad5ff7d32cdbdd5fdec29632da1cf40 Mon Sep 17 00:00:00 2001 From: "ala'n (Alexey Stsefanovich)" Date: Thu, 14 Nov 2024 02:43:10 +0100 Subject: [PATCH 09/14] fix(esl-tooltip): simplify code and remove overrides (according to esl-popup base state) --- src/modules/esl-tooltip/core/esl-tooltip.ts | 25 ++++----------------- 1 file changed, 4 insertions(+), 21 deletions(-) diff --git a/src/modules/esl-tooltip/core/esl-tooltip.ts b/src/modules/esl-tooltip/core/esl-tooltip.ts index 8013f8234..67b63152b 100644 --- a/src/modules/esl-tooltip/core/esl-tooltip.ts +++ b/src/modules/esl-tooltip/core/esl-tooltip.ts @@ -1,10 +1,8 @@ import {ExportNs} from '../../esl-utils/environment/export-ns'; import {ESLPopup} from '../../esl-popup/core'; -import {memoize, attr, boolAttr} from '../../esl-utils/decorators'; +import {memoize, boolAttr} from '../../esl-utils/decorators'; import type {ESLPopupActionParams} from '../../esl-popup/core'; -import type {PositionType} from '../../esl-popup/core/esl-popup-position'; -import type {FocusFlowType} from '../../esl-utils/dom/focus'; export interface ESLTooltipActionParams extends ESLPopupActionParams { /** text to be shown */ @@ -29,26 +27,10 @@ export class ESLTooltip extends ESLPopup { /** Default params to pass into the tooltip on show/hide actions */ public static override DEFAULT_PARAMS: ESLTooltipActionParams = { ...ESLPopup.DEFAULT_PARAMS, - autofocus: true + position: 'top', + hideDelay: 300 }; - /** - * Focus behaviour. Awailable values: - * - 'none' - no focus management - * - 'chain' (default) - focus on the first focusable element first and return focus to the activator after the last focusable element - * - 'loop' - focus on the first focusable element and loop through the focusable elements - */ - @attr({defaultValue: 'chain'}) public override focusBehaviour: FocusFlowType; - - /** - * Tooltip position relative to the trigger. - * Currently supported: 'top', 'bottom', 'left', 'right' position types ('top' by default) - */ - @attr({defaultValue: 'top'}) public override position: PositionType; - - /** Tooltip behavior if it does not fit in the window ('fit' by default) */ - @attr({defaultValue: 'fit'}) public override behavior: string; - /** Disable arrow at Tooltip */ @boolAttr() public disableArrow: boolean; @@ -94,6 +76,7 @@ export class ESLTooltip extends ESLPopup { if (params.html) { this.innerHTML = params.html; } + this.dir = params.dir || ''; this.lang = params.lang || ''; this.parentNode !== document.body && document.body.appendChild(this); From 1e457ad17af0018bd2e6122519c9e8e68fb54fa9 Mon Sep 17 00:00:00 2001 From: "ala'n (Alexey Stsefanovich)" Date: Thu, 14 Nov 2024 02:56:28 +0100 Subject: [PATCH 10/14] style(esl-popup): fix type import style for `FocusFlowType` --- src/modules/esl-popup/core/esl-popup.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/modules/esl-popup/core/esl-popup.ts b/src/modules/esl-popup/core/esl-popup.ts index cfafc072a..39aeea45f 100644 --- a/src/modules/esl-popup/core/esl-popup.ts +++ b/src/modules/esl-popup/core/esl-popup.ts @@ -4,13 +4,14 @@ import {bind, memoize, ready, attr, boolAttr, jsonAttr, listen, decorate} from ' import {ESLTraversingQuery} from '../../esl-traversing-query/core'; import {afterNextRender, rafDecorator} from '../../esl-utils/async/raf'; import {ESLToggleable} from '../../esl-toggleable/core'; -import {isElement, isRelativeNode, isRTL, Rect, getListScrollParents, getViewportRect, type FocusFlowType} from '../../esl-utils/dom'; +import {isElement, isRelativeNode, isRTL, Rect, getListScrollParents, getViewportRect} from '../../esl-utils/dom'; import {parseBoolean, parseNumber, toBooleanAttribute} from '../../esl-utils/misc/format'; import {copyDefinedKeys} from '../../esl-utils/misc/object'; import {ESLIntersectionTarget, ESLIntersectionEvent} from '../../esl-event-listener/core/targets/intersection.target'; import {calcPopupPosition, isOnHorizontalAxis} from './esl-popup-position'; import {ESLPopupPlaceholder} from './esl-popup-placeholder'; +import type {FocusFlowType} from '../../esl-utils/dom'; import type {ESLToggleableActionParams} from '../../esl-toggleable/core'; import type {PositionType, PositionOriginType, IntersectionRatioRect} from './esl-popup-position'; From f5e906b39817db1ccd1f3c0578314907bea6c241 Mon Sep 17 00:00:00 2001 From: "ala'n (Alexey Stsefanovich)" Date: Thu, 14 Nov 2024 22:19:50 +0100 Subject: [PATCH 11/14] docs: fix usage of british version of the word `behavior` --- docs/COMMIT_CONVENTION.md | 2 +- site/views/components/esl-panel-group.njk | 2 +- site/views/components/esl-toggleable.njk | 2 +- site/views/examples/image.njk | 2 +- .../renderers/esl-carousel.none.renderer.ts | 2 +- src/modules/esl-image/README.md | 2 +- src/modules/esl-image/core/esl-image.shape.ts | 2 +- src/modules/esl-panel-group/README.md | 2 +- .../core/esl-panel-group.shape.ts | 2 +- .../esl-panel-group/core/esl-panel-group.ts | 2 +- src/modules/esl-popup/core/esl-popup.ts | 4 ++-- .../esl-share/actions/external-action.ts | 2 +- .../core/esl-toggleable.shape.ts | 2 +- .../esl-toggleable/core/esl-toggleable.ts | 18 +++++++++--------- src/modules/esl-utils/async/delayed-task.ts | 2 +- src/modules/esl-utils/dom/rtl.ts | 2 +- 16 files changed, 25 insertions(+), 25 deletions(-) diff --git a/docs/COMMIT_CONVENTION.md b/docs/COMMIT_CONVENTION.md index 5ac07699e..fa5b05032 100644 --- a/docs/COMMIT_CONVENTION.md +++ b/docs/COMMIT_CONVENTION.md @@ -84,7 +84,7 @@ You can increase the importance of the patch changes to the minor using the `MIN feat!: Hey I'm breaking something that already exist ``` -**Identify everything that break or change existing API or behaviour with the `BREACKING CHAGES:` list +**Identify everything that break or change existing API or behavior with the `BREACKING CHAGES:` list ```text feat!: Hey I'm breaking something that already exist diff --git a/site/views/components/esl-panel-group.njk b/site/views/components/esl-panel-group.njk index 2bb9180f0..eb7a33768 100644 --- a/site/views/components/esl-panel-group.njk +++ b/site/views/components/esl-panel-group.njk @@ -1,7 +1,7 @@ --- layout: content title: ESL Panel Group -seoTitle: ESL Panel Group - custom element to group ESLPanel-s to have Tabs or Accordion behaviour +seoTitle: ESL Panel Group - custom element to group ESLPanel-s to have Tabs or Accordion behavior name: ESL Panel Group tags: components aside: diff --git a/site/views/components/esl-toggleable.njk b/site/views/components/esl-toggleable.njk index 20a7e9c87..331aee904 100644 --- a/site/views/components/esl-toggleable.njk +++ b/site/views/components/esl-toggleable.njk @@ -1,7 +1,7 @@ --- layout: content title: ESL Toggleable -seoTitle: ESL Toggleable - custom element to have basic show/hide or other state-full behaviour +seoTitle: ESL Toggleable - custom element to have basic show/hide or other state-full behavior name: ESL Toggleable tags: components aside: diff --git a/site/views/examples/image.njk b/site/views/examples/image.njk index aa8580938..e72c3a678 100644 --- a/site/views/examples/image.njk +++ b/site/views/examples/image.njk @@ -121,7 +121,7 @@ aside:

Mode: cover

No inner image, image is rendered by background image.

ESL Image has no own size. Can be used with img-container classes

-

Inscribe can be used to declare inscribe image behaviour

+

Inscribe can be used to declare inscribe image behavior

diff --git a/src/modules/esl-carousel/renderers/esl-carousel.none.renderer.ts b/src/modules/esl-carousel/renderers/esl-carousel.none.renderer.ts index ac09e3c51..30f2c3ec4 100644 --- a/src/modules/esl-carousel/renderers/esl-carousel.none.renderer.ts +++ b/src/modules/esl-carousel/renderers/esl-carousel.none.renderer.ts @@ -17,7 +17,7 @@ export class ESLNoneCarouselRenderer extends ESLCarouselRenderer { constructor($carousel: ESLCarousel, options: ESLCarouselConfig) { super($carousel, options); - // Note blocks touch plugin from activating (consider rework if scroll behaviour is requested) + // Note blocks touch plugin from activating (consider rework if scroll behavior is requested) Object.defineProperty(this, 'count', {get: () => this.size}); } diff --git a/src/modules/esl-image/README.md b/src/modules/esl-image/README.md index 5d10da7d8..0897bc349 100644 --- a/src/modules/esl-image/README.md +++ b/src/modules/esl-image/README.md @@ -34,7 +34,7 @@ Was originally developed as an alternative to `` element, but with more - Attributes observing. - A11y. -### Accessibility behaviour +### Accessibility behavior ESL Image uses 'img' role if the role is not explicitly provided. If the role is 'img' then `alt` attribute is used as the `aria-label` for the image. In case `alt` is not provided then an empty value is used as a fallback. diff --git a/src/modules/esl-image/core/esl-image.shape.ts b/src/modules/esl-image/core/esl-image.shape.ts index 68daf996c..de3a2c3bc 100644 --- a/src/modules/esl-image/core/esl-image.shape.ts +++ b/src/modules/esl-image/core/esl-image.shape.ts @@ -18,7 +18,7 @@ export interface ESLImageTagShape extends ESLBaseElementShape { lazy?: boolean | 'none' | 'manual' | 'auto'; /** Define load-allowed marker for lazy images */ 'lazy-triggered'?: boolean; - /** Define query change behaviour */ + /** Define query change behavior */ 'refresh-on-update'?: boolean; /** Define CSS class for inner image */ 'inner-image-class'?: string; diff --git a/src/modules/esl-panel-group/README.md b/src/modules/esl-panel-group/README.md index 7c6c8e6e8..550b5af9e 100644 --- a/src/modules/esl-panel-group/README.md +++ b/src/modules/esl-panel-group/README.md @@ -21,7 +21,7 @@ ESLPanelGroup.register(); - `mode-cls-target` - Element [ESLTraversingQuery](../esl-traversing-query/README.md) selector to add class that identifies mode (ESLPanelGroup itself by default) - `animation-class` - class(es) to be added during animation ('animate' by default) - `no-animate` - list of breakpoints to skip collapse/expand animation (for both Group and Panel animations) -- `refresh-strategy` - defines behaviour of active panel(s) in case of configuration change: +- `refresh-strategy` - defines behavior of active panel(s) in case of configuration change: * `initial` - activates initially opened panel(s) * `last` - maintains a currently active panel(s) open * `open` - open max of available panels diff --git a/src/modules/esl-panel-group/core/esl-panel-group.shape.ts b/src/modules/esl-panel-group/core/esl-panel-group.shape.ts index c85378b8c..63548378e 100644 --- a/src/modules/esl-panel-group/core/esl-panel-group.shape.ts +++ b/src/modules/esl-panel-group/core/esl-panel-group.shape.ts @@ -25,7 +25,7 @@ export interface ESLPanelGroupTagShape extends ESLBaseElementShape 'open'?: boolean; /** - * Define focus behaviour + * Define focus behavior * - 'none' - no focus management * - 'chain' - focus on the first focusable element first and return focus to the activator after the last focusable element * - 'loop' - focus on the first focusable element and loop through the focusable elements diff --git a/src/modules/esl-toggleable/core/esl-toggleable.ts b/src/modules/esl-toggleable/core/esl-toggleable.ts index 261457e48..d5a4d794a 100644 --- a/src/modules/esl-toggleable/core/esl-toggleable.ts +++ b/src/modules/esl-toggleable/core/esl-toggleable.ts @@ -117,12 +117,12 @@ export class ESLToggleable extends ESLBaseElement { @attr({parser: parseBoolean, serializer: toBooleanAttribute}) public closeOnOutsideAction: boolean; /** - * Focus behaviour. Awailable values: + * Focus behavior. Available values: * - 'none' - no focus management * - 'chain' - focus on the first focusable element first and return focus to the activator after the last focusable element * - 'loop' - focus on the first focusable element and loop through the focusable elements */ - @attr({defaultValue: 'none'}) public focusBehaviour: FocusFlowType; + @attr({defaultValue: 'none'}) public focusBehavior: FocusFlowType; /** Initial params to pass to show/hide action on the start */ @jsonAttr({defaultValue: {force: true, initiator: 'init'}}) @@ -286,7 +286,7 @@ export class ESLToggleable extends ESLBaseElement { * Inner state and 'open' attribute are not affected and updated before `onShow` execution. * Adds CSS classes, update a11y and fire {@link ESLToggleable.REFRESH_EVENT} event by default. */ - protected onShow(params: ESLToggleableActionParams): void | Promise { + protected onShow(params: ESLToggleableActionParams): void { this.open = true; CSSClassUtils.add(this, this.activeClass); CSSClassUtils.add(document.body, this.bodyClass, this); @@ -299,7 +299,7 @@ export class ESLToggleable extends ESLBaseElement { this.$$fire(this.REFRESH_EVENT); // To notify other components about content change // Focus on the first focusable element - if (this.focusBehaviour !== 'none') { + if (this.focusBehavior !== 'none') { queueMicrotask(() => afterNextRender(() => this.focus({preventScroll: true}))); } } @@ -317,7 +317,7 @@ export class ESLToggleable extends ESLBaseElement { * Inner state and 'open' attribute are not affected and updated before `onShow` execution. * Removes CSS classes and update a11y by default. */ - protected onHide(params: ESLToggleableActionParams): void | Promise { + protected onHide(params: ESLToggleableActionParams): void { this.open = false; CSSClassUtils.remove(this, this.activeClass); CSSClassUtils.remove(document.body, this.bodyClass, this); @@ -409,21 +409,21 @@ export class ESLToggleable extends ESLBaseElement { this.hide({initiator: 'keyboard', event: e}); e.stopPropagation(); } - if (this.focusBehaviour !== 'none' && e.key === TAB) { - handleFocusFlow(e, this.$focusables, this.activator || this, this.focusBehaviour); + if (this.focusBehavior !== 'none' && e.key === TAB) { + handleFocusFlow(e, this.$focusables, this.activator || this, this.focusBehavior); } } @listen('focusout') protected _onFocusOut(e: FocusEvent): void { if (!this.open) return; - if (this.focusBehaviour === 'chain') { + if (this.focusBehavior === 'chain') { afterNextRender(() => { if (this.hasFocus) return; this.hide({initiator: 'focusout', event: e}); }); } - if (this.focusBehaviour === 'loop') { + if (this.focusBehavior === 'loop') { this.focus({preventScroll: true}); } } diff --git a/src/modules/esl-utils/async/delayed-task.ts b/src/modules/esl-utils/async/delayed-task.ts index 94a495ff2..13117f83b 100644 --- a/src/modules/esl-utils/async/delayed-task.ts +++ b/src/modules/esl-utils/async/delayed-task.ts @@ -3,7 +3,7 @@ import type {AnyToVoidFnSignature} from '../misc/functions'; /** * Task placeholder with a single place for executing deferred task. * Only one task can be planed per DelayedTask instance. - * @see put DelayedTask.put behaviour description. + * @see put DelayedTask.put behavior description. */ export class DelayedTask { protected _fn: AnyToVoidFnSignature | null = null; diff --git a/src/modules/esl-utils/dom/rtl.ts b/src/modules/esl-utils/dom/rtl.ts index d6e1cd2d1..2f41814b0 100644 --- a/src/modules/esl-utils/dom/rtl.ts +++ b/src/modules/esl-utils/dom/rtl.ts @@ -1,5 +1,5 @@ // TODO: Revisit using https://caniuse.com/mdn-api_element_scrollleft in 5.0.0 -/** RTL scroll browser behaviours */ +/** RTL scroll browser behaviors */ export type ScrollType = 'default' | 'negative' | 'reverse'; /** Checks if the element in a RTL direction context */ From b729e08bb2294fd3fd279c044615bd89e0d29dc6 Mon Sep 17 00:00:00 2001 From: "ala'n (Alexey Stsefanovich)" Date: Thu, 14 Nov 2024 22:44:46 +0100 Subject: [PATCH 12/14] docs(esl-toggleable): TS doc fixes Co-authored-by: Dmytro Shovchko --- src/modules/esl-toggleable/core/esl-toggleable.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/modules/esl-toggleable/core/esl-toggleable.ts b/src/modules/esl-toggleable/core/esl-toggleable.ts index d5a4d794a..558f771ee 100644 --- a/src/modules/esl-toggleable/core/esl-toggleable.ts +++ b/src/modules/esl-toggleable/core/esl-toggleable.ts @@ -200,7 +200,7 @@ export class ESLToggleable extends ESLBaseElement { track ? this.$$on(this._onMouseLeave) : this.$$off(this._onMouseLeave); } - /** Focus the first focusable element or the element itself if it's focusable */ + /** Focuses on the first focusable element or the element itself if it's focusable */ public override focus(options?: FocusOptions): void { if (this.hasAttribute('tabindex')) { super.focus(options); @@ -211,7 +211,7 @@ export class ESLToggleable extends ESLBaseElement { } /** - * Delegate focus to the last activator (or move it out if there is no activator) + * Delegates focus to the last activator (or moves it out if there is no activator) * if the focused element is inside the Toggleable. * @param deep - if true, the inner focused element will be handled as well */ From cf1ed1b36fd0b176291079ecae7adbc76a796b1b Mon Sep 17 00:00:00 2001 From: "ala'n (Alexey Stsefanovich)" Date: Thu, 14 Nov 2024 23:31:17 +0100 Subject: [PATCH 13/14] style(esl-toggleable): fix focus behavior for chain focus flow --- src/modules/esl-popup/core/esl-popup.ts | 3 ++- .../core/esl-toggleable.shape.ts | 3 ++- .../esl-toggleable/core/esl-toggleable.ts | 19 ++++++++++--------- src/modules/esl-tooltip/core/esl-tooltip.ts | 1 + src/modules/esl-utils/dom/focus.ts | 4 ++-- 5 files changed, 17 insertions(+), 13 deletions(-) diff --git a/src/modules/esl-popup/core/esl-popup.ts b/src/modules/esl-popup/core/esl-popup.ts index 3af3471d7..48e1b131e 100644 --- a/src/modules/esl-popup/core/esl-popup.ts +++ b/src/modules/esl-popup/core/esl-popup.ts @@ -113,10 +113,11 @@ export class ESLPopup extends ESLToggleable { /** * Focus behavior. Available values: * - 'none' - no focus management + * - 'grab' - focus on the first focusable element * - 'chain' (default) - focus on the first focusable element first and return focus to the activator after the last focusable element * - 'loop' - focus on the first focusable element and loop through the focusable elements */ - @attr({defaultValue: 'none'}) + @attr({defaultValue: 'chain'}) public override focusBehavior: FocusFlowType; public $placeholder: ESLPopupPlaceholder | null; diff --git a/src/modules/esl-toggleable/core/esl-toggleable.shape.ts b/src/modules/esl-toggleable/core/esl-toggleable.shape.ts index 54dfe0ab2..967775fe8 100644 --- a/src/modules/esl-toggleable/core/esl-toggleable.shape.ts +++ b/src/modules/esl-toggleable/core/esl-toggleable.shape.ts @@ -22,10 +22,11 @@ export interface ESLToggleableTagShape /** * Define focus behavior * - 'none' - no focus management + * - 'grab' - focus on the first focusable element * - 'chain' - focus on the first focusable element first and return focus to the activator after the last focusable element * - 'loop' - focus on the first focusable element and loop through the focusable elements */ - 'focus-behaviour'?: 'none' | 'chain' | 'loop'; + 'focus-behavior'?: 'none' | 'chain' | 'loop'; /** Define Toggleable group meta information to organize groups */ 'group'?: string; diff --git a/src/modules/esl-toggleable/core/esl-toggleable.ts b/src/modules/esl-toggleable/core/esl-toggleable.ts index 558f771ee..d57db5b51 100644 --- a/src/modules/esl-toggleable/core/esl-toggleable.ts +++ b/src/modules/esl-toggleable/core/esl-toggleable.ts @@ -119,6 +119,7 @@ export class ESLToggleable extends ESLBaseElement { /** * Focus behavior. Available values: * - 'none' - no focus management + * - 'grab' - focus on the first focusable element * - 'chain' - focus on the first focusable element first and return focus to the activator after the last focusable element * - 'loop' - focus on the first focusable element and loop through the focusable elements */ @@ -284,7 +285,7 @@ export class ESLToggleable extends ESLBaseElement { /** * Actions to execute on show toggleable. * Inner state and 'open' attribute are not affected and updated before `onShow` execution. - * Adds CSS classes, update a11y and fire {@link ESLToggleable.REFRESH_EVENT} event by default. + * Adds CSS classes, update a11y and fire {@link ESLBaseElement.REFRESH_EVENT} event by default. */ protected onShow(params: ESLToggleableActionParams): void { this.open = true; @@ -417,15 +418,15 @@ export class ESLToggleable extends ESLBaseElement { @listen('focusout') protected _onFocusOut(e: FocusEvent): void { if (!this.open) return; - if (this.focusBehavior === 'chain') { - afterNextRender(() => { - if (this.hasFocus) return; + afterNextRender(() => { + if (this.hasFocus) return; + if (this.focusBehavior === 'chain') { this.hide({initiator: 'focusout', event: e}); - }); - } - if (this.focusBehavior === 'loop') { - this.focus({preventScroll: true}); - } + } + if (this.focusBehavior === 'loop') { + this.focus({preventScroll: true}); + } + }); } @listen({auto: false, event: 'mouseenter'}) diff --git a/src/modules/esl-tooltip/core/esl-tooltip.ts b/src/modules/esl-tooltip/core/esl-tooltip.ts index 2af19ab2e..25111e4cc 100644 --- a/src/modules/esl-tooltip/core/esl-tooltip.ts +++ b/src/modules/esl-tooltip/core/esl-tooltip.ts @@ -75,6 +75,7 @@ export class ESLTooltip extends ESLPopup { this.dir = params.dir || ''; this.lang = params.lang || ''; this.parentNode !== document.body && document.body.appendChild(this); + super.onShow(params); } diff --git a/src/modules/esl-utils/dom/focus.ts b/src/modules/esl-utils/dom/focus.ts index a47644c7f..58f39a839 100644 --- a/src/modules/esl-utils/dom/focus.ts +++ b/src/modules/esl-utils/dom/focus.ts @@ -34,8 +34,8 @@ export const handleFocusFlow = ( if (type === 'loop') return handleFocusChain(e, $first, $last); - if (type === 'chain' && $last && $fallback) { - if (e.target !== (e.shiftKey ? $first : $last)) return; + if (type === 'chain' && $fallback) { + if ($last && e.target !== (e.shiftKey ? $first : $last)) return; $fallback.focus(); e.preventDefault(); } From 60e951fa48f6558d5384c3e3d610b92967ddea98 Mon Sep 17 00:00:00 2001 From: "ala'n (Alexey Stsefanovich)" Date: Fri, 15 Nov 2024 02:06:31 +0100 Subject: [PATCH 14/14] style(esl-toggleable): fix false observation for closed toggleable --- src/modules/esl-toggleable/core/esl-toggleable.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/modules/esl-toggleable/core/esl-toggleable.ts b/src/modules/esl-toggleable/core/esl-toggleable.ts index d57db5b51..febed5b56 100644 --- a/src/modules/esl-toggleable/core/esl-toggleable.ts +++ b/src/modules/esl-toggleable/core/esl-toggleable.ts @@ -410,7 +410,7 @@ export class ESLToggleable extends ESLBaseElement { this.hide({initiator: 'keyboard', event: e}); e.stopPropagation(); } - if (this.focusBehavior !== 'none' && e.key === TAB) { + if (this.focusBehavior !== 'none' && e.key === TAB && this.open) { handleFocusFlow(e, this.$focusables, this.activator || this, this.focusBehavior); } }