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

feat(a11y): added support in the CloudImageEditor #671

Merged
merged 12 commits into from
Jun 24, 2024
10 changes: 3 additions & 7 deletions abstract/ActivityBlock.js
Original file line number Diff line number Diff line change
Expand Up @@ -138,9 +138,7 @@ export class ActivityBlock extends Block {
/** @type {string | null} */
const currentActivity = this.$['*currentActivity'];

/** @type {Set<import('./Block').Block>} */
let blocksRegistry = this.$['*blocksRegistry'];
const hasCurrentActivityInCtx = !![...blocksRegistry].find(
const hasCurrentActivityInCtx = !![...this.blocksRegistry].find(
(block) => block instanceof ActivityBlock && block.activityType === currentActivity,
);

Expand Down Expand Up @@ -177,10 +175,8 @@ export class ActivityBlock extends Block {
}
let couldOpenActivity = !!nextActivity;
if (nextActivity) {
/** @type {Set<ActivityBlock>} */
let blocksRegistry = this.$['*blocksRegistry'];
const nextActivityBlock = [...blocksRegistry].find((block) => block.activityType === nextActivity);
couldOpenActivity = nextActivityBlock?.couldOpenActivity ?? false;
const nextActivityBlock = [...this.blocksRegistry].find((block) => block.activityType === nextActivity);
couldOpenActivity = /** @type {ActivityBlock} */ (nextActivityBlock)?.couldOpenActivity ?? false;
}
nextActivity = couldOpenActivity ? nextActivity : undefined;
this.$['*currentActivity'] = nextActivity;
Expand Down
24 changes: 18 additions & 6 deletions abstract/Block.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { blockCtx } from './CTX.js';
import { LocaleManager, localeStateKey } from './LocaleManager.js';
import { l10nProcessor } from './l10nProcessor.js';
import { sharedConfigKey } from './sharedConfigKey.js';
import { A11y } from './a11y.js';

const TAG_PREFIX = 'lr-';

Expand Down Expand Up @@ -96,10 +97,7 @@ export class Block extends BaseComponent {
* @returns {Boolean}
*/
hasBlockInCtx(callback) {
// @ts-ignore TODO: fix this
/** @type {Set} */
let blocksRegistry = this.$['*blocksRegistry'];
for (let block of blocksRegistry) {
for (let block of this.blocksRegistry) {
if (callback(block)) {
return true;
}
Expand Down Expand Up @@ -180,6 +178,10 @@ export class Block extends BaseComponent {
this.add('*localeManager', new LocaleManager(this));
}

if (!this.has('*a11y')) {
this.add('*a11y', new A11y());
}

this.sub(localeStateKey('locale-id'), (localeId) => {
const direction = getLocaleDirection(localeId);
this.style.direction = direction === 'ltr' ? '' : direction;
Expand All @@ -191,9 +193,18 @@ export class Block extends BaseComponent {
return this.has('*localeManager') ? this.$['*localeManager'] : null;
}

/** @returns {A11y | null} */
get a11y() {
return this.has('*a11y') ? this.$['*a11y'] : null;
}

/** @type {Set<Block>} */
get blocksRegistry() {
return this.$['*blocksRegistry'];
}

destroyCallback() {
/** @type {Set<Block>} */
let blocksRegistry = this.$['*blocksRegistry'];
let blocksRegistry = this.blocksRegistry;
blocksRegistry.delete(this);

this.localeManager?.destroyL10nBindings(this);
Expand All @@ -220,6 +231,7 @@ export class Block extends BaseComponent {
Data.deleteCtx(this.ctxName);

this.localeManager?.destroy();
this.a11y?.destroy();
}

/**
Expand Down
5 changes: 5 additions & 0 deletions abstract/SolutionBlock.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@ export class SolutionBlock extends Block {
init$ = uploaderBlockCtx(this);
_template = null;

initCallback() {
super.initCallback();
this.a11y?.registerBlock(this);
}

static set template(value) {
this._template = svgIconsSprite + value + /** HTML */ `<slot></slot>`;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Static method context issue needs resolution.

The static methods set template and get template incorrectly use this to refer to instance properties, which is not valid in a static context. This could lead to runtime errors.

- this._template = svgIconsSprite + value + /** HTML */ `<slot></slot>`;
+ SolutionBlock._template = svgIconsSprite + value + /** HTML */ `<slot></slot>`;

- return this._template;
+ return SolutionBlock._template;

Also applies to: 21-21

Tools
Biome

[error] 17-17: Using this in a static context can be confusing. (lint/complexity/noThisInStatic)

this refers to the class.
Unsafe fix: Use the class name instead.

}
Expand Down
3 changes: 1 addition & 2 deletions abstract/UploaderBlock.js
Original file line number Diff line number Diff line change
Expand Up @@ -308,8 +308,7 @@ export class UploaderBlock extends ActivityBlock {
return;
}

/** @type {Set<import('./Block').Block>} */
const blocksRegistry = this.$['*blocksRegistry'];
const blocksRegistry = this.blocksRegistry;
/**
* @param {import('./Block').Block} block
* @returns {block is import('../blocks/SourceBtn/SourceBtn.js').SourceBtn}
Expand Down
118 changes: 118 additions & 0 deletions abstract/a11y.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
// @ts-check
import { startKeyUX, hiddenKeyUX, jumpKeyUX, focusGroupKeyUX, pressKeyUX } from 'keyux';

/**
* MinimalWindow interface is not exported by keyux, so we import it here using tricky way.
*
* @typedef {Parameters<import('keyux').KeyUXModule>[0]} MinimalWindow
*/

/**
* This is global window wrapper that allows to scope event listeners to a specific part of the DOM.
*
* It is used to scope the key UX to the widget.
*
* @implements {MinimalWindow}
*/
class ScopedMinimalWindow {
/**
* @private
* @type {Map<Function, (event: Event) => void>}
*/
_listeners = new Map();

/**
* @private
* @type {Node[]}
*/
_scope = [];

/**
* @param {'keydown' | 'keyup'} type
* @param {(event: Event) => void} listener
*/
addEventListener(type, listener) {
/** @param {Event} e */
const wrappedListener = (e) => {
const target = e.target;
if (!target) return;
if (this._scope.some((el) => el === e.target || el.contains(/** @type {Node} */ (target)))) {
listener(e);
}
};
this._listeners.set(listener, wrappedListener);
window.addEventListener(type, wrappedListener);
}

/**
* @param {'keydown' | 'keyup'} type
* @param {(event: {}) => void} listener
*/
removeEventListener(type, listener) {
const wrappedListener = this._listeners.get(listener);
if (wrappedListener) {
window.removeEventListener(type, wrappedListener);
}
this._listeners.delete(listener);
}

get CustomEvent() {
return window.CustomEvent;
}

get document() {
return window.document;
}

get navigator() {
return window.navigator;
}

/** @param {Node} scope */
registerScope(scope) {
this._scope.push(scope);
}

destroy() {
this._scope = [];
this._listeners.forEach((listener, originalListener) => {
window.removeEventListener('keydown', listener);
window.removeEventListener('keyup', listener);
this._listeners.delete(originalListener);
});
}
}

export class A11y {
/**
* @private
* @type {(() => void) | undefined}
*/
_destroyKeyUX;

/**
* @private
* @type {ScopedMinimalWindow}
*/
_scopedWindow;

constructor() {
this._scopedWindow = new ScopedMinimalWindow();
this._destroyKeyUX = startKeyUX(this._scopedWindow, [
focusGroupKeyUX(),
pressKeyUX('is-pressed'),
jumpKeyUX(),
hiddenKeyUX(),
]);
}

/** @param {import('./Block.js').Block} scope */
registerBlock(scope) {
this._scopedWindow.registerScope(scope);
}

destroy() {
this._destroyKeyUX?.();
this._scopedWindow.destroy();
}
}
12 changes: 4 additions & 8 deletions blocks/CloudImageEditor/src/EditorButtonControl.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,6 @@ export class EditorButtonControl extends Block {
this._titleEl = this.ref['title-el'];
this._iconEl = this.ref['icon-el'];

this.setAttribute('role', 'button');
if (this.tabIndex === -1) {
this.tabIndex = 0;
}

this.sub('title', (title) => {
let titleEl = this._titleEl;
if (titleEl) {
Expand All @@ -42,7 +37,8 @@ export class EditorButtonControl extends Block {
}

EditorButtonControl.template = /* HTML */ `
<div class="before"></div>
<lr-icon set="@name: icon;"></lr-icon>
<div class="title" ref="title-el">{{title}}</div>
<button role="option">
<lr-icon set="@name: icon;"></lr-icon>
<div class="title" ref="title-el">{{title}}</div>
</button>
`;
7 changes: 4 additions & 3 deletions blocks/CloudImageEditor/src/EditorFilterControl.js
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@
initCallback() {
super.initCallback();

this.$['on.click'] = (e) => {

Check warning on line 76 in blocks/CloudImageEditor/src/EditorFilterControl.js

View workflow job for this annotation

GitHub Actions / build

'e' is defined but never used
if (!this.$.active) {
this.$['*sliderEl'].setOperation(this._operation, this._filter);
this.$['*sliderEl'].apply();
Expand Down Expand Up @@ -148,7 +148,8 @@
}

EditorFilterControl.template = /* HTML */ `
<div class="before"></div>
<div class="preview" ref="preview-el"></div>
<lr-icon ref="icon-el" set="@name: icon; @size: iconSize;"></lr-icon>
<button role="option">
<div class="preview" ref="preview-el"></div>
<lr-icon ref="icon-el" set="@name: icon; @size: iconSize;"></lr-icon>
</button>
`;
23 changes: 17 additions & 6 deletions blocks/CloudImageEditor/src/EditorToolbar.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,7 @@ function renderTabToggle(id) {
ref="tab-toggle-${id}"
data-id="${id}"
icon="${id}"
tabindex="0"
set="onclick: on.clickTab;"
set="onclick: on.clickTab; aria-role:tab_role; aria-controls:tab_${id}"
>
</lr-btn-ui>
</lr-presence-toggle>
Expand All @@ -36,10 +35,14 @@ function renderTabToggle(id) {
/** @param {String} id */
function renderTabContent(id) {
return /* HTML */ `
<lr-presence-toggle class="tab-content" set="visible: presence.tabContent.${id}; styles: presence.tabContentStyles">
<lr-presence-toggle
id="tab_${id}"
class="tab-content"
set="visible: presence.tabContent.${id}; styles: presence.tabContentStyles"
>
<lr-editor-scroller hidden-scrollbar>
<div class="controls-list_align">
<div class="controls-list_inner" ref="controls-list-${id}"></div>
<div role="listbox" aria-orientation="horizontal" class="controls-list_inner" ref="controls-list-${id}"></div>
</div>
</lr-editor-scroller>
</lr-presence-toggle>
Expand Down Expand Up @@ -115,6 +118,10 @@ export class EditorToolbar extends Block {
this._activateTab(id, { fromViewer: false });
}
},
tab_role: 'tab',
[`tab_${TabId.TUNING}`]: `tab_${TabId.TUNING}`,
[`tab_${TabId.CROP}`]: `tab_${TabId.CROP}`,
[`tab_${TabId.FILTERS}`]: `tab_${TabId.FILTERS}`,
};

/** @private */
Expand Down Expand Up @@ -392,14 +399,18 @@ EditorToolbar.template = /* HTML */ `
</div>
</div>
<div class="toolbar-container">
<lr-presence-toggle class="sub-toolbar" set="visible: presence.mainToolbar; styles: presence.subTopToolbarStyles">
<lr-presence-toggle
role="tablist"
class="sub-toolbar"
set="visible: presence.mainToolbar; styles: presence.subTopToolbarStyles"
>
<div class="tab-content-row">${ALL_TABS.map(renderTabContent).join('')}</div>
<div class="controls-row">
<lr-btn-ui theme="secondary-icon" icon="closeMax" set="onclick: on.cancel"> </lr-btn-ui>
<lr-presence-toggle class="tab-toggles" set="visible: presence.tabToggles; styles: presence.tabTogglesStyles">
<div ref="tabs-indicator" class="tab-toggles_indicator"></div>
${ALL_TABS.map(renderTabToggle).join('')}
</lr-presence-toggle>
<lr-btn-ui style="order: -1" theme="secondary-icon" icon="closeMax" set="onclick: on.cancel"> </lr-btn-ui>
<lr-btn-ui theme="primary-icon" icon="done" set="onclick: on.apply"> </lr-btn-ui>
</div>
</lr-presence-toggle>
Expand Down
Loading
Loading