Skip to content

Commit

Permalink
Merge pull request #2753 from exadel-inc/feat/focus-management
Browse files Browse the repository at this point in the history
feat: ESLTogglable based focus management (UPDATED v2)
  • Loading branch information
ala-n authored Nov 15, 2024
2 parents e25c639 + a134f23 commit 6b2d4ef
Show file tree
Hide file tree
Showing 21 changed files with 178 additions and 78 deletions.
2 changes: 1 addition & 1 deletion docs/COMMIT_CONVENTION.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion site/views/components/esl-panel-group.njk
Original file line number Diff line number Diff line change
@@ -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:
Expand Down
2 changes: 1 addition & 1 deletion site/views/components/esl-toggleable.njk
Original file line number Diff line number Diff line change
@@ -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:
Expand Down
2 changes: 1 addition & 1 deletion site/views/examples/image.njk
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ aside:
<h3>Mode: cover</h3>
<p>No inner image, image is rendered by background image.</p>
<p>ESL Image has no own size. Can be used with img-container classes</p>
<p><b>Inscribe</b> can be used to declare inscribe image behaviour</p>
<p><b>Inscribe</b> can be used to declare inscribe image behavior</p>
</div>
</div>
</script>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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});
}

Expand Down
2 changes: 1 addition & 1 deletion src/modules/esl-image/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ Was originally developed as an alternative to `<picture>` 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.
Expand Down
2 changes: 1 addition & 1 deletion src/modules/esl-image/core/esl-image.shape.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export interface ESLImageTagShape extends ESLBaseElementShape<ESLImage> {
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;
Expand Down
2 changes: 1 addition & 1 deletion src/modules/esl-panel-group/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion src/modules/esl-panel-group/core/esl-panel-group.shape.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ export interface ESLPanelGroupTagShape extends ESLBaseElementShape<ESLPanelGroup
/** Define a list of breakpoints to disable collapse/expand animation (for both Group and Panel animations)*/
'no-animate'?: string;

/** Define active panel(s) behaviour in case of configuration change. Supported values: `last|initial|close|open`*/
/** Define active panel(s) behavior in case of configuration change. Supported values: `last|initial|close|open`*/
'refresh-strategy'?: string;

/** Define minimum number of panels that could be opened ('1' by default, supported values: values: `0 | 1 | number | all`) */
Expand Down
2 changes: 1 addition & 1 deletion src/modules/esl-panel-group/core/esl-panel-group.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ export class ESLPanelGroup extends ESLBaseElement {
@attr({defaultValue: '1'}) public maxOpenItems: string;

/**
* Define active panel(s) behaviour in case of configuration change (mode, min-open-items, max-open-items)
* Define active panel(s) behavior in case of configuration change (mode, min-open-items, max-open-items)
* `last` (default) - try to preserve currently active panel(s)
* `initial` - activates initially opened panel(s)
* `open` - open max of available panels
Expand Down
17 changes: 11 additions & 6 deletions src/modules/esl-popup/core/esl-popup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {ESLIntersectionTarget, ESLIntersectionEvent} from '../../esl-event-liste
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';

Expand Down Expand Up @@ -44,8 +45,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;
Expand Down Expand Up @@ -111,6 +110,16 @@ export class ESLPopup extends ESLToggleable {
@attr({parser: parseBoolean, serializer: toBooleanAttribute, defaultValue: true})
public override closeOnOutsideAction: boolean;

/**
* Focus behavior. Available values:
* - 'none' - no focus management
* - 'grab' - focus on the first focusable element, does not affect focus flow or behavior after the last 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: 'chain'})
public override focusBehavior: FocusFlowType;

public $placeholder: ESLPopupPlaceholder | null;

protected _extraClass?: string;
Expand Down Expand Up @@ -227,9 +236,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}));
}

/**
Expand All @@ -241,7 +247,6 @@ export class ESLPopup extends ESLToggleable {
this.beforeOnHide(params);
super.onHide(params);
this.afterOnHide(params);
params.autofocus && this.activator?.focus({preventScroll: true});
}

/**
Expand Down
2 changes: 1 addition & 1 deletion src/modules/esl-share/actions/external-action.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import {ESLShareUrlGenericAction} from './url-generic-action';

import type {ESLShareButton} from '../core/esl-share-button';

/** Sharing using default browser link behaviour {@link ESLShareBaseAction} implementation */
/** Sharing using default browser link behavior {@link ESLShareBaseAction} implementation */
@ESLShareUrlGenericAction.register
export class ESLShareExternalAction extends ESLShareUrlGenericAction {
public static override readonly is: string = 'external';
Expand Down
4 changes: 2 additions & 2 deletions src/modules/esl-share/core/esl-share-popup.shape.ts
Original file line number Diff line number Diff line change
@@ -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<ESLSharePopup> {
export interface ESLSharePopupTagShape extends ESLPopupTagShape<ESLSharePopup> {
/** Allowed children */
children?: any;
}
Expand Down
41 changes: 32 additions & 9 deletions src/modules/esl-share/core/esl-share-popup.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
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 {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';

export type {ESLSharePopupTagShape} from './esl-share-popup.shape';
Expand All @@ -13,7 +13,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;
}
Expand All @@ -28,12 +28,12 @@ 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,
position: 'top',
hideDelay: 300
};
Expand All @@ -49,24 +49,47 @@ export class ESLSharePopup extends ESLTooltip {

/** Shared instance of ESLSharePopup */
@memoize()
public static override get sharedInstance(): ESLSharePopup {
public static get sharedInstance(): ESLSharePopup {
return ESLSharePopup.create();
}

@prop(true) public override hasFocusLoop: boolean;
/** Disable arrow at Tooltip */
@boolAttr() public disableArrow: boolean;

/** Hashstring with a list of buttons already rendered in the popup */
protected _list: string = '';

public override onShow(params: ESLTooltipActionParams): void {
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: ESLSharePopupActionParams): 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: ESLSharePopupActionParams): 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;
Expand Down
8 changes: 8 additions & 0 deletions src/modules/esl-toggleable/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,18 @@ Use `ESLToggleableDispatcher.init()` to initialize (and bind) `ESLToggleableDisp

- `group` (`groupName`) - Toggleable group meta information to organize groups
- `no-auto-id` - Disallow automatic id creation when it's empty

- `close-on` (`closeTrigger`) - Selector to mark inner close triggers
- `close-on-esc` - Close the Toggleable on ESC keyboard event
- `close-on-outside-action` - Close the Toggleable on a click/tap outside

- `focus-behavior` - Focus flow behavior. <i class="badge badge-sup badge-success">new</i>
Available values:
- `none` (default) - does not affect focus management
- `grab` - focus on the first focusable element, does not affect focus flow or behavior after the last 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

- `initial-params` - Initial params to pass to show/hide action on start
- `default-params` - Default params to merge into passed action params

Expand Down
9 changes: 9 additions & 0 deletions src/modules/esl-toggleable/core/esl-toggleable.shape.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,15 @@ export interface ESLToggleableTagShape<T extends ESLToggleable = ESLToggleable>
/** Open toggleable marker. Can be used to define initial state */
'open'?: boolean;

/**
* Define focus behavior
* - 'none' - no focus management
* - 'grab' - focus on the first focusable element, does not affect focus flow or behavior after the last 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-behavior'?: 'none' | 'chain' | 'loop' | 'grab';

/** Define Toggleable group meta information to organize groups */
'group'?: string;

Expand Down
Loading

0 comments on commit 6b2d4ef

Please sign in to comment.