From 135fe9a0d2c2328c8a80d54546193f1e517609b2 Mon Sep 17 00:00:00 2001 From: Aleksandr Bunin Date: Sat, 25 Jun 2022 12:10:50 +0300 Subject: [PATCH 001/185] Add aria directive classes and modal role engine --- src/base/b-window/b-window.ss | 10 ++- src/core/component/directives/aria/helpers.ts | 82 +++++++++++++++++++ src/core/component/directives/aria/index.ts | 40 +++++++++ .../component/directives/aria/interface.ts | 24 ++++++ .../directives/aria/roles-engines/dialog.ts | 41 ++++++++++ .../directives/aria/roles-engines/index.ts | 1 + .../aria/roles-engines/interface.ts | 22 +++++ src/core/component/directives/index.ts | 4 + src/super/i-input/i-input.ss | 2 +- src/traits/i-open/i-open.ts | 14 ++++ 10 files changed, 237 insertions(+), 3 deletions(-) create mode 100644 src/core/component/directives/aria/helpers.ts create mode 100644 src/core/component/directives/aria/index.ts create mode 100644 src/core/component/directives/aria/interface.ts create mode 100644 src/core/component/directives/aria/roles-engines/dialog.ts create mode 100644 src/core/component/directives/aria/roles-engines/index.ts create mode 100644 src/core/component/directives/aria/roles-engines/interface.ts diff --git a/src/base/b-window/b-window.ss b/src/base/b-window/b-window.ss index 480ef0e322..616d92ba40 100644 --- a/src/base/b-window/b-window.ss +++ b/src/base/b-window/b-window.ss @@ -24,7 +24,10 @@ opt.ifOnce('opened', m.opened === 'true') && delete watchModsStore.opened . - < :section.&__window ref = window + < :section.&__window & + ref = window | + v-aria:dialog.#title + . - if thirdPartySlots < template v-if = slotName : isSlot = /^windowSlot[A-Z]/ @@ -36,7 +39,10 @@ < template v-else += self.slot() - < h1.&__title v-if = title || vdom.getSlot('title') + < h1.&__title & + v-if = title || vdom.getSlot('title') | + :id = dom.getId('title') + . += self.slot('title', {':title': 'title'}) - block title {{ title }} diff --git a/src/core/component/directives/aria/helpers.ts b/src/core/component/directives/aria/helpers.ts new file mode 100644 index 0000000000..f2746c0620 --- /dev/null +++ b/src/core/component/directives/aria/helpers.ts @@ -0,0 +1,82 @@ +import type { DirectiveHookParams, AriaRoleEngine } from 'core/component/directives/aria/interface'; +import type iBlock from 'super/i-block/i-block'; +import * as ariaRoles from 'core/component/directives/aria/roles-engines/index'; + +export function setAriaLabel({el, opts, vnode}: DirectiveHookParams): void { + const + {dom, vdom, $createElement: createElem} = Object.cast(vnode.fakeContext), + value = opts.value ?? {}; + + for (const mod in opts.modifiers) { + if (!mod.startsWith('#')) { + continue; + } + + const + title = mod.slice(1), + id = dom.getId(title); + + if ('labelledby' in opts.modifiers) { + el.setAttribute('aria-labelledby', id); + + } else { + el.setAttribute('id', id); + + const + labelNode = createElem.call(vnode.fakeContext, + 'label', + { + attrs: {for: id} + }); + + const + labelElem = vdom.render(labelNode); + + el.prepend(labelElem); + } + } + + if (value.label != null) { + el.setAttribute('aria-label', value.label); + + } else if (value.labelledby != null) { + el.setAttribute('aria-labelledby', dom.getId(value.labelledby)); + } + + if (value.description != null) { + el.setAttribute('aria-description', value.description); + + } else if (value.describedby != null) { + el.setAttribute('aria-describedby', dom.getId(value.describedby)); + } +} + +export function setAriaTabIndex({opts, vnode}: DirectiveHookParams): void { + if (opts.value == null) { + return; + } + + const + names = opts.value.children, + {block} = Object.cast(vnode.fakeContext); + + for (const name of names) { + const + elems = block?.elements(name); + + elems?.forEach((el: Element) => { + el.setAttribute('tabindex', '0'); + }); + } +} + +export function setAriaRole(options: DirectiveHookParams): CanUndef { + const + {arg: role} = options.opts; + + if (role == null) { + return; + } + + return new ariaRoles[role](options); +} diff --git a/src/core/component/directives/aria/index.ts b/src/core/component/directives/aria/index.ts new file mode 100644 index 0000000000..7288f71c6f --- /dev/null +++ b/src/core/component/directives/aria/index.ts @@ -0,0 +1,40 @@ +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ + +/** + * [[include:core/component/directives/aria/README.md]] + * @packageDocumentation + */ + +import { ComponentEngine, VNode, VNodeDirective } from 'core/component/engines'; +import { setAriaLabel, setAriaRole, setAriaTabIndex } from 'core/component/directives/aria/helpers'; + +ComponentEngine.directive('aria', { + inserted(el: Element, opts: VNodeDirective, vnode: VNode): void { + const + {value, arg, modifiers} = opts; + + if (value == null && arg == null && modifiers == null) { + return; + } + + const + options = {el, opts, vnode}; + + setAriaLabel(options); + setAriaTabIndex(options); + setAriaRole(options)?.init(); + }, + + unbind(el: Element, opts: VNodeDirective, vnode: VNode) { + const + options = {el, opts, vnode}; + + setAriaRole(options)?.clear(); + } +}); diff --git a/src/core/component/directives/aria/interface.ts b/src/core/component/directives/aria/interface.ts new file mode 100644 index 0000000000..d48eb0748c --- /dev/null +++ b/src/core/component/directives/aria/interface.ts @@ -0,0 +1,24 @@ +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ + +import type { VNodeDirective, VNode } from 'core/component/engines'; + +export interface DirectiveHookParams { + el: Element; + opts: VNodeDirective; + vnode: VNode; +} + +export interface AriaRoleEngine { + el: Element; + value: any; + vnode: VNode; + + init(): void; + clear(): void; +} diff --git a/src/core/component/directives/aria/roles-engines/dialog.ts b/src/core/component/directives/aria/roles-engines/dialog.ts new file mode 100644 index 0000000000..fec7e6a538 --- /dev/null +++ b/src/core/component/directives/aria/roles-engines/dialog.ts @@ -0,0 +1,41 @@ +import iOpen from 'traits/i-open/i-open'; +import type iBlock from 'super/i-block/i-block'; +import type { DirectiveHookParams } from 'core/component/directives/aria/interface'; +import RoleEngine from 'core/component/directives/aria/roles-engines/interface'; + +export default class DialogEngine extends RoleEngine { + group: Dictionary = {}; + + constructor(options: DirectiveHookParams) { + super(options); + + if (!iOpen.is(options.vnode.fakeContext)) { + Object.throw('Dialog directive expects the component to realize iOpen interface'); + } + } + + override init(): void { + const + {localEmitter: $e} = Object.cast(this.vnode.fakeContext); + + this.el.setAttribute('role', 'dialog'); + this.el.setAttribute('aria-modal', 'false'); + + this.group = {group: 'ariaAttributes'}; + + $e.on('open', () => { + this.el.setAttribute('aria-modal', 'true'); + }, this.group); + + $e.on('close', () => { + this.el.setAttribute('aria-modal', 'false'); + }, this.group); + } + + override clear(): void { + const + {localEmitter: $e} = Object.cast(this.vnode.fakeContext); + + $e.off(this.group); + } +} diff --git a/src/core/component/directives/aria/roles-engines/index.ts b/src/core/component/directives/aria/roles-engines/index.ts new file mode 100644 index 0000000000..4ff45a36fc --- /dev/null +++ b/src/core/component/directives/aria/roles-engines/index.ts @@ -0,0 +1 @@ +export * from 'core/component/directives/aria/roles-engines/dialog'; diff --git a/src/core/component/directives/aria/roles-engines/interface.ts b/src/core/component/directives/aria/roles-engines/interface.ts new file mode 100644 index 0000000000..3ab1b6f073 --- /dev/null +++ b/src/core/component/directives/aria/roles-engines/interface.ts @@ -0,0 +1,22 @@ +import type { AriaRoleEngine, DirectiveHookParams } from 'core/component/directives/aria/interface'; +import type { VNode } from 'core/component'; + +export default abstract class RoleEngine implements AriaRoleEngine { + el: Element; + value: any; + vnode: VNode; + + constructor({el, opts, vnode}: DirectiveHookParams) { + this.el = el; + this.value = opts.value; + this.vnode = vnode; + } + + init(): void { + // + } + + clear(): void { + // + } +} diff --git a/src/core/component/directives/index.ts b/src/core/component/directives/index.ts index ca12cad9c1..710b6fdb56 100644 --- a/src/core/component/directives/index.ts +++ b/src/core/component/directives/index.ts @@ -22,4 +22,8 @@ import 'core/component/directives/image'; import 'core/component/directives/update-on'; //#endif +//#if runtime has directives/aria +import 'core/component/directives/aria'; +//#endif + import 'core/component/directives/hook'; diff --git a/src/super/i-input/i-input.ss b/src/super/i-input/i-input.ss index 5fb3be3649..8d8c85a2c8 100644 --- a/src/super/i-input/i-input.ss +++ b/src/super/i-input/i-input.ss @@ -36,7 +36,7 @@ * *) [type=nativeInputType] - value of the `:type` attribute * * *) [autofocus] - value of the `:autofocus` attribute - * *) [tabIndex] - value of the `:autofocus` attribute + * *) [tabIndex] - value of the `:tabindex` attribute * * *) [focusHandler] - value of the `@focus` attribute * *) [blurHandler] - value of the `@blur` attribute diff --git a/src/traits/i-open/i-open.ts b/src/traits/i-open/i-open.ts index 63a9e9df7d..96efa485ea 100644 --- a/src/traits/i-open/i-open.ts +++ b/src/traits/i-open/i-open.ts @@ -134,6 +134,20 @@ export default abstract class iOpen { }); } + /** + * Checks if the component realize current trait + * + * @param obj + */ + static is(obj: unknown): obj is iOpen { + if (Object.isPrimitive(obj)) { + return true; + } + + const dict = Object.cast(obj); + return Object.isFunction(dict.open) && Object.isFunction(dict.close); + } + /** * Opens the component * @param args From 24b63749937e588b82d267ae1072c0dd3cf2bce8 Mon Sep 17 00:00:00 2001 From: Aleksandr Bunin Date: Thu, 21 Jul 2022 12:05:35 +0300 Subject: [PATCH 002/185] add aria engines --- src/base/b-list/CHANGELOG.md | 10 + src/base/b-list/b-list.ss | 23 +- src/base/b-list/b-list.ts | 66 +++--- src/base/b-tree/CHANGELOG.md | 9 + src/base/b-tree/b-tree.ss | 98 +++++---- src/base/b-tree/b-tree.ts | 36 ++- .../component/directives/aria/CHANGELOG.md | 10 + src/core/component/directives/aria/README.md | 44 ++++ .../component/directives/aria/aria-setter.ts | 130 +++++++++++ src/core/component/directives/aria/helpers.ts | 82 ------- src/core/component/directives/aria/index.ts | 38 +++- .../component/directives/aria/interface.ts | 35 ++- .../directives/aria/roles-engines/combobox.ts | 55 +++++ .../directives/aria/roles-engines/controls.ts | 43 ++++ .../directives/aria/roles-engines/dialog.ts | 46 ++-- .../directives/aria/roles-engines/index.ts | 19 +- .../aria/roles-engines/interface.ts | 43 ++-- .../directives/aria/roles-engines/listbox.ts | 19 ++ .../directives/aria/roles-engines/option.ts | 27 +++ .../directives/aria/roles-engines/tab.ts | 154 +++++++++++++ .../directives/aria/roles-engines/tablist.ts | 28 +++ .../directives/aria/roles-engines/tabpanel.ts | 22 ++ .../directives/aria/roles-engines/tree.ts | 38 ++++ .../directives/aria/roles-engines/treeitem.ts | 208 ++++++++++++++++++ src/core/component/directives/index.ts | 2 - src/form/b-checkbox/CHANGELOG.md | 6 + src/form/b-checkbox/b-checkbox.ss | 14 +- src/form/b-select/CHANGELOG.md | 13 ++ src/form/b-select/b-select.ss | 31 ++- src/form/b-select/b-select.ts | 43 ++-- src/form/b-select/modules/handlers.ts | 26 ++- src/super/i-input/CHANGELOG.md | 6 + src/super/i-input/README.md | 2 +- src/super/i-input/i-input.ts | 8 +- src/traits/i-access/CHANGELOG.md | 9 + src/traits/i-access/const.ts | 2 + src/traits/i-access/i-access.ts | 139 +++++++++++- src/traits/i-open/CHANGELOG.md | 6 + src/traits/i-open/i-open.ts | 3 +- 39 files changed, 1328 insertions(+), 265 deletions(-) create mode 100644 src/core/component/directives/aria/CHANGELOG.md create mode 100644 src/core/component/directives/aria/README.md create mode 100644 src/core/component/directives/aria/aria-setter.ts delete mode 100644 src/core/component/directives/aria/helpers.ts create mode 100644 src/core/component/directives/aria/roles-engines/combobox.ts create mode 100644 src/core/component/directives/aria/roles-engines/controls.ts create mode 100644 src/core/component/directives/aria/roles-engines/listbox.ts create mode 100644 src/core/component/directives/aria/roles-engines/option.ts create mode 100644 src/core/component/directives/aria/roles-engines/tab.ts create mode 100644 src/core/component/directives/aria/roles-engines/tablist.ts create mode 100644 src/core/component/directives/aria/roles-engines/tabpanel.ts create mode 100644 src/core/component/directives/aria/roles-engines/tree.ts create mode 100644 src/core/component/directives/aria/roles-engines/treeitem.ts create mode 100644 src/traits/i-access/const.ts diff --git a/src/base/b-list/CHANGELOG.md b/src/base/b-list/CHANGELOG.md index 028effae00..18fbd9fe1c 100644 --- a/src/base/b-list/CHANGELOG.md +++ b/src/base/b-list/CHANGELOG.md @@ -9,6 +9,16 @@ Changelog > - :house: [Internal] > - :nail_care: [Polish] +## v3.0.0-rc.??? (2022-??-??) + +#### :rocket: New Feature + +* Added `v-aria` directive +* Added a new prop `vertical` +* Added `isTablist` +* Added `onActiveChange` +* Now the component derive `iAccess` + ## v3.0.0-rc.211 (2021-07-21) #### :boom: Breaking Change diff --git a/src/base/b-list/b-list.ss b/src/base/b-list/b-list.ss index b1b663dd6c..a41fc70787 100644 --- a/src/base/b-list/b-list.ss +++ b/src/base/b-list/b-list.ss @@ -32,7 +32,6 @@ :href = el.href | :value = el.value | - :aria-selected = el.href === undefined ? isActive(el.value) : undefined | :-id = values.get(el.value) | :-hint = el.hint | @@ -48,7 +47,17 @@ } })) | - :v-attrs = el.attrs + :v-attrs = isTablist + ? { + 'v-aria:tab': { + isFirst: i === 0, + isVertical: vertical, + onChange: onActiveChange, + activeElement + }, + ...el.attrs + } + : el.attrs . - block preIcon < span.&__cell.&__link-icon.&__link-pre-icon v-if = el.preIcon || vdom.getSlot('preIcon') @@ -101,6 +110,14 @@ < tag.&__wrapper & :is = listTag | - :v-attrs = attrs + :v-attrs = isTablist + ? { + 'v-aria:tablist': { + isMultiple: multiple, + isVertical: vertical + }, + ...attrs + } + : attrs . += self.list('items') diff --git a/src/base/b-list/b-list.ts b/src/base/b-list/b-list.ts index 5033b2251b..625f973cb7 100644 --- a/src/base/b-list/b-list.ts +++ b/src/base/b-list/b-list.ts @@ -20,12 +20,14 @@ import SyncPromise from 'core/promise/sync'; import { isAbsURL } from 'core/url'; +import { derive } from 'core/functools/trait'; import iVisible from 'traits/i-visible/i-visible'; import iWidth from 'traits/i-width/i-width'; import iItems, { IterationKey } from 'traits/i-items/i-items'; import iData, { component, prop, field, system, computed, hook, watch, ModsDecl } from 'super/i-data/i-data'; import type { Active, Item, Items } from 'base/b-list/interface'; +import iAccess from 'traits/i-access/i-access'; export * from 'super/i-data/i-data'; export * from 'base/b-list/interface'; @@ -33,6 +35,8 @@ export * from 'base/b-list/interface'; export const $$ = symbolGenerator(); +interface bList extends Trait {} + /** * Component to create a list of tabs/links */ @@ -47,7 +51,8 @@ export const } }) -export default class bList extends iData implements iVisible, iWidth, iItems { +@derive(iAccess) +class bList extends iData implements iVisible, iWidth, iItems, iAccess { /** * Type: component active item */ @@ -107,6 +112,12 @@ export default class bList extends iData implements iVisible, iWidth, iItems { @prop(Boolean) readonly multiple: boolean = false; + /** + * If true, the component view orientation is vertical. Horizontal is default + */ + @prop(Boolean) + readonly vertical: boolean = false; + /** * If true, the active item can be unset by using another click to it. * By default, if the component is switched to the `multiple` mode, this value is set to `true`, @@ -126,15 +137,7 @@ export default class bList extends iData implements iVisible, iWidth, iItems { * @see [[bList.attrsProp]] */ get attrs(): Dictionary { - const - attrs = {...this.attrsProp}; - - if (this.items.some((el) => el.href === undefined)) { - attrs.role = 'tablist'; - attrs['aria-multiselectable'] = this.multiple; - } - - return attrs; + return {...this.attrsProp}; } /** @@ -381,10 +384,6 @@ export default class bList extends iData implements iVisible, iWidth, iItems { if (previousLinkEl !== linkEl) { $b.setElMod(previousLinkEl, 'link', 'active', false); - - if (previousLinkEl.hasAttribute('aria-selected')) { - previousLinkEl.setAttribute('aria-selected', 'false'); - } } } } @@ -396,10 +395,6 @@ export default class bList extends iData implements iVisible, iWidth, iItems { for (let i = 0; i < els.length; i++) { const el = els[i]; $b.setElMod(el, 'link', 'active', true); - - if (el.hasAttribute('aria-selected')) { - el.setAttribute('aria-selected', 'true'); - } } }, stderr); } @@ -485,10 +480,6 @@ export default class bList extends iData implements iVisible, iWidth, iItems { if (needChangeMod) { $b.setElMod(el, 'link', 'active', false); - - if (el.hasAttribute('aria-selected')) { - el.setAttribute('aria-selected', 'false'); - } } } }, stderr); @@ -617,13 +608,6 @@ export default class bList extends iData implements iVisible, iWidth, iItems { item.classes = this.provide.hintClasses(item.hintPos) .concat(item.classes ?? []); - if (href === undefined) { - item.attrs = { - ...item.attrs, - role: 'tab' - }; - } - normalizedItems.push({...item, value, href}); } @@ -703,6 +687,13 @@ export default class bList extends iData implements iVisible, iWidth, iItems { } } + /** + * Returns true if the component is used as tab list + */ + protected get isTablist(): boolean { + return this.items.some((el) => el.href === undefined); + } + protected override onAddData(data: unknown): void { Object.assign(this.db, this.convertDataToDB(data)); } @@ -734,4 +725,21 @@ export default class bList extends iData implements iVisible, iWidth, iItems { this.toggleActive(this.indexes[id]); this.emit('actionChange', this.active); } + + /** + * Handler: on active element changes + * @param cb + */ + protected onActiveChange(cb: Function): void { + this.on('change', () => { + if (Object.isSet(this.active)) { + cb(this.block?.elements('link', {active: true})); + + } else { + cb(this.block?.element('link', {active: true})); + } + }); + } } + +export default bList; diff --git a/src/base/b-tree/CHANGELOG.md b/src/base/b-tree/CHANGELOG.md index 5920f061c2..12a877eb58 100644 --- a/src/base/b-tree/CHANGELOG.md +++ b/src/base/b-tree/CHANGELOG.md @@ -9,6 +9,15 @@ Changelog > - :house: [Internal] > - :nail_care: [Polish] +## v3.0.0-rc.??? (2022-??-??) + +#### :rocket: New Feature + +* Added `v-aria` directive +* Added a new prop `vertical` +* Added `changeFoldedMod` +* Now the component derive `iAccess` + ## v3.0.0-rc.164 (2021-03-22) #### :house: Internal diff --git a/src/base/b-tree/b-tree.ss b/src/base/b-tree/b-tree.ss index 4849e3a49f..4ba11ef0a9 100644 --- a/src/base/b-tree/b-tree.ss +++ b/src/base/b-tree/b-tree.ss @@ -12,51 +12,65 @@ - template index() extends ['i-data'].index - block body - < template & - v-for = (el, i) in asyncRender.iterate(items, renderChunks, renderTaskParams) | - :key = getItemKey(el, i) + < .&__root & + v-aria:tree = { + isVertical: vertical, + isRootTree: top == null, + onChange: (cb) => on('fold', (ctx, el, item, value) => cb(el, value)) + } . - < .&__node & - :-id = dom.getId(el.id) | - :-level = level | - :class = provide.elClasses({ - node: { - level, - folded: getFoldedPropValue(el) - } - }) + < template & + v-for = (el, i) in asyncRender.iterate(items, renderChunks, renderTaskParams) | + :key = getItemKey(el, i) . - < .&__item-wrapper - < .&__marker - - block fold - < template v-if = Object.size(field.get('children.length', el)) > 0 - += self.slot('fold', {':params': 'getFoldProps(el)'}) - < .&__fold :v-attrs = getFoldProps(el) + < .&__node & + :-id = dom.getId(el.id) | + :-level = level | + :class = provide.elClasses({ + node: { + level, + folded: el.children && getFoldedPropValue(el) + } + }) | + v-aria:treeitem = { + getRootElement: () => (top ? top.$el : $el), + toggleFold: changeFoldedMod.bind(this, el), + getFoldedMod: getFoldedModById.bind(this, el.id), + isVeryFirstItem: top == null && i === 0, + } + . + < .&__item-wrapper + < .&__marker + - block fold + < template v-if = Object.size(field.get('children.length', el)) > 0 + += self.slot('fold', {':params': 'getFoldProps(el)'}) + < .&__fold :v-attrs = getFoldProps(el) - - block item - += self.slot('default', {':item': 'getItemProps(el, i)'}) - < component.&__item & - v-if = item | - :is = Object.isFunction(item) ? item(el, i) : item | - :v-attrs = getItemProps(el, i) - . + - block item + += self.slot('default', {':item': 'getItemProps(el, i)'}) + < component.&__item & + v-if = item | + :is = Object.isFunction(item) ? item(el, i) : item | + :v-attrs = getItemProps(el, i) | + dispatching = true + . - - block children - < .&__children v-if = Object.size(field.get('children', el)) > 0 - < b-tree.&__child & - :items = el.children | - :folded = getFoldedPropValue(el) | - :item = item | - :v-attrs = nestedTreeProps - . - < template & - #default = o | - v-if = vdom.getSlot('default') + - block children + < .&__children v-if = Object.size(field.get('children', el)) > 0 + < b-tree.&__child & + :items = el.children | + :folded = getFoldedPropValue(el) | + :item = item | + :v-attrs = nestedTreeProps . - += self.slot('default', {':item': 'o.item'}) + < template & + #default = o | + v-if = vdom.getSlot('default') + . + += self.slot('default', {':item': 'o.item'}) - < template & - #fold = o | - v-if = vdom.getSlot('fold') - . - += self.slot('fold', {':params': 'o.params'}) + < template & + #fold = o | + v-if = vdom.getSlot('fold') + . + += self.slot('fold', {':params': 'o.params'}) diff --git a/src/base/b-tree/b-tree.ts b/src/base/b-tree/b-tree.ts index ee70c44885..fae29b1f57 100644 --- a/src/base/b-tree/b-tree.ts +++ b/src/base/b-tree/b-tree.ts @@ -17,10 +17,12 @@ import 'models/demo/nested-list'; import symbolGenerator from 'core/symbol'; +import { derive } from 'core/functools/trait'; import iItems, { IterationKey } from 'traits/i-items/i-items'; import iData, { component, prop, field, TaskParams, TaskI } from 'super/i-data/i-data'; import type { Item, RenderFilter } from 'base/b-tree/interface'; +import iAccess from 'traits/i-access/i-access'; export * from 'super/i-data/i-data'; export * from 'base/b-tree/interface'; @@ -28,11 +30,14 @@ export * from 'base/b-tree/interface'; export const $$ = symbolGenerator(); +interface bTree extends Trait {} + /** * Component to render tree of any elements */ @component() -export default class bTree extends iData implements iItems { +@derive(iAccess) +class bTree extends iData implements iItems, iAccess { /** @see [[iItems.Item]] */ readonly Item!: Item; @@ -98,6 +103,12 @@ export default class bTree extends iData implements iItems { @prop(Boolean) readonly folded: boolean = true; + /** + * If true, the component view orientation is vertical. Horizontal is default + */ + @prop(Boolean) + readonly vertical: boolean = false; + /** * Link to the top level component (internal parameter) */ @@ -255,4 +266,27 @@ export default class bTree extends iData implements iItems { this.emit('fold', target, item, newVal); } } + + /** + * Toggle folded state + * + * @params target, value + * @emits `fold(target: HTMLElement, item:` [[Item]]`, value: boolean)` + */ + protected changeFoldedMod(item: this['Item'], target: HTMLElement, value?: boolean): void { + const + mod = this.block?.getElMod(target, 'node', 'folded'); + + if (mod == null) { + return; + } + + const + newVal = value ? value : mod === 'false'; + + this.block?.setElMod(target, 'node', 'folded', newVal); + this.emit('fold', target, item, newVal); + } } + +export default bTree; diff --git a/src/core/component/directives/aria/CHANGELOG.md b/src/core/component/directives/aria/CHANGELOG.md new file mode 100644 index 0000000000..1670fa7e71 --- /dev/null +++ b/src/core/component/directives/aria/CHANGELOG.md @@ -0,0 +1,10 @@ +Changelog +========= + +> **Tags:** +> - :boom: [Breaking Change] +> - :rocket: [New Feature] +> - :bug: [Bug Fix] +> - :memo: [Documentation] +> - :house: [Internal] +> - :nail_care: [Polish] diff --git a/src/core/component/directives/aria/README.md b/src/core/component/directives/aria/README.md new file mode 100644 index 0000000000..e12cd09021 --- /dev/null +++ b/src/core/component/directives/aria/README.md @@ -0,0 +1,44 @@ +# core/component/directives/aria + +This module provides a directive to add aria attributes and logic to elements through single API. + +## Usage + +``` +< &__foo v-aria.#bla + +< &__foo v-aria = {labelledby: dom.getId('bla')} + +``` + +## Available modifiers: + +- .#[string] (ex. '.#title') the same as = {labelledby: [id-'title']} + + +-- Roles: +- controls: +Directive can be added to any tag that includes tag with needed role. Role should be passed as a modifier. +ID or IDs are passed as value. +ID could be single or multiple written in string with space between. + +Example: +``` +< &__foo v-aria:controls.select = {id: 'id1 id2 id3'} + +same as + +< select aria-controls = "id1 id2 id3" +``` + +- tabs: +Tabs always expect the 'controls' role engine to be added. + + +## Available standard values: +Value is expected to always be an object type. Possible keys: +- label +- labelledby +- description +- describedby +- id diff --git a/src/core/component/directives/aria/aria-setter.ts b/src/core/component/directives/aria/aria-setter.ts new file mode 100644 index 0000000000..dbd1d87292 --- /dev/null +++ b/src/core/component/directives/aria/aria-setter.ts @@ -0,0 +1,130 @@ +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ + +import * as ariaRoles from 'core/component/directives/aria/roles-engines'; +import Async from 'core/async'; +import AriaRoleEngine from 'core/component/directives/aria/interface'; +import type iBlock from 'super/i-block/i-block'; +import type { DirectiveOptions } from 'core/component/directives/aria/interface'; + +export default class AriaSetter extends AriaRoleEngine { + override $a: Async; + role: CanUndef; + + constructor(options: DirectiveOptions) { + super(options); + + this.$a = new Async(); + this.setAriaRole(); + + if (this.role != null) { + this.role.$a = this.$a; + } + } + + init(): void { + this.setAriaLabel(); + this.addEventHandlers(); + + this.role?.init(); + } + + override update(): void { + const + ctx = this.options.vnode.fakeContext; + + if (ctx.isFunctional) { + ctx.off(); + } + + if (this.role != null) { + this.role.options = this.options; + this.role.update?.(); + } + } + + override clear(): void { + this.$a.clearAll(); + + this.role?.clear?.(); + } + + addEventHandlers(): void { + if (this.role == null) { + return; + } + + const + $v = this.options.binding.value; + + for (const p in $v) { + if (p === 'onOpen' || p === 'onClose' || p === 'onChange') { + const + callback = this.role[p], + property = $v[p]; + + if (Object.isFunction(property)) { + property(callback); + + } else if (Object.isPromiseLike(property)) { + void property.then(callback); + + } else if (Object.isString(property)) { + const + ctx = this.options.vnode.fakeContext; + + ctx.on(property, callback); + } + } + } + } + + setAriaRole(): CanUndef { + const + {arg: role} = this.options.binding; + + if (role == null) { + return; + } + + this.role = new ariaRoles[role](this.options); + } + + setAriaLabel(): void { + const + {vnode, binding, el} = this.options, + {dom} = Object.cast(vnode.fakeContext), + value = Object.isCustomObject(binding.value) ? binding.value : {}; + + for (const mod in binding.modifiers) { + if (!mod.startsWith('#')) { + continue; + } + + const + title = mod.slice(1), + id = dom.getId(title); + + el.setAttribute('aria-labelledby', id); + } + + if (value.label != null) { + el.setAttribute('aria-label', value.label); + + } else if (value.labelledby != null) { + el.setAttribute('aria-labelledby', dom.getId(value.labelledby)); + } + + if (value.description != null) { + el.setAttribute('aria-description', value.description); + + } else if (value.describedby != null) { + el.setAttribute('aria-describedby', dom.getId(value.describedby)); + } + } +} diff --git a/src/core/component/directives/aria/helpers.ts b/src/core/component/directives/aria/helpers.ts deleted file mode 100644 index f2746c0620..0000000000 --- a/src/core/component/directives/aria/helpers.ts +++ /dev/null @@ -1,82 +0,0 @@ -import type { DirectiveHookParams, AriaRoleEngine } from 'core/component/directives/aria/interface'; -import type iBlock from 'super/i-block/i-block'; -import * as ariaRoles from 'core/component/directives/aria/roles-engines/index'; - -export function setAriaLabel({el, opts, vnode}: DirectiveHookParams): void { - const - {dom, vdom, $createElement: createElem} = Object.cast(vnode.fakeContext), - value = opts.value ?? {}; - - for (const mod in opts.modifiers) { - if (!mod.startsWith('#')) { - continue; - } - - const - title = mod.slice(1), - id = dom.getId(title); - - if ('labelledby' in opts.modifiers) { - el.setAttribute('aria-labelledby', id); - - } else { - el.setAttribute('id', id); - - const - labelNode = createElem.call(vnode.fakeContext, - 'label', - { - attrs: {for: id} - }); - - const - labelElem = vdom.render(labelNode); - - el.prepend(labelElem); - } - } - - if (value.label != null) { - el.setAttribute('aria-label', value.label); - - } else if (value.labelledby != null) { - el.setAttribute('aria-labelledby', dom.getId(value.labelledby)); - } - - if (value.description != null) { - el.setAttribute('aria-description', value.description); - - } else if (value.describedby != null) { - el.setAttribute('aria-describedby', dom.getId(value.describedby)); - } -} - -export function setAriaTabIndex({opts, vnode}: DirectiveHookParams): void { - if (opts.value == null) { - return; - } - - const - names = opts.value.children, - {block} = Object.cast(vnode.fakeContext); - - for (const name of names) { - const - elems = block?.elements(name); - - elems?.forEach((el: Element) => { - el.setAttribute('tabindex', '0'); - }); - } -} - -export function setAriaRole(options: DirectiveHookParams): CanUndef { - const - {arg: role} = options.opts; - - if (role == null) { - return; - } - - return new ariaRoles[role](options); -} diff --git a/src/core/component/directives/aria/index.ts b/src/core/component/directives/aria/index.ts index 7288f71c6f..255fa3b2f8 100644 --- a/src/core/component/directives/aria/index.ts +++ b/src/core/component/directives/aria/index.ts @@ -11,30 +11,48 @@ * @packageDocumentation */ +import symbolGenerator from 'core/symbol'; import { ComponentEngine, VNode, VNodeDirective } from 'core/component/engines'; -import { setAriaLabel, setAriaRole, setAriaTabIndex } from 'core/component/directives/aria/helpers'; +import AriaSetter from 'core/component/directives/aria/aria-setter'; + +const + ariaMap = new Map(); + +const + $$ = symbolGenerator(); ComponentEngine.directive('aria', { - inserted(el: Element, opts: VNodeDirective, vnode: VNode): void { + inserted(el: HTMLElement, binding: VNodeDirective, vnode: VNode): void { const - {value, arg, modifiers} = opts; + {value, arg, modifiers} = binding; if (value == null && arg == null && modifiers == null) { return; } const - options = {el, opts, vnode}; + aria = new AriaSetter({el, binding, vnode}); + + aria.init(); - setAriaLabel(options); - setAriaTabIndex(options); - setAriaRole(options)?.init(); + ariaMap.set($$.aria, aria); }, - unbind(el: Element, opts: VNodeDirective, vnode: VNode) { + update(el: HTMLElement, binding: VNodeDirective, vnode: VNode) { const - options = {el, opts, vnode}; + aria: AriaSetter = ariaMap.get($$.aria); + + aria.options = {el, binding, vnode}; + + aria.update(); + }, + + unbind(el: HTMLElement, binding: VNodeDirective, vnode: VNode) { + const + aria: AriaSetter = ariaMap.get($$.aria); + + aria.options = {el, binding, vnode}; - setAriaRole(options)?.clear(); + aria.clear(); } }); diff --git a/src/core/component/directives/aria/interface.ts b/src/core/component/directives/aria/interface.ts index d48eb0748c..499b73f1c7 100644 --- a/src/core/component/directives/aria/interface.ts +++ b/src/core/component/directives/aria/interface.ts @@ -6,19 +6,34 @@ * https://github.com/V4Fire/Client/blob/master/LICENSE */ -import type { VNodeDirective, VNode } from 'core/component/engines'; +import type { VNode, VNodeDirective } from 'core/component/engines'; +import type Async from 'core/async'; -export interface DirectiveHookParams { - el: Element; - opts: VNodeDirective; +export interface DirectiveOptions { + el: HTMLElement; + binding: VNodeDirective; vnode: VNode; } -export interface AriaRoleEngine { - el: Element; - value: any; - vnode: VNode; +export default abstract class AriaRoleEngine { + options: DirectiveOptions; + $a: CanUndef; + + protected constructor(options: DirectiveOptions) { + this.options = options; + } + + abstract init(): void; + update?(): void; + clear?(): void; +} - init(): void; - clear(): void; +export enum keyCodes { + ENTER = 'Enter', + END = 'End', + HOME = 'Home', + LEFT = 'ArrowLeft', + UP = 'ArrowUp', + RIGHT = 'ArrowRight', + DOWN = 'ArrowDown' } diff --git a/src/core/component/directives/aria/roles-engines/combobox.ts b/src/core/component/directives/aria/roles-engines/combobox.ts new file mode 100644 index 0000000000..e0ed0a71d2 --- /dev/null +++ b/src/core/component/directives/aria/roles-engines/combobox.ts @@ -0,0 +1,55 @@ +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ + +import AriaRoleEngine, { DirectiveOptions } from 'core/component/directives/aria/interface'; +import { FOCUSABLE_SELECTOR } from 'traits/i-access/const'; +import type { ComboboxBindingValue } from 'core/component/directives/aria/roles-engines/interface'; + +export default class ComboboxEngine extends AriaRoleEngine { + el: Element; + $v: ComboboxBindingValue; + + constructor(options: DirectiveOptions) { + super(options); + + const + {el} = this.options; + + this.el = el.querySelector(FOCUSABLE_SELECTOR) ?? el; + this.$v = this.options.binding.value; + } + + init(): void { + this.el.setAttribute('role', 'combobox'); + this.el.setAttribute('aria-expanded', 'false'); + + if (this.$v.isMultiple) { + this.el.setAttribute('aria-multiselectable', 'true'); + } + } + + onOpen = (element: HTMLElement): void => { + this.el.setAttribute('aria-expanded', 'true'); + + this.setAriaActive(element); + }; + + onClose = (): void => { + this.el.setAttribute('aria-expanded', 'false'); + + this.setAriaActive(); + }; + + onChange = (element: HTMLElement): void => { + this.setAriaActive(element); + }; + + setAriaActive = (element?: HTMLElement): void => { + this.el.setAttribute('aria-activedescendant', element?.id ?? ''); + }; +} diff --git a/src/core/component/directives/aria/roles-engines/controls.ts b/src/core/component/directives/aria/roles-engines/controls.ts new file mode 100644 index 0000000000..98fc5e68b6 --- /dev/null +++ b/src/core/component/directives/aria/roles-engines/controls.ts @@ -0,0 +1,43 @@ +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ + +import AriaRoleEngine from 'core/component/directives/aria/interface'; + +export default class ControlsEngine extends AriaRoleEngine { + init(): void { + const + {vnode, binding, el} = this.options, + {fakeContext: ctx} = vnode; + + if (binding.modifiers == null) { + Object.throw('Controls aria directive expects the role modifier to be passed'); + return; + } + + if (binding.value?.controls == null) { + Object.throw('Controls aria directive expects the controls value to be passed'); + return; + } + + const + roleName = Object.keys(binding.modifiers)[0]; + + ctx?.$nextTick().then(() => { + const + elems = el.querySelectorAll(`[role=${roleName}]`); + + for (let i = 0; i < elems.length; i++) { + const + elem = elems[i], + {id} = binding.value; + + elem.setAttribute('aria-controls', id); + } + }); + } +} diff --git a/src/core/component/directives/aria/roles-engines/dialog.ts b/src/core/component/directives/aria/roles-engines/dialog.ts index fec7e6a538..9a8d91cfc4 100644 --- a/src/core/component/directives/aria/roles-engines/dialog.ts +++ b/src/core/component/directives/aria/roles-engines/dialog.ts @@ -1,41 +1,29 @@ -import iOpen from 'traits/i-open/i-open'; -import type iBlock from 'super/i-block/i-block'; -import type { DirectiveHookParams } from 'core/component/directives/aria/interface'; -import RoleEngine from 'core/component/directives/aria/roles-engines/interface'; +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ -export default class DialogEngine extends RoleEngine { - group: Dictionary = {}; +import iOpen from 'traits/i-open/i-open'; +import AriaRoleEngine from 'core/component/directives/aria/interface'; +import type { DirectiveOptions } from 'core/component/directives/aria/interface'; - constructor(options: DirectiveHookParams) { +export default class DialogEngine extends AriaRoleEngine { + constructor(options: DirectiveOptions) { super(options); if (!iOpen.is(options.vnode.fakeContext)) { - Object.throw('Dialog directive expects the component to realize iOpen interface'); + Object.throw('Dialog aria directive expects the component to realize iOpen interface'); } } - override init(): void { - const - {localEmitter: $e} = Object.cast(this.vnode.fakeContext); - - this.el.setAttribute('role', 'dialog'); - this.el.setAttribute('aria-modal', 'false'); - - this.group = {group: 'ariaAttributes'}; - - $e.on('open', () => { - this.el.setAttribute('aria-modal', 'true'); - }, this.group); - - $e.on('close', () => { - this.el.setAttribute('aria-modal', 'false'); - }, this.group); - } - - override clear(): void { + init(): void { const - {localEmitter: $e} = Object.cast(this.vnode.fakeContext); + {el} = this.options; - $e.off(this.group); + el.setAttribute('role', 'dialog'); + el.setAttribute('aria-modal', 'true'); } } diff --git a/src/core/component/directives/aria/roles-engines/index.ts b/src/core/component/directives/aria/roles-engines/index.ts index 4ff45a36fc..7e8e48a6f5 100644 --- a/src/core/component/directives/aria/roles-engines/index.ts +++ b/src/core/component/directives/aria/roles-engines/index.ts @@ -1 +1,18 @@ -export * from 'core/component/directives/aria/roles-engines/dialog'; +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ + +export { default as dialog } from 'core/component/directives/aria/roles-engines/dialog'; +export { default as tablist } from 'core/component/directives/aria/roles-engines/tablist'; +export { default as tab } from 'core/component/directives/aria/roles-engines/tab'; +export { default as tabpanel } from 'core/component/directives/aria/roles-engines/tabpanel'; +export { default as controls } from 'core/component/directives/aria/roles-engines/controls'; +export { default as combobox } from 'core/component/directives/aria/roles-engines/combobox'; +export { default as listbox } from 'core/component/directives/aria/roles-engines/listbox'; +export { default as option } from 'core/component/directives/aria/roles-engines/option'; +export { default as tree } from 'core/component/directives/aria/roles-engines/tree'; +export { default as treeitem } from 'core/component/directives/aria/roles-engines/treeitem'; diff --git a/src/core/component/directives/aria/roles-engines/interface.ts b/src/core/component/directives/aria/roles-engines/interface.ts index 3ab1b6f073..e3666bd148 100644 --- a/src/core/component/directives/aria/roles-engines/interface.ts +++ b/src/core/component/directives/aria/roles-engines/interface.ts @@ -1,22 +1,31 @@ -import type { AriaRoleEngine, DirectiveHookParams } from 'core/component/directives/aria/interface'; -import type { VNode } from 'core/component'; +export interface TabBindingValue { + isFirst: boolean; + isVertical: boolean; + activeElement: CanUndef>>; + onChange(cb: Function): void; +} -export default abstract class RoleEngine implements AriaRoleEngine { - el: Element; - value: any; - vnode: VNode; +export interface TablistBindingValue { + isVertical: boolean; + isMultiple: boolean; +} - constructor({el, opts, vnode}: DirectiveHookParams) { - this.el = el; - this.value = opts.value; - this.vnode = vnode; - } +export interface TreeBindingValue { + isVertical: boolean; + isRootTree: boolean; + onChange(cb: Function): void; +} - init(): void { - // - } +export interface TreeitemBindingValue { + isVeryFirstItem: boolean; + getRootElement(): CanUndef; + toggleFold(el: Element, value?: boolean): void; + getFoldedMod(): CanUndef; +} - clear(): void { - // - } +export interface ComboboxBindingValue { + isMultiple: boolean; + onChange(cb: Function): void; + onOpen(cb: Function): void; + onClose(cb: Function): void; } diff --git a/src/core/component/directives/aria/roles-engines/listbox.ts b/src/core/component/directives/aria/roles-engines/listbox.ts new file mode 100644 index 0000000000..203b12cde6 --- /dev/null +++ b/src/core/component/directives/aria/roles-engines/listbox.ts @@ -0,0 +1,19 @@ +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ + +import AriaRoleEngine from 'core/component/directives/aria/interface'; + +export default class ListboxEngine extends AriaRoleEngine { + init(): void { + const + {el} = this.options; + + el.setAttribute('role', 'listbox'); + el.setAttribute('tabindex', '-1'); + } +} diff --git a/src/core/component/directives/aria/roles-engines/option.ts b/src/core/component/directives/aria/roles-engines/option.ts new file mode 100644 index 0000000000..be180c2e80 --- /dev/null +++ b/src/core/component/directives/aria/roles-engines/option.ts @@ -0,0 +1,27 @@ +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ + +import AriaRoleEngine from 'core/component/directives/aria/interface'; + +export default class ListboxEngine extends AriaRoleEngine { + init(): void { + const + {el} = this.options, + {value: {preSelected}} = this.options.binding; + + el.setAttribute('role', 'option'); + el.setAttribute('aria-selected', String(preSelected)); + } + + onChange = (isSelected: boolean): void => { + const + {el} = this.options; + + el.setAttribute('aria-selected', String(isSelected)); + }; +} diff --git a/src/core/component/directives/aria/roles-engines/tab.ts b/src/core/component/directives/aria/roles-engines/tab.ts new file mode 100644 index 0000000000..6e5e43834c --- /dev/null +++ b/src/core/component/directives/aria/roles-engines/tab.ts @@ -0,0 +1,154 @@ +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + * + * This software or document includes material copied from or derived from ["Example of Tabs with Manual Activation", https://www.w3.org/WAI/ARIA/apg/example-index/tabs/tabs-manual.html]. + * Copyright © [2022] W3C® (MIT, ERCIM, Keio, Beihang). + */ + +import AriaRoleEngine, { DirectiveOptions, keyCodes } from 'core/component/directives/aria/interface'; +import type { TabBindingValue } from 'core/component/directives/aria/roles-engines/interface'; +import type iAccess from 'traits/i-access/i-access'; +import type iBlock from 'super/i-block/i-block'; +import { FOCUSABLE_SELECTOR } from 'traits/i-access/const'; + +export default class TabEngine extends AriaRoleEngine { + $v: TabBindingValue; + ctx: iAccess & iBlock; + + constructor(options: DirectiveOptions) { + super(options); + + this.$v = this.options.binding.value; + this.ctx = Object.cast(this.options.vnode.fakeContext); + } + + init(): void { + const + {el} = this.options, + {isFirst} = this.$v; + + el.setAttribute('role', 'tab'); + el.setAttribute('aria-selected', 'false'); + + if (isFirst) { + if (el.tabIndex < 0) { + el.setAttribute('tabindex', '0'); + } + + } else { + el.setAttribute('tabindex', '-1'); + } + + this.$v.activeElement?.then((el) => { + if (Object.isArray(el)) { + for (let i = 0; i < el.length; i++) { + const + activeEl = el[i]; + + if (activeEl.getAttribute('aria-selected') !== 'true') { + activeEl.setAttribute('aria-selected', 'true'); + } + } + + return; + } + + if (el.getAttribute('aria-selected') !== 'true') { + el.setAttribute('aria-selected', 'true'); + } + }); + + if (this.$a != null) { + this.$a.on(el, 'keydown', this.onKeydown); + } + } + + onChange = (active: Element | NodeListOf): void => { + const + {el} = this.options; + + if (Object.isArrayLike(active)) { + for (let i = 0; i < active.length; i++) { + el.setAttribute('aria-selected', String(el === active[i])); + } + + return; + } + + el.setAttribute('aria-selected', String(el === active)); + }; + + moveFocusToFirstTab(): void { + const + firstEl = >this.ctx.$el?.querySelector(FOCUSABLE_SELECTOR); + + firstEl?.focus(); + } + + moveFocusToLastTab(): void { + const + focusable = >>this.ctx.$el?.querySelectorAll(FOCUSABLE_SELECTOR); + + if (focusable != null && focusable.length > 0) { + focusable[focusable.length - 1].focus(); + } + } + + focusNext(): void { + this.ctx.nextFocusableElement(1)?.focus(); + } + + focusPrev(): void { + this.ctx.nextFocusableElement(-1)?.focus(); + } + + onKeydown = (event: Event): void => { + const + evt = (event), + {isVertical} = this.$v; + + switch (evt.key) { + case keyCodes.LEFT: + this.focusPrev(); + break; + + case keyCodes.UP: + if (isVertical) { + this.focusPrev(); + break; + } + + return; + + case keyCodes.RIGHT: + this.focusNext(); + break; + + case keyCodes.DOWN: + if (isVertical) { + this.focusNext(); + break; + } + + return; + + case keyCodes.HOME: + this.moveFocusToFirstTab(); + break; + + case keyCodes.END: + this.moveFocusToLastTab(); + break; + + default: + return; + } + + event.stopPropagation(); + event.preventDefault(); + }; +} diff --git a/src/core/component/directives/aria/roles-engines/tablist.ts b/src/core/component/directives/aria/roles-engines/tablist.ts new file mode 100644 index 0000000000..2105747cb3 --- /dev/null +++ b/src/core/component/directives/aria/roles-engines/tablist.ts @@ -0,0 +1,28 @@ +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ + +import AriaRoleEngine from 'core/component/directives/aria/interface'; +import type { TablistBindingValue } from 'core/component/directives/aria/roles-engines/interface'; + +export default class TablistEngine extends AriaRoleEngine { + init(): void { + const + {el, binding} = this.options, + $v: TablistBindingValue = binding.value; + + el.setAttribute('role', 'tablist'); + + if ($v.isMultiple) { + el.setAttribute('aria-multiselectable', 'true'); + } + + if ($v.isVertical) { + el.setAttribute('aria-orientation', 'vertical'); + } + } +} diff --git a/src/core/component/directives/aria/roles-engines/tabpanel.ts b/src/core/component/directives/aria/roles-engines/tabpanel.ts new file mode 100644 index 0000000000..b128b2b079 --- /dev/null +++ b/src/core/component/directives/aria/roles-engines/tabpanel.ts @@ -0,0 +1,22 @@ +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ + +import AriaRoleEngine from 'core/component/directives/aria/interface'; + +export default class TabpanelEngine extends AriaRoleEngine { + init(): void { + const + {el, binding} = this.options; + + el.setAttribute('role', 'tabpanel'); + + if (binding.value?.labelledby == null) { + Object.throw('Tabpanel aria directive expects "label" or "labelledby" value to be passed'); + } + } +} diff --git a/src/core/component/directives/aria/roles-engines/tree.ts b/src/core/component/directives/aria/roles-engines/tree.ts new file mode 100644 index 0000000000..3ce6bf3a27 --- /dev/null +++ b/src/core/component/directives/aria/roles-engines/tree.ts @@ -0,0 +1,38 @@ +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ + +import AriaRoleEngine, { DirectiveOptions } from 'core/component/directives/aria/interface'; +import type { TreeBindingValue } from 'core/component/directives/aria/roles-engines/interface'; + +export default class TreeEngine extends AriaRoleEngine { + $v: TreeBindingValue; + el: HTMLElement; + + constructor(options: DirectiveOptions) { + super(options); + + this.$v = options.binding.value; + this.el = this.options.el; + } + + init(): void { + this.setRootRole(); + + if (this.$v.isVertical) { + this.el.setAttribute('aria-orientation', 'vertical'); + } + } + + setRootRole(): void { + this.el.setAttribute('role', this.$v.isRootTree ? 'tree' : 'group'); + } + + onChange = (el: HTMLElement, isFolded: boolean): void => { + el.setAttribute('aria-expanded', String(!isFolded)); + }; +} diff --git a/src/core/component/directives/aria/roles-engines/treeitem.ts b/src/core/component/directives/aria/roles-engines/treeitem.ts new file mode 100644 index 0000000000..c1f385d431 --- /dev/null +++ b/src/core/component/directives/aria/roles-engines/treeitem.ts @@ -0,0 +1,208 @@ +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + * + * This software or document includes material copied from or derived from ["Example of Tabs with Manual Activation", https://www.w3.org/WAI/ARIA/apg/example-index/tabs/tabs-manual.html]. + * Copyright © [2022] W3C® (MIT, ERCIM, Keio, Beihang). + */ + +import symbolGenerator from 'core/symbol'; +import AriaRoleEngine, { DirectiveOptions, keyCodes } from 'core/component/directives/aria/interface'; +import iAccess from 'traits/i-access/i-access'; +import { FOCUSABLE_SELECTOR } from 'traits/i-access/const'; +import type { TreeitemBindingValue } from 'core/component/directives/aria/roles-engines/interface'; +import type iBlock from 'super/i-block/i-block'; + +export const + $$ = symbolGenerator(); + +export default class TreeItemEngine extends AriaRoleEngine { + ctx: iAccess & iBlock['unsafe']; + el: HTMLElement; + $v: TreeitemBindingValue; + + constructor(options: DirectiveOptions) { + super(options); + + if (!iAccess.is(options.vnode.fakeContext)) { + Object.throw('Treeitem aria directive expects the component to realize iAccess interface'); + } + + this.ctx = Object.cast(options.vnode.fakeContext); + this.el = this.options.el; + this.$v = this.options.binding.value; + } + + init(): void { + this.$a?.on(this.el, 'keydown', this.onKeyDown); + + const + isMuted = this.ctx.muteTabIndexes(this.el); + + if (this.$v.isVeryFirstItem) { + if (isMuted) { + this.ctx.unmuteTabIndexes(this.el); + + } else { + this.el.tabIndex = 0; + } + } + + this.el.setAttribute('role', 'treeitem'); + + this.ctx.$nextTick(() => { + if (this.isExpandable) { + this.el.setAttribute('aria-expanded', String(this.isExpanded)); + } + }); + } + + onKeyDown = (e: KeyboardEvent): void => { + if (e.altKey || e.ctrlKey || e.metaKey || e.shiftKey) { + return; + } + + switch (e.code) { + case keyCodes.UP: + this.moveFocus(-1); + break; + + case keyCodes.DOWN: + this.moveFocus(1); + break; + + case keyCodes.ENTER: + this.$v.toggleFold(this.el); + break; + + case keyCodes.RIGHT: + if (this.isExpandable) { + if (this.isExpanded) { + this.moveFocus(1); + + } else { + this.openFold(); + } + } + + break; + + case keyCodes.LEFT: + if (this.isExpandable && this.isExpanded) { + this.closeFold(); + + } else { + this.focusParent(); + } + + break; + + case keyCodes.HOME: + void this.setFocusToFirstItem(); + break; + + case keyCodes.END: + void this.setFocusToLastItem(); + break; + + default: + return; + } + + e.stopPropagation(); + e.preventDefault(); + }; + + focusNext(nextEl: HTMLElement): void { + this.ctx.muteTabIndexes(this.el); + this.ctx.unmuteTabIndexes(nextEl); + nextEl.focus(); + } + + moveFocus(step: 1 | -1): void { + const + nextEl = this.ctx.nextFocusableElement(step); + + if (nextEl != null) { + this.focusNext(nextEl); + } + } + + get isExpandable(): boolean { + return this.$v.getFoldedMod() != null; + } + + get isExpanded(): boolean { + return this.$v.getFoldedMod() === 'false'; + } + + openFold(): void { + this.$v.toggleFold(this.el, false); + } + + closeFold(): void { + this.$v.toggleFold(this.el, true); + } + + focusParent(): void { + let + parent = this.el.parentElement; + + while (parent != null) { + if (parent.getAttribute('role') === 'treeitem') { + break; + } + + parent = parent.parentElement; + } + + const + focusableParent = (>parent?.querySelector(FOCUSABLE_SELECTOR)); + + if (focusableParent != null) { + this.focusNext(focusableParent); + } + } + + async setFocusToFirstItem(): Promise { + await this.ctx.async.wait( + this.$v.getRootElement.bind(this), + {label: $$.waitRoot} + ); + + const + firstEl = >this.$v.getRootElement()?.querySelector(FOCUSABLE_SELECTOR); + + if (firstEl != null) { + this.focusNext(firstEl); + } + } + + async setFocusToLastItem(): Promise { + await this.ctx.async.wait( + this.$v.getRootElement.bind(this), + {label: $$.waitRoot} + ); + + const + items = >>this.$v.getRootElement()?.querySelectorAll(FOCUSABLE_SELECTOR); + + const visibleItems: HTMLElement[] = [].filter.call( + items, + (el: HTMLElement) => ( + el.offsetWidth > 0 || + el.offsetHeight > 0 + ) + ); + + const + lastEl = visibleItems.at(-1); + + if (lastEl != null) { + this.focusNext(lastEl); + } + } +} diff --git a/src/core/component/directives/index.ts b/src/core/component/directives/index.ts index 710b6fdb56..6874942b5c 100644 --- a/src/core/component/directives/index.ts +++ b/src/core/component/directives/index.ts @@ -22,8 +22,6 @@ import 'core/component/directives/image'; import 'core/component/directives/update-on'; //#endif -//#if runtime has directives/aria import 'core/component/directives/aria'; -//#endif import 'core/component/directives/hook'; diff --git a/src/form/b-checkbox/CHANGELOG.md b/src/form/b-checkbox/CHANGELOG.md index f39903c9e3..647883be9e 100644 --- a/src/form/b-checkbox/CHANGELOG.md +++ b/src/form/b-checkbox/CHANGELOG.md @@ -9,6 +9,12 @@ Changelog > - :house: [Internal] > - :nail_care: [Polish] +## v3.0.0-rc.??? (2022-??-??) + +#### :bug: Bug Fix + +* Added `for` link for label and `id` for nativeInput in template + ## v3.0.0-rc.199 (2021-06-16) #### :boom: Breaking Change diff --git a/src/form/b-checkbox/b-checkbox.ss b/src/form/b-checkbox/b-checkbox.ss index a670526800..660bb9f08d 100644 --- a/src/form/b-checkbox/b-checkbox.ss +++ b/src/form/b-checkbox/b-checkbox.ss @@ -14,6 +14,16 @@ - nativeInputType = "'checkbox'" - nativeInputModel = undefined + - block hiddenInput() + += self.nativeInput({ & + elName: 'hidden-input', + id: 'id || dom.getId("input")', + + attrs: { + autocomplete: 'off' + } + }) . + - block rootAttrs - super ? rootAttrs[':-parent-id'] = 'parentId' @@ -37,6 +47,8 @@ < _.&__check - block label - < span.&__label v-if = label || vdom.getSlot('label') + < label.&__label & + v-if = label || vdom.getSlot('label') | + :for = id || dom.getId('input') . += self.slot('label', {':label': 'label'}) {{ t(label) }} diff --git a/src/form/b-select/CHANGELOG.md b/src/form/b-select/CHANGELOG.md index 0db12f6a78..4b0b85a820 100644 --- a/src/form/b-select/CHANGELOG.md +++ b/src/form/b-select/CHANGELOG.md @@ -9,6 +9,19 @@ Changelog > - :house: [Internal] > - :nail_care: [Polish] +## v3.0.0-rc.??? (2022-??-??) + +#### :rocket: New Feature + +* Added `v-aria` directive +* Added `onItemMarked` +* Added `onOpen` +* Now the component derive `iAccess` + +#### :bug: Bug Fix + +* Fixed the component to emit `iOpen` events + ## v3.5.3 (2021-10-06) #### :bug: Bug Fix diff --git a/src/form/b-select/b-select.ss b/src/form/b-select/b-select.ss index b89db45cd3..3d6b74dbe8 100644 --- a/src/form/b-select/b-select.ss +++ b/src/form/b-select/b-select.ss @@ -27,10 +27,6 @@ - if tag === 'option' ? itemAttrs[':selected'] = 'isSelected(el.value)' - - else - ? itemAttrs.role = 'option' - ? itemAttrs[':aria-selected'] = 'isSelected(el.value)' - < ${tag} & :-id = values.get(el.value) | @@ -43,7 +39,16 @@ } })) | - :v-attrs = el.attrs | + :v-attrs = native + ? el.attrs + : {'v-aria:option': { + preSelected: isSelected(el.value), + onChange: (cb) => on('actionChange', () => cb(isSelected(el.value))) + }, + ...el.attrs + } | + + :id = dom.getId(el.value) | ${itemAttrs} . += self.slot('default', {':item': 'el'}) @@ -87,12 +92,19 @@ . - block input - < _.&__cell.&__input-wrapper - < template v-if = native + < template v-if = native + < _.&__cell.&__input-wrapper += self.nativeInput({tag: 'select', model: 'undefined', attrs: {'@change': 'onNativeChange'}}) += self.items('option') - < template v-else + < template v-else + < _.&__cell.&__input-wrapper & + v-aria:combobox = { + onOpen, + onClose: (cb) => on('close', cb), + onChange: onItemMarked, + isMultiple: multiple + } . += self.nativeInput({model: 'textStore', attrs: {'@input': 'onSearchInput'}}) - block icon @@ -151,6 +163,7 @@ v-if = !native && items.length && ( isFunctional || opt.ifOnce('opened', m.opened !== 'false') && delete watchModsStore.opened - ) + ) | + v-aria:listbox . += self.items() diff --git a/src/form/b-select/b-select.ts b/src/form/b-select/b-select.ts index 631d29aab8..d20654d3a0 100644 --- a/src/form/b-select/b-select.ts +++ b/src/form/b-select/b-select.ts @@ -257,18 +257,9 @@ class bSelect extends iInputText implements iOpenToggle, iItems { } override get rootAttrs(): Dictionary { - const attrs = { + return { ...super['rootAttrsGetter']() }; - - if (!this.native) { - Object.assign(attrs, { - role: 'listbox', - 'aria-multiselectable': this.multiple - }); - } - - return attrs; } override get value(): this['Value'] { @@ -581,9 +572,6 @@ class bSelect extends iInputText implements iOpenToggle, iItems { if (this.native) { previousItemEl.selected = false; - - } else { - previousItemEl.setAttribute('aria-selected', 'false'); } } } @@ -599,9 +587,6 @@ class bSelect extends iInputText implements iOpenToggle, iItems { if (this.native) { el.selected = true; - - } else { - el.setAttribute('aria-selected', 'true'); } } }).catch(stderr); @@ -688,9 +673,6 @@ class bSelect extends iInputText implements iOpenToggle, iItems { if (this.native) { el.selected = false; - - } else { - el.setAttribute('aria-selected', 'false'); } } } @@ -883,6 +865,9 @@ class bSelect extends iInputText implements iOpenToggle, iItems { protected override initModEvents(): void { super.initModEvents(); + + iOpenToggle.initModEvents(this); + this.sync.mod('native', 'native', Boolean); this.sync.mod('multiple', 'multiple', Boolean); this.sync.mod('opened', 'multiple', Boolean); @@ -980,6 +965,26 @@ class bSelect extends iInputText implements iOpenToggle, iItems { protected onItemsNavigate(e: KeyboardEvent): void { void on.itemsNavigate(this, e); } + + /** + * Handler: executes callback on "open" event + * @param cb + */ + protected onOpen(cb: Function): void { + this.on('open', () => { + void this.$nextTick(() => { + cb.call(this, this.selectedElement); + }); + }); + } + + /** + * Handler: executes callback on item set "marked" mod + * @param cb + */ + protected onItemMarked(cb: Function): void { + this.localEmitter.on('el.mod.set.**', ({link}) => cb(link)); + } } export default bSelect; diff --git a/src/form/b-select/modules/handlers.ts b/src/form/b-select/modules/handlers.ts index f88496340a..3ca6943462 100644 --- a/src/form/b-select/modules/handlers.ts +++ b/src/form/b-select/modules/handlers.ts @@ -225,11 +225,21 @@ export async function itemsNavigate(component: C, e: Keyboard break; case 'ArrowUp': + if (unsafe.mods.opened !== 'true') { + await unsafe.open(); + break; + } + if (currentItemEl?.previousElementSibling != null) { markItem(currentItemEl.previousElementSibling); + } + + if (currentItemEl == null) { + const + items = $b.elements('item'), + lastItem = items[items.length - 1]; - } else { - await unsafe.close(); + markItem(lastItem); } break; @@ -245,7 +255,17 @@ export async function itemsNavigate(component: C, e: Keyboard currentItemEl ??= getMarkedOrSelectedItem(); } - markItem(currentItemEl?.nextElementSibling) || markItem($b.element('item')); + if (currentItemEl?.nextElementSibling != null) { + markItem(currentItemEl.nextElementSibling); + } + + if (currentItemEl == null) { + const + firstItem = $b.elements('item')[0]; + + markItem(firstItem); + } + break; } diff --git a/src/super/i-input/CHANGELOG.md b/src/super/i-input/CHANGELOG.md index 35945e1404..8aeb101da1 100644 --- a/src/super/i-input/CHANGELOG.md +++ b/src/super/i-input/CHANGELOG.md @@ -9,6 +9,12 @@ Changelog > - :house: [Internal] > - :nail_care: [Polish] +## v3.0.0-rc.??? (2021-??-??) + +#### :rocket: New Feature + +* Now the component derive `iAccess` + ## v3.0.0-rc.199 (2021-06-16) #### :boom: Breaking Change diff --git a/src/super/i-input/README.md b/src/super/i-input/README.md index 4719bb593f..12a5abf63b 100644 --- a/src/super/i-input/README.md +++ b/src/super/i-input/README.md @@ -459,7 +459,7 @@ You can also manage a type of the created tag and other options by using the pre * *) [type=nativeInputType] - value of the `:type` attribute * * *) [autofocus] - value of the `:autofocus` attribute - * *) [tabIndex] - value of the `:autofocus` attribute + * *) [tabIndex] - value of the `:tabindex` attribute * * *) [focusHandler] - value of the `@focus` attribute * *) [blurHandler] - value of the `@blur` attribute diff --git a/src/super/i-input/i-input.ts b/src/super/i-input/i-input.ts index 821d22be70..309d412ebc 100644 --- a/src/super/i-input/i-input.ts +++ b/src/super/i-input/i-input.ts @@ -16,6 +16,7 @@ import SyncPromise from 'core/promise/sync'; import { Option } from 'core/prelude/structures'; +import { derive } from 'core/functools/trait'; import iAccess from 'traits/i-access/i-access'; import iVisible from 'traits/i-visible/i-visible'; @@ -62,6 +63,8 @@ export * from 'super/i-input/interface'; export const $$ = symbolGenerator(); +interface iInput extends Trait {} + /** * Superclass for all form components */ @@ -76,7 +79,8 @@ export const } }) -export default abstract class iInput extends iData implements iVisible, iAccess { +@derive(iAccess) +abstract class iInput extends iData implements iVisible, iAccess { /** * Type: component value */ @@ -1036,3 +1040,5 @@ export default abstract class iInput extends iData implements iVisible, iAccess } } } + +export default iInput; diff --git a/src/traits/i-access/CHANGELOG.md b/src/traits/i-access/CHANGELOG.md index 3d5a9e651b..0a0c8f3527 100644 --- a/src/traits/i-access/CHANGELOG.md +++ b/src/traits/i-access/CHANGELOG.md @@ -9,6 +9,15 @@ Changelog > - :house: [Internal] > - :nail_care: [Polish] +## v3.0.0-rc.??? (2022-??-??) + +#### :rocket: New Feature + +* Added `muteTabIndexes` +* Added `unmuteTabIndexes` +* Added `nextFocusableElement` +* Added `is` + ## v3.0.0-rc.211 (2021-07-21) * Now the trait uses `aria` attributes diff --git a/src/traits/i-access/const.ts b/src/traits/i-access/const.ts new file mode 100644 index 0000000000..e4b3110847 --- /dev/null +++ b/src/traits/i-access/const.ts @@ -0,0 +1,2 @@ +export const + FOCUSABLE_SELECTOR = '[tabindex]:not([disabled]), a:not([disabled]), button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled])'; diff --git a/src/traits/i-access/i-access.ts b/src/traits/i-access/i-access.ts index 489836c10b..61b8f43327 100644 --- a/src/traits/i-access/i-access.ts +++ b/src/traits/i-access/i-access.ts @@ -17,6 +17,7 @@ import SyncPromise from 'core/promise/sync'; import type iBlock from 'super/i-block/i-block'; import type { ModsDecl, ModEvent } from 'super/i-block/i-block'; +import { FOCUSABLE_SELECTOR } from 'traits/i-access/const'; export default abstract class iAccess { /** @@ -147,10 +148,10 @@ export default abstract class iAccess { return; } - if ($el.hasAttribute('tab-index')) { - const - el = ($el); + const + el = ($el); + if (el.hasAttribute('tabindex') || el.tabIndex > -1) { if (focused) { el.focus(); @@ -162,6 +163,114 @@ export default abstract class iAccess { }); } + /** @see [[iAccess.muteTabIndexes]] */ + static muteTabIndexes: AddSelf = + (component, ctx?): boolean => { + const + el = ctx ?? component.$el; + + if (el == null) { + return false; + } + + const + elems = el.querySelectorAll(FOCUSABLE_SELECTOR); + + for (let i = 0; i < elems.length; i++) { + const + elem = (elems[i]); + + if (elem.dataset.tabindex == null) { + elem.dataset.tabindex = String(elem.tabIndex); + } + + elem.tabIndex = -1; + } + + if (ctx != null && ctx.tabIndex > -1) { + if (ctx.dataset.tabindex == null) { + ctx.dataset.tabindex = String(ctx.tabIndex); + } + + ctx.tabIndex = -1; + + return true; + } + + return elems.length > 0; + }; + + /** @see [[iAccess.unmuteTabIndexes]] */ + static unmuteTabIndexes: AddSelf = + (component, ctx?): boolean => { + const + el = ctx ?? component.$el; + + if (el == null) { + return false; + } + + const + elems = el.querySelectorAll('[data-tabindex]'); + + for (let i = 0; i < elems.length; i++) { + const + elem = (elems[i]); + + elem.tabIndex = Number(elem.dataset.tabindex); + delete elem.dataset.tabindex; + } + + if (ctx?.dataset.tabindex != null) { + ctx.tabIndex = Number(ctx.dataset.tabindex); + delete ctx.dataset.tabindex; + + return true; + } + + return elems.length > 0; + }; + + /** @see [[iAccess.unmuteTabIndexes]] */ + static nextFocusableElement: AddSelf = + (component, step, el?): CanUndef => { + if (document.activeElement == null) { + return; + } + + const + nodeListOfFocusable = (el ?? document).querySelectorAll(FOCUSABLE_SELECTOR); + + const focusable: HTMLElement[] = [].filter.call( + nodeListOfFocusable, + (el: HTMLElement) => ( + el.offsetWidth > 0 || + el.offsetHeight > 0 || + el === document.activeElement + ) + ); + + const + index = focusable.indexOf(document.activeElement); + + if (index > -1) { + return focusable[index + step]; + } + }; + + /** + * Checks if the component realize current trait + * @param obj + */ + static is(obj: unknown): obj is iAccess { + if (Object.isPrimitive(obj)) { + return false; + } + + const dict = Object.cast(obj); + return Object.isFunction(dict.muteTabIndexes) && Object.isFunction(dict.nextFocusableElement); + } + /** * A Boolean attribute which, if present, indicates that the component should automatically * have focus when the page has finished loading (or when the `` containing the element has been displayed) @@ -218,4 +327,28 @@ export default abstract class iAccess { blur(...args: unknown[]): Promise { return Object.throw(); } + + /** + * Remove all descendants with tabindex attribute from tab sequence and saves previous value. + * @param el + */ + muteTabIndexes(el?: HTMLElement): boolean { + return Object.throw(); + } + + /** + * Recovers previous saved tabindex values to the elements that were changed. + * @param el + */ + unmuteTabIndexes(el?: HTMLElement): boolean { + return Object.throw(); + } + + /** + * Sets the focus to the next or previous focusable element via the step parameter + * @params step, el? + */ + nextFocusableElement(step: 1 | -1, el?: HTMLElement): CanUndef { + return Object.throw(); + } } diff --git a/src/traits/i-open/CHANGELOG.md b/src/traits/i-open/CHANGELOG.md index f435f65495..7f2f3680c6 100644 --- a/src/traits/i-open/CHANGELOG.md +++ b/src/traits/i-open/CHANGELOG.md @@ -9,6 +9,12 @@ Changelog > - :house: [Internal] > - :nail_care: [Polish] +## v3.0.0-rc.??? (2022-??-??) + +#### :rocket: New Feature + +* Added `is` + ## v3.0.0-rc.184 (2021-05-12) #### :rocket: New Feature diff --git a/src/traits/i-open/i-open.ts b/src/traits/i-open/i-open.ts index 96efa485ea..93ef25a1a6 100644 --- a/src/traits/i-open/i-open.ts +++ b/src/traits/i-open/i-open.ts @@ -136,12 +136,11 @@ export default abstract class iOpen { /** * Checks if the component realize current trait - * * @param obj */ static is(obj: unknown): obj is iOpen { if (Object.isPrimitive(obj)) { - return true; + return false; } const dict = Object.cast(obj); From 238cc7abdac0d1f828bdf9699e2bd8fe2b8d7ba8 Mon Sep 17 00:00:00 2001 From: Aleksandr Bunin Date: Thu, 21 Jul 2022 19:34:28 +0300 Subject: [PATCH 003/185] fix tests --- src/base/b-list/b-list.ss | 16 ++++++-------- src/base/b-tree/b-tree.ss | 22 +++++++++---------- .../aria/roles-engines/interface.ts | 3 ++- .../directives/aria/roles-engines/treeitem.ts | 18 +++++---------- src/form/b-checkbox/CHANGELOG.md | 2 +- src/form/b-checkbox/b-checkbox.ss | 3 ++- src/form/b-checkbox/b-checkbox.ts | 7 ++++++ 7 files changed, 35 insertions(+), 36 deletions(-) diff --git a/src/base/b-list/b-list.ss b/src/base/b-list/b-list.ss index a41fc70787..4d42dd8b02 100644 --- a/src/base/b-list/b-list.ss +++ b/src/base/b-list/b-list.ss @@ -48,15 +48,14 @@ })) | :v-attrs = isTablist - ? { + ? Object.assign(el.attrs, { 'v-aria:tab': { isFirst: i === 0, isVertical: vertical, onChange: onActiveChange, activeElement - }, - ...el.attrs - } + } + }) : el.attrs . - block preIcon @@ -111,13 +110,12 @@ < tag.&__wrapper & :is = listTag | :v-attrs = isTablist - ? { + ? Object.assign(attrs, { 'v-aria:tablist': { isMultiple: multiple, - isVertical: vertical - }, - ...attrs - } + isVertical: vertical + } + }) : attrs . += self.list('items') diff --git a/src/base/b-tree/b-tree.ss b/src/base/b-tree/b-tree.ss index 4ba11ef0a9..287def532d 100644 --- a/src/base/b-tree/b-tree.ss +++ b/src/base/b-tree/b-tree.ss @@ -29,14 +29,15 @@ :class = provide.elClasses({ node: { level, - folded: el.children && getFoldedPropValue(el) + folded: getFoldedPropValue(el) } }) | v-aria:treeitem = { getRootElement: () => (top ? top.$el : $el), - toggleFold: changeFoldedMod.bind(this, el), - getFoldedMod: getFoldedModById.bind(this, el.id), - isVeryFirstItem: top == null && i === 0, + toggleFold: changeFoldedMod.bind(this, el) , + isExpanded: () => getFoldedModById(el.id) === 'false', + isExpandable: el.children != null, + isVeryFirstItem: top == null && i === 0 } . < .&__item-wrapper @@ -51,8 +52,7 @@ < component.&__item & v-if = item | :is = Object.isFunction(item) ? item(el, i) : item | - :v-attrs = getItemProps(el, i) | - dispatching = true + :v-attrs = getItemProps(el, i) . - block children @@ -69,8 +69,8 @@ . += self.slot('default', {':item': 'o.item'}) - < template & - #fold = o | - v-if = vdom.getSlot('fold') - . - += self.slot('fold', {':params': 'o.params'}) + < template & + #fold = o | + v-if = vdom.getSlot('fold') + . + += self.slot('fold', {':params': 'o.params'}) diff --git a/src/core/component/directives/aria/roles-engines/interface.ts b/src/core/component/directives/aria/roles-engines/interface.ts index e3666bd148..293701d0cb 100644 --- a/src/core/component/directives/aria/roles-engines/interface.ts +++ b/src/core/component/directives/aria/roles-engines/interface.ts @@ -18,9 +18,10 @@ export interface TreeBindingValue { export interface TreeitemBindingValue { isVeryFirstItem: boolean; + isExpandable: boolean; + isExpanded(): boolean; getRootElement(): CanUndef; toggleFold(el: Element, value?: boolean): void; - getFoldedMod(): CanUndef; } export interface ComboboxBindingValue { diff --git a/src/core/component/directives/aria/roles-engines/treeitem.ts b/src/core/component/directives/aria/roles-engines/treeitem.ts index c1f385d431..78b61b5145 100644 --- a/src/core/component/directives/aria/roles-engines/treeitem.ts +++ b/src/core/component/directives/aria/roles-engines/treeitem.ts @@ -54,8 +54,8 @@ export default class TreeItemEngine extends AriaRoleEngine { this.el.setAttribute('role', 'treeitem'); this.ctx.$nextTick(() => { - if (this.isExpandable) { - this.el.setAttribute('aria-expanded', String(this.isExpanded)); + if (this.$v.isExpandable) { + this.el.setAttribute('aria-expanded', String(this.$v.isExpanded())); } }); } @@ -79,8 +79,8 @@ export default class TreeItemEngine extends AriaRoleEngine { break; case keyCodes.RIGHT: - if (this.isExpandable) { - if (this.isExpanded) { + if (this.$v.isExpandable) { + if (this.$v.isExpanded()) { this.moveFocus(1); } else { @@ -91,7 +91,7 @@ export default class TreeItemEngine extends AriaRoleEngine { break; case keyCodes.LEFT: - if (this.isExpandable && this.isExpanded) { + if (this.$v.isExpandable && this.$v.isExpanded()) { this.closeFold(); } else { @@ -131,14 +131,6 @@ export default class TreeItemEngine extends AriaRoleEngine { } } - get isExpandable(): boolean { - return this.$v.getFoldedMod() != null; - } - - get isExpanded(): boolean { - return this.$v.getFoldedMod() === 'false'; - } - openFold(): void { this.$v.toggleFold(this.el, false); } diff --git a/src/form/b-checkbox/CHANGELOG.md b/src/form/b-checkbox/CHANGELOG.md index 647883be9e..5a05e47de2 100644 --- a/src/form/b-checkbox/CHANGELOG.md +++ b/src/form/b-checkbox/CHANGELOG.md @@ -13,7 +13,7 @@ Changelog #### :bug: Bug Fix -* Added `for` link for label and `id` for nativeInput in template +* Added `label` tag with `for` attribute to label and `id` to nativeInput in template ## v3.0.0-rc.199 (2021-06-16) diff --git a/src/form/b-checkbox/b-checkbox.ss b/src/form/b-checkbox/b-checkbox.ss index 660bb9f08d..8a92080ac7 100644 --- a/src/form/b-checkbox/b-checkbox.ss +++ b/src/form/b-checkbox/b-checkbox.ss @@ -49,6 +49,7 @@ - block label < label.&__label & v-if = label || vdom.getSlot('label') | - :for = id || dom.getId('input') . + :for = id || dom.getId('input') + . += self.slot('label', {':label': 'label'}) {{ t(label) }} diff --git a/src/form/b-checkbox/b-checkbox.ts b/src/form/b-checkbox/b-checkbox.ts index a34a2657b6..8e712e8ee4 100644 --- a/src/form/b-checkbox/b-checkbox.ts +++ b/src/form/b-checkbox/b-checkbox.ts @@ -316,6 +316,13 @@ export default class bCheckbox extends iInput implements iSize { */ // eslint-disable-next-line @typescript-eslint/no-unused-vars-experimental protected onClick(e: Event): void { + const + target = e.target; + + if (target.tagName === 'LABEL') { + e.preventDefault(); + } + void this.focus(); if (this.value === undefined || this.value === false || this.changeable) { From 7b3163ab4e1d8a30ecc34e5957129e95eef8b069 Mon Sep 17 00:00:00 2001 From: Aleksandr Bunin Date: Fri, 22 Jul 2022 08:04:48 +0300 Subject: [PATCH 004/185] fix checkbox styles --- src/form/b-checkbox/b-checkbox.styl | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/form/b-checkbox/b-checkbox.styl b/src/form/b-checkbox/b-checkbox.styl index 70b1c55b59..6d1605175d 100644 --- a/src/form/b-checkbox/b-checkbox.styl +++ b/src/form/b-checkbox/b-checkbox.styl @@ -17,13 +17,14 @@ b-checkbox extends i-input contain paint position relative + &__wrapper, &__checkbox, &__label + cursor pointer + &__wrapper display flex - cursor pointer &__checkbox display block - cursor pointer &__label user-select none From 5774bb0c661292e7c403de6fe7ff6557b5dc60f9 Mon Sep 17 00:00:00 2001 From: Aleksandr Bunin Date: Fri, 22 Jul 2022 13:57:26 +0300 Subject: [PATCH 005/185] fix b-list --- src/base/b-list/b-list.ss | 20 ++++++++++-------- src/base/b-list/b-list.ts | 2 +- .../aria/roles-engines/interface.ts | 2 +- .../directives/aria/roles-engines/tab.ts | 21 +------------------ 4 files changed, 14 insertions(+), 31 deletions(-) diff --git a/src/base/b-list/b-list.ss b/src/base/b-list/b-list.ss index 4d42dd8b02..1b01addf63 100644 --- a/src/base/b-list/b-list.ss +++ b/src/base/b-list/b-list.ss @@ -47,15 +47,16 @@ } })) | - :v-attrs = isTablist - ? Object.assign(el.attrs, { + :v-attrs = isTablist() + ? { 'v-aria:tab': { isFirst: i === 0, isVertical: vertical, onChange: onActiveChange, - activeElement - } - }) + isActive: isActive(el.value) + }, + ...el.attrs + } : el.attrs . - block preIcon @@ -109,13 +110,14 @@ < tag.&__wrapper & :is = listTag | - :v-attrs = isTablist - ? Object.assign(attrs, { + :v-attrs = isTablist() + ? { 'v-aria:tablist': { isMultiple: multiple, isVertical: vertical - } - }) + }, + ...attrs + } : attrs . += self.list('items') diff --git a/src/base/b-list/b-list.ts b/src/base/b-list/b-list.ts index 625f973cb7..64487175e7 100644 --- a/src/base/b-list/b-list.ts +++ b/src/base/b-list/b-list.ts @@ -690,7 +690,7 @@ class bList extends iData implements iVisible, iWidth, iItems, iAccess { /** * Returns true if the component is used as tab list */ - protected get isTablist(): boolean { + protected isTablist(): boolean { return this.items.some((el) => el.href === undefined); } diff --git a/src/core/component/directives/aria/roles-engines/interface.ts b/src/core/component/directives/aria/roles-engines/interface.ts index 293701d0cb..4b7c81c3c1 100644 --- a/src/core/component/directives/aria/roles-engines/interface.ts +++ b/src/core/component/directives/aria/roles-engines/interface.ts @@ -1,7 +1,7 @@ export interface TabBindingValue { isFirst: boolean; isVertical: boolean; - activeElement: CanUndef>>; + isActive: boolean; onChange(cb: Function): void; } diff --git a/src/core/component/directives/aria/roles-engines/tab.ts b/src/core/component/directives/aria/roles-engines/tab.ts index 6e5e43834c..083952a10d 100644 --- a/src/core/component/directives/aria/roles-engines/tab.ts +++ b/src/core/component/directives/aria/roles-engines/tab.ts @@ -32,7 +32,7 @@ export default class TabEngine extends AriaRoleEngine { {isFirst} = this.$v; el.setAttribute('role', 'tab'); - el.setAttribute('aria-selected', 'false'); + el.setAttribute('aria-selected', String(this.$v.isActive)); if (isFirst) { if (el.tabIndex < 0) { @@ -43,25 +43,6 @@ export default class TabEngine extends AriaRoleEngine { el.setAttribute('tabindex', '-1'); } - this.$v.activeElement?.then((el) => { - if (Object.isArray(el)) { - for (let i = 0; i < el.length; i++) { - const - activeEl = el[i]; - - if (activeEl.getAttribute('aria-selected') !== 'true') { - activeEl.setAttribute('aria-selected', 'true'); - } - } - - return; - } - - if (el.getAttribute('aria-selected') !== 'true') { - el.setAttribute('aria-selected', 'true'); - } - }); - if (this.$a != null) { this.$a.on(el, 'keydown', this.onKeydown); } From 9123831afb3e882d0009e9f3bfedbbcb77ec6945 Mon Sep 17 00:00:00 2001 From: Aleksandr Bunin Date: Sun, 24 Jul 2022 10:12:02 +0300 Subject: [PATCH 006/185] refactoring --- src/base/b-list/b-list.ss | 16 +--- src/base/b-list/b-list.ts | 67 +++++++++++---- src/base/b-tree/b-tree.ss | 14 +--- src/base/b-tree/b-tree.ts | 84 +++++++++++++++---- .../component/directives/aria/aria-setter.ts | 10 +-- .../component/directives/aria/interface.ts | 6 ++ .../directives/aria/roles-engines/combobox.ts | 2 - .../aria/roles-engines/interface.ts | 18 ++-- .../directives/aria/roles-engines/option.ts | 4 +- .../directives/aria/roles-engines/tree.ts | 2 +- .../directives/aria/roles-engines/treeitem.ts | 30 +++---- src/form/b-select/b-select.ss | 18 ++-- src/form/b-select/b-select.ts | 60 ++++++++----- 13 files changed, 202 insertions(+), 129 deletions(-) diff --git a/src/base/b-list/b-list.ss b/src/base/b-list/b-list.ss index 1b01addf63..b702cfa292 100644 --- a/src/base/b-list/b-list.ss +++ b/src/base/b-list/b-list.ss @@ -47,14 +47,9 @@ } })) | - :v-attrs = isTablist() + :v-attrs = isTablist ? { - 'v-aria:tab': { - isFirst: i === 0, - isVertical: vertical, - onChange: onActiveChange, - isActive: isActive(el.value) - }, + 'v-aria:tab': getAriaOpt('tab', el, i), ...el.attrs } : el.attrs @@ -110,12 +105,9 @@ < tag.&__wrapper & :is = listTag | - :v-attrs = isTablist() + :v-attrs = isTablist ? { - 'v-aria:tablist': { - isMultiple: multiple, - isVertical: vertical - }, + 'v-aria:tablist': getAriaOpt('tablist'), ...attrs } : attrs diff --git a/src/base/b-list/b-list.ts b/src/base/b-list/b-list.ts index 64487175e7..32ffe1ae6c 100644 --- a/src/base/b-list/b-list.ts +++ b/src/base/b-list/b-list.ts @@ -690,7 +690,7 @@ class bList extends iData implements iVisible, iWidth, iItems, iAccess { /** * Returns true if the component is used as tab list */ - protected isTablist(): boolean { + protected get isTablist(): boolean { return this.items.some((el) => el.href === undefined); } @@ -707,30 +707,47 @@ class bList extends iData implements iVisible, iWidth, iItems, iAccess { } /** - * Handler: click to some item element + * Returns a dictionary with options for aria directive for tab role + * @param role + */ + protected getAriaOpt(role: 'tab'): Dictionary; + + /** + * Returns a dictionary with options for aria directive for tablist role * - * @param e - * @emits `actionChange(active: this['Active'])` + * @param role + * @param item + * @param i - position index */ - @watch({ - field: '?$el:click', - wrapper: (o, cb) => o.dom.delegateElement('link', cb) - }) + protected getAriaOpt(role: 'tablist', item: this['Item'], i: number): Dictionary; - protected onItemClick(e: Event): void { + protected getAriaOpt(role: 'tab' | 'tablist', item?: this['Item'], i?: number): Dictionary { const - target = e.delegateTarget, - id = Number(target.getAttribute('data-id')); + isActive = this.isActive.bind(this, item?.value); + + const opts = { + tablist: { + isMultiple: this.multiple, + isVertical: this.vertical + }, + tab: { + isFirst: i === 0, + isVertical: this.vertical, + changeEvent: this.bindToChange.bind(this), + get isActive() { + return isActive(); + } + } + }; - this.toggleActive(this.indexes[id]); - this.emit('actionChange', this.active); + return opts[role]; } /** - * Handler: on active element changes + * Binds callback to change event * @param cb */ - protected onActiveChange(cb: Function): void { + protected bindToChange(cb: Function): void { this.on('change', () => { if (Object.isSet(this.active)) { cb(this.block?.elements('link', {active: true})); @@ -740,6 +757,26 @@ class bList extends iData implements iVisible, iWidth, iItems, iAccess { } }); } + + /** + * Handler: click to some item element + * + * @param e + * @emits `actionChange(active: this['Active'])` + */ + @watch({ + field: '?$el:click', + wrapper: (o, cb) => o.dom.delegateElement('link', cb) + }) + + protected onItemClick(e: Event): void { + const + target = e.delegateTarget, + id = Number(target.getAttribute('data-id')); + + this.toggleActive(this.indexes[id]); + this.emit('actionChange', this.active); + } } export default bList; diff --git a/src/base/b-tree/b-tree.ss b/src/base/b-tree/b-tree.ss index 287def532d..8b9146a4e9 100644 --- a/src/base/b-tree/b-tree.ss +++ b/src/base/b-tree/b-tree.ss @@ -13,11 +13,7 @@ - template index() extends ['i-data'].index - block body < .&__root & - v-aria:tree = { - isVertical: vertical, - isRootTree: top == null, - onChange: (cb) => on('fold', (ctx, el, item, value) => cb(el, value)) - } + v-aria:tree = getAriaOpt('tree') . < template & v-for = (el, i) in asyncRender.iterate(items, renderChunks, renderTaskParams) | @@ -32,13 +28,7 @@ folded: getFoldedPropValue(el) } }) | - v-aria:treeitem = { - getRootElement: () => (top ? top.$el : $el), - toggleFold: changeFoldedMod.bind(this, el) , - isExpanded: () => getFoldedModById(el.id) === 'false', - isExpandable: el.children != null, - isVeryFirstItem: top == null && i === 0 - } + v-aria:treeitem = getAriaOpt('treeitem', el, i) . < .&__item-wrapper < .&__marker diff --git a/src/base/b-tree/b-tree.ts b/src/base/b-tree/b-tree.ts index fae29b1f57..f4d993baaa 100644 --- a/src/base/b-tree/b-tree.ts +++ b/src/base/b-tree/b-tree.ts @@ -250,27 +250,12 @@ class bTree extends iData implements iItems, iAccess { return this.$el?.querySelector(`[data-id=${itemId}]`) ?? undefined; } - /** - * Handler: fold element has been clicked - * - * @param item - * @emits `fold(target: HTMLElement, item:` [[Item]]`, value: boolean)` - */ - protected onFoldClick(item: this['Item']): void { - const - target = this.findItemElement(item.id), - newVal = this.getFoldedModById(item.id) === 'false'; - - if (target) { - this.block?.setElMod(target, 'node', 'folded', newVal); - this.emit('fold', target, item, newVal); - } - } - /** * Toggle folded state * - * @params target, value + * @param item + * @param target + * @param value? * @emits `fold(target: HTMLElement, item:` [[Item]]`, value: boolean)` */ protected changeFoldedMod(item: this['Item'], target: HTMLElement, value?: boolean): void { @@ -287,6 +272,69 @@ class bTree extends iData implements iItems, iAccess { this.block?.setElMod(target, 'node', 'folded', newVal); this.emit('fold', target, item, newVal); } + + /** + * Returns a dictionary with options for aria directive for tree role + * @param role + */ + protected getAriaOpt(role: 'tree'): Dictionary + + /** + * Returns a dictionary with options for aria directive for treeitem role + * + * @param role + * @param item + * @param i - position index + */ + protected getAriaOpt(role: 'treeitem', item: this['Item'], i: number): Dictionary + + protected getAriaOpt(role: 'tree' | 'treeitem', item?: this['Item'], i?: number): Dictionary { + const + getFoldedMod = this.getFoldedModById.bind(this, item?.id), + root = () => this.top?.$el ?? this.$el; + + const opts = { + tree: { + isVertical: this.vertical, + isRoot: this.top == null, + changeEvent: (cb: Function) => { + this.on('fold', (ctx, el, item, value) => cb(el, value)); + } + }, + treeitem: { + isRootFirstItem: this.top == null && i === 0, + toggleFold: this.changeFoldedMod.bind(this, item), + get rootElement() { + return root(); + }, + get isExpanded() { + return getFoldedMod() === 'false'; + }, + get isExpandable() { + return item?.children != null; + } + } + }; + + return opts[role]; + } + + /** + * Handler: fold element has been clicked + * + * @param item + * @emits `fold(target: HTMLElement, item:` [[Item]]`, value: boolean)` + */ + protected onFoldClick(item: this['Item']): void { + const + target = this.findItemElement(item.id), + newVal = this.getFoldedModById(item.id) === 'false'; + + if (target) { + this.block?.setElMod(target, 'node', 'folded', newVal); + this.emit('fold', target, item, newVal); + } + } } export default bTree; diff --git a/src/core/component/directives/aria/aria-setter.ts b/src/core/component/directives/aria/aria-setter.ts index dbd1d87292..a3f73a1f89 100644 --- a/src/core/component/directives/aria/aria-setter.ts +++ b/src/core/component/directives/aria/aria-setter.ts @@ -8,7 +8,7 @@ import * as ariaRoles from 'core/component/directives/aria/roles-engines'; import Async from 'core/async'; -import AriaRoleEngine from 'core/component/directives/aria/interface'; +import AriaRoleEngine, { eventsNames } from 'core/component/directives/aria/interface'; import type iBlock from 'super/i-block/i-block'; import type { DirectiveOptions } from 'core/component/directives/aria/interface'; @@ -62,11 +62,11 @@ export default class AriaSetter extends AriaRoleEngine { const $v = this.options.binding.value; - for (const p in $v) { - if (p === 'onOpen' || p === 'onClose' || p === 'onChange') { + for (const key in $v) { + if (key in eventsNames) { const - callback = this.role[p], - property = $v[p]; + callback = this.role[eventsNames[key]], + property = $v[key]; if (Object.isFunction(property)) { property(callback); diff --git a/src/core/component/directives/aria/interface.ts b/src/core/component/directives/aria/interface.ts index 499b73f1c7..3ced68a655 100644 --- a/src/core/component/directives/aria/interface.ts +++ b/src/core/component/directives/aria/interface.ts @@ -37,3 +37,9 @@ export enum keyCodes { RIGHT = 'ArrowRight', DOWN = 'ArrowDown' } + +export enum eventsNames { + openEvent = 'onOpen', + closeEvent = 'onClose', + changeEvent = 'onChange' +} diff --git a/src/core/component/directives/aria/roles-engines/combobox.ts b/src/core/component/directives/aria/roles-engines/combobox.ts index e0ed0a71d2..c63785b16f 100644 --- a/src/core/component/directives/aria/roles-engines/combobox.ts +++ b/src/core/component/directives/aria/roles-engines/combobox.ts @@ -35,13 +35,11 @@ export default class ComboboxEngine extends AriaRoleEngine { onOpen = (element: HTMLElement): void => { this.el.setAttribute('aria-expanded', 'true'); - this.setAriaActive(element); }; onClose = (): void => { this.el.setAttribute('aria-expanded', 'false'); - this.setAriaActive(); }; diff --git a/src/core/component/directives/aria/roles-engines/interface.ts b/src/core/component/directives/aria/roles-engines/interface.ts index 4b7c81c3c1..f135f22028 100644 --- a/src/core/component/directives/aria/roles-engines/interface.ts +++ b/src/core/component/directives/aria/roles-engines/interface.ts @@ -2,7 +2,7 @@ export interface TabBindingValue { isFirst: boolean; isVertical: boolean; isActive: boolean; - onChange(cb: Function): void; + changeEvent(cb: Function): void; } export interface TablistBindingValue { @@ -11,22 +11,22 @@ export interface TablistBindingValue { } export interface TreeBindingValue { + isRoot: boolean; isVertical: boolean; - isRootTree: boolean; - onChange(cb: Function): void; + changeEvent(cb: Function): void; } export interface TreeitemBindingValue { - isVeryFirstItem: boolean; + isRootFirstItem: boolean; isExpandable: boolean; - isExpanded(): boolean; - getRootElement(): CanUndef; + isExpanded: boolean; + rootElement: CanUndef; toggleFold(el: Element, value?: boolean): void; } export interface ComboboxBindingValue { isMultiple: boolean; - onChange(cb: Function): void; - onOpen(cb: Function): void; - onClose(cb: Function): void; + changeEvent(cb: Function): void; + openEvent(cb: Function): void; + closeEvent(cb: Function): void; } diff --git a/src/core/component/directives/aria/roles-engines/option.ts b/src/core/component/directives/aria/roles-engines/option.ts index be180c2e80..522bbb1e1a 100644 --- a/src/core/component/directives/aria/roles-engines/option.ts +++ b/src/core/component/directives/aria/roles-engines/option.ts @@ -8,7 +8,7 @@ import AriaRoleEngine from 'core/component/directives/aria/interface'; -export default class ListboxEngine extends AriaRoleEngine { +export default class OptionEngine extends AriaRoleEngine { init(): void { const {el} = this.options, @@ -21,7 +21,7 @@ export default class ListboxEngine extends AriaRoleEngine { onChange = (isSelected: boolean): void => { const {el} = this.options; - + console.log(isSelected) el.setAttribute('aria-selected', String(isSelected)); }; } diff --git a/src/core/component/directives/aria/roles-engines/tree.ts b/src/core/component/directives/aria/roles-engines/tree.ts index 3ce6bf3a27..09affe7603 100644 --- a/src/core/component/directives/aria/roles-engines/tree.ts +++ b/src/core/component/directives/aria/roles-engines/tree.ts @@ -29,7 +29,7 @@ export default class TreeEngine extends AriaRoleEngine { } setRootRole(): void { - this.el.setAttribute('role', this.$v.isRootTree ? 'tree' : 'group'); + this.el.setAttribute('role', this.$v.isRoot ? 'tree' : 'group'); } onChange = (el: HTMLElement, isFolded: boolean): void => { diff --git a/src/core/component/directives/aria/roles-engines/treeitem.ts b/src/core/component/directives/aria/roles-engines/treeitem.ts index 78b61b5145..7d4247e5c2 100644 --- a/src/core/component/directives/aria/roles-engines/treeitem.ts +++ b/src/core/component/directives/aria/roles-engines/treeitem.ts @@ -42,7 +42,7 @@ export default class TreeItemEngine extends AriaRoleEngine { const isMuted = this.ctx.muteTabIndexes(this.el); - if (this.$v.isVeryFirstItem) { + if (this.$v.isRootFirstItem) { if (isMuted) { this.ctx.unmuteTabIndexes(this.el); @@ -55,7 +55,7 @@ export default class TreeItemEngine extends AriaRoleEngine { this.ctx.$nextTick(() => { if (this.$v.isExpandable) { - this.el.setAttribute('aria-expanded', String(this.$v.isExpanded())); + this.el.setAttribute('aria-expanded', String(this.$v.isExpanded)); } }); } @@ -80,7 +80,7 @@ export default class TreeItemEngine extends AriaRoleEngine { case keyCodes.RIGHT: if (this.$v.isExpandable) { - if (this.$v.isExpanded()) { + if (this.$v.isExpanded) { this.moveFocus(1); } else { @@ -91,7 +91,7 @@ export default class TreeItemEngine extends AriaRoleEngine { break; case keyCodes.LEFT: - if (this.$v.isExpandable && this.$v.isExpanded()) { + if (this.$v.isExpandable && this.$v.isExpanded) { this.closeFold(); } else { @@ -101,11 +101,11 @@ export default class TreeItemEngine extends AriaRoleEngine { break; case keyCodes.HOME: - void this.setFocusToFirstItem(); + this.setFocusToFirstItem(); break; case keyCodes.END: - void this.setFocusToLastItem(); + this.setFocusToLastItem(); break; default: @@ -159,28 +159,18 @@ export default class TreeItemEngine extends AriaRoleEngine { } } - async setFocusToFirstItem(): Promise { - await this.ctx.async.wait( - this.$v.getRootElement.bind(this), - {label: $$.waitRoot} - ); - + setFocusToFirstItem(): void { const - firstEl = >this.$v.getRootElement()?.querySelector(FOCUSABLE_SELECTOR); + firstEl = >this.$v.rootElement?.querySelector(FOCUSABLE_SELECTOR); if (firstEl != null) { this.focusNext(firstEl); } } - async setFocusToLastItem(): Promise { - await this.ctx.async.wait( - this.$v.getRootElement.bind(this), - {label: $$.waitRoot} - ); - + setFocusToLastItem(): void { const - items = >>this.$v.getRootElement()?.querySelectorAll(FOCUSABLE_SELECTOR); + items = >>this.$v.rootElement?.querySelectorAll(FOCUSABLE_SELECTOR); const visibleItems: HTMLElement[] = [].filter.call( items, diff --git a/src/form/b-select/b-select.ss b/src/form/b-select/b-select.ss index 3d6b74dbe8..b98776de2f 100644 --- a/src/form/b-select/b-select.ss +++ b/src/form/b-select/b-select.ss @@ -41,12 +41,10 @@ :v-attrs = native ? el.attrs - : {'v-aria:option': { - preSelected: isSelected(el.value), - onChange: (cb) => on('actionChange', () => cb(isSelected(el.value))) - }, - ...el.attrs - } | + : { + 'v-aria:option': getAriaOpt('option', el), + ...el.attrs + } | :id = dom.getId(el.value) | ${itemAttrs} @@ -98,13 +96,7 @@ += self.items('option') < template v-else - < _.&__cell.&__input-wrapper & - v-aria:combobox = { - onOpen, - onClose: (cb) => on('close', cb), - onChange: onItemMarked, - isMultiple: multiple - } . + < _.&__cell.&__input-wrapper v-aria:combobox = getAriaOpt('combobox') += self.nativeInput({model: 'textStore', attrs: {'@input': 'onSearchInput'}}) - block icon diff --git a/src/form/b-select/b-select.ts b/src/form/b-select/b-select.ts index d20654d3a0..7eab4472f3 100644 --- a/src/form/b-select/b-select.ts +++ b/src/form/b-select/b-select.ts @@ -932,6 +932,46 @@ class bSelect extends iInputText implements iOpenToggle, iItems { return false; } + /** + * Returns a dictionary with options for aria directive for combobox role + * @param role + */ + protected getAriaOpt(role: 'combobox'): Dictionary; + + /** + * Returns a dictionary with options for aria directive for option role + * + * @param role + * @param item + */ + protected getAriaOpt(role: 'option', item: this['Item']): Dictionary; + + protected getAriaOpt(role: 'combobox' | 'option', item?: this['Item']): Dictionary { + const + event = 'el.mod.set.*.marked.*', + isSelected = this.isSelected.bind(this, item?.value); + + const + opts = { + combobox: { + isMultiple: this.multiple, + changeEvent: (cb) => this.localEmitter.on(event, ({link}) => cb(link)), + closeEvent: (cb) => this.on('close', cb), + openEvent: (cb) => this.on('open', () => { + void this.$nextTick(() => cb(this.selectedElement)); + }) + }, + option: { + get preSelected() { + return isSelected(); + }, + changeEvent: (cb) => this.on('actionChange', () => cb(isSelected())) + } + }; + + return opts[role]; + } + /** * Handler: typing text into a helper text input to search select options * @@ -965,26 +1005,6 @@ class bSelect extends iInputText implements iOpenToggle, iItems { protected onItemsNavigate(e: KeyboardEvent): void { void on.itemsNavigate(this, e); } - - /** - * Handler: executes callback on "open" event - * @param cb - */ - protected onOpen(cb: Function): void { - this.on('open', () => { - void this.$nextTick(() => { - cb.call(this, this.selectedElement); - }); - }); - } - - /** - * Handler: executes callback on item set "marked" mod - * @param cb - */ - protected onItemMarked(cb: Function): void { - this.localEmitter.on('el.mod.set.**', ({link}) => cb(link)); - } } export default bSelect; From b617a3c72350bb0dbf550747f6b7040154073298 Mon Sep 17 00:00:00 2001 From: Aleksandr Bunin Date: Sun, 24 Jul 2022 10:28:48 +0300 Subject: [PATCH 007/185] refactoring --- src/core/component/directives/aria/roles-engines/option.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/core/component/directives/aria/roles-engines/option.ts b/src/core/component/directives/aria/roles-engines/option.ts index 522bbb1e1a..109e240835 100644 --- a/src/core/component/directives/aria/roles-engines/option.ts +++ b/src/core/component/directives/aria/roles-engines/option.ts @@ -16,12 +16,16 @@ export default class OptionEngine extends AriaRoleEngine { el.setAttribute('role', 'option'); el.setAttribute('aria-selected', String(preSelected)); + + if (!el.hasAttribute('id')) { + Object.throw('Option aria directive expects the Element id to be added'); + } } onChange = (isSelected: boolean): void => { const {el} = this.options; - console.log(isSelected) + el.setAttribute('aria-selected', String(isSelected)); }; } From b4a0bb5ce7b40470728094852685fa6a1c8cccff Mon Sep 17 00:00:00 2001 From: Aleksandr Bunin Date: Tue, 26 Jul 2022 15:55:34 +0300 Subject: [PATCH 008/185] refactoring in progress --- src/base/b-list/CHANGELOG.md | 2 +- src/base/b-list/b-list.ss | 24 ++-- src/base/b-list/b-list.ts | 128 +++++++++--------- src/base/b-list/interface.ts | 2 + src/base/b-tree/CHANGELOG.md | 2 +- src/base/b-tree/b-tree.ss | 7 +- src/base/b-tree/b-tree.ts | 109 ++++++++------- src/base/b-tree/interface.ts | 2 + .../component/directives/aria/aria-setter.ts | 4 +- .../directives/aria/roles-engines/tab.ts | 10 +- .../directives/aria/roles-engines/treeitem.ts | 10 +- src/form/b-select/CHANGELOG.md | 2 +- .../p-v4-components-demo.ss | 2 + src/traits/i-access/CHANGELOG.md | 6 +- src/traits/i-access/const.ts | 18 ++- 15 files changed, 174 insertions(+), 154 deletions(-) diff --git a/src/base/b-list/CHANGELOG.md b/src/base/b-list/CHANGELOG.md index 18fbd9fe1c..712f6a9004 100644 --- a/src/base/b-list/CHANGELOG.md +++ b/src/base/b-list/CHANGELOG.md @@ -13,7 +13,7 @@ Changelog #### :rocket: New Feature -* Added `v-aria` directive +* Added a new directive `v-aria` * Added a new prop `vertical` * Added `isTablist` * Added `onActiveChange` diff --git a/src/base/b-list/b-list.ss b/src/base/b-list/b-list.ss index b702cfa292..323280bec9 100644 --- a/src/base/b-list/b-list.ss +++ b/src/base/b-list/b-list.ss @@ -47,12 +47,12 @@ } })) | - :v-attrs = isTablist - ? { - 'v-aria:tab': getAriaOpt('tab', el, i), - ...el.attrs - } - : el.attrs + :v-attrs = isTablist ? + { + 'v-aria:tab': getAriaConfig('tab', el, i), + ...el.attrs + } : + el.attrs . - block preIcon < span.&__cell.&__link-icon.&__link-pre-icon v-if = el.preIcon || vdom.getSlot('preIcon') @@ -105,11 +105,11 @@ < tag.&__wrapper & :is = listTag | - :v-attrs = isTablist - ? { - 'v-aria:tablist': getAriaOpt('tablist'), - ...attrs - } - : attrs + :v-attrs = isTablist ? + { + 'v-aria:tablist': getAriaConfig('tablist'), + ...attrs + } : + attrs . += self.list('items') diff --git a/src/base/b-list/b-list.ts b/src/base/b-list/b-list.ts index 32ffe1ae6c..8f8ce22d36 100644 --- a/src/base/b-list/b-list.ts +++ b/src/base/b-list/b-list.ts @@ -21,13 +21,14 @@ import SyncPromise from 'core/promise/sync'; import { isAbsURL } from 'core/url'; import { derive } from 'core/functools/trait'; + import iVisible from 'traits/i-visible/i-visible'; import iWidth from 'traits/i-width/i-width'; import iItems, { IterationKey } from 'traits/i-items/i-items'; - -import iData, { component, prop, field, system, computed, hook, watch, ModsDecl } from 'super/i-data/i-data'; -import type { Active, Item, Items } from 'base/b-list/interface'; import iAccess from 'traits/i-access/i-access'; +import iData, { component, prop, field, system, computed, hook, watch, ModsDecl } from 'super/i-data/i-data'; + +import type { Active, Item, Items, Orientation } from 'base/b-list/interface'; export * from 'super/i-data/i-data'; export * from 'base/b-list/interface'; @@ -113,10 +114,10 @@ class bList extends iData implements iVisible, iWidth, iItems, iAccess { readonly multiple: boolean = false; /** - * If true, the component view orientation is vertical. Horizontal is default + * The component view orientation */ - @prop(Boolean) - readonly vertical: boolean = false; + @prop(String) + readonly orientation: Orientation = 'horizontal'; /** * If true, the active item can be unset by using another click to it. @@ -262,15 +263,19 @@ class bList extends iData implements iVisible, iWidth, iItems, iAccess { protected activeStore!: this['Active']; + /** + * True if the component is used as a tablist + */ + @computed({dependencies: ['items']}) + protected get isTablist(): boolean { + return this.items.some((el) => el.href === undefined); + } + /** * A link to the active item element. * If the component is switched to the `multiple` mode, the getter will return an array of elements. */ - @computed({ - cache: true, - dependencies: ['active'] - }) - + @computed({dependencies: ['active']}) protected get activeElement(): CanPromise>> { const {active} = this; @@ -688,74 +693,67 @@ class bList extends iData implements iVisible, iWidth, iItems, iAccess { } /** - * Returns true if the component is used as tab list - */ - protected get isTablist(): boolean { - return this.items.some((el) => el.href === undefined); - } - - protected override onAddData(data: unknown): void { - Object.assign(this.db, this.convertDataToDB(data)); - } - - protected override onUpdData(data: unknown): void { - Object.assign(this.db, this.convertDataToDB(data)); - } - - protected override onDelData(data: unknown): void { - Object.assign(this.db, this.convertDataToDB(data)); - } - - /** - * Returns a dictionary with options for aria directive for tab role + * Returns a dictionary with configurations for the v-aria directive used as a tablist * @param role */ - protected getAriaOpt(role: 'tab'): Dictionary; + protected getAriaConfig(role: 'tablist'): Dictionary; /** - * Returns a dictionary with options for aria directive for tablist role + * Returns a dictionary with configurations for the v-aria directive used as a tab * * @param role - * @param item - * @param i - position index + * @param item - tab item data + * @param i - tab item position index */ - protected getAriaOpt(role: 'tablist', item: this['Item'], i: number): Dictionary; + protected getAriaConfig(role: 'tab', item: this['Item'], i: number): Dictionary; - protected getAriaOpt(role: 'tab' | 'tablist', item?: this['Item'], i?: number): Dictionary { + protected getAriaConfig(role: 'tab' | 'tablist', item?: this['Item'], i?: number): Dictionary { const - isActive = this.isActive.bind(this, item?.value); - - const opts = { - tablist: { - isMultiple: this.multiple, - isVertical: this.vertical - }, - tab: { - isFirst: i === 0, - isVertical: this.vertical, - changeEvent: this.bindToChange.bind(this), - get isActive() { - return isActive(); - } + isActive = this.isActive.bind(this, item?.value), + isVertical = this.orientation === 'vertical'; + + const changeEvent = (cb: Function) => { + this.on('change', () => { + if (Object.isSet(this.active)) { + cb(this.block?.elements('link', {active: true})); + + } else { + cb(this.block?.element('link', {active: true})); } - }; + }); + }; + + const tablistConfig = { + isVertical, + isMultiple: this.multiple + }; + + const tabConfig = { + isVertical, + isFirst: i === 0, + changeEvent, + get isActive() { + return isActive(); + } + }; - return opts[role]; + switch (role) { + case 'tablist': return tablistConfig; + case 'tab': return tabConfig; + default: return {}; + } } - /** - * Binds callback to change event - * @param cb - */ - protected bindToChange(cb: Function): void { - this.on('change', () => { - if (Object.isSet(this.active)) { - cb(this.block?.elements('link', {active: true})); + protected override onAddData(data: unknown): void { + Object.assign(this.db, this.convertDataToDB(data)); + } - } else { - cb(this.block?.element('link', {active: true})); - } - }); + protected override onUpdData(data: unknown): void { + Object.assign(this.db, this.convertDataToDB(data)); + } + + protected override onDelData(data: unknown): void { + Object.assign(this.db, this.convertDataToDB(data)); } /** diff --git a/src/base/b-list/interface.ts b/src/base/b-list/interface.ts index 17dced5469..4139904270 100644 --- a/src/base/b-list/interface.ts +++ b/src/base/b-list/interface.ts @@ -102,3 +102,5 @@ export interface Item extends Dictionary { export type Items = Item[]; export type Active = unknown | Set; + +export type Orientation = 'vertical' | 'horizontal'; diff --git a/src/base/b-tree/CHANGELOG.md b/src/base/b-tree/CHANGELOG.md index 12a877eb58..afe156f069 100644 --- a/src/base/b-tree/CHANGELOG.md +++ b/src/base/b-tree/CHANGELOG.md @@ -13,7 +13,7 @@ Changelog #### :rocket: New Feature -* Added `v-aria` directive +* Added a new directive `v-aria` * Added a new prop `vertical` * Added `changeFoldedMod` * Now the component derive `iAccess` diff --git a/src/base/b-tree/b-tree.ss b/src/base/b-tree/b-tree.ss index 8b9146a4e9..c9c6a114c6 100644 --- a/src/base/b-tree/b-tree.ss +++ b/src/base/b-tree/b-tree.ss @@ -12,9 +12,7 @@ - template index() extends ['i-data'].index - block body - < .&__root & - v-aria:tree = getAriaOpt('tree') - . + < .&__root v-aria:tree = getAriaConfig('tree') < template & v-for = (el, i) in asyncRender.iterate(items, renderChunks, renderTaskParams) | :key = getItemKey(el, i) @@ -28,7 +26,8 @@ folded: getFoldedPropValue(el) } }) | - v-aria:treeitem = getAriaOpt('treeitem', el, i) + + v-aria:treeitem = getAriaConfig('treeitem', el, i) . < .&__item-wrapper < .&__marker diff --git a/src/base/b-tree/b-tree.ts b/src/base/b-tree/b-tree.ts index f4d993baaa..e68db8673f 100644 --- a/src/base/b-tree/b-tree.ts +++ b/src/base/b-tree/b-tree.ts @@ -18,19 +18,21 @@ import 'models/demo/nested-list'; import symbolGenerator from 'core/symbol'; import { derive } from 'core/functools/trait'; -import iItems, { IterationKey } from 'traits/i-items/i-items'; +import iItems, { IterationKey } from 'traits/i-items/i-items'; import iData, { component, prop, field, TaskParams, TaskI } from 'super/i-data/i-data'; -import type { Item, RenderFilter } from 'base/b-tree/interface'; import iAccess from 'traits/i-access/i-access'; +import type { Item, Orientation, RenderFilter } from 'base/b-tree/interface'; + export * from 'super/i-data/i-data'; export * from 'base/b-tree/interface'; export const $$ = symbolGenerator(); -interface bTree extends Trait {} +interface bTree extends Trait { +} /** * Component to render tree of any elements @@ -104,10 +106,10 @@ class bTree extends iData implements iItems, iAccess { readonly folded: boolean = true; /** - * If true, the component view orientation is vertical. Horizontal is default + * The component view orientation */ - @prop(Boolean) - readonly vertical: boolean = false; + @prop(String) + readonly orientation: Orientation = 'horizontal'; /** * Link to the top level component (internal parameter) @@ -251,72 +253,67 @@ class bTree extends iData implements iItems, iAccess { } /** - * Toggle folded state - * - * @param item - * @param target - * @param value? - * @emits `fold(target: HTMLElement, item:` [[Item]]`, value: boolean)` - */ - protected changeFoldedMod(item: this['Item'], target: HTMLElement, value?: boolean): void { - const - mod = this.block?.getElMod(target, 'node', 'folded'); - - if (mod == null) { - return; - } - - const - newVal = value ? value : mod === 'false'; - - this.block?.setElMod(target, 'node', 'folded', newVal); - this.emit('fold', target, item, newVal); - } - - /** - * Returns a dictionary with options for aria directive for tree role + * Returns a dictionary with configurations for the v-aria directive used as a tree * @param role */ - protected getAriaOpt(role: 'tree'): Dictionary + protected getAriaConfig(role: 'tree'): Dictionary /** - * Returns a dictionary with options for aria directive for treeitem role + * Returns a dictionary with configurations for the v-aria directive used as a treeitem * * @param role - * @param item - * @param i - position index + * @param item - tab item data + * @param i - tab item position index */ - protected getAriaOpt(role: 'treeitem', item: this['Item'], i: number): Dictionary + protected getAriaConfig(role: 'treeitem', item: this['Item'], i: number): Dictionary - protected getAriaOpt(role: 'tree' | 'treeitem', item?: this['Item'], i?: number): Dictionary { + protected getAriaConfig(role: 'tree' | 'treeitem', item?: this['Item'], i?: number): Dictionary { const getFoldedMod = this.getFoldedModById.bind(this, item?.id), root = () => this.top?.$el ?? this.$el; - const opts = { - tree: { - isVertical: this.vertical, - isRoot: this.top == null, - changeEvent: (cb: Function) => { - this.on('fold', (ctx, el, item, value) => cb(el, value)); - } + const toggleFold = (target: HTMLElement, value?: boolean): void => { + const + mod = this.block?.getElMod(target, 'node', 'folded'); + + if (mod == null) { + return; + } + + const + newVal = value ? value : mod === 'false'; + + this.block?.setElMod(target, 'node', 'folded', newVal); + this.emit('fold', target, item, newVal); + }; + + const treeConfig = { + isRoot: this.top == null, + isVertical: this.orientation === 'vertical', + changeEvent: (cb: Function) => { + this.on('fold', (ctx, el, item, value) => cb(el, value)); + } + }; + + const treeitemConfig = { + isRootFirstItem: this.top == null && i === 0, + toggleFold, + get rootElement() { + return root(); }, - treeitem: { - isRootFirstItem: this.top == null && i === 0, - toggleFold: this.changeFoldedMod.bind(this, item), - get rootElement() { - return root(); - }, - get isExpanded() { - return getFoldedMod() === 'false'; - }, - get isExpandable() { - return item?.children != null; - } + get isExpanded() { + return getFoldedMod() === 'false'; + }, + get isExpandable() { + return item?.children != null; } }; - return opts[role]; + switch (role) { + case 'tree': return treeConfig; + case 'treeitem': return treeitemConfig; + default: return {}; + } } /** diff --git a/src/base/b-tree/interface.ts b/src/base/b-tree/interface.ts index 1f513b2974..33471ae590 100644 --- a/src/base/b-tree/interface.ts +++ b/src/base/b-tree/interface.ts @@ -38,3 +38,5 @@ export interface Item extends Dictionary { export interface RenderFilter { (ctx: bTree, el: Item, i: number, task: TaskI): CanPromise; } + +export type Orientation = 'vertical' | 'horizontal'; diff --git a/src/core/component/directives/aria/aria-setter.ts b/src/core/component/directives/aria/aria-setter.ts index a3f73a1f89..2465382642 100644 --- a/src/core/component/directives/aria/aria-setter.ts +++ b/src/core/component/directives/aria/aria-setter.ts @@ -117,14 +117,14 @@ export default class AriaSetter extends AriaRoleEngine { el.setAttribute('aria-label', value.label); } else if (value.labelledby != null) { - el.setAttribute('aria-labelledby', dom.getId(value.labelledby)); + el.setAttribute('aria-labelledby', value.labelledby); } if (value.description != null) { el.setAttribute('aria-description', value.description); } else if (value.describedby != null) { - el.setAttribute('aria-describedby', dom.getId(value.describedby)); + el.setAttribute('aria-describedby', value.describedby); } } } diff --git a/src/core/component/directives/aria/roles-engines/tab.ts b/src/core/component/directives/aria/roles-engines/tab.ts index 083952a10d..27cfb859dc 100644 --- a/src/core/component/directives/aria/roles-engines/tab.ts +++ b/src/core/component/directives/aria/roles-engines/tab.ts @@ -80,11 +80,17 @@ export default class TabEngine extends AriaRoleEngine { } focusNext(): void { - this.ctx.nextFocusableElement(1)?.focus(); + const + focusable = >this.ctx.getNextFocusableElement(1); + + focusable?.focus(); } focusPrev(): void { - this.ctx.nextFocusableElement(-1)?.focus(); + const + focusable = >this.ctx.getNextFocusableElement(-1); + + focusable?.focus(); } onKeydown = (event: Event): void => { diff --git a/src/core/component/directives/aria/roles-engines/treeitem.ts b/src/core/component/directives/aria/roles-engines/treeitem.ts index 7d4247e5c2..3c6b5ddd59 100644 --- a/src/core/component/directives/aria/roles-engines/treeitem.ts +++ b/src/core/component/directives/aria/roles-engines/treeitem.ts @@ -40,11 +40,11 @@ export default class TreeItemEngine extends AriaRoleEngine { this.$a?.on(this.el, 'keydown', this.onKeyDown); const - isMuted = this.ctx.muteTabIndexes(this.el); + isMuted = this.ctx.removeAllFromTabSequence(this.el); if (this.$v.isRootFirstItem) { if (isMuted) { - this.ctx.unmuteTabIndexes(this.el); + this.ctx.restoreAllToTabSequence(this.el); } else { this.el.tabIndex = 0; @@ -117,14 +117,14 @@ export default class TreeItemEngine extends AriaRoleEngine { }; focusNext(nextEl: HTMLElement): void { - this.ctx.muteTabIndexes(this.el); - this.ctx.unmuteTabIndexes(nextEl); + this.ctx.removeAllFromTabSequence(this.el); + this.ctx.restoreAllToTabSequence(nextEl); nextEl.focus(); } moveFocus(step: 1 | -1): void { const - nextEl = this.ctx.nextFocusableElement(step); + nextEl = >this.ctx.getNextFocusableElement(step); if (nextEl != null) { this.focusNext(nextEl); diff --git a/src/form/b-select/CHANGELOG.md b/src/form/b-select/CHANGELOG.md index 4b0b85a820..4a18185ae7 100644 --- a/src/form/b-select/CHANGELOG.md +++ b/src/form/b-select/CHANGELOG.md @@ -13,7 +13,7 @@ Changelog #### :rocket: New Feature -* Added `v-aria` directive +* Added a new directive `v-aria` * Added `onItemMarked` * Added `onOpen` * Now the component derive `iAccess` diff --git a/src/pages/p-v4-components-demo/p-v4-components-demo.ss b/src/pages/p-v4-components-demo/p-v4-components-demo.ss index 176043a8e0..40cb2eb066 100644 --- a/src/pages/p-v4-components-demo/p-v4-components-demo.ss +++ b/src/pages/p-v4-components-demo/p-v4-components-demo.ss @@ -14,6 +14,8 @@ - block body : config = require('@config/config').build + < b-checkbox label = 'bla' | :id = 55 + - forEach config.components => @component - if config.inspectComponents < b-v4-component-demo diff --git a/src/traits/i-access/CHANGELOG.md b/src/traits/i-access/CHANGELOG.md index 0a0c8f3527..f4cab58731 100644 --- a/src/traits/i-access/CHANGELOG.md +++ b/src/traits/i-access/CHANGELOG.md @@ -13,9 +13,9 @@ Changelog #### :rocket: New Feature -* Added `muteTabIndexes` -* Added `unmuteTabIndexes` -* Added `nextFocusableElement` +* Added `removeAllFromTabSequence` +* Added `restoreAllToTabSequence` +* Added `getNextFocusableElement` * Added `is` ## v3.0.0-rc.211 (2021-07-21) diff --git a/src/traits/i-access/const.ts b/src/traits/i-access/const.ts index e4b3110847..ca78dba7b1 100644 --- a/src/traits/i-access/const.ts +++ b/src/traits/i-access/const.ts @@ -1,2 +1,16 @@ -export const - FOCUSABLE_SELECTOR = '[tabindex]:not([disabled]), a:not([disabled]), button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled])'; +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ + +export const FOCUSABLE_SELECTOR = [ + 'a', + 'input', + 'select', + 'button', + 'textarea', + '[tabindex]' +].join(); From ada7afb4c9f4fc05a42ecb7f7dd4237ff7d959ba Mon Sep 17 00:00:00 2001 From: Aleksandr Bunin Date: Tue, 26 Jul 2022 17:19:46 +0300 Subject: [PATCH 009/185] refactoring i-access --- src/traits/i-access/i-access.ts | 203 +++++++++++++++++++++----------- 1 file changed, 137 insertions(+), 66 deletions(-) diff --git a/src/traits/i-access/i-access.ts b/src/traits/i-access/i-access.ts index 61b8f43327..88c983254b 100644 --- a/src/traits/i-access/i-access.ts +++ b/src/traits/i-access/i-access.ts @@ -14,9 +14,12 @@ */ import SyncPromise from 'core/promise/sync'; +import { sequence } from 'core/iter/combinators'; +import { intoIter } from 'core/iter'; import type iBlock from 'super/i-block/i-block'; import type { ModsDecl, ModEvent } from 'super/i-block/i-block'; + import { FOCUSABLE_SELECTOR } from 'traits/i-access/const'; export default abstract class iAccess { @@ -163,99 +166,145 @@ export default abstract class iAccess { }); } - /** @see [[iAccess.muteTabIndexes]] */ - static muteTabIndexes: AddSelf = - (component, ctx?): boolean => { + /** @see [[iAccess.removeAllFromTabSequence]] */ + static removeAllFromTabSequence: AddSelf = + (component, el?): boolean => { const - el = ctx ?? component.$el; + ctx = el ?? component.$el; - if (el == null) { + if (ctx == null) { return false; } - const - elems = el.querySelectorAll(FOCUSABLE_SELECTOR); - - for (let i = 0; i < elems.length; i++) { - const - elem = (elems[i]); + let + areElementsRemoved = false; - if (elem.dataset.tabindex == null) { - elem.dataset.tabindex = String(elem.tabIndex); - } - - elem.tabIndex = -1; - } + const + focusableIter = this.findAllFocusableElements(component, ctx); - if (ctx != null && ctx.tabIndex > -1) { - if (ctx.dataset.tabindex == null) { - ctx.dataset.tabindex = String(ctx.tabIndex); + for (const focusableEl of >focusableIter) { + if (!focusableEl.hasAttribute('data-tabindex')) { + focusableEl.setAttribute('data-tabindex', String(focusableEl.tabIndex)); } - ctx.tabIndex = -1; - - return true; + focusableEl.tabIndex = -1; + areElementsRemoved = true; } - return elems.length > 0; + return areElementsRemoved; }; - /** @see [[iAccess.unmuteTabIndexes]] */ - static unmuteTabIndexes: AddSelf = - (component, ctx?): boolean => { + /** @see [[iAccess.restoreAllToTabSequence]] */ + static restoreAllToTabSequence: AddSelf = + (component, el?): boolean => { const - el = ctx ?? component.$el; + ctx = el ?? component.$el; - if (el == null) { + if (ctx == null) { return false; } - const - elems = el.querySelectorAll('[data-tabindex]'); + let + areElementsRestored = false; - for (let i = 0; i < elems.length; i++) { - const - elem = (elems[i]); + let + removedElemsIter = intoIter(ctx.querySelectorAll('[data-tabindex]')); - elem.tabIndex = Number(elem.dataset.tabindex); - delete elem.dataset.tabindex; + if (el?.hasAttribute('data-tabindex')) { + removedElemsIter = sequence(removedElemsIter, intoIter([el])); } - if (ctx?.dataset.tabindex != null) { - ctx.tabIndex = Number(ctx.dataset.tabindex); - delete ctx.dataset.tabindex; + for (const elem of >removedElemsIter) { + const + originalTabIndex = elem.getAttribute('data-tabindex'); + + if (originalTabIndex != null) { + elem.tabIndex = Number(originalTabIndex); + elem.removeAttribute('data-tabindex'); - return true; + areElementsRestored = true; + } } - return elems.length > 0; + return areElementsRestored; }; - /** @see [[iAccess.unmuteTabIndexes]] */ - static nextFocusableElement: AddSelf = - (component, step, el?): CanUndef => { + /** @see [[iAccess.getNextFocusableElement]] */ + static getNextFocusableElement: AddSelf = + (component, step, el?): CanUndef => { if (document.activeElement == null) { return; } const - nodeListOfFocusable = (el ?? document).querySelectorAll(FOCUSABLE_SELECTOR); - - const focusable: HTMLElement[] = [].filter.call( - nodeListOfFocusable, - (el: HTMLElement) => ( - el.offsetWidth > 0 || - el.offsetHeight > 0 || - el === document.activeElement - ) - ); + ctx = el ?? document.documentElement, + focusableIter = this.findAllFocusableElements(component, ctx), + visibleFocusable: HTMLElement[] = []; + + for (const element of >focusableIter) { + if ( + element.offsetWidth > 0 || + element.offsetHeight > 0 || + element === document.activeElement + ) { + visibleFocusable.push(element); + } + } const - index = focusable.indexOf(document.activeElement); + index = visibleFocusable.indexOf(document.activeElement); if (index > -1) { - return focusable[index + step]; + return visibleFocusable[index + step]; + } + }; + + /** @see [[iAccess.findFocusableElement]] */ + static findFocusableElement: AddSelf = + (component, el?): CanUndef => { + const + ctx = el ?? component.$el, + focusableIter = this.findAllFocusableElements(component, ctx); + + for (const element of focusableIter) { + if (!element.hasAttribute('disabled')) { + return element; + } + } + }; + + /** @see [[iAccess.findAllFocusableElements]] */ + static findAllFocusableElements: AddSelf = + (component, el?): IterableIterator => { + const + ctx = el ?? component.$el, + focusableElems = ctx?.querySelectorAll(FOCUSABLE_SELECTOR); + + let + focusableIter = intoIter(focusableElems ?? []); + + if (el?.hasAttribute('tabindex')) { + focusableIter = sequence(focusableIter, intoIter([el])); } + + function* createFocusableWithoutDisabled(iter: IterableIterator): IterableIterator { + for (const iterEl of iter) { + if (!iterEl.hasAttribute('disabled')) { + yield iterEl; + } + } + } + + const + focusableWithoutDisabled = createFocusableWithoutDisabled(focusableIter); + + return { + [Symbol.iterator]() { + return this; + }, + + next: focusableWithoutDisabled.next.bind(focusableWithoutDisabled) + }; }; /** @@ -268,7 +317,7 @@ export default abstract class iAccess { } const dict = Object.cast(obj); - return Object.isFunction(dict.muteTabIndexes) && Object.isFunction(dict.nextFocusableElement); + return Object.isFunction(dict.removeAllFromTabSequence) && Object.isFunction(dict.getNextFocusableElement); } /** @@ -329,26 +378,48 @@ export default abstract class iAccess { } /** - * Remove all descendants with tabindex attribute from tab sequence and saves previous value. - * @param el + * Removes all children of the specified element that can be focused from the Tab toggle sequence. + * In effect, these elements are set to -1 for the tabindex attribute + * @param el - a context to search, if not set, the root element of the component will be used + */ + removeAllFromTabSequence(el?: Element): boolean { + return Object.throw(); + } + + /** + * Reverts all children of the specified element that can be focused to the Tab toggle sequence. + * This method is used to restore the state of elements to the state + * they had before removeAllFromTabSequence was applied + * + * @param el - a context to search, if not set, the root element of the component will be used + */ + restoreAllToTabSequence(el?: Element): boolean { + return Object.throw(); + } + + /** + * Gets a next or previous focusable element via the step parameter from the current focused element + * + * @param step + * @param el - a context to search, if not set, document will be used */ - muteTabIndexes(el?: HTMLElement): boolean { + getNextFocusableElement(step: 1 | -1, el?: Element): CanUndef { return Object.throw(); } /** - * Recovers previous saved tabindex values to the elements that were changed. - * @param el + * Find focusable element except disabled ones + * @param el - a context to search, if not set, component will be used */ - unmuteTabIndexes(el?: HTMLElement): boolean { + findFocusableElement(el?: Element): CanUndef { return Object.throw(); } /** - * Sets the focus to the next or previous focusable element via the step parameter - * @params step, el? + * Find all focusable elements except disabled ones. Search includes the specified element + * @param el - a context to search, if not set, component will be used */ - nextFocusableElement(step: 1 | -1, el?: HTMLElement): CanUndef { + findAllFocusableElements(el?: Element): IterableIterator { return Object.throw(); } } From 0493ff5a31e153793dac4f0152b7b359de2f4f24 Mon Sep 17 00:00:00 2001 From: Aleksandr Bunin Date: Tue, 26 Jul 2022 18:28:08 +0300 Subject: [PATCH 010/185] refactoring --- .../component/directives/aria/CHANGELOG.md | 6 ++ .../component/directives/aria/aria-setter.ts | 99 +++++++++---------- src/core/component/directives/aria/index.ts | 24 ++--- .../component/directives/aria/interface.ts | 8 +- .../directives/aria/roles-engines/combobox.ts | 15 +-- .../aria/roles-engines/interface.ts | 10 +- .../directives/aria/roles-engines/tab.ts | 58 +++++------ .../directives/aria/roles-engines/tablist.ts | 8 +- .../directives/aria/roles-engines/tree.ts | 10 +- .../directives/aria/roles-engines/treeitem.ts | 67 +++++++------ .../p-v4-components-demo.ss | 2 - src/traits/i-access/i-access.ts | 30 +++--- 12 files changed, 163 insertions(+), 174 deletions(-) diff --git a/src/core/component/directives/aria/CHANGELOG.md b/src/core/component/directives/aria/CHANGELOG.md index 1670fa7e71..ba2980fb55 100644 --- a/src/core/component/directives/aria/CHANGELOG.md +++ b/src/core/component/directives/aria/CHANGELOG.md @@ -8,3 +8,9 @@ Changelog > - :memo: [Documentation] > - :house: [Internal] > - :nail_care: [Polish] + +## v?.??.? (2022-0?-??) + +#### :rocket: New Feature + +* Initial release diff --git a/src/core/component/directives/aria/aria-setter.ts b/src/core/component/directives/aria/aria-setter.ts index 2465382642..f395d86804 100644 --- a/src/core/component/directives/aria/aria-setter.ts +++ b/src/core/component/directives/aria/aria-setter.ts @@ -13,18 +13,20 @@ import type iBlock from 'super/i-block/i-block'; import type { DirectiveOptions } from 'core/component/directives/aria/interface'; export default class AriaSetter extends AriaRoleEngine { - override $a: Async; + override async: Async; role: CanUndef; constructor(options: DirectiveOptions) { super(options); - this.$a = new Async(); + this.async = new Async(); this.setAriaRole(); if (this.role != null) { - this.role.$a = this.$a; + this.role.async = this.async; } + + this.init(); } init(): void { @@ -34,54 +36,17 @@ export default class AriaSetter extends AriaRoleEngine { this.role?.init(); } - override update(): void { + update(): void { const ctx = this.options.vnode.fakeContext; if (ctx.isFunctional) { ctx.off(); } - - if (this.role != null) { - this.role.options = this.options; - this.role.update?.(); - } } - override clear(): void { - this.$a.clearAll(); - - this.role?.clear?.(); - } - - addEventHandlers(): void { - if (this.role == null) { - return; - } - - const - $v = this.options.binding.value; - - for (const key in $v) { - if (key in eventsNames) { - const - callback = this.role[eventsNames[key]], - property = $v[key]; - - if (Object.isFunction(property)) { - property(callback); - - } else if (Object.isPromiseLike(property)) { - void property.then(callback); - - } else if (Object.isString(property)) { - const - ctx = this.options.vnode.fakeContext; - - ctx.on(property, callback); - } - } - } + destroy(): void { + this.async.clearAll(); } setAriaRole(): CanUndef { @@ -99,7 +64,7 @@ export default class AriaSetter extends AriaRoleEngine { const {vnode, binding, el} = this.options, {dom} = Object.cast(vnode.fakeContext), - value = Object.isCustomObject(binding.value) ? binding.value : {}; + params = Object.isCustomObject(binding.value) ? binding.value : {}; for (const mod in binding.modifiers) { if (!mod.startsWith('#')) { @@ -113,18 +78,48 @@ export default class AriaSetter extends AriaRoleEngine { el.setAttribute('aria-labelledby', id); } - if (value.label != null) { - el.setAttribute('aria-label', value.label); + if (params.label != null) { + el.setAttribute('aria-label', params.label); - } else if (value.labelledby != null) { - el.setAttribute('aria-labelledby', value.labelledby); + } else if (params.labelledby != null) { + el.setAttribute('aria-labelledby', params.labelledby); } - if (value.description != null) { - el.setAttribute('aria-description', value.description); + if (params.description != null) { + el.setAttribute('aria-description', params.description); + + } else if (params.describedby != null) { + el.setAttribute('aria-describedby', params.describedby); + } + } - } else if (value.describedby != null) { - el.setAttribute('aria-describedby', value.describedby); + addEventHandlers(): void { + if (this.role == null) { + return; + } + + const + params = this.options.binding.value; + + for (const key in params) { + if (key in eventsNames) { + const + callback = this.role[eventsNames[key]], + property = params[key]; + + if (Object.isFunction(property)) { + property(callback); + + } else if (Object.isPromiseLike(property)) { + void property.then(callback); + + } else if (Object.isString(property)) { + const + ctx = this.options.vnode.fakeContext; + + ctx.on(property, callback); + } + } } } } diff --git a/src/core/component/directives/aria/index.ts b/src/core/component/directives/aria/index.ts index 255fa3b2f8..9ba81852e6 100644 --- a/src/core/component/directives/aria/index.ts +++ b/src/core/component/directives/aria/index.ts @@ -11,15 +11,11 @@ * @packageDocumentation */ -import symbolGenerator from 'core/symbol'; import { ComponentEngine, VNode, VNodeDirective } from 'core/component/engines'; import AriaSetter from 'core/component/directives/aria/aria-setter'; const - ariaMap = new Map(); - -const - $$ = symbolGenerator(); + ariaMap = new WeakMap(); ComponentEngine.directive('aria', { inserted(el: HTMLElement, binding: VNodeDirective, vnode: VNode): void { @@ -33,26 +29,20 @@ ComponentEngine.directive('aria', { const aria = new AriaSetter({el, binding, vnode}); - aria.init(); - - ariaMap.set($$.aria, aria); + ariaMap.set(el, aria); }, - update(el: HTMLElement, binding: VNodeDirective, vnode: VNode) { + update(el: HTMLElement) { const - aria: AriaSetter = ariaMap.get($$.aria); - - aria.options = {el, binding, vnode}; + aria: AriaSetter = ariaMap.get(el); aria.update(); }, - unbind(el: HTMLElement, binding: VNodeDirective, vnode: VNode) { + unbind(el: HTMLElement) { const - aria: AriaSetter = ariaMap.get($$.aria); - - aria.options = {el, binding, vnode}; + aria: AriaSetter = ariaMap.get(el); - aria.clear(); + aria.destroy(); } }); diff --git a/src/core/component/directives/aria/interface.ts b/src/core/component/directives/aria/interface.ts index 3ced68a655..8acdae7f86 100644 --- a/src/core/component/directives/aria/interface.ts +++ b/src/core/component/directives/aria/interface.ts @@ -17,18 +17,16 @@ export interface DirectiveOptions { export default abstract class AriaRoleEngine { options: DirectiveOptions; - $a: CanUndef; + async: CanUndef; protected constructor(options: DirectiveOptions) { this.options = options; } abstract init(): void; - update?(): void; - clear?(): void; } -export enum keyCodes { +export const enum keyCodes { ENTER = 'Enter', END = 'End', HOME = 'Home', @@ -38,7 +36,7 @@ export enum keyCodes { DOWN = 'ArrowDown' } -export enum eventsNames { +export const enum eventsNames { openEvent = 'onOpen', closeEvent = 'onClose', changeEvent = 'onChange' diff --git a/src/core/component/directives/aria/roles-engines/combobox.ts b/src/core/component/directives/aria/roles-engines/combobox.ts index c63785b16f..8ecf46a460 100644 --- a/src/core/component/directives/aria/roles-engines/combobox.ts +++ b/src/core/component/directives/aria/roles-engines/combobox.ts @@ -7,28 +7,29 @@ */ import AriaRoleEngine, { DirectiveOptions } from 'core/component/directives/aria/interface'; -import { FOCUSABLE_SELECTOR } from 'traits/i-access/const'; -import type { ComboboxBindingValue } from 'core/component/directives/aria/roles-engines/interface'; +import type { ComboboxParams } from 'core/component/directives/aria/roles-engines/interface'; +import type iAccess from 'traits/i-access/i-access'; export default class ComboboxEngine extends AriaRoleEngine { el: Element; - $v: ComboboxBindingValue; + params: ComboboxParams; constructor(options: DirectiveOptions) { super(options); const - {el} = this.options; + {el} = this.options, + ctx = Object.cast(this.options.vnode.fakeContext); - this.el = el.querySelector(FOCUSABLE_SELECTOR) ?? el; - this.$v = this.options.binding.value; + this.el = ctx.findFocusableElement() ?? el; + this.params = this.options.binding.value; } init(): void { this.el.setAttribute('role', 'combobox'); this.el.setAttribute('aria-expanded', 'false'); - if (this.$v.isMultiple) { + if (this.params.isMultiple) { this.el.setAttribute('aria-multiselectable', 'true'); } } diff --git a/src/core/component/directives/aria/roles-engines/interface.ts b/src/core/component/directives/aria/roles-engines/interface.ts index f135f22028..61922d6fa1 100644 --- a/src/core/component/directives/aria/roles-engines/interface.ts +++ b/src/core/component/directives/aria/roles-engines/interface.ts @@ -1,22 +1,22 @@ -export interface TabBindingValue { +export interface TabParams { isFirst: boolean; isVertical: boolean; isActive: boolean; changeEvent(cb: Function): void; } -export interface TablistBindingValue { +export interface TablistParams { isVertical: boolean; isMultiple: boolean; } -export interface TreeBindingValue { +export interface TreeParams { isRoot: boolean; isVertical: boolean; changeEvent(cb: Function): void; } -export interface TreeitemBindingValue { +export interface TreeitemParams { isRootFirstItem: boolean; isExpandable: boolean; isExpanded: boolean; @@ -24,7 +24,7 @@ export interface TreeitemBindingValue { toggleFold(el: Element, value?: boolean): void; } -export interface ComboboxBindingValue { +export interface ComboboxParams { isMultiple: boolean; changeEvent(cb: Function): void; openEvent(cb: Function): void; diff --git a/src/core/component/directives/aria/roles-engines/tab.ts b/src/core/component/directives/aria/roles-engines/tab.ts index 27cfb859dc..1d7260f6c1 100644 --- a/src/core/component/directives/aria/roles-engines/tab.ts +++ b/src/core/component/directives/aria/roles-engines/tab.ts @@ -10,29 +10,28 @@ */ import AriaRoleEngine, { DirectiveOptions, keyCodes } from 'core/component/directives/aria/interface'; -import type { TabBindingValue } from 'core/component/directives/aria/roles-engines/interface'; +import type { TabParams } from 'core/component/directives/aria/roles-engines/interface'; import type iAccess from 'traits/i-access/i-access'; import type iBlock from 'super/i-block/i-block'; -import { FOCUSABLE_SELECTOR } from 'traits/i-access/const'; export default class TabEngine extends AriaRoleEngine { - $v: TabBindingValue; + params: TabParams; ctx: iAccess & iBlock; constructor(options: DirectiveOptions) { super(options); - this.$v = this.options.binding.value; + this.params = this.options.binding.value; this.ctx = Object.cast(this.options.vnode.fakeContext); } init(): void { const {el} = this.options, - {isFirst} = this.$v; + {isFirst} = this.params; el.setAttribute('role', 'tab'); - el.setAttribute('aria-selected', String(this.$v.isActive)); + el.setAttribute('aria-selected', String(this.params.isActive)); if (isFirst) { if (el.tabIndex < 0) { @@ -43,8 +42,8 @@ export default class TabEngine extends AriaRoleEngine { el.setAttribute('tabindex', '-1'); } - if (this.$a != null) { - this.$a.on(el, 'keydown', this.onKeydown); + if (this.async != null) { + this.async.on(el, 'keydown', this.onKeydown); } } @@ -52,43 +51,46 @@ export default class TabEngine extends AriaRoleEngine { const {el} = this.options; + function setAttributes(isSelected: boolean) { + el.setAttribute('aria-selected', String(isSelected)); + el.setAttribute('tabindex', isSelected ? '0' : '-1'); + } + if (Object.isArrayLike(active)) { for (let i = 0; i < active.length; i++) { - el.setAttribute('aria-selected', String(el === active[i])); + setAttributes(el === active[i]); } return; } - el.setAttribute('aria-selected', String(el === active)); + setAttributes(el === active); }; moveFocusToFirstTab(): void { const - firstEl = >this.ctx.$el?.querySelector(FOCUSABLE_SELECTOR); + firstTab = >this.ctx.findFocusableElement(); - firstEl?.focus(); + firstTab?.focus(); } moveFocusToLastTab(): void { const - focusable = >>this.ctx.$el?.querySelectorAll(FOCUSABLE_SELECTOR); + tabs = >this.ctx.findAllFocusableElements(); - if (focusable != null && focusable.length > 0) { - focusable[focusable.length - 1].focus(); - } - } + let + lastTab: CanUndef; - focusNext(): void { - const - focusable = >this.ctx.getNextFocusableElement(1); + for (const tab of tabs) { + lastTab = tab; + } - focusable?.focus(); + lastTab?.focus(); } - focusPrev(): void { + moveFocus(step: 1 | -1): void { const - focusable = >this.ctx.getNextFocusableElement(-1); + focusable = >this.ctx.getNextFocusableElement(step); focusable?.focus(); } @@ -96,28 +98,28 @@ export default class TabEngine extends AriaRoleEngine { onKeydown = (event: Event): void => { const evt = (event), - {isVertical} = this.$v; + {isVertical} = this.params; switch (evt.key) { case keyCodes.LEFT: - this.focusPrev(); + this.moveFocus(-1); break; case keyCodes.UP: if (isVertical) { - this.focusPrev(); + this.moveFocus(-1); break; } return; case keyCodes.RIGHT: - this.focusNext(); + this.moveFocus(1); break; case keyCodes.DOWN: if (isVertical) { - this.focusNext(); + this.moveFocus(1); break; } diff --git a/src/core/component/directives/aria/roles-engines/tablist.ts b/src/core/component/directives/aria/roles-engines/tablist.ts index 2105747cb3..00cc31bcaa 100644 --- a/src/core/component/directives/aria/roles-engines/tablist.ts +++ b/src/core/component/directives/aria/roles-engines/tablist.ts @@ -7,21 +7,21 @@ */ import AriaRoleEngine from 'core/component/directives/aria/interface'; -import type { TablistBindingValue } from 'core/component/directives/aria/roles-engines/interface'; +import type { TablistParams } from 'core/component/directives/aria/roles-engines/interface'; export default class TablistEngine extends AriaRoleEngine { init(): void { const {el, binding} = this.options, - $v: TablistBindingValue = binding.value; + params: TablistParams = binding.value; el.setAttribute('role', 'tablist'); - if ($v.isMultiple) { + if (params.isMultiple) { el.setAttribute('aria-multiselectable', 'true'); } - if ($v.isVertical) { + if (params.isVertical) { el.setAttribute('aria-orientation', 'vertical'); } } diff --git a/src/core/component/directives/aria/roles-engines/tree.ts b/src/core/component/directives/aria/roles-engines/tree.ts index 09affe7603..4f7630d2fc 100644 --- a/src/core/component/directives/aria/roles-engines/tree.ts +++ b/src/core/component/directives/aria/roles-engines/tree.ts @@ -7,29 +7,29 @@ */ import AriaRoleEngine, { DirectiveOptions } from 'core/component/directives/aria/interface'; -import type { TreeBindingValue } from 'core/component/directives/aria/roles-engines/interface'; +import type { TreeParams } from 'core/component/directives/aria/roles-engines/interface'; export default class TreeEngine extends AriaRoleEngine { - $v: TreeBindingValue; + params: TreeParams; el: HTMLElement; constructor(options: DirectiveOptions) { super(options); - this.$v = options.binding.value; + this.params = options.binding.value; this.el = this.options.el; } init(): void { this.setRootRole(); - if (this.$v.isVertical) { + if (this.params.isVertical) { this.el.setAttribute('aria-orientation', 'vertical'); } } setRootRole(): void { - this.el.setAttribute('role', this.$v.isRoot ? 'tree' : 'group'); + this.el.setAttribute('role', this.params.isRoot ? 'tree' : 'group'); } onChange = (el: HTMLElement, isFolded: boolean): void => { diff --git a/src/core/component/directives/aria/roles-engines/treeitem.ts b/src/core/component/directives/aria/roles-engines/treeitem.ts index 3c6b5ddd59..529da86244 100644 --- a/src/core/component/directives/aria/roles-engines/treeitem.ts +++ b/src/core/component/directives/aria/roles-engines/treeitem.ts @@ -9,20 +9,16 @@ * Copyright © [2022] W3C® (MIT, ERCIM, Keio, Beihang). */ -import symbolGenerator from 'core/symbol'; import AriaRoleEngine, { DirectiveOptions, keyCodes } from 'core/component/directives/aria/interface'; import iAccess from 'traits/i-access/i-access'; -import { FOCUSABLE_SELECTOR } from 'traits/i-access/const'; -import type { TreeitemBindingValue } from 'core/component/directives/aria/roles-engines/interface'; -import type iBlock from 'super/i-block/i-block'; -export const - $$ = symbolGenerator(); +import type { TreeitemParams } from 'core/component/directives/aria/roles-engines/interface'; +import type iBlock from 'super/i-block/i-block'; export default class TreeItemEngine extends AriaRoleEngine { ctx: iAccess & iBlock['unsafe']; el: HTMLElement; - $v: TreeitemBindingValue; + params: TreeitemParams; constructor(options: DirectiveOptions) { super(options); @@ -33,16 +29,16 @@ export default class TreeItemEngine extends AriaRoleEngine { this.ctx = Object.cast(options.vnode.fakeContext); this.el = this.options.el; - this.$v = this.options.binding.value; + this.params = this.options.binding.value; } init(): void { - this.$a?.on(this.el, 'keydown', this.onKeyDown); + this.async?.on(this.el, 'keydown', this.onKeyDown); const isMuted = this.ctx.removeAllFromTabSequence(this.el); - if (this.$v.isRootFirstItem) { + if (this.params.isRootFirstItem) { if (isMuted) { this.ctx.restoreAllToTabSequence(this.el); @@ -54,8 +50,8 @@ export default class TreeItemEngine extends AriaRoleEngine { this.el.setAttribute('role', 'treeitem'); this.ctx.$nextTick(() => { - if (this.$v.isExpandable) { - this.el.setAttribute('aria-expanded', String(this.$v.isExpanded)); + if (this.params.isExpandable) { + this.el.setAttribute('aria-expanded', String(this.params.isExpanded)); } }); } @@ -75,12 +71,12 @@ export default class TreeItemEngine extends AriaRoleEngine { break; case keyCodes.ENTER: - this.$v.toggleFold(this.el); + this.params.toggleFold(this.el); break; case keyCodes.RIGHT: - if (this.$v.isExpandable) { - if (this.$v.isExpanded) { + if (this.params.isExpandable) { + if (this.params.isExpanded) { this.moveFocus(1); } else { @@ -91,7 +87,7 @@ export default class TreeItemEngine extends AriaRoleEngine { break; case keyCodes.LEFT: - if (this.$v.isExpandable && this.$v.isExpanded) { + if (this.params.isExpandable && this.params.isExpanded) { this.closeFold(); } else { @@ -119,6 +115,7 @@ export default class TreeItemEngine extends AriaRoleEngine { focusNext(nextEl: HTMLElement): void { this.ctx.removeAllFromTabSequence(this.el); this.ctx.restoreAllToTabSequence(nextEl); + nextEl.focus(); } @@ -132,11 +129,11 @@ export default class TreeItemEngine extends AriaRoleEngine { } openFold(): void { - this.$v.toggleFold(this.el, false); + this.params.toggleFold(this.el, false); } closeFold(): void { - this.$v.toggleFold(this.el, true); + this.params.toggleFold(this.el, true); } focusParent(): void { @@ -151,8 +148,12 @@ export default class TreeItemEngine extends AriaRoleEngine { parent = parent.parentElement; } + if (parent == null) { + return; + } + const - focusableParent = (>parent?.querySelector(FOCUSABLE_SELECTOR)); + focusableParent = >this.ctx.findFocusableElement(parent); if (focusableParent != null) { this.focusNext(focusableParent); @@ -161,30 +162,28 @@ export default class TreeItemEngine extends AriaRoleEngine { setFocusToFirstItem(): void { const - firstEl = >this.$v.rootElement?.querySelector(FOCUSABLE_SELECTOR); + firstItem = >this.ctx.findFocusableElement(this.params.rootElement); - if (firstEl != null) { - this.focusNext(firstEl); + if (firstItem != null) { + this.focusNext(firstItem); } } setFocusToLastItem(): void { const - items = >>this.$v.rootElement?.querySelectorAll(FOCUSABLE_SELECTOR); + items = >this.ctx.findAllFocusableElements(this.params.rootElement); - const visibleItems: HTMLElement[] = [].filter.call( - items, - (el: HTMLElement) => ( - el.offsetWidth > 0 || - el.offsetHeight > 0 - ) - ); + let + lastItem: CanUndef; - const - lastEl = visibleItems.at(-1); + for (const item of items) { + if (item.offsetWidth > 0 || item.offsetHeight > 0) { + lastItem = item; + } + } - if (lastEl != null) { - this.focusNext(lastEl); + if (lastItem != null) { + this.focusNext(lastItem); } } } diff --git a/src/pages/p-v4-components-demo/p-v4-components-demo.ss b/src/pages/p-v4-components-demo/p-v4-components-demo.ss index 40cb2eb066..176043a8e0 100644 --- a/src/pages/p-v4-components-demo/p-v4-components-demo.ss +++ b/src/pages/p-v4-components-demo/p-v4-components-demo.ss @@ -14,8 +14,6 @@ - block body : config = require('@config/config').build - < b-checkbox label = 'bla' | :id = 55 - - forEach config.components => @component - if config.inspectComponents < b-v4-component-demo diff --git a/src/traits/i-access/i-access.ts b/src/traits/i-access/i-access.ts index 88c983254b..2baf4546b6 100644 --- a/src/traits/i-access/i-access.ts +++ b/src/traits/i-access/i-access.ts @@ -180,9 +180,9 @@ export default abstract class iAccess { areElementsRemoved = false; const - focusableIter = this.findAllFocusableElements(component, ctx); + focusableElems = >this.findAllFocusableElements(component, ctx); - for (const focusableEl of >focusableIter) { + for (const focusableEl of focusableElems) { if (!focusableEl.hasAttribute('data-tabindex')) { focusableEl.setAttribute('data-tabindex', String(focusableEl.tabIndex)); } @@ -208,13 +208,13 @@ export default abstract class iAccess { areElementsRestored = false; let - removedElemsIter = intoIter(ctx.querySelectorAll('[data-tabindex]')); + removedElems = intoIter(ctx.querySelectorAll('[data-tabindex]')); if (el?.hasAttribute('data-tabindex')) { - removedElemsIter = sequence(removedElemsIter, intoIter([el])); + removedElems = sequence(removedElems, intoIter([el])); } - for (const elem of >removedElemsIter) { + for (const elem of >removedElems) { const originalTabIndex = elem.getAttribute('data-tabindex'); @@ -238,16 +238,16 @@ export default abstract class iAccess { const ctx = el ?? document.documentElement, - focusableIter = this.findAllFocusableElements(component, ctx), + focusableElems = >this.findAllFocusableElements(component, ctx), visibleFocusable: HTMLElement[] = []; - for (const element of >focusableIter) { + for (const focusableEl of focusableElems) { if ( - element.offsetWidth > 0 || - element.offsetHeight > 0 || - element === document.activeElement + focusableEl.offsetWidth > 0 || + focusableEl.offsetHeight > 0 || + focusableEl === document.activeElement ) { - visibleFocusable.push(element); + visibleFocusable.push(focusableEl); } } @@ -264,11 +264,11 @@ export default abstract class iAccess { (component, el?): CanUndef => { const ctx = el ?? component.$el, - focusableIter = this.findAllFocusableElements(component, ctx); + focusableElems = this.findAllFocusableElements(component, ctx); - for (const element of focusableIter) { - if (!element.hasAttribute('disabled')) { - return element; + for (const focusableEl of focusableElems) { + if (!focusableEl.hasAttribute('disabled')) { + return focusableEl; } } }; From 1eb959d55c606712e793a0db1f6b8447086961cb Mon Sep 17 00:00:00 2001 From: Aleksandr Bunin Date: Wed, 27 Jul 2022 13:53:11 +0300 Subject: [PATCH 011/185] add v-id directive and implement in code --- src/base/b-window/CHANGELOG.md | 7 +++++ src/base/b-window/b-window.ss | 2 +- src/core/component/directives/id/CHANGELOG.md | 16 +++++++++++ src/core/component/directives/id/README.md | 17 ++++++++++++ src/core/component/directives/id/index.ts | 27 +++++++++++++++++++ src/core/component/directives/index.ts | 2 ++ .../b-v4-component-demo.ss | 2 +- 7 files changed, 71 insertions(+), 2 deletions(-) create mode 100644 src/core/component/directives/id/CHANGELOG.md create mode 100644 src/core/component/directives/id/README.md create mode 100644 src/core/component/directives/id/index.ts diff --git a/src/base/b-window/CHANGELOG.md b/src/base/b-window/CHANGELOG.md index 6fa59df32b..4fe7a2cff7 100644 --- a/src/base/b-window/CHANGELOG.md +++ b/src/base/b-window/CHANGELOG.md @@ -9,6 +9,13 @@ Changelog > - :house: [Internal] > - :nail_care: [Polish] +## v3.?.? (2022-0?-??) + +#### :rocket: New Feature + +* Added a new directive `v-aria` +* Added a new directive `v-id` + ## v3.0.0-rc.211 (2021-07-21) #### :boom: Breaking Change diff --git a/src/base/b-window/b-window.ss b/src/base/b-window/b-window.ss index 616d92ba40..38ecae2b96 100644 --- a/src/base/b-window/b-window.ss +++ b/src/base/b-window/b-window.ss @@ -41,7 +41,7 @@ += self.slot() < h1.&__title & v-if = title || vdom.getSlot('title') | - :id = dom.getId('title') + v-id = 'title' . += self.slot('title', {':title': 'title'}) - block title diff --git a/src/core/component/directives/id/CHANGELOG.md b/src/core/component/directives/id/CHANGELOG.md new file mode 100644 index 0000000000..ba2980fb55 --- /dev/null +++ b/src/core/component/directives/id/CHANGELOG.md @@ -0,0 +1,16 @@ +Changelog +========= + +> **Tags:** +> - :boom: [Breaking Change] +> - :rocket: [New Feature] +> - :bug: [Bug Fix] +> - :memo: [Documentation] +> - :house: [Internal] +> - :nail_care: [Polish] + +## v?.??.? (2022-0?-??) + +#### :rocket: New Feature + +* Initial release diff --git a/src/core/component/directives/id/README.md b/src/core/component/directives/id/README.md new file mode 100644 index 0000000000..4a8008f9bd --- /dev/null +++ b/src/core/component/directives/id/README.md @@ -0,0 +1,17 @@ +# core/component/directives/aria + +This module provides a directive for easy adding of id attribute. + +## Usage + +``` +< &__foo v-id = 'title' + +``` + +The same as +``` +< &__foo :id = dom.getId('title') + +``` + diff --git a/src/core/component/directives/id/index.ts b/src/core/component/directives/id/index.ts new file mode 100644 index 0000000000..d8636c4965 --- /dev/null +++ b/src/core/component/directives/id/index.ts @@ -0,0 +1,27 @@ +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ + +/** + * [[include:core/component/directives/id/README.md]] + * @packageDocumentation + */ + +import { ComponentEngine, VNode, VNodeDirective } from 'core/component/engines'; +import type iBlock from 'super/i-block/i-block'; + +ComponentEngine.directive('id', { + inserted(el: HTMLElement, binding: VNodeDirective, vnode: VNode): void { + const + ctx = Object.cast(vnode.fakeContext); + + const + id = ctx.dom.getId(binding.value); + + el.setAttribute('id', id); + } +}); diff --git a/src/core/component/directives/index.ts b/src/core/component/directives/index.ts index 6874942b5c..e90a8b1f94 100644 --- a/src/core/component/directives/index.ts +++ b/src/core/component/directives/index.ts @@ -25,3 +25,5 @@ import 'core/component/directives/update-on'; import 'core/component/directives/aria'; import 'core/component/directives/hook'; + +import 'core/component/directives/id'; diff --git a/src/pages/p-v4-components-demo/b-v4-component-demo/b-v4-component-demo.ss b/src/pages/p-v4-components-demo/b-v4-component-demo/b-v4-component-demo.ss index 8808a6aa9f..456c3784e8 100644 --- a/src/pages/p-v4-components-demo/b-v4-component-demo/b-v4-component-demo.ss +++ b/src/pages/p-v4-components-demo/b-v4-component-demo/b-v4-component-demo.ss @@ -47,7 +47,7 @@ < template v-else < input & :type = 'checkbox' | - :id = dom.getId(key) | + v-id = key | :checked = debugComponent.mods[key] === getModValue(mod[0]) | :class = provide.elClasses({ highlighted: field.get(['highlighting', key, mod[0]].join('.')) || false From 16764b5d3ad79ab7364339d453b6cc19f0523ae8 Mon Sep 17 00:00:00 2001 From: Aleksandr Bunin Date: Wed, 27 Jul 2022 13:55:30 +0300 Subject: [PATCH 012/185] fix changlelogs --- src/base/b-list/CHANGELOG.md | 6 +-- src/base/b-tree/CHANGELOG.md | 6 +-- .../component/directives/aria/aria-setter.ts | 6 +-- .../component/directives/aria/interface.ts | 2 +- src/form/b-button/CHANGELOG.md | 6 +++ src/form/b-button/b-button.ss | 2 +- src/form/b-select/CHANGELOG.md | 6 +-- src/form/b-select/b-select.ss | 6 +-- src/form/b-select/b-select.ts | 41 ++++++++++--------- src/super/i-input/CHANGELOG.md | 2 +- 10 files changed, 46 insertions(+), 37 deletions(-) diff --git a/src/base/b-list/CHANGELOG.md b/src/base/b-list/CHANGELOG.md index 712f6a9004..bd365587c4 100644 --- a/src/base/b-list/CHANGELOG.md +++ b/src/base/b-list/CHANGELOG.md @@ -14,10 +14,10 @@ Changelog #### :rocket: New Feature * Added a new directive `v-aria` -* Added a new prop `vertical` +* Added a new prop `orientation` * Added `isTablist` -* Added `onActiveChange` -* Now the component derive `iAccess` +* Added `getAriaConfig` +* Now the component derives `iAccess` ## v3.0.0-rc.211 (2021-07-21) diff --git a/src/base/b-tree/CHANGELOG.md b/src/base/b-tree/CHANGELOG.md index afe156f069..9aeec86d84 100644 --- a/src/base/b-tree/CHANGELOG.md +++ b/src/base/b-tree/CHANGELOG.md @@ -14,9 +14,9 @@ Changelog #### :rocket: New Feature * Added a new directive `v-aria` -* Added a new prop `vertical` -* Added `changeFoldedMod` -* Now the component derive `iAccess` +* Added a new prop `orientation` +* Added `getAriaConfig` +* Now the component derives `iAccess` ## v3.0.0-rc.164 (2021-03-22) diff --git a/src/core/component/directives/aria/aria-setter.ts b/src/core/component/directives/aria/aria-setter.ts index f395d86804..27a72f804f 100644 --- a/src/core/component/directives/aria/aria-setter.ts +++ b/src/core/component/directives/aria/aria-setter.ts @@ -8,7 +8,7 @@ import * as ariaRoles from 'core/component/directives/aria/roles-engines'; import Async from 'core/async'; -import AriaRoleEngine, { eventsNames } from 'core/component/directives/aria/interface'; +import AriaRoleEngine, { eventNames } from 'core/component/directives/aria/interface'; import type iBlock from 'super/i-block/i-block'; import type { DirectiveOptions } from 'core/component/directives/aria/interface'; @@ -102,9 +102,9 @@ export default class AriaSetter extends AriaRoleEngine { params = this.options.binding.value; for (const key in params) { - if (key in eventsNames) { + if (key in eventNames) { const - callback = this.role[eventsNames[key]], + callback = this.role[eventNames[key]], property = params[key]; if (Object.isFunction(property)) { diff --git a/src/core/component/directives/aria/interface.ts b/src/core/component/directives/aria/interface.ts index 8acdae7f86..9a788e5dc1 100644 --- a/src/core/component/directives/aria/interface.ts +++ b/src/core/component/directives/aria/interface.ts @@ -36,7 +36,7 @@ export const enum keyCodes { DOWN = 'ArrowDown' } -export const enum eventsNames { +export enum eventNames { openEvent = 'onOpen', closeEvent = 'onClose', changeEvent = 'onChange' diff --git a/src/form/b-button/CHANGELOG.md b/src/form/b-button/CHANGELOG.md index 567364f2d1..95f35cb6b7 100644 --- a/src/form/b-button/CHANGELOG.md +++ b/src/form/b-button/CHANGELOG.md @@ -9,6 +9,12 @@ Changelog > - :house: [Internal] > - :nail_care: [Polish] +## v3.?.? (2022-0?-??) + +#### :rocket: New Feature + +* Added a new directive `v-id` + ## v3.0.0-rc.211 (2021-07-21) #### :rocket: New Feature diff --git a/src/form/b-button/b-button.ss b/src/form/b-button/b-button.ss index 27480985c3..235ae8da1e 100644 --- a/src/form/b-button/b-button.ss +++ b/src/form/b-button/b-button.ss @@ -89,7 +89,7 @@ < . & ref dropdown | v-if = hasDropdown | - :id = dom.getId('dropdown') | + v-id = 'dropdown' | :class = provide.elClasses({dropdown: {pos: dropdown}}) . < .&__dropdown-content diff --git a/src/form/b-select/CHANGELOG.md b/src/form/b-select/CHANGELOG.md index 4a18185ae7..334f6805f1 100644 --- a/src/form/b-select/CHANGELOG.md +++ b/src/form/b-select/CHANGELOG.md @@ -14,9 +14,9 @@ Changelog #### :rocket: New Feature * Added a new directive `v-aria` -* Added `onItemMarked` -* Added `onOpen` -* Now the component derive `iAccess` +* Added a new directive `v-id` +* Added `getAriaConfig` +* Now the component derives `iAccess` #### :bug: Bug Fix diff --git a/src/form/b-select/b-select.ss b/src/form/b-select/b-select.ss index b98776de2f..db1a29e258 100644 --- a/src/form/b-select/b-select.ss +++ b/src/form/b-select/b-select.ss @@ -42,11 +42,11 @@ :v-attrs = native ? el.attrs : { - 'v-aria:option': getAriaOpt('option', el), + 'v-aria:option': getAriaConfig('option', el), ...el.attrs } | - :id = dom.getId(el.value) | + v-id = el.value | ${itemAttrs} . += self.slot('default', {':item': 'el'}) @@ -96,7 +96,7 @@ += self.items('option') < template v-else - < _.&__cell.&__input-wrapper v-aria:combobox = getAriaOpt('combobox') + < _.&__cell.&__input-wrapper v-aria:combobox = getAriaConfig('combobox') += self.nativeInput({model: 'textStore', attrs: {'@input': 'onSearchInput'}}) - block icon diff --git a/src/form/b-select/b-select.ts b/src/form/b-select/b-select.ts index 7eab4472f3..d48147cf25 100644 --- a/src/form/b-select/b-select.ts +++ b/src/form/b-select/b-select.ts @@ -936,7 +936,7 @@ class bSelect extends iInputText implements iOpenToggle, iItems { * Returns a dictionary with options for aria directive for combobox role * @param role */ - protected getAriaOpt(role: 'combobox'): Dictionary; + protected getAriaConfig(role: 'combobox'): Dictionary; /** * Returns a dictionary with options for aria directive for option role @@ -944,32 +944,35 @@ class bSelect extends iInputText implements iOpenToggle, iItems { * @param role * @param item */ - protected getAriaOpt(role: 'option', item: this['Item']): Dictionary; + protected getAriaConfig(role: 'option', item: this['Item']): Dictionary; - protected getAriaOpt(role: 'combobox' | 'option', item?: this['Item']): Dictionary { + protected getAriaConfig(role: 'combobox' | 'option', item?: this['Item']): Dictionary { const event = 'el.mod.set.*.marked.*', isSelected = this.isSelected.bind(this, item?.value); const - opts = { - combobox: { - isMultiple: this.multiple, - changeEvent: (cb) => this.localEmitter.on(event, ({link}) => cb(link)), - closeEvent: (cb) => this.on('close', cb), - openEvent: (cb) => this.on('open', () => { - void this.$nextTick(() => cb(this.selectedElement)); - }) - }, - option: { - get preSelected() { - return isSelected(); - }, - changeEvent: (cb) => this.on('actionChange', () => cb(isSelected())) - } + comboboxConfig = { + isMultiple: this.multiple, + changeEvent: (cb) => this.localEmitter.on(event, ({link}) => cb(link)), + closeEvent: (cb) => this.on('close', cb), + openEvent: (cb) => this.on('open', () => { + void this.$nextTick(() => cb(this.selectedElement)); + }) }; - return opts[role]; + const optionConfig = { + get preSelected() { + return isSelected(); + }, + changeEvent: (cb) => this.on('actionChange', () => cb(isSelected())) + }; + + switch (role) { + case 'combobox': return comboboxConfig; + case 'option': return optionConfig; + default: return {}; + } } /** diff --git a/src/super/i-input/CHANGELOG.md b/src/super/i-input/CHANGELOG.md index 8aeb101da1..672b1738a9 100644 --- a/src/super/i-input/CHANGELOG.md +++ b/src/super/i-input/CHANGELOG.md @@ -13,7 +13,7 @@ Changelog #### :rocket: New Feature -* Now the component derive `iAccess` +* Now the component derives `iAccess` ## v3.0.0-rc.199 (2021-06-16) From 5a64e7e9f6fc4568282887418b7f49bd6091f88f Mon Sep 17 00:00:00 2001 From: Aleksandr Bunin Date: Wed, 27 Jul 2022 18:45:18 +0300 Subject: [PATCH 013/185] add methods description --- .../component/directives/aria/aria-setter.ts | 40 +++- .../component/directives/aria/interface.ts | 2 +- .../directives/aria/roles-engines/combobox.ts | 56 ++++-- .../directives/aria/roles-engines/controls.ts | 7 +- .../directives/aria/roles-engines/dialog.ts | 18 +- .../directives/aria/roles-engines/listbox.ts | 3 + .../directives/aria/roles-engines/option.ts | 11 +- .../directives/aria/roles-engines/tab.ts | 85 ++++++--- .../directives/aria/roles-engines/tablist.ts | 3 + .../directives/aria/roles-engines/tabpanel.ts | 3 + .../directives/aria/roles-engines/tree.ts | 32 +++- .../directives/aria/roles-engines/treeitem.ts | 174 +++++++++++------- 12 files changed, 297 insertions(+), 137 deletions(-) diff --git a/src/core/component/directives/aria/aria-setter.ts b/src/core/component/directives/aria/aria-setter.ts index 27a72f804f..5b402b2870 100644 --- a/src/core/component/directives/aria/aria-setter.ts +++ b/src/core/component/directives/aria/aria-setter.ts @@ -12,8 +12,18 @@ import AriaRoleEngine, { eventNames } from 'core/component/directives/aria/inter import type iBlock from 'super/i-block/i-block'; import type { DirectiveOptions } from 'core/component/directives/aria/interface'; +/** + * Class-helper for making base operations for the directive + */ export default class AriaSetter extends AriaRoleEngine { - override async: Async; + /** + * Async instance for aria directive + */ + override readonly async: Async; + + /** + * Role engine instance + */ role: CanUndef; constructor(options: DirectiveOptions) { @@ -29,6 +39,9 @@ export default class AriaSetter extends AriaRoleEngine { this.init(); } + /** + * Initiates the base logic of the directive + */ init(): void { this.setAriaLabel(); this.addEventHandlers(); @@ -36,6 +49,9 @@ export default class AriaSetter extends AriaRoleEngine { this.role?.init(); } + /** + * Runs on update directive hook. Removes listeners from component if the component is Functional + */ update(): void { const ctx = this.options.vnode.fakeContext; @@ -45,11 +61,17 @@ export default class AriaSetter extends AriaRoleEngine { } } + /** + * Runs on unbind directive hook. Clears the Async instance + */ destroy(): void { this.async.clearAll(); } - setAriaRole(): CanUndef { + /** + * If the role was passed as a directive argument sets specified engine + */ + protected setAriaRole(): CanUndef { const {arg: role} = this.options.binding; @@ -60,7 +82,11 @@ export default class AriaSetter extends AriaRoleEngine { this.role = new ariaRoles[role](this.options); } - setAriaLabel(): void { + /** + * Sets aria-label, aria-labelledby, aria-description and aria-describedby attributes to the element + * from passed parameters + */ + protected setAriaLabel(): void { const {vnode, binding, el} = this.options, {dom} = Object.cast(vnode.fakeContext), @@ -93,7 +119,11 @@ export default class AriaSetter extends AriaRoleEngine { } } - addEventHandlers(): void { + /** + * Sets handlers for the base role events: open, close, change. + * Expects the passed into directive specified event properties to be Function, Promise or String + */ + protected addEventHandlers(): void { if (this.role == null) { return; } @@ -104,7 +134,7 @@ export default class AriaSetter extends AriaRoleEngine { for (const key in params) { if (key in eventNames) { const - callback = this.role[eventNames[key]], + callback = this.role[eventNames[key]].bind(this.role), property = params[key]; if (Object.isFunction(property)) { diff --git a/src/core/component/directives/aria/interface.ts b/src/core/component/directives/aria/interface.ts index 9a788e5dc1..3c18527bfc 100644 --- a/src/core/component/directives/aria/interface.ts +++ b/src/core/component/directives/aria/interface.ts @@ -16,7 +16,7 @@ export interface DirectiveOptions { } export default abstract class AriaRoleEngine { - options: DirectiveOptions; + readonly options: DirectiveOptions; async: CanUndef; protected constructor(options: DirectiveOptions) { diff --git a/src/core/component/directives/aria/roles-engines/combobox.ts b/src/core/component/directives/aria/roles-engines/combobox.ts index 8ecf46a460..70921a3858 100644 --- a/src/core/component/directives/aria/roles-engines/combobox.ts +++ b/src/core/component/directives/aria/roles-engines/combobox.ts @@ -11,9 +11,16 @@ import type { ComboboxParams } from 'core/component/directives/aria/roles-engine import type iAccess from 'traits/i-access/i-access'; export default class ComboboxEngine extends AriaRoleEngine { - el: Element; + /** + * Passed directive params + */ params: ComboboxParams; + /** + * First focusable element inside the element with directive or this element if there is no focusable inside + */ + el: HTMLElement; + constructor(options: DirectiveOptions) { super(options); @@ -21,10 +28,13 @@ export default class ComboboxEngine extends AriaRoleEngine { {el} = this.options, ctx = Object.cast(this.options.vnode.fakeContext); - this.el = ctx.findFocusableElement() ?? el; + this.el = (>ctx.findFocusableElement()) ?? el; this.params = this.options.binding.value; } + /** + * Sets base aria attributes for current role + */ init(): void { this.el.setAttribute('role', 'combobox'); this.el.setAttribute('aria-expanded', 'false'); @@ -32,23 +42,41 @@ export default class ComboboxEngine extends AriaRoleEngine { if (this.params.isMultiple) { this.el.setAttribute('aria-multiselectable', 'true'); } + + if (this.el.tabIndex < 0) { + this.el.setAttribute('tabindex', '0'); + } + } + + /** + * Sets or deletes the id of active descendant element + */ + protected setAriaActive(el?: HTMLElement): void { + this.el.setAttribute('aria-activedescendant', el?.id ?? ''); } - onOpen = (element: HTMLElement): void => { + /** + * Handler: the option list is expanded + * @param el + */ + protected onOpen(el: HTMLElement): void { this.el.setAttribute('aria-expanded', 'true'); - this.setAriaActive(element); - }; + this.setAriaActive(el); + } - onClose = (): void => { + /** + * Handler: the option list is closed + */ + protected onClose(): void { this.el.setAttribute('aria-expanded', 'false'); this.setAriaActive(); - }; - - onChange = (element: HTMLElement): void => { - this.setAriaActive(element); - }; + } - setAriaActive = (element?: HTMLElement): void => { - this.el.setAttribute('aria-activedescendant', element?.id ?? ''); - }; + /** + * Handler: active option element was changed + * @param el + */ + protected onChange(el: HTMLElement): void { + this.setAriaActive(el); + } } diff --git a/src/core/component/directives/aria/roles-engines/controls.ts b/src/core/component/directives/aria/roles-engines/controls.ts index 98fc5e68b6..1d7b3d9a25 100644 --- a/src/core/component/directives/aria/roles-engines/controls.ts +++ b/src/core/component/directives/aria/roles-engines/controls.ts @@ -9,6 +9,9 @@ import AriaRoleEngine from 'core/component/directives/aria/interface'; export default class ControlsEngine extends AriaRoleEngine { + /** + * Sets base aria attributes for current role + */ init(): void { const {vnode, binding, el} = this.options, @@ -19,8 +22,8 @@ export default class ControlsEngine extends AriaRoleEngine { return; } - if (binding.value?.controls == null) { - Object.throw('Controls aria directive expects the controls value to be passed'); + if (binding.value?.id == null) { + Object.throw('Controls aria directive expects the id of controlling elements to be passed'); return; } diff --git a/src/core/component/directives/aria/roles-engines/dialog.ts b/src/core/component/directives/aria/roles-engines/dialog.ts index 9a8d91cfc4..4888432ccc 100644 --- a/src/core/component/directives/aria/roles-engines/dialog.ts +++ b/src/core/component/directives/aria/roles-engines/dialog.ts @@ -8,22 +8,20 @@ import iOpen from 'traits/i-open/i-open'; import AriaRoleEngine from 'core/component/directives/aria/interface'; -import type { DirectiveOptions } from 'core/component/directives/aria/interface'; export default class DialogEngine extends AriaRoleEngine { - constructor(options: DirectiveOptions) { - super(options); - - if (!iOpen.is(options.vnode.fakeContext)) { - Object.throw('Dialog aria directive expects the component to realize iOpen interface'); - } - } - + /** + * Sets base aria attributes for current role + */ init(): void { const - {el} = this.options; + {el, vnode} = this.options; el.setAttribute('role', 'dialog'); el.setAttribute('aria-modal', 'true'); + + if (!iOpen.is(vnode.fakeContext)) { + Object.throw('Dialog aria directive expects the component to realize iOpen interface'); + } } } diff --git a/src/core/component/directives/aria/roles-engines/listbox.ts b/src/core/component/directives/aria/roles-engines/listbox.ts index 203b12cde6..9bfd9acec3 100644 --- a/src/core/component/directives/aria/roles-engines/listbox.ts +++ b/src/core/component/directives/aria/roles-engines/listbox.ts @@ -9,6 +9,9 @@ import AriaRoleEngine from 'core/component/directives/aria/interface'; export default class ListboxEngine extends AriaRoleEngine { + /** + * Sets base aria attributes for current role + */ init(): void { const {el} = this.options; diff --git a/src/core/component/directives/aria/roles-engines/option.ts b/src/core/component/directives/aria/roles-engines/option.ts index 109e240835..b0cb7b3bbc 100644 --- a/src/core/component/directives/aria/roles-engines/option.ts +++ b/src/core/component/directives/aria/roles-engines/option.ts @@ -9,6 +9,9 @@ import AriaRoleEngine from 'core/component/directives/aria/interface'; export default class OptionEngine extends AriaRoleEngine { + /** + * Sets base aria attributes for current role + */ init(): void { const {el} = this.options, @@ -22,10 +25,14 @@ export default class OptionEngine extends AriaRoleEngine { } } - onChange = (isSelected: boolean): void => { + /** + * Handler: selected option changes + * @param isSelected + */ + protected onChange(isSelected: boolean): void { const {el} = this.options; el.setAttribute('aria-selected', String(isSelected)); - }; + } } diff --git a/src/core/component/directives/aria/roles-engines/tab.ts b/src/core/component/directives/aria/roles-engines/tab.ts index 1d7260f6c1..f2b656ffc6 100644 --- a/src/core/component/directives/aria/roles-engines/tab.ts +++ b/src/core/component/directives/aria/roles-engines/tab.ts @@ -15,7 +15,14 @@ import type iAccess from 'traits/i-access/i-access'; import type iBlock from 'super/i-block/i-block'; export default class TabEngine extends AriaRoleEngine { + /** + * Passed directive params + */ params: TabParams; + + /** + * Component instance + */ ctx: iAccess & iBlock; constructor(options: DirectiveOptions) { @@ -25,6 +32,9 @@ export default class TabEngine extends AriaRoleEngine { this.ctx = Object.cast(this.options.vnode.fakeContext); } + /** + * Sets base aria attributes for current role + */ init(): void { const {el} = this.options, @@ -43,38 +53,24 @@ export default class TabEngine extends AriaRoleEngine { } if (this.async != null) { - this.async.on(el, 'keydown', this.onKeydown); + this.async.on(el, 'keydown', this.onKeydown.bind(this)); } } - onChange = (active: Element | NodeListOf): void => { - const - {el} = this.options; - - function setAttributes(isSelected: boolean) { - el.setAttribute('aria-selected', String(isSelected)); - el.setAttribute('tabindex', isSelected ? '0' : '-1'); - } - - if (Object.isArrayLike(active)) { - for (let i = 0; i < active.length; i++) { - setAttributes(el === active[i]); - } - - return; - } - - setAttributes(el === active); - }; - - moveFocusToFirstTab(): void { + /** + * Moves focus to the first tab in tablist + */ + protected moveFocusToFirstTab(): void { const firstTab = >this.ctx.findFocusableElement(); firstTab?.focus(); } - moveFocusToLastTab(): void { + /** + * Moves focus to the last tab in tablist + */ + protected moveFocusToLastTab(): void { const tabs = >this.ctx.findAllFocusableElements(); @@ -88,16 +84,47 @@ export default class TabEngine extends AriaRoleEngine { lastTab?.focus(); } - moveFocus(step: 1 | -1): void { + /** + * Moves focus to the next or previous focusable element via the step parameter + * @param step + */ + protected moveFocus(step: 1 | -1): void { const focusable = >this.ctx.getNextFocusableElement(step); focusable?.focus(); } - onKeydown = (event: Event): void => { + /** + * Handler: active tab changes + * @param active + */ + protected onChange(active: Element | NodeListOf): void { const - evt = (event), + {el} = this.options; + + function setAttributes(isSelected: boolean) { + el.setAttribute('aria-selected', String(isSelected)); + el.setAttribute('tabindex', isSelected ? '0' : '-1'); + } + + if (Object.isArrayLike(active)) { + for (let i = 0; i < active.length; i++) { + setAttributes(el === active[i]); + } + + return; + } + + setAttributes(el === active); + } + + /** + * Handler: keyboard event + */ + protected onKeydown(e: Event): void { + const + evt = (e), {isVertical} = this.params; switch (evt.key) { @@ -137,7 +164,7 @@ export default class TabEngine extends AriaRoleEngine { return; } - event.stopPropagation(); - event.preventDefault(); - }; + e.stopPropagation(); + e.preventDefault(); + } } diff --git a/src/core/component/directives/aria/roles-engines/tablist.ts b/src/core/component/directives/aria/roles-engines/tablist.ts index 00cc31bcaa..9140b6246b 100644 --- a/src/core/component/directives/aria/roles-engines/tablist.ts +++ b/src/core/component/directives/aria/roles-engines/tablist.ts @@ -10,6 +10,9 @@ import AriaRoleEngine from 'core/component/directives/aria/interface'; import type { TablistParams } from 'core/component/directives/aria/roles-engines/interface'; export default class TablistEngine extends AriaRoleEngine { + /** + * Sets base aria attributes for current role + */ init(): void { const {el, binding} = this.options, diff --git a/src/core/component/directives/aria/roles-engines/tabpanel.ts b/src/core/component/directives/aria/roles-engines/tabpanel.ts index b128b2b079..9d59cac654 100644 --- a/src/core/component/directives/aria/roles-engines/tabpanel.ts +++ b/src/core/component/directives/aria/roles-engines/tabpanel.ts @@ -9,6 +9,9 @@ import AriaRoleEngine from 'core/component/directives/aria/interface'; export default class TabpanelEngine extends AriaRoleEngine { + /** + * Sets base aria attributes for current role + */ init(): void { const {el, binding} = this.options; diff --git a/src/core/component/directives/aria/roles-engines/tree.ts b/src/core/component/directives/aria/roles-engines/tree.ts index 4f7630d2fc..cc4216d758 100644 --- a/src/core/component/directives/aria/roles-engines/tree.ts +++ b/src/core/component/directives/aria/roles-engines/tree.ts @@ -10,29 +10,47 @@ import AriaRoleEngine, { DirectiveOptions } from 'core/component/directives/aria import type { TreeParams } from 'core/component/directives/aria/roles-engines/interface'; export default class TreeEngine extends AriaRoleEngine { + /** + * Passed directive params + */ params: TreeParams; - el: HTMLElement; constructor(options: DirectiveOptions) { super(options); this.params = options.binding.value; - this.el = this.options.el; } + /** + * Sets base aria attributes for current role + */ init(): void { + const + {el} = this.options; + this.setRootRole(); if (this.params.isVertical) { - this.el.setAttribute('aria-orientation', 'vertical'); + el.setAttribute('aria-orientation', 'vertical'); } } - setRootRole(): void { - this.el.setAttribute('role', this.params.isRoot ? 'tree' : 'group'); + /** + * Sets the role to the element depending on whether the tree is root or nested + */ + protected setRootRole(): void { + const + {el} = this.options; + + el.setAttribute('role', this.params.isRoot ? 'tree' : 'group'); } - onChange = (el: HTMLElement, isFolded: boolean): void => { + /** + * Handler: treeitem was expanded or closed + * @param el + * @param isFolded + */ + protected onChange(el: HTMLElement, isFolded: boolean): void { el.setAttribute('aria-expanded', String(!isFolded)); - }; + } } diff --git a/src/core/component/directives/aria/roles-engines/treeitem.ts b/src/core/component/directives/aria/roles-engines/treeitem.ts index 529da86244..5a2722424d 100644 --- a/src/core/component/directives/aria/roles-engines/treeitem.ts +++ b/src/core/component/directives/aria/roles-engines/treeitem.ts @@ -16,9 +16,20 @@ import type { TreeitemParams } from 'core/component/directives/aria/roles-engine import type iBlock from 'super/i-block/i-block'; export default class TreeItemEngine extends AriaRoleEngine { + /** + * Passed directive params + */ + params: TreeitemParams; + + /** + * Component instance + */ ctx: iAccess & iBlock['unsafe']; + + /** + * Element with current directive + */ el: HTMLElement; - params: TreeitemParams; constructor(options: DirectiveOptions) { super(options); @@ -32,8 +43,11 @@ export default class TreeItemEngine extends AriaRoleEngine { this.params = this.options.binding.value; } + /** + * Sets base aria attributes for current role + */ init(): void { - this.async?.on(this.el, 'keydown', this.onKeyDown); + this.async?.on(this.el, 'keydown', this.onKeyDown.bind(this)); const isMuted = this.ctx.removeAllFromTabSequence(this.el); @@ -56,70 +70,22 @@ export default class TreeItemEngine extends AriaRoleEngine { }); } - onKeyDown = (e: KeyboardEvent): void => { - if (e.altKey || e.ctrlKey || e.metaKey || e.shiftKey) { - return; - } - - switch (e.code) { - case keyCodes.UP: - this.moveFocus(-1); - break; - - case keyCodes.DOWN: - this.moveFocus(1); - break; - - case keyCodes.ENTER: - this.params.toggleFold(this.el); - break; - - case keyCodes.RIGHT: - if (this.params.isExpandable) { - if (this.params.isExpanded) { - this.moveFocus(1); - - } else { - this.openFold(); - } - } - - break; - - case keyCodes.LEFT: - if (this.params.isExpandable && this.params.isExpanded) { - this.closeFold(); - - } else { - this.focusParent(); - } - - break; - - case keyCodes.HOME: - this.setFocusToFirstItem(); - break; - - case keyCodes.END: - this.setFocusToLastItem(); - break; - - default: - return; - } - - e.stopPropagation(); - e.preventDefault(); - }; - - focusNext(nextEl: HTMLElement): void { + /** + * Changes focus from the current focused element to the passed one + * @param el + */ + protected focusNext(el: HTMLElement): void { this.ctx.removeAllFromTabSequence(this.el); - this.ctx.restoreAllToTabSequence(nextEl); + this.ctx.restoreAllToTabSequence(el); - nextEl.focus(); + el.focus(); } - moveFocus(step: 1 | -1): void { + /** + * Moves focus to the next or previous focusable element via the step parameter + * @param step + */ + protected moveFocus(step: 1 | -1): void { const nextEl = >this.ctx.getNextFocusableElement(step); @@ -128,15 +94,24 @@ export default class TreeItemEngine extends AriaRoleEngine { } } - openFold(): void { + /** + * Expands the treeitem + */ + protected openFold(): void { this.params.toggleFold(this.el, false); } - closeFold(): void { + /** + * Closes the treeitem + */ + protected closeFold(): void { this.params.toggleFold(this.el, true); } - focusParent(): void { + /** + * Moves focus to the parent treeitem + */ + protected focusParent(): void { let parent = this.el.parentElement; @@ -160,7 +135,10 @@ export default class TreeItemEngine extends AriaRoleEngine { } } - setFocusToFirstItem(): void { + /** + * Moves focus to the first visible treeitem + */ + protected setFocusToFirstItem(): void { const firstItem = >this.ctx.findFocusableElement(this.params.rootElement); @@ -169,7 +147,10 @@ export default class TreeItemEngine extends AriaRoleEngine { } } - setFocusToLastItem(): void { + /** + * Moves focus to the last visible treeitem + */ + protected setFocusToLastItem(): void { const items = >this.ctx.findAllFocusableElements(this.params.rootElement); @@ -186,4 +167,63 @@ export default class TreeItemEngine extends AriaRoleEngine { this.focusNext(lastItem); } } + + /** + * Handler: keyboard event + */ + protected onKeyDown(e: KeyboardEvent): void { + if (e.altKey || e.ctrlKey || e.metaKey || e.shiftKey) { + return; + } + + switch (e.key) { + case keyCodes.UP: + this.moveFocus(-1); + break; + + case keyCodes.DOWN: + this.moveFocus(1); + break; + + case keyCodes.ENTER: + this.params.toggleFold(this.el); + break; + + case keyCodes.RIGHT: + if (this.params.isExpandable) { + if (this.params.isExpanded) { + this.moveFocus(1); + + } else { + this.openFold(); + } + } + + break; + + case keyCodes.LEFT: + if (this.params.isExpandable && this.params.isExpanded) { + this.closeFold(); + + } else { + this.focusParent(); + } + + break; + + case keyCodes.HOME: + this.setFocusToFirstItem(); + break; + + case keyCodes.END: + this.setFocusToLastItem(); + break; + + default: + return; + } + + e.stopPropagation(); + e.preventDefault(); + } } From 409c59afab01cf5867a9c5c7abfebc39dfaded80 Mon Sep 17 00:00:00 2001 From: Aleksandr Bunin Date: Fri, 29 Jul 2022 15:40:38 +0300 Subject: [PATCH 014/185] fix v-attrs parse regexp --- src/core/component/render-function/const.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/component/render-function/const.ts b/src/core/component/render-function/const.ts index 833defaf0c..18dc3de3d9 100644 --- a/src/core/component/render-function/const.ts +++ b/src/core/component/render-function/const.ts @@ -7,4 +7,4 @@ */ export const - vAttrsRgxp = /(v-(.*?))(?::(.*?))?(\..*)?$/; + vAttrsRgxp = /(v-(.*?))(?::(.*?))?(?:\.(.*))?$/; From 4abc2b694972e5f1827649d238d4ebbeeee0ecbf Mon Sep 17 00:00:00 2001 From: Aleksandr Bunin Date: Tue, 2 Aug 2022 16:42:48 +0300 Subject: [PATCH 015/185] fixes --- src/base/b-list/b-list.ts | 10 +-- src/base/b-tree/b-tree.ts | 5 +- src/core/component/directives/aria/README.md | 28 ++++++++- .../directives/aria/roles-engines/controls.ts | 44 +++++++------ .../directives/aria/roles-engines/dialog.ts | 7 +-- .../aria/roles-engines/interface.ts | 8 ++- .../directives/aria/roles-engines/option.ts | 4 -- .../directives/aria/roles-engines/tab.ts | 19 +++++- .../directives/aria/roles-engines/tablist.ts | 4 +- .../directives/aria/roles-engines/tabpanel.ts | 9 +-- .../directives/aria/roles-engines/tree.ts | 7 ++- .../directives/aria/roles-engines/treeitem.ts | 61 ++++++++++++++----- src/core/component/directives/id/README.md | 9 +++ src/core/component/directives/id/index.ts | 7 ++- src/form/b-select/b-select.ss | 17 +++--- src/traits/i-access/i-access.ts | 6 +- 16 files changed, 164 insertions(+), 81 deletions(-) diff --git a/src/base/b-list/b-list.ts b/src/base/b-list/b-list.ts index 8f8ce22d36..f9636ec772 100644 --- a/src/base/b-list/b-list.ts +++ b/src/base/b-list/b-list.ts @@ -709,8 +709,7 @@ class bList extends iData implements iVisible, iWidth, iItems, iAccess { protected getAriaConfig(role: 'tab' | 'tablist', item?: this['Item'], i?: number): Dictionary { const - isActive = this.isActive.bind(this, item?.value), - isVertical = this.orientation === 'vertical'; + isActive = this.isActive.bind(this, item?.value); const changeEvent = (cb: Function) => { this.on('change', () => { @@ -724,13 +723,14 @@ class bList extends iData implements iVisible, iWidth, iItems, iAccess { }; const tablistConfig = { - isVertical, - isMultiple: this.multiple + isMultiple: this.multiple, + orientation: this.orientation }; const tabConfig = { - isVertical, + preSelected: this.active != null, isFirst: i === 0, + orientation: this.orientation, changeEvent, get isActive() { return isActive(); diff --git a/src/base/b-tree/b-tree.ts b/src/base/b-tree/b-tree.ts index e68db8673f..1d763346bb 100644 --- a/src/base/b-tree/b-tree.ts +++ b/src/base/b-tree/b-tree.ts @@ -109,7 +109,7 @@ class bTree extends iData implements iItems, iAccess { * The component view orientation */ @prop(String) - readonly orientation: Orientation = 'horizontal'; + readonly orientation: Orientation = 'vertical'; /** * Link to the top level component (internal parameter) @@ -289,7 +289,7 @@ class bTree extends iData implements iItems, iAccess { const treeConfig = { isRoot: this.top == null, - isVertical: this.orientation === 'vertical', + orientation: this.orientation, changeEvent: (cb: Function) => { this.on('fold', (ctx, el, item, value) => cb(el, value)); } @@ -297,6 +297,7 @@ class bTree extends iData implements iItems, iAccess { const treeitemConfig = { isRootFirstItem: this.top == null && i === 0, + orientation: this.orientation, toggleFold, get rootElement() { return root(); diff --git a/src/core/component/directives/aria/README.md b/src/core/component/directives/aria/README.md index e12cd09021..15c7ff29f8 100644 --- a/src/core/component/directives/aria/README.md +++ b/src/core/component/directives/aria/README.md @@ -22,13 +22,35 @@ Directive can be added to any tag that includes tag with needed role. Role shoul ID or IDs are passed as value. ID could be single or multiple written in string with space between. +There are two ways to use this engine: +1. To add role as a modifier to which passed IDs in `for` value should be added. `for` could be `string` or `string[]`. +If element controls several elements `for` should be passed as a string with IDs separated with space. +(!) Notice that this role attribute should already be added to the element. The engine does not set passed role to the current element. + Example: ``` -< &__foo v-aria:controls.select = {id: 'id1 id2 id3'} +< &__foo v-aria:controls.tab = {for: 'id1 id2 id3'} + +converts to + +< tab aria-controls = "id1 id2 id3" role = "tab" +``` -same as +2. To pass value `for` as an array of tuples. +First id in a tuple is an id of an element to which one the aria attributes should be added. +The second one is an id of an element to set as value in aria-controls attribute. +(!) Notice that id attribute should already be added to the element. The engine does not set passed ids to the elements. + +Example: +``` +< &__foo v-aria:controls = {for: [[id1, id3], [id2, id4]]} + < span :id = "id1" + < span :id = "id2" -< select aria-controls = "id1 id2 id3" +converts to +< &__foo + < span :id = "id1" aria-controls = "id3" + < span :id = "id2" aria-controls = "id4" ``` - tabs: diff --git a/src/core/component/directives/aria/roles-engines/controls.ts b/src/core/component/directives/aria/roles-engines/controls.ts index 1d7b3d9a25..76b2d597f9 100644 --- a/src/core/component/directives/aria/roles-engines/controls.ts +++ b/src/core/component/directives/aria/roles-engines/controls.ts @@ -15,32 +15,40 @@ export default class ControlsEngine extends AriaRoleEngine { init(): void { const {vnode, binding, el} = this.options, + {modifiers, value} = binding, {fakeContext: ctx} = vnode; - if (binding.modifiers == null) { - Object.throw('Controls aria directive expects the role modifier to be passed'); - return; - } - - if (binding.value?.id == null) { - Object.throw('Controls aria directive expects the id of controlling elements to be passed'); + if (value?.for == null) { + Object.throw('Controls aria directive expects the id of controlling elements to be passed as "for" prop'); return; } const - roleName = Object.keys(binding.modifiers)[0]; - - ctx?.$nextTick().then(() => { - const - elems = el.querySelectorAll(`[role=${roleName}]`); + isForPropArray = Object.isArray(value.for), + isForPropArrayOfTuples = Object.isArray(value.for) && Object.isArray(value.for[0]); - for (let i = 0; i < elems.length; i++) { + if (modifiers != null && Object.size(modifiers) > 0) { + ctx?.$nextTick().then(() => { const - elem = elems[i], - {id} = binding.value; + roleName = Object.keys(modifiers)[0], + elems = el.querySelectorAll(`[role=${roleName}]`); - elem.setAttribute('aria-controls', id); - } - }); + if (isForPropArray && value.for.length !== elems.length) { + Object.throw('Controls aria directive expects prop "for" length to be equal to amount of elements with specified role or string type'); + return; + } + + elems.forEach((el, i) => { + el.setAttribute('aria-controls', isForPropArray ? value.for[i] : value.for); + }); + }); + + } else if (isForPropArrayOfTuples) { + value.for.forEach(([elId, controlsId]) => { + const element = el.querySelector(`#${elId}`); + + element?.setAttribute('aria-controls', controlsId); + }); + } } } diff --git a/src/core/component/directives/aria/roles-engines/dialog.ts b/src/core/component/directives/aria/roles-engines/dialog.ts index 4888432ccc..de7e868539 100644 --- a/src/core/component/directives/aria/roles-engines/dialog.ts +++ b/src/core/component/directives/aria/roles-engines/dialog.ts @@ -6,7 +6,6 @@ * https://github.com/V4Fire/Client/blob/master/LICENSE */ -import iOpen from 'traits/i-open/i-open'; import AriaRoleEngine from 'core/component/directives/aria/interface'; export default class DialogEngine extends AriaRoleEngine { @@ -15,13 +14,9 @@ export default class DialogEngine extends AriaRoleEngine { */ init(): void { const - {el, vnode} = this.options; + {el} = this.options; el.setAttribute('role', 'dialog'); el.setAttribute('aria-modal', 'true'); - - if (!iOpen.is(vnode.fakeContext)) { - Object.throw('Dialog aria directive expects the component to realize iOpen interface'); - } } } diff --git a/src/core/component/directives/aria/roles-engines/interface.ts b/src/core/component/directives/aria/roles-engines/interface.ts index 61922d6fa1..3ab7869aa3 100644 --- a/src/core/component/directives/aria/roles-engines/interface.ts +++ b/src/core/component/directives/aria/roles-engines/interface.ts @@ -1,18 +1,19 @@ export interface TabParams { + preSelected: boolean; isFirst: boolean; - isVertical: boolean; isActive: boolean; + orientation: string; changeEvent(cb: Function): void; } export interface TablistParams { - isVertical: boolean; isMultiple: boolean; + orientation: string; } export interface TreeParams { isRoot: boolean; - isVertical: boolean; + orientation: string; changeEvent(cb: Function): void; } @@ -20,6 +21,7 @@ export interface TreeitemParams { isRootFirstItem: boolean; isExpandable: boolean; isExpanded: boolean; + orientation: string; rootElement: CanUndef; toggleFold(el: Element, value?: boolean): void; } diff --git a/src/core/component/directives/aria/roles-engines/option.ts b/src/core/component/directives/aria/roles-engines/option.ts index b0cb7b3bbc..3b30bfc48b 100644 --- a/src/core/component/directives/aria/roles-engines/option.ts +++ b/src/core/component/directives/aria/roles-engines/option.ts @@ -19,10 +19,6 @@ export default class OptionEngine extends AriaRoleEngine { el.setAttribute('role', 'option'); el.setAttribute('aria-selected', String(preSelected)); - - if (!el.hasAttribute('id')) { - Object.throw('Option aria directive expects the Element id to be added'); - } } /** diff --git a/src/core/component/directives/aria/roles-engines/tab.ts b/src/core/component/directives/aria/roles-engines/tab.ts index f2b656ffc6..ce7b7d845b 100644 --- a/src/core/component/directives/aria/roles-engines/tab.ts +++ b/src/core/component/directives/aria/roles-engines/tab.ts @@ -38,12 +38,17 @@ export default class TabEngine extends AriaRoleEngine { init(): void { const {el} = this.options, - {isFirst} = this.params; + {isFirst, preSelected} = this.params; el.setAttribute('role', 'tab'); el.setAttribute('aria-selected', String(this.params.isActive)); - if (isFirst) { + if (isFirst && !preSelected) { + if (el.tabIndex < 0) { + el.setAttribute('tabindex', '0'); + } + + } else if (preSelected && this.params.isActive) { if (el.tabIndex < 0) { el.setAttribute('tabindex', '0'); } @@ -125,10 +130,14 @@ export default class TabEngine extends AriaRoleEngine { protected onKeydown(e: Event): void { const evt = (e), - {isVertical} = this.params; + isVertical = this.params.orientation === 'vertical'; switch (evt.key) { case keyCodes.LEFT: + if (isVertical) { + return; + } + this.moveFocus(-1); break; @@ -141,6 +150,10 @@ export default class TabEngine extends AriaRoleEngine { return; case keyCodes.RIGHT: + if (isVertical) { + return; + } + this.moveFocus(1); break; diff --git a/src/core/component/directives/aria/roles-engines/tablist.ts b/src/core/component/directives/aria/roles-engines/tablist.ts index 9140b6246b..b5c4653cd4 100644 --- a/src/core/component/directives/aria/roles-engines/tablist.ts +++ b/src/core/component/directives/aria/roles-engines/tablist.ts @@ -24,8 +24,8 @@ export default class TablistEngine extends AriaRoleEngine { el.setAttribute('aria-multiselectable', 'true'); } - if (params.isVertical) { - el.setAttribute('aria-orientation', 'vertical'); + if (params.orientation === 'vertical') { + el.setAttribute('aria-orientation', params.orientation); } } } diff --git a/src/core/component/directives/aria/roles-engines/tabpanel.ts b/src/core/component/directives/aria/roles-engines/tabpanel.ts index 9d59cac654..ca3537088c 100644 --- a/src/core/component/directives/aria/roles-engines/tabpanel.ts +++ b/src/core/component/directives/aria/roles-engines/tabpanel.ts @@ -14,12 +14,13 @@ export default class TabpanelEngine extends AriaRoleEngine { */ init(): void { const - {el, binding} = this.options; + {el} = this.options; - el.setAttribute('role', 'tabpanel'); - - if (binding.value?.labelledby == null) { + if (!el.hasAttribute('aria-label') && !el.hasAttribute('aria-labelledby')) { Object.throw('Tabpanel aria directive expects "label" or "labelledby" value to be passed'); + return; } + + el.setAttribute('role', 'tabpanel'); } } diff --git a/src/core/component/directives/aria/roles-engines/tree.ts b/src/core/component/directives/aria/roles-engines/tree.ts index cc4216d758..3b8986e196 100644 --- a/src/core/component/directives/aria/roles-engines/tree.ts +++ b/src/core/component/directives/aria/roles-engines/tree.ts @@ -26,12 +26,13 @@ export default class TreeEngine extends AriaRoleEngine { */ init(): void { const - {el} = this.options; + {el} = this.options, + {orientation} = this.params; this.setRootRole(); - if (this.params.isVertical) { - el.setAttribute('aria-orientation', 'vertical'); + if (orientation === 'horizontal' && this.params.isRoot) { + el.setAttribute('aria-orientation', orientation); } } diff --git a/src/core/component/directives/aria/roles-engines/treeitem.ts b/src/core/component/directives/aria/roles-engines/treeitem.ts index 5a2722424d..9215388542 100644 --- a/src/core/component/directives/aria/roles-engines/treeitem.ts +++ b/src/core/component/directives/aria/roles-engines/treeitem.ts @@ -176,39 +176,68 @@ export default class TreeItemEngine extends AriaRoleEngine { return; } + const + isHorizontal = this.params.orientation === 'horizontal'; + + const open = () => { + if (this.params.isExpandable) { + if (this.params.isExpanded) { + this.moveFocus(1); + + } else { + this.openFold(); + } + } + }; + + const close = () => { + if (this.params.isExpandable && this.params.isExpanded) { + this.closeFold(); + + } else { + this.focusParent(); + } + }; + switch (e.key) { case keyCodes.UP: + if (isHorizontal) { + close(); + break; + } + this.moveFocus(-1); break; case keyCodes.DOWN: - this.moveFocus(1); - break; + if (isHorizontal) { + open(); + break; + } - case keyCodes.ENTER: - this.params.toggleFold(this.el); + this.moveFocus(1); break; case keyCodes.RIGHT: - if (this.params.isExpandable) { - if (this.params.isExpanded) { - this.moveFocus(1); - - } else { - this.openFold(); - } + if (isHorizontal) { + this.moveFocus(1); + break; } + open(); break; case keyCodes.LEFT: - if (this.params.isExpandable && this.params.isExpanded) { - this.closeFold(); - - } else { - this.focusParent(); + if (isHorizontal) { + this.moveFocus(-1); + break; } + close(); + break; + + case keyCodes.ENTER: + this.params.toggleFold(this.el); break; case keyCodes.HOME: diff --git a/src/core/component/directives/id/README.md b/src/core/component/directives/id/README.md index 4a8008f9bd..58c7b90465 100644 --- a/src/core/component/directives/id/README.md +++ b/src/core/component/directives/id/README.md @@ -15,3 +15,12 @@ The same as ``` +## Modifiers + +1. `preserve` means that if there is already an id attribute on the element, +the directive will left it and will not set another one + +``` +< &__foo v-id.preserve = 'title' + +``` diff --git a/src/core/component/directives/id/index.ts b/src/core/component/directives/id/index.ts index d8636c4965..7e1c4a51ab 100644 --- a/src/core/component/directives/id/index.ts +++ b/src/core/component/directives/id/index.ts @@ -17,7 +17,12 @@ import type iBlock from 'super/i-block/i-block'; ComponentEngine.directive('id', { inserted(el: HTMLElement, binding: VNodeDirective, vnode: VNode): void { const - ctx = Object.cast(vnode.fakeContext); + ctx = Object.cast(vnode.fakeContext), + {modifiers: mod} = binding; + + if (mod?.preserve != null && el.hasAttribute('id')) { + return; + } const id = ctx.dom.getId(binding.value); diff --git a/src/form/b-select/b-select.ss b/src/form/b-select/b-select.ss index db1a29e258..cd567ca839 100644 --- a/src/form/b-select/b-select.ss +++ b/src/form/b-select/b-select.ss @@ -39,14 +39,15 @@ } })) | - :v-attrs = native - ? el.attrs - : { - 'v-aria:option': getAriaConfig('option', el), - ...el.attrs - } | - - v-id = el.value | + :v-attrs = native ? + el.attrs : + { + 'v-aria:option': getAriaConfig('option', el), + ...el.attrs + } | + + v-id.preserve = el.value | + ${itemAttrs} . += self.slot('default', {':item': 'el'}) diff --git a/src/traits/i-access/i-access.ts b/src/traits/i-access/i-access.ts index 2baf4546b6..c5c6aced57 100644 --- a/src/traits/i-access/i-access.ts +++ b/src/traits/i-access/i-access.ts @@ -267,7 +267,7 @@ export default abstract class iAccess { focusableElems = this.findAllFocusableElements(component, ctx); for (const focusableEl of focusableElems) { - if (!focusableEl.hasAttribute('disabled')) { + if (!focusableEl?.hasAttribute('disabled')) { return focusableEl; } } @@ -275,7 +275,7 @@ export default abstract class iAccess { /** @see [[iAccess.findAllFocusableElements]] */ static findAllFocusableElements: AddSelf = - (component, el?): IterableIterator => { + (component, el?): IterableIterator> => { const ctx = el ?? component.$el, focusableElems = ctx?.querySelectorAll(FOCUSABLE_SELECTOR); @@ -419,7 +419,7 @@ export default abstract class iAccess { * Find all focusable elements except disabled ones. Search includes the specified element * @param el - a context to search, if not set, component will be used */ - findAllFocusableElements(el?: Element): IterableIterator { + findAllFocusableElements(el?: Element): IterableIterator> { return Object.throw(); } } From 769531d02ffebc1ae629b3d0ea84d5e0b31ae0b9 Mon Sep 17 00:00:00 2001 From: Aleksandr Bunin Date: Tue, 2 Aug 2022 16:43:24 +0300 Subject: [PATCH 016/185] add tests --- .../directives/aria/test/unit/combobox.ts | 131 ++++++++++ .../directives/aria/test/unit/controls.ts | 159 ++++++++++++ .../directives/aria/test/unit/dialog.ts | 51 ++++ .../directives/aria/test/unit/listbox.ts | 66 +++++ .../directives/aria/test/unit/option.ts | 117 +++++++++ .../directives/aria/test/unit/simple.ts | 72 ++++++ .../directives/aria/test/unit/tab.ts | 226 ++++++++++++++++++ .../directives/aria/test/unit/tablist.ts | 72 ++++++ .../directives/aria/test/unit/tabpanel.ts | 49 ++++ .../directives/aria/test/unit/tree.ts | 103 ++++++++ .../directives/aria/test/unit/treeitem.ts | 182 ++++++++++++++ .../directives/id/test/unit/functional.ts | 47 ++++ 12 files changed, 1275 insertions(+) create mode 100644 src/core/component/directives/aria/test/unit/combobox.ts create mode 100644 src/core/component/directives/aria/test/unit/controls.ts create mode 100644 src/core/component/directives/aria/test/unit/dialog.ts create mode 100644 src/core/component/directives/aria/test/unit/listbox.ts create mode 100644 src/core/component/directives/aria/test/unit/option.ts create mode 100644 src/core/component/directives/aria/test/unit/simple.ts create mode 100644 src/core/component/directives/aria/test/unit/tab.ts create mode 100644 src/core/component/directives/aria/test/unit/tablist.ts create mode 100644 src/core/component/directives/aria/test/unit/tabpanel.ts create mode 100644 src/core/component/directives/aria/test/unit/tree.ts create mode 100644 src/core/component/directives/aria/test/unit/treeitem.ts create mode 100644 src/core/component/directives/id/test/unit/functional.ts diff --git a/src/core/component/directives/aria/test/unit/combobox.ts b/src/core/component/directives/aria/test/unit/combobox.ts new file mode 100644 index 0000000000..8b99af6f76 --- /dev/null +++ b/src/core/component/directives/aria/test/unit/combobox.ts @@ -0,0 +1,131 @@ +// @ts-check + +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ +import type { JSHandle, Page } from 'playwright'; +import type iBlock from 'super/i-block/i-block'; + +import test from 'tests/config/unit/test'; +import Component from 'tests/helpers/component'; + +test.describe('v-aria:combobox', () => { + test.beforeEach(async ({demoPage}) => { + await demoPage.goto(); + }); + + const + selector = '[data-id="target"]'; + + /** + * Initial attributes + */ + test('role is set', async ({page}) => { + const target = await init(page); + + test.expect( + await target.evaluate((ctx) => ctx.unsafe.block?.element('input')?.getAttribute('role')) + ).toBe('combobox'); + }); + + test('aria-expanded is set to false', async ({page}) => { + const target = await init(page); + + test.expect( + await target.evaluate((ctx) => ctx.unsafe.block?.element('input')?.getAttribute('aria-expanded')) + ).toBe('false'); + }); + + test('aria-multiselectable is set', async ({page}) => { + const target = await init(page, {multiple: true}); + + test.expect( + await target.evaluate((ctx) => ctx.unsafe.block?.element('input')?.getAttribute('aria-multiselectable')) + ).toBe('true'); + }); + + test('element\'s tabindex is 0', async ({page}) => { + const target = await init(page); + + test.expect( + await target.evaluate((ctx) => { + const input = ctx.unsafe.block?.element('input'); + return input.tabIndex; + }) + ).toBe(0); + }); + + /** + * Handling events + */ + test('select is opened with no preselected option', async ({page}) => { + const target = await init(page); + + await page.click(selector); + + test.expect( + await target.evaluate((ctx) => ctx.unsafe.block?.element('input')?.getAttribute('aria-expanded')) + ).toBe('true'); + + test.expect( + await target.evaluate((ctx) => ctx.unsafe.block?.element('input')?.getAttribute('aria-activedescendant')) + ).toBe(''); + }); + + test('select is opened with preselected option', async ({page}) => { + const target = await init(page, {value: 1}); + + await page.focus('input'); + + const id = await target.evaluate((ctx) => ctx.unsafe.dom.getId('1')); + + test.expect( + await target.evaluate((ctx) => ctx.unsafe.block?.element('input')?.getAttribute('aria-expanded')) + ).toBe('true'); + + test.expect( + await target.evaluate((ctx) => ctx.unsafe.block?.element('input')?.getAttribute('aria-activedescendant')) + ).toBe(id); + }); + + test('select is opened and closed', async ({page}) => { + const target = await init(page, {value: 1}); + + await page.focus('input'); + + test.expect( + await target.evaluate((ctx) => ctx.unsafe.block?.element('input')?.getAttribute('aria-expanded')) + ).toBe('true'); + + await page.click('body'); + + test.expect( + await target.evaluate((ctx) => ctx.unsafe.block?.element('input')?.getAttribute('aria-expanded')) + ).toBe('false'); + + test.expect( + await target.evaluate((ctx) => ctx.unsafe.block?.element('input')?.getAttribute('aria-activedescendant')) + ).toBe(''); + }); + + /** + * @param page + * @param attrs + */ + async function init(page: Page, attrs: Dictionary = {}): Promise> { + return Component.createComponent(page, 'b-select', { + attrs: { + 'data-id': 'target', + items: [ + {label: 'foo', value: 0}, + {label: 'bar', value: 1} + ], + ...attrs + } + }); + } +}); diff --git a/src/core/component/directives/aria/test/unit/controls.ts b/src/core/component/directives/aria/test/unit/controls.ts new file mode 100644 index 0000000000..5d014fe155 --- /dev/null +++ b/src/core/component/directives/aria/test/unit/controls.ts @@ -0,0 +1,159 @@ +// @ts-check + +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ +import type { JSHandle, Page } from 'playwright'; +import type iBlock from 'super/i-block/i-block'; + +import test from 'tests/config/unit/test'; +import Component from 'tests/helpers/component'; + +test.describe('v-aria:controls', () => { + test.beforeEach(async ({demoPage}) => { + await demoPage.goto(); + }); + + /** + * With modifiers + */ + test('modifiers. "for" is a string', async ({page}) => { + const target = await init(page, {for: 'id3'}); + + test.expect( + await target.evaluate((ctx) => { + if (ctx.unsafe.block == null) { + return; + } + + const [el1, el2] = Array.from(ctx.unsafe.block.elements('link')); + + return el1.getAttribute('aria-controls') === 'id3' && el2.getAttribute('aria-controls') === 'id3'; + }) + ).toBe(true); + }); + + test('modifiers. "for" is an array', async ({page}) => { + const target = await init(page, {for: ['id3', 'id4']}); + + test.expect( + await target.evaluate((ctx) => { + if (ctx.unsafe.block == null) { + return; + } + + const [el1, el2]: HTMLElement[] = Array.from(ctx.unsafe.block.elements('link')); + + return [el1.getAttribute('aria-controls'), el2.getAttribute('aria-controls')]; + }) + ).toEqual(['id3', 'id4']); + }); + + test('modifiers. "for" is an array with wrong length', async ({page}) => { + const target = await init(page, {for: ['id3', 'id4', 'id5']}); + + test.expect( + await target.evaluate((ctx) => { + if (ctx.unsafe.block == null) { + return; + } + + const [el1, el2]: HTMLElement[] = Array.from(ctx.unsafe.block.elements('link')); + + return [el1.hasAttribute('aria-controls'), el2.hasAttribute('aria-controls')]; + }) + ).toEqual([false, false]); + }); + + test('modifiers. no "for" value passed', async ({page}) => { + const target = await init(page); + + test.expect( + await target.evaluate((ctx) => { + if (ctx.unsafe.block == null) { + return; + } + + const [el1, el2]: HTMLElement[] = Array.from(ctx.unsafe.block.elements('link')); + + return [el1.hasAttribute('aria-controls'), el2.hasAttribute('aria-controls')]; + }) + ).toEqual([false, false]); + }); + + /** + * With 'for' param as an array of tuples + */ + test('tuples', async ({page}) => { + const target = await init(page, {for: [['id1', 'id3'], ['id2', 'id4']]}, 'v-aria:controls'); + + test.expect( + await target.evaluate((ctx) => { + if (ctx.unsafe.block == null) { + return; + } + + const [el1, el2]: HTMLElement[] = Array.from(ctx.unsafe.block.elements('link')); + + return [el1.getAttribute('aria-controls'), el2.getAttribute('aria-controls')]; + }) + ).toEqual(['id3', 'id4']); + }); + + test('tuples. wrong ids', async ({page}) => { + const target = await init(page, {for: [['id5', 'id6'], ['id3', 'id8']]}, 'v-aria:controls'); + + test.expect( + await target.evaluate((ctx) => { + if (ctx.unsafe.block == null) { + return; + } + + const [el1, el2]: HTMLElement[] = Array.from(ctx.unsafe.block.elements('link')); + + return [el1.hasAttribute('aria-controls'), el2.hasAttribute('aria-controls')]; + }) + ).toEqual([false, false]); + }); + + test('no params passed', async ({page}) => { + const target = await init(page); + + test.expect( + await target.evaluate((ctx) => { + if (ctx.unsafe.block == null) { + return; + } + + const [el1, el2]: HTMLElement[] = Array.from(ctx.unsafe.block.elements('link')); + + return [el1.hasAttribute('aria-controls'), el2.hasAttribute('aria-controls')]; + }) + ).toEqual([false, false]); + }); + + /** + * @param page + * @param ariaConfig + * @param directive + */ + async function init( + page: Page, + ariaConfig: Dictionary = {}, + directive: string = 'v-aria:controls.tab' + ): Promise> { + return Component.createComponent(page, 'b-list', { + attrs: { + [directive]: ariaConfig, + items: [ + {label: 'foo', value: 0, attrs: {id: 'id1'}}, + {label: 'bla', value: 1, attrs: {id: 'id2'}} + ] + } + }); + } +}); diff --git a/src/core/component/directives/aria/test/unit/dialog.ts b/src/core/component/directives/aria/test/unit/dialog.ts new file mode 100644 index 0000000000..f1f2c031f1 --- /dev/null +++ b/src/core/component/directives/aria/test/unit/dialog.ts @@ -0,0 +1,51 @@ +// @ts-check + +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ +import type { JSHandle, Page } from 'playwright'; +import type iBlock from 'super/i-block/i-block'; + +import test from 'tests/config/unit/test'; +import Component from 'tests/helpers/component'; + +test.describe('v-aria:dialog', () => { + test.beforeEach(async ({demoPage}) => { + await demoPage.goto(); + }); + + test('role is set', async ({page}) => { + const target = await init(page); + + test.expect( + await target.evaluate((ctx) => { + const el = ctx.unsafe.block?.element('window'); + + return el?.getAttribute('role'); + }) + ).toBe('dialog'); + }); + + test('aria-modal is set', async ({page}) => { + const target = await init(page); + + test.expect( + await target.evaluate((ctx) => { + const el = ctx.unsafe.block?.element('window'); + + return el?.getAttribute('aria-modal'); + }) + ).toBe('true'); + }); + + /** + * @param page + */ + async function init(page: Page): Promise> { + return Component.createComponent(page, 'b-window'); + } +}); diff --git a/src/core/component/directives/aria/test/unit/listbox.ts b/src/core/component/directives/aria/test/unit/listbox.ts new file mode 100644 index 0000000000..ff654c727b --- /dev/null +++ b/src/core/component/directives/aria/test/unit/listbox.ts @@ -0,0 +1,66 @@ +// @ts-check + +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ +import type { JSHandle, Page } from 'playwright'; +import type iBlock from 'super/i-block/i-block'; + +import test from 'tests/config/unit/test'; +import Component from 'tests/helpers/component'; + +test.describe('v-aria:listbox', () => { + test.beforeEach(async ({demoPage}) => { + await demoPage.goto(); + }); + + const + selector = '[data-id="target"]'; + + test('role is set', async ({page}) => { + const target = await init(page); + + await page.click(selector); + + test.expect( + await target.evaluate((ctx) => { + const el = ctx.unsafe.block?.element('dropdown'); + + return el?.getAttribute('role'); + }) + ).toBe('listbox'); + }); + + test('tabindex is -1', async ({page}) => { + const target = await init(page); + + await page.click(selector); + + test.expect( + await target.evaluate((ctx) => { + const el = ctx.unsafe.block?.element('dropdown'); + + return el?.getAttribute('tabindex'); + }) + ).toBe('-1'); + }); + + /** + * @param page + */ + async function init(page: Page): Promise> { + return Component.createComponent(page, 'b-select', { + attrs: { + 'data-id': 'target', + items: [ + {label: 'foo', value: 0}, + {label: 'bar', value: 1} + ] + } + }); + } +}); diff --git a/src/core/component/directives/aria/test/unit/option.ts b/src/core/component/directives/aria/test/unit/option.ts new file mode 100644 index 0000000000..0b24a690fb --- /dev/null +++ b/src/core/component/directives/aria/test/unit/option.ts @@ -0,0 +1,117 @@ +// @ts-check + +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ +import type { JSHandle, Page } from 'playwright'; +import type iBlock from 'super/i-block/i-block'; + +import test from 'tests/config/unit/test'; +import Component from 'tests/helpers/component'; + +test.describe('v-aria:option', () => { + test.beforeEach(async ({demoPage}) => { + await demoPage.goto(); + }); + + const + selector = '[data-id="target"]'; + + test('role is set', async ({page}) => { + const target = await init(page); + + await page.click(selector); + + test.expect( + await target.evaluate((ctx) => { + if (ctx.unsafe.block == null) { + return; + } + + const [el1, el2]: HTMLElement[] = Array.from(ctx.unsafe.block.elements('item')); + + return [el1.getAttribute('role'), el2.getAttribute('role')]; + }) + ).toEqual(['option', 'option']); + }); + + test('has no preselected value', async ({page}) => { + const target = await init(page); + + await page.click(selector); + + test.expect( + await target.evaluate((ctx) => { + if (ctx.unsafe.block == null) { + return; + } + + const [el1, el2]: HTMLElement[] = Array.from(ctx.unsafe.block.elements('item')); + + return [el1.getAttribute('aria-selected'), el2.getAttribute('aria-selected')]; + }) + ).toEqual(['false', 'false']); + }); + + test('options with preselected value', async ({page}) => { + const target = await init(page, {value: 0}); + + await page.click(selector); + + test.expect( + await target.evaluate((ctx) => { + if (ctx.unsafe.block == null) { + return; + } + + const [el1, el2]: HTMLElement[] = Array.from(ctx.unsafe.block.elements('item')); + + return [el1.getAttribute('aria-selected'), el2.getAttribute('aria-selected')]; + }) + ).toEqual(['true', 'false']); + }); + + test('selected option changed', async ({page}) => { + const target = await init(page, {value: 0}); + + await page.click(selector); + + test.expect( + await target.evaluate((ctx) => { + if (ctx.unsafe.block == null) { + return; + } + + const input = ctx.unsafe.block.element('input'); + const [el1, el2]: HTMLElement[] = Array.from(ctx.unsafe.block.elements('item')); + + input?.dispatchEvent(new KeyboardEvent('keydown', {key: 'ArrowDown', bubbles: true})); + + input?.dispatchEvent(new KeyboardEvent('keydown', {key: 'Enter', bubbles: true})); + + return [el1.getAttribute('aria-selected'), el2.getAttribute('aria-selected')]; + }) + ).toEqual(['false', 'true']); + }); + + /** + * @param page + * @param attrs + */ + async function init(page: Page, attrs: Dictionary = {}): Promise> { + return Component.createComponent(page, 'b-select', { + attrs: { + 'data-id': 'target', + items: [ + {label: 'foo', value: 0, attrs: {id: 'item1'}}, + {label: 'bar', value: 1, attrs: {id: 'item2'}} + ], + ...attrs + } + }); + } +}); diff --git a/src/core/component/directives/aria/test/unit/simple.ts b/src/core/component/directives/aria/test/unit/simple.ts new file mode 100644 index 0000000000..649d116a97 --- /dev/null +++ b/src/core/component/directives/aria/test/unit/simple.ts @@ -0,0 +1,72 @@ +// @ts-check + +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ +import type { JSHandle, Page } from 'playwright'; +import type iBlock from 'super/i-block/i-block'; + +import test from 'tests/config/unit/test'; +import Component from 'tests/helpers/component'; + +test.describe('v-aria', () => { + test.beforeEach(async ({demoPage}) => { + await demoPage.goto(); + }); + + test('aria-label is added', async ({page}) => { + const target = await init(page, {'v-aria': {label: 'bla'}}); + + test.expect( + await target.evaluate((ctx) => ctx.$el?.getAttribute('aria-label')) + ).toBe('bla'); + }); + + test('aria-labelledby is added', async ({page}) => { + const target = await init(page, {'v-aria': {labelledby: 'bla'}}); + + test.expect( + await target.evaluate((ctx) => ctx.$el?.getAttribute('aria-labelledby')) + ).toBe('bla'); + }); + + test('aria-description is added', async ({page}) => { + const target = await init(page, {'v-aria': {description: 'bla'}}); + + test.expect( + await target.evaluate((ctx) => ctx.$el?.getAttribute('aria-description')) + ).toBe('bla'); + }); + + test('aria-describedby is added', async ({page}) => { + const target = await init(page, {'v-aria': {describedby: 'bla'}}); + + test.expect( + await target.evaluate((ctx) => ctx.$el?.getAttribute('aria-describedby')) + ).toBe('bla'); + }); + + test('aria-labelledby sugar syntax', async ({page}) => { + const target = await init(page, {'v-aria.#bla': {}}); + + const id = await target.evaluate((ctx) => ctx.$root.unsafe.dom.getId('bla')); + + test.expect( + await target.evaluate((ctx) => ctx.$el?.getAttribute('aria-labelledby')) + ).toBe(id); + }); + + /** + * @param page + * @param attrs + */ + async function init(page: Page, attrs: Dictionary = {}): Promise> { + return Component.createComponent(page, 'b-dummy', { + attrs + }); + } +}); diff --git a/src/core/component/directives/aria/test/unit/tab.ts b/src/core/component/directives/aria/test/unit/tab.ts new file mode 100644 index 0000000000..f2f1d8a31a --- /dev/null +++ b/src/core/component/directives/aria/test/unit/tab.ts @@ -0,0 +1,226 @@ +// @ts-check + +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ +import type { JSHandle, Page } from 'playwright'; +import type iBlock from 'super/i-block/i-block'; + +import test from 'tests/config/unit/test'; +import Component from 'tests/helpers/component'; + +test.describe('v-aria:tab', () => { + test.beforeEach(async ({demoPage}) => { + await demoPage.goto(); + }); + + const + selector = '[data-id="target"]'; + + test('role is set', async ({page}) => { + const target = await init(page); + + test.expect( + await target.evaluate((ctx) => { + if (ctx.unsafe.block == null) { + return; + } + + const tabs = ctx.unsafe.block.elements('link'); + + const res: Array> = []; + + tabs.forEach((el) => res.push(el.getAttribute('role'))); + + return res; + }) + ).toEqual(['tab', 'tab', 'tab']); + }); + + test('has active value', async ({page}) => { + const target = await init(page, {active: 1}); + + await page.focus(selector); + + test.expect( + await target.evaluate((ctx) => { + if (ctx.unsafe.block == null) { + return; + } + + const tabs = ctx.unsafe.block.elements('link'); + + const res: Array> = []; + + tabs.forEach((el) => res.push(el.getAttribute('aria-selected'))); + + return res; + }) + ).toEqual(['false', 'true', 'false']); + }); + + test('tabindexes are set without active item', async ({page}) => { + const target = await init(page); + + test.expect( + await target.evaluate((ctx) => { + if (ctx.unsafe.block == null) { + return; + } + + const tabs: NodeListOf = ctx.unsafe.block.elements('link'); + + const res: number[] = []; + + tabs.forEach((el) => res.push(el.tabIndex)); + + return res; + }) + ).toEqual([0, -1, -1]); + }); + + test('tabindexes are set with active item', async ({page}) => { + const target = await init(page, {active: 1}); + + test.expect( + await target.evaluate((ctx) => { + if (ctx.unsafe.block == null) { + return; + } + + const tabs: NodeListOf = ctx.unsafe.block.elements('link'); + + const res: number[] = []; + + tabs.forEach((el) => res.push(el.tabIndex)); + + return res; + }) + ).toEqual([-1, 0, -1]); + }); + + test('active item changed', async ({page}) => { + const target = await init(page); + + test.expect( + await target.evaluate((ctx) => { + if (ctx.unsafe.block == null) { + return; + } + + let tabs: NodeListOf = ctx.unsafe.block.elements('link'); + + tabs[1].click(); + + const res: Array<[number, Nullable]> = []; + + tabs = ctx.unsafe.block.elements('link'); + + tabs.forEach((el, i) => res[i] = [el.tabIndex, el.ariaSelected]); + + return res; + }) + ).toEqual([ + [-1, 'false'], + [0, 'true'], + [-1, 'false'] + ]); + }); + + test('keyboard keys handle on horizontal orientation', async ({page}) => { + const target = await init(page); + + test.expect( + await target.evaluate((ctx) => { + if (ctx.unsafe.block == null) { + return; + } + + const + res: Array> = [], + tab: CanUndef = ctx.unsafe.block.element('link'); + + tab?.focus(); + tab?.dispatchEvent(new KeyboardEvent('keydown', {key: 'ArrowRight'})); + res.push(document.activeElement?.id); + + document.activeElement?.dispatchEvent(new KeyboardEvent('keydown', {key: 'ArrowLeft'})); + res.push(document.activeElement?.id); + + document.activeElement?.dispatchEvent(new KeyboardEvent('keydown', {key: 'ArrowUp'})); + res.push(document.activeElement?.id); + + document.activeElement?.dispatchEvent(new KeyboardEvent('keydown', {key: 'ArrowDown'})); + res.push(document.activeElement?.id); + + document.activeElement?.dispatchEvent(new KeyboardEvent('keydown', {key: 'End'})); + res.push(document.activeElement?.id); + + document.activeElement?.dispatchEvent(new KeyboardEvent('keydown', {key: 'Home'})); + res.push(document.activeElement?.id); + + return res; + }) + ).toEqual(['id2', 'id1', 'id1', 'id1', 'id3', 'id1']); + }); + + test('keyboard keys handle on vertical orientation', async ({page}) => { + const target = await init(page, {orientation: 'vertical'}); + + test.expect( + await target.evaluate((ctx) => { + if (ctx.unsafe.block == null) { + return; + } + + const + res: Array> = [], + tab: CanUndef = ctx.unsafe.block.element('link'); + + tab?.focus(); + tab?.dispatchEvent(new KeyboardEvent('keydown', {key: 'ArrowRight'})); + res.push(document.activeElement?.id); + + document.activeElement?.dispatchEvent(new KeyboardEvent('keydown', {key: 'ArrowLeft'})); + res.push(document.activeElement?.id); + + document.activeElement?.dispatchEvent(new KeyboardEvent('keydown', {key: 'ArrowDown'})); + res.push(document.activeElement?.id); + + document.activeElement?.dispatchEvent(new KeyboardEvent('keydown', {key: 'ArrowUp'})); + res.push(document.activeElement?.id); + + document.activeElement?.dispatchEvent(new KeyboardEvent('keydown', {key: 'End'})); + res.push(document.activeElement?.id); + + document.activeElement?.dispatchEvent(new KeyboardEvent('keydown', {key: 'Home'})); + res.push(document.activeElement?.id); + + return res; + }) + ).toEqual(['id1', 'id1', 'id2', 'id1', 'id3', 'id1']); + }); + + /** + * @param page + * @param attrs + */ + async function init(page: Page, attrs: Dictionary = {}): Promise> { + return Component.createComponent(page, 'b-list', { + attrs: { + 'data-id': 'target', + items: [ + {label: 'Male', value: 0, attrs: {id: 'id1'}}, + {label: 'Female', value: 1, attrs: {id: 'id2'}}, + {label: 'Other', value: 2, attrs: {id: 'id3'}} + ], + ...attrs + } + }); + } +}); + diff --git a/src/core/component/directives/aria/test/unit/tablist.ts b/src/core/component/directives/aria/test/unit/tablist.ts new file mode 100644 index 0000000000..e688c633bc --- /dev/null +++ b/src/core/component/directives/aria/test/unit/tablist.ts @@ -0,0 +1,72 @@ +// @ts-check + +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ +import type { JSHandle, Page } from 'playwright'; +import type iBlock from 'super/i-block/i-block'; + +import test from 'tests/config/unit/test'; +import Component from 'tests/helpers/component'; + +test.describe('v-aria:tablist', () => { + test.beforeEach(async ({demoPage}) => { + await demoPage.goto(); + }); + + test('role is set', async ({page}) => { + const target = await init(page); + + test.expect( + await target.evaluate((ctx) => { + const el = ctx.unsafe.block?.element('wrapper'); + + return el?.getAttribute('role'); + }) + ).toBe('tablist'); + }); + + test('multiselectable is set', async ({page}) => { + const target = await init(page, {multiple: true}); + + test.expect( + await target.evaluate((ctx) => { + const el = ctx.unsafe.block?.element('wrapper'); + + return el?.getAttribute('aria-multiselectable'); + }) + ).toBe('true'); + }); + + test('orientation is set', async ({page}) => { + const target = await init(page, {orientation: 'vertical'}); + + test.expect( + await target.evaluate((ctx) => { + const el = ctx.unsafe.block?.element('wrapper'); + + return el?.getAttribute('aria-orientation'); + }) + ).toBe('vertical'); + }); + + /** + * @param page + * @param attrs + */ + async function init(page: Page, attrs: Dictionary = {}): Promise> { + return Component.createComponent(page, 'b-list', { + attrs: { + items: [ + {label: 'foo', value: 0}, + {label: 'bar', value: 1} + ], + ...attrs + } + }); + } +}); diff --git a/src/core/component/directives/aria/test/unit/tabpanel.ts b/src/core/component/directives/aria/test/unit/tabpanel.ts new file mode 100644 index 0000000000..f99e3050d1 --- /dev/null +++ b/src/core/component/directives/aria/test/unit/tabpanel.ts @@ -0,0 +1,49 @@ +// @ts-check + +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ +import type { JSHandle, Page } from 'playwright'; +import type iBlock from 'super/i-block/i-block'; + +import test from 'tests/config/unit/test'; +import Component from 'tests/helpers/component'; + +test.describe('v-aria:tabpanel', () => { + test.beforeEach(async ({demoPage}) => { + await demoPage.goto(); + }); + + test('role is set', async ({page}) => { + const target = await init(page, {}); + + test.expect( + await target.evaluate((ctx) => ctx.$el?.getAttribute('role')) + ).toBe('tabpanel'); + }); + + test('no label passed', async ({page}) => { + const target = await init(page, {'v-aria:tabpanel': {}}); + + test.expect( + await target.evaluate((ctx) => ctx.$el?.hasAttribute('role')) + ).toBe(false); + }); + + /** + * @param page + * @param attrs + */ + async function init(page: Page, attrs: Dictionary = {}): Promise> { + return Component.createComponent(page, 'b-dummy', { + attrs: { + 'v-aria:tabpanel': {label: 'foo'}, + ...attrs + } + }); + } +}); diff --git a/src/core/component/directives/aria/test/unit/tree.ts b/src/core/component/directives/aria/test/unit/tree.ts new file mode 100644 index 0000000000..5f76193047 --- /dev/null +++ b/src/core/component/directives/aria/test/unit/tree.ts @@ -0,0 +1,103 @@ +// @ts-check + +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ +import type { JSHandle, Page } from 'playwright'; +import type iBlock from 'super/i-block/i-block'; + +import test from 'tests/config/unit/test'; +import Component from 'tests/helpers/component'; + +test.describe('v-aria:option', () => { + test.beforeEach(async ({demoPage}) => { + await demoPage.goto(); + }); + + test('role is set', async ({page}) => { + const target = await init(page); + + await page.waitForSelector('[role="group"]'); + + test.expect( + await target.evaluate(() => { + const + roots = document.querySelectorAll('[role="tree"]'), + groups = document.querySelectorAll('[role="group"]'); + + return [roots.length, groups.length]; + }) + ).toEqual([1, 2]); + }); + + test('orientation is set', async ({page}) => { + const target = await init(page, {orientation: 'horizontal'}); + + test.expect( + await target.evaluate((ctx) => { + const el = ctx.unsafe.block?.element('root'); + + return el?.getAttribute('aria-orientation'); + }) + ).toBe('horizontal'); + }); + + test('treeitem is expanded', async ({page}) => { + const target = await init(page); + + test.expect( + await target.evaluate((ctx) => { + const + fold: CanUndef = ctx.unsafe.block?.element('fold'), + items = ctx.unsafe.block?.elements('node'), + expandableItem = items?.[1]; + + const + res: Array>> = []; + + fold?.click(); + res.push(expandableItem?.getAttribute('aria-expanded')); + + fold?.click(); + res.push(expandableItem?.getAttribute('aria-expanded')); + + return res; + }) + ).toEqual(['true', 'false']); + }); + + /** + * @param page + * @param attrs + */ + async function init(page: Page, attrs: Dictionary = {}): Promise> { + return Component.createComponent(page, 'b-tree', { + attrs: { + item: 'b-checkbox', + items: [ + {id: 'bar', label: 'bar', attrs: {id: 'bar'}}, + { + id: 'foo', + label: 'foo', + children: [ + {id: 'fooone', label: 'foo1'}, + {id: 'footwo', label: 'foo2'}, + { + id: 'foothree', + label: 'foo3', + children: [{id: 'foothreeone', label: 'foo4'}] + }, + {id: 'foosix', label: 'foo5'} + ] + } + ], + ...attrs + } + }); + } +}); + diff --git a/src/core/component/directives/aria/test/unit/treeitem.ts b/src/core/component/directives/aria/test/unit/treeitem.ts new file mode 100644 index 0000000000..109de5887b --- /dev/null +++ b/src/core/component/directives/aria/test/unit/treeitem.ts @@ -0,0 +1,182 @@ +// @ts-check + +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ +import type { JSHandle, Page } from 'playwright'; +import type iBlock from 'super/i-block/i-block'; + +import test from 'tests/config/unit/test'; +import Component from 'tests/helpers/component'; + +test.describe('v-aria:treeitem', () => { + test.beforeEach(async ({demoPage}) => { + await demoPage.goto(); + }); + + test('role is set', async ({page}) => { + const target = await init(page); + + test.expect( + await target.evaluate((ctx) => { + const el = ctx.unsafe.block?.element('node'); + + return el?.getAttribute('role'); + }) + ).toBe('treeitem'); + }); + + test('aria-expanded is set', async ({page}) => { + const target = await init(page); + + await page.waitForSelector('[role="group"]'); + + test.expect( + await target.evaluate((ctx) => { + const + items = ctx.unsafe.block?.elements('node'), + expandableItem = items?.[1]; + + return expandableItem?.getAttribute('aria-expanded'); + }) + ).toBe('false'); + }); + + test('keyboard keys handle on vertical orientation', async ({page}) => { + const target = await init(page); + + await page.waitForSelector('[role="group"]'); + + test.expect( + await target.evaluate((ctx) => { + if (ctx.unsafe.block == null) { + return; + } + + const + input = document.querySelector('input'), + items = ctx.unsafe.block.elements('node'), + labels = document.querySelectorAll('label'); + + const res: any[] = []; + + input?.focus(); + + input?.dispatchEvent(new KeyboardEvent('keydown', {key: 'ArrowDown', bubbles: true})); + res.push(document.activeElement?.id === labels[1].getAttribute('for')); + + document.activeElement?.dispatchEvent(new KeyboardEvent('keydown', {key: 'ArrowUp', bubbles: true})); + res.push(document.activeElement?.id === labels[0].getAttribute('for')); + + document.activeElement?.dispatchEvent(new KeyboardEvent('keydown', {key: 'ArrowDown', bubbles: true})); + document.activeElement?.dispatchEvent(new KeyboardEvent('keydown', {key: 'Enter', bubbles: true})); + res.push(items[1].getAttribute('aria-expanded')); + + document.activeElement?.dispatchEvent(new KeyboardEvent('keydown', {key: 'Enter', bubbles: true})); + res.push(items[1].getAttribute('aria-expanded')); + + document.activeElement?.dispatchEvent(new KeyboardEvent('keydown', {key: 'ArrowRight', bubbles: true})); + res.push(items[1].getAttribute('aria-expanded')); + + document.activeElement?.dispatchEvent(new KeyboardEvent('keydown', {key: 'ArrowRight', bubbles: true})); + res.push(document.activeElement?.id === labels[2].getAttribute('for')); + + document.activeElement?.dispatchEvent(new KeyboardEvent('keydown', {key: 'ArrowLeft', bubbles: true})); + res.push(document.activeElement?.id === labels[1].getAttribute('for')); + + document.activeElement?.dispatchEvent(new KeyboardEvent('keydown', {key: 'ArrowLeft', bubbles: true})); + res.push(items[1].getAttribute('aria-expanded')); + + document.activeElement?.dispatchEvent(new KeyboardEvent('keydown', {key: 'Home', bubbles: true})); + res.push(document.activeElement?.id === labels[0].getAttribute('for')); + + document.activeElement?.dispatchEvent(new KeyboardEvent('keydown', {key: 'End', bubbles: true})); + res.push(document.activeElement?.id === labels[3].getAttribute('for')); + + return res; + }) + ).toEqual([true, true, 'true', 'false', 'true', true, true, 'false', true, true]); + }); + + test('keyboard keys handle on horizontal orientation', async ({page}) => { + const target = await init(page, {orientation: 'horizontal'}); + + await page.waitForSelector('[role="group"]'); + + test.expect( + await target.evaluate((ctx) => { + if (ctx.unsafe.block == null) { + return; + } + + const + input = document.querySelector('input'), + items = ctx.unsafe.block.elements('node'), + labels = document.querySelectorAll('label'); + + const res: any[] = []; + + input?.focus(); + + input?.dispatchEvent(new KeyboardEvent('keydown', {key: 'ArrowRight', bubbles: true})); + res.push(document.activeElement?.id === labels[1].getAttribute('for')); + + document.activeElement?.dispatchEvent(new KeyboardEvent('keydown', {key: 'ArrowLeft', bubbles: true})); + res.push(document.activeElement?.id === labels[0].getAttribute('for')); + + document.activeElement?.dispatchEvent(new KeyboardEvent('keydown', {key: 'ArrowRight', bubbles: true})); + document.activeElement?.dispatchEvent(new KeyboardEvent('keydown', {key: 'Enter', bubbles: true})); + res.push(items[1].getAttribute('aria-expanded')); + + document.activeElement?.dispatchEvent(new KeyboardEvent('keydown', {key: 'Enter', bubbles: true})); + res.push(items[1].getAttribute('aria-expanded')); + + document.activeElement?.dispatchEvent(new KeyboardEvent('keydown', {key: 'ArrowDown', bubbles: true})); + res.push(items[1].getAttribute('aria-expanded')); + + document.activeElement?.dispatchEvent(new KeyboardEvent('keydown', {key: 'ArrowDown', bubbles: true})); + res.push(document.activeElement?.id === labels[2].getAttribute('for')); + + document.activeElement?.dispatchEvent(new KeyboardEvent('keydown', {key: 'ArrowUp', bubbles: true})); + res.push(document.activeElement?.id === labels[1].getAttribute('for')); + + document.activeElement?.dispatchEvent(new KeyboardEvent('keydown', {key: 'ArrowUp', bubbles: true})); + res.push(items[1].getAttribute('aria-expanded')); + + document.activeElement?.dispatchEvent(new KeyboardEvent('keydown', {key: 'Home', bubbles: true})); + res.push(document.activeElement?.id === labels[0].getAttribute('for')); + + document.activeElement?.dispatchEvent(new KeyboardEvent('keydown', {key: 'End', bubbles: true})); + res.push(document.activeElement?.id === labels[3].getAttribute('for')); + + return res; + }) + ).toEqual([true, true, 'true', 'false', 'true', true, true, 'false', true, true]); + }); + + /** + * @param page + * @param attrs + */ + async function init(page: Page, attrs: Dictionary = {}): Promise> { + return Component.createComponent(page, 'b-tree', { + attrs: { + item: 'b-checkbox', + items: [ + {id: 'bar', label: 'bar'}, + { + id: 'foo', + label: 'foo', + children: [{id: 'fooone', label: 'foo1'}] + }, + {id: 'bla', label: 'bla'} + ], + ...attrs + } + }); + } +}); diff --git a/src/core/component/directives/id/test/unit/functional.ts b/src/core/component/directives/id/test/unit/functional.ts new file mode 100644 index 0000000000..7c992d0aee --- /dev/null +++ b/src/core/component/directives/id/test/unit/functional.ts @@ -0,0 +1,47 @@ +// @ts-check + +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ +import type { JSHandle, Page } from 'playwright'; +import type bDummy from 'dummies/b-dummy/b-dummy'; + +import test from 'tests/config/unit/test'; +import Component from 'tests/helpers/component'; + +test.describe('v-id', () => { + test.beforeEach(async ({demoPage}) => { + await demoPage.goto(); + }); + + test('id is added', async ({page}) => { + const target = await init(page); + const id = await target.evaluate((ctx) => ctx.$root.unsafe.dom.getId('dummy')); + + test.expect( + await target.evaluate((ctx) => ctx.$el?.id) + ).toBe(id); + }); + + test('preserve mod', async ({page}) => { + const target = await init(page, {'v-id.preserve': 'dummy', id: 'foo'}); + + test.expect( + await target.evaluate((ctx) => ctx.$el?.id) + ).toBe('foo'); + }); + + /** + * @param page + * @param attrs + */ + async function init(page: Page, attrs: Dictionary = {}): Promise> { + return Component.createComponent(page, 'b-dummy', { + attrs: {'v-id': 'dummy', ...attrs} + }); + } +}); From 9d348d9b60f9814a9350faab06f78e8769968e98 Mon Sep 17 00:00:00 2001 From: Aleksandr Bunin Date: Tue, 2 Aug 2022 18:25:43 +0300 Subject: [PATCH 017/185] fixes --- src/base/b-tree/b-tree.ts | 3 +-- .../component/directives/aria/roles-engines/combobox.ts | 1 + src/core/component/directives/aria/roles-engines/tab.ts | 1 + src/traits/i-access/i-access.ts | 7 ++++--- 4 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/base/b-tree/b-tree.ts b/src/base/b-tree/b-tree.ts index 1d763346bb..7c08ed7ea5 100644 --- a/src/base/b-tree/b-tree.ts +++ b/src/base/b-tree/b-tree.ts @@ -31,8 +31,7 @@ export * from 'base/b-tree/interface'; export const $$ = symbolGenerator(); -interface bTree extends Trait { -} +interface bTree extends Trait {} /** * Component to render tree of any elements diff --git a/src/core/component/directives/aria/roles-engines/combobox.ts b/src/core/component/directives/aria/roles-engines/combobox.ts index 70921a3858..78ece722c3 100644 --- a/src/core/component/directives/aria/roles-engines/combobox.ts +++ b/src/core/component/directives/aria/roles-engines/combobox.ts @@ -7,6 +7,7 @@ */ import AriaRoleEngine, { DirectiveOptions } from 'core/component/directives/aria/interface'; + import type { ComboboxParams } from 'core/component/directives/aria/roles-engines/interface'; import type iAccess from 'traits/i-access/i-access'; diff --git a/src/core/component/directives/aria/roles-engines/tab.ts b/src/core/component/directives/aria/roles-engines/tab.ts index ce7b7d845b..ab2d0b3309 100644 --- a/src/core/component/directives/aria/roles-engines/tab.ts +++ b/src/core/component/directives/aria/roles-engines/tab.ts @@ -10,6 +10,7 @@ */ import AriaRoleEngine, { DirectiveOptions, keyCodes } from 'core/component/directives/aria/interface'; + import type { TabParams } from 'core/component/directives/aria/roles-engines/interface'; import type iAccess from 'traits/i-access/i-access'; import type iBlock from 'super/i-block/i-block'; diff --git a/src/traits/i-access/i-access.ts b/src/traits/i-access/i-access.ts index c5c6aced57..d49cadad51 100644 --- a/src/traits/i-access/i-access.ts +++ b/src/traits/i-access/i-access.ts @@ -283,7 +283,7 @@ export default abstract class iAccess { let focusableIter = intoIter(focusableElems ?? []); - if (el?.hasAttribute('tabindex')) { + if (ctx?.matches(FOCUSABLE_SELECTOR)) { focusableIter = sequence(focusableIter, intoIter([el])); } @@ -379,7 +379,8 @@ export default abstract class iAccess { /** * Removes all children of the specified element that can be focused from the Tab toggle sequence. - * In effect, these elements are set to -1 for the tabindex attribute + * In effect, these elements are set to -1 for the tabindex attribute. + * * @param el - a context to search, if not set, the root element of the component will be used */ removeAllFromTabSequence(el?: Element): boolean { @@ -389,7 +390,7 @@ export default abstract class iAccess { /** * Reverts all children of the specified element that can be focused to the Tab toggle sequence. * This method is used to restore the state of elements to the state - * they had before removeAllFromTabSequence was applied + * they had before 'removeAllFromTabSequence' was applied. * * @param el - a context to search, if not set, the root element of the component will be used */ From 4d1edf399d05d954639f565d994a6d857b82b2ff Mon Sep 17 00:00:00 2001 From: Aleksandr Bunin Date: Wed, 3 Aug 2022 13:06:58 +0300 Subject: [PATCH 018/185] add controls interface and fix roles --- .../directives/aria/roles-engines/controls.ts | 45 ++++++++++++++----- .../directives/aria/roles-engines/dialog.ts | 7 ++- .../aria/roles-engines/interface.ts | 4 ++ 3 files changed, 45 insertions(+), 11 deletions(-) diff --git a/src/core/component/directives/aria/roles-engines/controls.ts b/src/core/component/directives/aria/roles-engines/controls.ts index 76b2d597f9..ea1ab731ea 100644 --- a/src/core/component/directives/aria/roles-engines/controls.ts +++ b/src/core/component/directives/aria/roles-engines/controls.ts @@ -6,26 +6,39 @@ * https://github.com/V4Fire/Client/blob/master/LICENSE */ -import AriaRoleEngine from 'core/component/directives/aria/interface'; +import AriaRoleEngine, { DirectiveOptions } from 'core/component/directives/aria/interface'; +import type { ControlsParams } from 'core/component/directives/aria/roles-engines/interface'; export default class ControlsEngine extends AriaRoleEngine { + /** + * Passed directive params + */ + params: ControlsParams; + + constructor(options: DirectiveOptions) { + super(options); + + this.params = this.options.binding.value; + } + /** * Sets base aria attributes for current role */ init(): void { const {vnode, binding, el} = this.options, - {modifiers, value} = binding, - {fakeContext: ctx} = vnode; + {modifiers} = binding, + {fakeContext: ctx} = vnode, + {for: forId} = this.params; - if (value?.for == null) { + if (forId == null) { Object.throw('Controls aria directive expects the id of controlling elements to be passed as "for" prop'); return; } const - isForPropArray = Object.isArray(value.for), - isForPropArrayOfTuples = Object.isArray(value.for) && Object.isArray(value.for[0]); + isForPropArray = Object.isArray(forId), + isForPropArrayOfTuples = isForPropArray && Object.isArray(forId[0]); if (modifiers != null && Object.size(modifiers) > 0) { ctx?.$nextTick().then(() => { @@ -33,19 +46,31 @@ export default class ControlsEngine extends AriaRoleEngine { roleName = Object.keys(modifiers)[0], elems = el.querySelectorAll(`[role=${roleName}]`); - if (isForPropArray && value.for.length !== elems.length) { + if (isForPropArray && forId.length !== elems.length) { Object.throw('Controls aria directive expects prop "for" length to be equal to amount of elements with specified role or string type'); return; } elems.forEach((el, i) => { - el.setAttribute('aria-controls', isForPropArray ? value.for[i] : value.for); + if (Object.isString(forId)) { + el.setAttribute('aria-controls', forId); + return; + } + + const + id = forId[i]; + + if (Object.isString(id)) { + el.setAttribute('aria-controls', id); + } }); }); } else if (isForPropArrayOfTuples) { - value.for.forEach(([elId, controlsId]) => { - const element = el.querySelector(`#${elId}`); + forId.forEach((param) => { + const + [elId, controlsId] = param, + element = el.querySelector(`#${elId}`); element?.setAttribute('aria-controls', controlsId); }); diff --git a/src/core/component/directives/aria/roles-engines/dialog.ts b/src/core/component/directives/aria/roles-engines/dialog.ts index de7e868539..beb96370a6 100644 --- a/src/core/component/directives/aria/roles-engines/dialog.ts +++ b/src/core/component/directives/aria/roles-engines/dialog.ts @@ -7,6 +7,7 @@ */ import AriaRoleEngine from 'core/component/directives/aria/interface'; +import iOpen from 'traits/i-open/i-open'; export default class DialogEngine extends AriaRoleEngine { /** @@ -14,9 +15,13 @@ export default class DialogEngine extends AriaRoleEngine { */ init(): void { const - {el} = this.options; + {el, vnode} = this.options; el.setAttribute('role', 'dialog'); el.setAttribute('aria-modal', 'true'); + + if (!iOpen.is(vnode.fakeContext)) { + Object.throw('Dialog aria directive expects the component to realize iOpen interface'); + } } } diff --git a/src/core/component/directives/aria/roles-engines/interface.ts b/src/core/component/directives/aria/roles-engines/interface.ts index 3ab7869aa3..b30a06b543 100644 --- a/src/core/component/directives/aria/roles-engines/interface.ts +++ b/src/core/component/directives/aria/roles-engines/interface.ts @@ -32,3 +32,7 @@ export interface ComboboxParams { openEvent(cb: Function): void; closeEvent(cb: Function): void; } + +export interface ControlsParams { + for: CanArray | Array<[string, string]> | undefined; +} From b12c6f4a9a615c7cd05fd5b8b760e1b52f162f3b Mon Sep 17 00:00:00 2001 From: Aleksandr Bunin Date: Wed, 3 Aug 2022 13:07:18 +0300 Subject: [PATCH 019/185] add readme --- src/core/component/directives/aria/README.md | 101 ++++++++++++++++--- 1 file changed, 86 insertions(+), 15 deletions(-) diff --git a/src/core/component/directives/aria/README.md b/src/core/component/directives/aria/README.md index 15c7ff29f8..334d9c20ad 100644 --- a/src/core/component/directives/aria/README.md +++ b/src/core/component/directives/aria/README.md @@ -8,16 +8,25 @@ This module provides a directive to add aria attributes and logic to elements th < &__foo v-aria.#bla < &__foo v-aria = {labelledby: dom.getId('bla')} - ``` ## Available modifiers: -- .#[string] (ex. '.#title') the same as = {labelledby: [id-'title']} +- .#[string] (ex. '.#title') + +Example +``` +< v-aria.#title +the same as +< v-aria = {labelledby: dom.getId('title')} +``` -- Roles: -- controls: +- `controls`: +The engine to set aria-controls attribute. +For more information go to [`https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes/aria-controls`]. + Directive can be added to any tag that includes tag with needed role. Role should be passed as a modifier. ID or IDs are passed as value. ID could be single or multiple written in string with space between. @@ -25,21 +34,21 @@ ID could be single or multiple written in string with space between. There are two ways to use this engine: 1. To add role as a modifier to which passed IDs in `for` value should be added. `for` could be `string` or `string[]`. If element controls several elements `for` should be passed as a string with IDs separated with space. -(!) Notice that this role attribute should already be added to the element. The engine does not set passed role to the current element. +(!) Notice that this role attribute should already be added to the element. The engine does not set passed role to any element. Example: ``` < &__foo v-aria:controls.tab = {for: 'id1 id2 id3'} -converts to - -< tab aria-controls = "id1 id2 id3" role = "tab" +the same as +< &__foo + < button aria-controls = "id1 id2 id3" role = "tab" ``` 2. To pass value `for` as an array of tuples. -First id in a tuple is an id of an element to which one the aria attributes should be added. +First id in a tuple is an id of an element to add the aria attributes. The second one is an id of an element to set as value in aria-controls attribute. -(!) Notice that id attribute should already be added to the element. The engine does not set passed ids to the elements. +(!) Notice that id attribute should already be added to the element. The engine does not set passed ids to any element. Example: ``` @@ -47,20 +56,82 @@ Example: < span :id = "id1" < span :id = "id2" -converts to +the same as < &__foo < span :id = "id1" aria-controls = "id3" < span :id = "id2" aria-controls = "id4" ``` -- tabs: -Tabs always expect the 'controls' role engine to be added. +- `dialog`: +The engine to set `dialog` role attribute. +For more information go to [`https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/dialog_role`]. + +Always expects `iOpen` trait to be realized. + +- `tab`: +The engine to set `tab` role attribute. +For more information go to [`https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/tab_role`]. + +Tabs always expect the `controls` role engine to be added. It should 'point' to the element with role `tabpanel`. + +Example: +``` +< button v-aria:tab | v-aria:controls = {for: 'id1'} + +< v-aria:tabpanel = {labelledby: 'id2'} | :id = 'id1' + < span :id = 'id2' + // content +``` + +- `tablist`: +The engine to set `tablist` role attribute. +For more information go to [`https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/tablist_role`]. + +- `tabpanel`: +The engine to set `tablist` role attribute. +For more information go to [`https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/tabpanel_role`]. + +Always expects `label` or `labelledby` params to be passed. + +Example: +``` +< v-aria:tabpanel = {labelledby: 'id1'} + < span :id = 'id1' + // content +``` + +- `tree`: +The engine to set `tree` role attribute. +For more information go to [`https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/tree_role`]. +- `treeitem`: +The engine to set `treeitem` role attribute. +For more information go to [`https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/treeitem_role`]. -## Available standard values: -Value is expected to always be an object type. Possible keys: +- `combobox`: +The engine to set `combobox` role attribute. +For more information go to [`https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/combobox_role`]. + +- `listbox`: +The engine to set `listbox` role attribute. +For more information go to [`https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/listbox_role`]. + +- `option`: +The engine to set `option` role attribute. +For more information go to [`https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/option_role`]. + +## Available values: +Parameters passed to the directive are expected to always be object type. Any directive handle common keys: - label +Expects string as 'title' to the specified element + - labelledby +Expects string as an id of the element. This element is a 'title' of to the specified element + - description +Expects string as expanded 'description' to the specified element + - describedby -- id +Expects string as an id of the element. This element is an expanded 'description' to the specified element + +Also, there are specific role keys. For info go to [`core/component/directives/role-engines/interface.ts`](core/component/directives/role-engines/interface.ts). From 4127c80b24ceaccaed2eea051e2cec75a6849d70 Mon Sep 17 00:00:00 2001 From: Aleksandr Bunin Date: Wed, 3 Aug 2022 13:20:20 +0300 Subject: [PATCH 020/185] refactoring --- .../component/directives/aria/roles-engines/controls.ts | 1 + .../component/directives/aria/roles-engines/interface.ts | 2 +- src/core/component/directives/aria/test/unit/treeitem.ts | 2 +- .../p-v4-components-demo/b-v4-component-demo/CHANGELOG.md | 6 ++++++ 4 files changed, 9 insertions(+), 2 deletions(-) diff --git a/src/core/component/directives/aria/roles-engines/controls.ts b/src/core/component/directives/aria/roles-engines/controls.ts index ea1ab731ea..efa51321a8 100644 --- a/src/core/component/directives/aria/roles-engines/controls.ts +++ b/src/core/component/directives/aria/roles-engines/controls.ts @@ -31,6 +31,7 @@ export default class ControlsEngine extends AriaRoleEngine { {fakeContext: ctx} = vnode, {for: forId} = this.params; + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition if (forId == null) { Object.throw('Controls aria directive expects the id of controlling elements to be passed as "for" prop'); return; diff --git a/src/core/component/directives/aria/roles-engines/interface.ts b/src/core/component/directives/aria/roles-engines/interface.ts index b30a06b543..8d2db1ab10 100644 --- a/src/core/component/directives/aria/roles-engines/interface.ts +++ b/src/core/component/directives/aria/roles-engines/interface.ts @@ -34,5 +34,5 @@ export interface ComboboxParams { } export interface ControlsParams { - for: CanArray | Array<[string, string]> | undefined; + for: CanArray | Array<[string, string]>; } diff --git a/src/core/component/directives/aria/test/unit/treeitem.ts b/src/core/component/directives/aria/test/unit/treeitem.ts index 109de5887b..58b3b56b50 100644 --- a/src/core/component/directives/aria/test/unit/treeitem.ts +++ b/src/core/component/directives/aria/test/unit/treeitem.ts @@ -118,7 +118,7 @@ test.describe('v-aria:treeitem', () => { items = ctx.unsafe.block.elements('node'), labels = document.querySelectorAll('label'); - const res: any[] = []; + const res: Array> = []; input?.focus(); diff --git a/src/pages/p-v4-components-demo/b-v4-component-demo/CHANGELOG.md b/src/pages/p-v4-components-demo/b-v4-component-demo/CHANGELOG.md index 76418ffae2..edbedc52a3 100644 --- a/src/pages/p-v4-components-demo/b-v4-component-demo/CHANGELOG.md +++ b/src/pages/p-v4-components-demo/b-v4-component-demo/CHANGELOG.md @@ -9,6 +9,12 @@ Changelog > - :house: [Internal] > - :nail_care: [Polish] +## v3.?.? (2022-0?-??) + +#### :rocket: New Feature + +* Added a new directive `v-id` + ## v3.0.0-rc.37 (2020-07-20) #### :house: Internal From 58af58417f0236f226748666ded9ed184e934016 Mon Sep 17 00:00:00 2001 From: Aleksandr Bunin Date: Wed, 3 Aug 2022 13:27:09 +0300 Subject: [PATCH 021/185] refactoring --- src/core/component/directives/aria/README.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/core/component/directives/aria/README.md b/src/core/component/directives/aria/README.md index 334d9c20ad..90463467fc 100644 --- a/src/core/component/directives/aria/README.md +++ b/src/core/component/directives/aria/README.md @@ -66,13 +66,13 @@ the same as The engine to set `dialog` role attribute. For more information go to [`https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/dialog_role`]. -Always expects `iOpen` trait to be realized. +Expects `iOpen` trait to be realized. - `tab`: The engine to set `tab` role attribute. For more information go to [`https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/tab_role`]. -Tabs always expect the `controls` role engine to be added. It should 'point' to the element with role `tabpanel`. +Tabs expect the `controls` role engine to be added in addition. ID passed to `controls` engine should be the id of the element with role `tabpanel`. Example: ``` @@ -91,7 +91,7 @@ For more information go to [`https://developer.mozilla.org/en-US/docs/Web/Access The engine to set `tablist` role attribute. For more information go to [`https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/tabpanel_role`]. -Always expects `label` or `labelledby` params to be passed. +Expects `label` or `labelledby` params to be passed. Example: ``` @@ -108,6 +108,8 @@ For more information go to [`https://developer.mozilla.org/en-US/docs/Web/Access The engine to set `treeitem` role attribute. For more information go to [`https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/treeitem_role`]. +Expects `iAccess` trait to be realized. + - `combobox`: The engine to set `combobox` role attribute. For more information go to [`https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/combobox_role`]. From 40304fb16446bbb850c8840bb8e450854b62e75b Mon Sep 17 00:00:00 2001 From: Aleksandr Bunin Date: Tue, 9 Aug 2022 16:43:04 +0300 Subject: [PATCH 022/185] refactoring --- package.json | 4 +- src/base/b-list/b-list.ts | 25 +++-- src/base/b-tree/b-tree.ts | 11 +- src/core/component/directives/aria/README.md | 104 +----------------- .../component/directives/aria/aria-setter.ts | 54 +++++++-- src/core/component/directives/aria/index.ts | 19 +--- .../component/directives/aria/interface.ts | 28 ----- .../aria/roles-engines/combobox/CHANGELOG.md | 16 +++ .../aria/roles-engines/combobox/README.md | 14 +++ .../{combobox.ts => combobox/index.ts} | 21 ++-- .../aria/roles-engines/combobox/interface.ts | 16 +++ .../aria/roles-engines/controls/CHANGELOG.md | 16 +++ .../aria/roles-engines/controls/README.md | 50 +++++++++ .../{controls.ts => controls/index.ts} | 16 ++- .../aria/roles-engines/controls/interface.ts | 11 ++ .../aria/roles-engines/dialog/CHANGELOG.md | 16 +++ .../aria/roles-engines/dialog/README.md | 16 +++ .../{dialog.ts => dialog/index.ts} | 13 +-- .../directives/aria/roles-engines/index.ts | 22 ++-- .../aria/roles-engines/interface.ts | 86 ++++++++++----- .../aria/roles-engines/listbox/CHANGELOG.md | 16 +++ .../aria/roles-engines/listbox/README.md | 14 +++ .../{listbox.ts => listbox/index.ts} | 11 +- .../directives/aria/roles-engines/option.ts | 34 ------ .../aria/roles-engines/option/CHANGELOG.md | 16 +++ .../aria/roles-engines/option/README.md | 13 +++ .../aria/roles-engines/option/index.ts | 39 +++++++ .../aria/roles-engines/option/interface.ts | 14 +++ .../aria/roles-engines/tab/CHANGELOG.md | 16 +++ .../aria/roles-engines/tab/README.md | 27 +++++ .../roles-engines/{tab.ts => tab/index.ts} | 55 +++++---- .../aria/roles-engines/tab/interface.ts | 17 +++ .../aria/roles-engines/tablist/CHANGELOG.md | 16 +++ .../aria/roles-engines/tablist/README.md | 14 +++ .../{tablist.ts => tablist/index.ts} | 20 +++- .../aria/roles-engines/tablist/interface.ts | 12 ++ .../aria/roles-engines/tabpanel/CHANGELOG.md | 16 +++ .../aria/roles-engines/tabpanel/README.md | 25 +++++ .../{tabpanel.ts => tabpanel/index.ts} | 7 +- .../aria/roles-engines/tree/CHANGELOG.md | 16 +++ .../aria/roles-engines/tree/README.md | 15 +++ .../roles-engines/{tree.ts => tree/index.ts} | 26 ++--- .../aria/roles-engines/tree/interface.ts | 15 +++ .../aria/roles-engines/treeitem/CHANGELOG.md | 16 +++ .../aria/roles-engines/treeitem/README.md | 17 +++ .../{treeitem.ts => treeitem/index.ts} | 61 ++++------ .../aria/roles-engines/treeitem/interface.ts | 16 +++ .../component/render-function/CHANGELOG.md | 6 + src/form/b-select/b-select.ts | 10 +- src/traits/i-access/i-access.ts | 14 +-- 50 files changed, 772 insertions(+), 380 deletions(-) create mode 100644 src/core/component/directives/aria/roles-engines/combobox/CHANGELOG.md create mode 100644 src/core/component/directives/aria/roles-engines/combobox/README.md rename src/core/component/directives/aria/roles-engines/{combobox.ts => combobox/index.ts} (73%) create mode 100644 src/core/component/directives/aria/roles-engines/combobox/interface.ts create mode 100644 src/core/component/directives/aria/roles-engines/controls/CHANGELOG.md create mode 100644 src/core/component/directives/aria/roles-engines/controls/README.md rename src/core/component/directives/aria/roles-engines/{controls.ts => controls/index.ts} (80%) create mode 100644 src/core/component/directives/aria/roles-engines/controls/interface.ts create mode 100644 src/core/component/directives/aria/roles-engines/dialog/CHANGELOG.md create mode 100644 src/core/component/directives/aria/roles-engines/dialog/README.md rename src/core/component/directives/aria/roles-engines/{dialog.ts => dialog/index.ts} (56%) create mode 100644 src/core/component/directives/aria/roles-engines/listbox/CHANGELOG.md create mode 100644 src/core/component/directives/aria/roles-engines/listbox/README.md rename src/core/component/directives/aria/roles-engines/{listbox.ts => listbox/index.ts} (50%) delete mode 100644 src/core/component/directives/aria/roles-engines/option.ts create mode 100644 src/core/component/directives/aria/roles-engines/option/CHANGELOG.md create mode 100644 src/core/component/directives/aria/roles-engines/option/README.md create mode 100644 src/core/component/directives/aria/roles-engines/option/index.ts create mode 100644 src/core/component/directives/aria/roles-engines/option/interface.ts create mode 100644 src/core/component/directives/aria/roles-engines/tab/CHANGELOG.md create mode 100644 src/core/component/directives/aria/roles-engines/tab/README.md rename src/core/component/directives/aria/roles-engines/{tab.ts => tab/index.ts} (69%) create mode 100644 src/core/component/directives/aria/roles-engines/tab/interface.ts create mode 100644 src/core/component/directives/aria/roles-engines/tablist/CHANGELOG.md create mode 100644 src/core/component/directives/aria/roles-engines/tablist/README.md rename src/core/component/directives/aria/roles-engines/{tablist.ts => tablist/index.ts} (59%) create mode 100644 src/core/component/directives/aria/roles-engines/tablist/interface.ts create mode 100644 src/core/component/directives/aria/roles-engines/tabpanel/CHANGELOG.md create mode 100644 src/core/component/directives/aria/roles-engines/tabpanel/README.md rename src/core/component/directives/aria/roles-engines/{tabpanel.ts => tabpanel/index.ts} (73%) create mode 100644 src/core/component/directives/aria/roles-engines/tree/CHANGELOG.md create mode 100644 src/core/component/directives/aria/roles-engines/tree/README.md rename src/core/component/directives/aria/roles-engines/{tree.ts => tree/index.ts} (52%) create mode 100644 src/core/component/directives/aria/roles-engines/tree/interface.ts create mode 100644 src/core/component/directives/aria/roles-engines/treeitem/CHANGELOG.md create mode 100644 src/core/component/directives/aria/roles-engines/treeitem/README.md rename src/core/component/directives/aria/roles-engines/{treeitem.ts => treeitem/index.ts} (72%) create mode 100644 src/core/component/directives/aria/roles-engines/treeitem/interface.ts diff --git a/package.json b/package.json index e17106f728..9a00a89922 100644 --- a/package.json +++ b/package.json @@ -144,7 +144,7 @@ "@types/glob": "7.2.0", "@types/semver": "7.3.10", "@types/webpack": "5.28.0", - "@v4fire/core": "3.86.2", + "@v4fire/core": "3.87.0", "@v4fire/linters": "1.9.0", "husky": "7.0.4", "nyc": "15.1.0", @@ -152,7 +152,7 @@ "webpack": "5.70.0" }, "peerDependencies": { - "@v4fire/core": "^3.86.2", + "@v4fire/core": "^3.87.0", "webpack": "^5.70.0" } } diff --git a/src/base/b-list/b-list.ts b/src/base/b-list/b-list.ts index f9636ec772..90fae79c76 100644 --- a/src/base/b-list/b-list.ts +++ b/src/base/b-list/b-list.ts @@ -36,7 +36,8 @@ export * from 'base/b-list/interface'; export const $$ = symbolGenerator(); -interface bList extends Trait {} +interface bList extends Trait { +} /** * Component to create a list of tabs/links @@ -693,13 +694,13 @@ class bList extends iData implements iVisible, iWidth, iItems, iAccess { } /** - * Returns a dictionary with configurations for the v-aria directive used as a tablist + * Returns a dictionary with configurations for the `v-aria` directive used as a tablist * @param role */ protected getAriaConfig(role: 'tablist'): Dictionary; /** - * Returns a dictionary with configurations for the v-aria directive used as a tab + * Returns a dictionary with configurations for the `v-aria` directive used as a tab * * @param role * @param item - tab item data @@ -711,7 +712,7 @@ class bList extends iData implements iVisible, iWidth, iItems, iAccess { const isActive = this.isActive.bind(this, item?.value); - const changeEvent = (cb: Function) => { + const bindChangeEvent = (cb: Function) => { this.on('change', () => { if (Object.isSet(this.active)) { cb(this.block?.elements('link', {active: true})); @@ -728,19 +729,23 @@ class bList extends iData implements iVisible, iWidth, iItems, iAccess { }; const tabConfig = { - preSelected: this.active != null, + hasDefaultSelectedTabs: this.active != null, isFirst: i === 0, orientation: this.orientation, - changeEvent, - get isActive() { + '@change': bindChangeEvent, + + get isSelected() { return isActive(); } }; switch (role) { - case 'tablist': return tablistConfig; - case 'tab': return tabConfig; - default: return {}; + case 'tablist': + return tablistConfig; + case 'tab': + return tabConfig; + default: + return {}; } } diff --git a/src/base/b-tree/b-tree.ts b/src/base/b-tree/b-tree.ts index 7c08ed7ea5..013cda0154 100644 --- a/src/base/b-tree/b-tree.ts +++ b/src/base/b-tree/b-tree.ts @@ -252,13 +252,13 @@ class bTree extends iData implements iItems, iAccess { } /** - * Returns a dictionary with configurations for the v-aria directive used as a tree + * Returns a dictionary with configurations for the `v-aria` directive used as a tree * @param role */ protected getAriaConfig(role: 'tree'): Dictionary /** - * Returns a dictionary with configurations for the v-aria directive used as a treeitem + * Returns a dictionary with configurations for the `v-aria` directive used as a treeitem * * @param role * @param item - tab item data @@ -289,21 +289,24 @@ class bTree extends iData implements iItems, iAccess { const treeConfig = { isRoot: this.top == null, orientation: this.orientation, - changeEvent: (cb: Function) => { + '@change': (cb: Function) => { this.on('fold', (ctx, el, item, value) => cb(el, value)); } }; const treeitemConfig = { - isRootFirstItem: this.top == null && i === 0, + isFirstRootItem: this.top == null && i === 0, orientation: this.orientation, toggleFold, + get rootElement() { return root(); }, + get isExpanded() { return getFoldedMod() === 'false'; }, + get isExpandable() { return item?.children != null; } diff --git a/src/core/component/directives/aria/README.md b/src/core/component/directives/aria/README.md index 90463467fc..aabae564b4 100644 --- a/src/core/component/directives/aria/README.md +++ b/src/core/component/directives/aria/README.md @@ -7,7 +7,7 @@ This module provides a directive to add aria attributes and logic to elements th ``` < &__foo v-aria.#bla -< &__foo v-aria = {labelledby: dom.getId('bla')} +< &__foo v-aria = {label: 'title'} ``` ## Available modifiers: @@ -22,106 +22,6 @@ the same as < v-aria = {labelledby: dom.getId('title')} ``` --- Roles: -- `controls`: -The engine to set aria-controls attribute. -For more information go to [`https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes/aria-controls`]. - -Directive can be added to any tag that includes tag with needed role. Role should be passed as a modifier. -ID or IDs are passed as value. -ID could be single or multiple written in string with space between. - -There are two ways to use this engine: -1. To add role as a modifier to which passed IDs in `for` value should be added. `for` could be `string` or `string[]`. -If element controls several elements `for` should be passed as a string with IDs separated with space. -(!) Notice that this role attribute should already be added to the element. The engine does not set passed role to any element. - -Example: -``` -< &__foo v-aria:controls.tab = {for: 'id1 id2 id3'} - -the same as -< &__foo - < button aria-controls = "id1 id2 id3" role = "tab" -``` - -2. To pass value `for` as an array of tuples. -First id in a tuple is an id of an element to add the aria attributes. -The second one is an id of an element to set as value in aria-controls attribute. -(!) Notice that id attribute should already be added to the element. The engine does not set passed ids to any element. - -Example: -``` -< &__foo v-aria:controls = {for: [[id1, id3], [id2, id4]]} - < span :id = "id1" - < span :id = "id2" - -the same as -< &__foo - < span :id = "id1" aria-controls = "id3" - < span :id = "id2" aria-controls = "id4" -``` - -- `dialog`: -The engine to set `dialog` role attribute. -For more information go to [`https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/dialog_role`]. - -Expects `iOpen` trait to be realized. - -- `tab`: -The engine to set `tab` role attribute. -For more information go to [`https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/tab_role`]. - -Tabs expect the `controls` role engine to be added in addition. ID passed to `controls` engine should be the id of the element with role `tabpanel`. - -Example: -``` -< button v-aria:tab | v-aria:controls = {for: 'id1'} - -< v-aria:tabpanel = {labelledby: 'id2'} | :id = 'id1' - < span :id = 'id2' - // content -``` - -- `tablist`: -The engine to set `tablist` role attribute. -For more information go to [`https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/tablist_role`]. - -- `tabpanel`: -The engine to set `tablist` role attribute. -For more information go to [`https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/tabpanel_role`]. - -Expects `label` or `labelledby` params to be passed. - -Example: -``` -< v-aria:tabpanel = {labelledby: 'id1'} - < span :id = 'id1' - // content -``` - -- `tree`: -The engine to set `tree` role attribute. -For more information go to [`https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/tree_role`]. - -- `treeitem`: -The engine to set `treeitem` role attribute. -For more information go to [`https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/treeitem_role`]. - -Expects `iAccess` trait to be realized. - -- `combobox`: -The engine to set `combobox` role attribute. -For more information go to [`https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/combobox_role`]. - -- `listbox`: -The engine to set `listbox` role attribute. -For more information go to [`https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/listbox_role`]. - -- `option`: -The engine to set `option` role attribute. -For more information go to [`https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/option_role`]. - ## Available values: Parameters passed to the directive are expected to always be object type. Any directive handle common keys: - label @@ -136,4 +36,4 @@ Expects string as expanded 'description' to the specified element - describedby Expects string as an id of the element. This element is an expanded 'description' to the specified element -Also, there are specific role keys. For info go to [`core/component/directives/role-engines/interface.ts`](core/component/directives/role-engines/interface.ts). +Also, there are specific role keys. For info go to [`core/component/directives/role-engines/`](core/component/directives/role-engines/). diff --git a/src/core/component/directives/aria/aria-setter.ts b/src/core/component/directives/aria/aria-setter.ts index 5b402b2870..05d62f9549 100644 --- a/src/core/component/directives/aria/aria-setter.ts +++ b/src/core/component/directives/aria/aria-setter.ts @@ -8,18 +8,26 @@ import * as ariaRoles from 'core/component/directives/aria/roles-engines'; import Async from 'core/async'; -import AriaRoleEngine, { eventNames } from 'core/component/directives/aria/interface'; + import type iBlock from 'super/i-block/i-block'; +import type iAccess from 'traits/i-access/i-access'; + import type { DirectiveOptions } from 'core/component/directives/aria/interface'; +import { AriaRoleEngine, EngineOptions, EventNames } from 'core/component/directives/aria/roles-engines'; /** * Class-helper for making base operations for the directive */ -export default class AriaSetter extends AriaRoleEngine { +export default class AriaSetter { + /** + * Aria directive options + */ + readonly options: DirectiveOptions; + /** * Async instance for aria directive */ - override readonly async: Async; + readonly async: Async; /** * Role engine instance @@ -27,8 +35,7 @@ export default class AriaSetter extends AriaRoleEngine { role: CanUndef; constructor(options: DirectiveOptions) { - super(options); - + this.options = options; this.async = new Async(); this.setAriaRole(); @@ -79,12 +86,40 @@ export default class AriaSetter extends AriaRoleEngine { return; } - this.role = new ariaRoles[role](this.options); + const + engine = this.createEngineName(role), + options = this.createRoleOptions(); + + this.role = new ariaRoles[engine](options); + } + + /** + * Creates an engine name from a passed parameter + * @param role + */ + protected createEngineName(role: string): string { + return `${role.capitalize()}Engine`; + } + + /** + * Creates a dictionary with engine options + */ + protected createRoleOptions(): EngineOptions { + const + {el, binding, vnode} = this.options, + {value, modifiers} = binding; + + return { + el, + modifiers, + params: value, + ctx: Object.cast(vnode.fakeContext) + }; } /** * Sets aria-label, aria-labelledby, aria-description and aria-describedby attributes to the element - * from passed parameters + * from directive parameters */ protected setAriaLabel(): void { const @@ -132,9 +167,10 @@ export default class AriaSetter extends AriaRoleEngine { params = this.options.binding.value; for (const key in params) { - if (key in eventNames) { + if (key in EventNames) { + const - callback = this.role[eventNames[key]].bind(this.role), + callback = this.role[EventNames[key]].bind(this.role), property = params[key]; if (Object.isFunction(property)) { diff --git a/src/core/component/directives/aria/index.ts b/src/core/component/directives/aria/index.ts index 9ba81852e6..0155ea4e8c 100644 --- a/src/core/component/directives/aria/index.ts +++ b/src/core/component/directives/aria/index.ts @@ -14,8 +14,10 @@ import { ComponentEngine, VNode, VNodeDirective } from 'core/component/engines'; import AriaSetter from 'core/component/directives/aria/aria-setter'; +export * from 'core/component/directives/aria/interface'; + const - ariaMap = new WeakMap(); + ariaInstances = new WeakMap(); ComponentEngine.directive('aria', { inserted(el: HTMLElement, binding: VNodeDirective, vnode: VNode): void { @@ -26,23 +28,14 @@ ComponentEngine.directive('aria', { return; } - const - aria = new AriaSetter({el, binding, vnode}); - - ariaMap.set(el, aria); + ariaInstances.set(el, new AriaSetter({el, binding, vnode})); }, update(el: HTMLElement) { - const - aria: AriaSetter = ariaMap.get(el); - - aria.update(); + ariaInstances.get(el)?.update(); }, unbind(el: HTMLElement) { - const - aria: AriaSetter = ariaMap.get(el); - - aria.destroy(); + ariaInstances.get(el)?.destroy(); } }); diff --git a/src/core/component/directives/aria/interface.ts b/src/core/component/directives/aria/interface.ts index 3c18527bfc..8fd1a114f2 100644 --- a/src/core/component/directives/aria/interface.ts +++ b/src/core/component/directives/aria/interface.ts @@ -7,37 +7,9 @@ */ import type { VNode, VNodeDirective } from 'core/component/engines'; -import type Async from 'core/async'; export interface DirectiveOptions { el: HTMLElement; binding: VNodeDirective; vnode: VNode; } - -export default abstract class AriaRoleEngine { - readonly options: DirectiveOptions; - async: CanUndef; - - protected constructor(options: DirectiveOptions) { - this.options = options; - } - - abstract init(): void; -} - -export const enum keyCodes { - ENTER = 'Enter', - END = 'End', - HOME = 'Home', - LEFT = 'ArrowLeft', - UP = 'ArrowUp', - RIGHT = 'ArrowRight', - DOWN = 'ArrowDown' -} - -export enum eventNames { - openEvent = 'onOpen', - closeEvent = 'onClose', - changeEvent = 'onChange' -} diff --git a/src/core/component/directives/aria/roles-engines/combobox/CHANGELOG.md b/src/core/component/directives/aria/roles-engines/combobox/CHANGELOG.md new file mode 100644 index 0000000000..ba2980fb55 --- /dev/null +++ b/src/core/component/directives/aria/roles-engines/combobox/CHANGELOG.md @@ -0,0 +1,16 @@ +Changelog +========= + +> **Tags:** +> - :boom: [Breaking Change] +> - :rocket: [New Feature] +> - :bug: [Bug Fix] +> - :memo: [Documentation] +> - :house: [Internal] +> - :nail_care: [Polish] + +## v?.??.? (2022-0?-??) + +#### :rocket: New Feature + +* Initial release diff --git a/src/core/component/directives/aria/roles-engines/combobox/README.md b/src/core/component/directives/aria/roles-engines/combobox/README.md new file mode 100644 index 0000000000..57e6f9c96f --- /dev/null +++ b/src/core/component/directives/aria/roles-engines/combobox/README.md @@ -0,0 +1,14 @@ +# core/component/directives/aria/roles-engines/combobox + +This module provides an engine for `v-aria` directive. + +The engine to set `combobox` role attribute. +For more information about attributes go to [`https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/combobox_role`]. +For recommendations how to make accessible widget go to [`https://www.w3.org/WAI/ARIA/apg/patterns/combobox/`]. + +## Usage + +``` +< &__foo v-aria:combobox = {...} + +``` diff --git a/src/core/component/directives/aria/roles-engines/combobox.ts b/src/core/component/directives/aria/roles-engines/combobox/index.ts similarity index 73% rename from src/core/component/directives/aria/roles-engines/combobox.ts rename to src/core/component/directives/aria/roles-engines/combobox/index.ts index 78ece722c3..ad703aa2f3 100644 --- a/src/core/component/directives/aria/roles-engines/combobox.ts +++ b/src/core/component/directives/aria/roles-engines/combobox/index.ts @@ -6,31 +6,28 @@ * https://github.com/V4Fire/Client/blob/master/LICENSE */ -import AriaRoleEngine, { DirectiveOptions } from 'core/component/directives/aria/interface'; +import type { ComboboxParams } from 'core/component/directives/aria/roles-engines/combobox/interface'; +import { AriaRoleEngine, EngineOptions } from 'core/component/directives/aria/roles-engines/interface'; -import type { ComboboxParams } from 'core/component/directives/aria/roles-engines/interface'; -import type iAccess from 'traits/i-access/i-access'; - -export default class ComboboxEngine extends AriaRoleEngine { +export class ComboboxEngine extends AriaRoleEngine { /** - * Passed directive params + * Engine params */ params: ComboboxParams; /** * First focusable element inside the element with directive or this element if there is no focusable inside */ - el: HTMLElement; + override el: HTMLElement; - constructor(options: DirectiveOptions) { + constructor(options: EngineOptions) { super(options); const - {el} = this.options, - ctx = Object.cast(this.options.vnode.fakeContext); + {el} = this; - this.el = (>ctx.findFocusableElement()) ?? el; - this.params = this.options.binding.value; + this.el = this.ctx?.findFocusableElement() ?? el; + this.params = options.params; } /** diff --git a/src/core/component/directives/aria/roles-engines/combobox/interface.ts b/src/core/component/directives/aria/roles-engines/combobox/interface.ts new file mode 100644 index 0000000000..7675f2fbc4 --- /dev/null +++ b/src/core/component/directives/aria/roles-engines/combobox/interface.ts @@ -0,0 +1,16 @@ +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ + +import type { EventBinder } from 'core/component/directives/aria/roles-engines/interface'; + +export interface ComboboxParams { + isMultiple: boolean; + '@change': EventBinder; + '@open': EventBinder; + '@close': EventBinder; +} diff --git a/src/core/component/directives/aria/roles-engines/controls/CHANGELOG.md b/src/core/component/directives/aria/roles-engines/controls/CHANGELOG.md new file mode 100644 index 0000000000..ba2980fb55 --- /dev/null +++ b/src/core/component/directives/aria/roles-engines/controls/CHANGELOG.md @@ -0,0 +1,16 @@ +Changelog +========= + +> **Tags:** +> - :boom: [Breaking Change] +> - :rocket: [New Feature] +> - :bug: [Bug Fix] +> - :memo: [Documentation] +> - :house: [Internal] +> - :nail_care: [Polish] + +## v?.??.? (2022-0?-??) + +#### :rocket: New Feature + +* Initial release diff --git a/src/core/component/directives/aria/roles-engines/controls/README.md b/src/core/component/directives/aria/roles-engines/controls/README.md new file mode 100644 index 0000000000..1b3744eddd --- /dev/null +++ b/src/core/component/directives/aria/roles-engines/controls/README.md @@ -0,0 +1,50 @@ +# core/component/directives/aria/roles-engines/controls + +This module provides an engine for `v-aria` directive. + +The engine to set aria-controls attribute. +For more information go to [`https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes/aria-controls`]. + +## Usage + +``` +< &__foo v-aria:controls = {...} + +``` + +## How to use + +Directive can be added to any tag that includes tag with needed role. Role should be passed as a modifier. +ID or IDs are passed as value. +ID could be single or multiple written in string with space between. + +There are two ways to use this engine: +1. To add role as a modifier to which passed IDs in `for` value should be added. `for` could be `string` or `string[]`. +If element controls several elements `for` should be passed as a string with IDs separated with space. +(!) Notice that this role attribute should already be added to the element. The engine does not set passed role to any element. + +Example: +``` +< &__foo v-aria:controls.tab = {for: 'id1 id2 id3'} + +the same as +< &__foo + < button aria-controls = "id1 id2 id3" role = "tab" +``` + +2. To pass value `for` as an array of tuples. +First id in a tuple is an id of an element to add the aria attributes. +The second one is an id of an element to set as value in aria-controls attribute. +(!) Notice that id attribute should already be added to the element. The engine does not set passed ids to any element. + +Example: +``` +< &__foo v-aria:controls = {for: [[id1, id3], [id2, id4]]} + < span :id = "id1" + < span :id = "id2" + +the same as +< &__foo + < span :id = "id1" aria-controls = "id3" + < span :id = "id2" aria-controls = "id4" +``` diff --git a/src/core/component/directives/aria/roles-engines/controls.ts b/src/core/component/directives/aria/roles-engines/controls/index.ts similarity index 80% rename from src/core/component/directives/aria/roles-engines/controls.ts rename to src/core/component/directives/aria/roles-engines/controls/index.ts index efa51321a8..f106754ddd 100644 --- a/src/core/component/directives/aria/roles-engines/controls.ts +++ b/src/core/component/directives/aria/roles-engines/controls/index.ts @@ -6,19 +6,19 @@ * https://github.com/V4Fire/Client/blob/master/LICENSE */ -import AriaRoleEngine, { DirectiveOptions } from 'core/component/directives/aria/interface'; -import type { ControlsParams } from 'core/component/directives/aria/roles-engines/interface'; +import type { ControlsParams } from 'core/component/directives/aria/roles-engines/controls/interface'; +import { AriaRoleEngine, EngineOptions } from 'core/component/directives/aria/roles-engines/interface'; -export default class ControlsEngine extends AriaRoleEngine { +export class ControlsEngine extends AriaRoleEngine { /** - * Passed directive params + * Engine params */ params: ControlsParams; - constructor(options: DirectiveOptions) { + constructor(options: EngineOptions) { super(options); - this.params = this.options.binding.value; + this.params = options.params; } /** @@ -26,9 +26,7 @@ export default class ControlsEngine extends AriaRoleEngine { */ init(): void { const - {vnode, binding, el} = this.options, - {modifiers} = binding, - {fakeContext: ctx} = vnode, + {ctx, modifiers, el} = this, {for: forId} = this.params; // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition diff --git a/src/core/component/directives/aria/roles-engines/controls/interface.ts b/src/core/component/directives/aria/roles-engines/controls/interface.ts new file mode 100644 index 0000000000..608bf5c139 --- /dev/null +++ b/src/core/component/directives/aria/roles-engines/controls/interface.ts @@ -0,0 +1,11 @@ +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ + +export interface ControlsParams { + for: CanArray | Array<[string, string]>; +} diff --git a/src/core/component/directives/aria/roles-engines/dialog/CHANGELOG.md b/src/core/component/directives/aria/roles-engines/dialog/CHANGELOG.md new file mode 100644 index 0000000000..ba2980fb55 --- /dev/null +++ b/src/core/component/directives/aria/roles-engines/dialog/CHANGELOG.md @@ -0,0 +1,16 @@ +Changelog +========= + +> **Tags:** +> - :boom: [Breaking Change] +> - :rocket: [New Feature] +> - :bug: [Bug Fix] +> - :memo: [Documentation] +> - :house: [Internal] +> - :nail_care: [Polish] + +## v?.??.? (2022-0?-??) + +#### :rocket: New Feature + +* Initial release diff --git a/src/core/component/directives/aria/roles-engines/dialog/README.md b/src/core/component/directives/aria/roles-engines/dialog/README.md new file mode 100644 index 0000000000..342b5b3e6c --- /dev/null +++ b/src/core/component/directives/aria/roles-engines/dialog/README.md @@ -0,0 +1,16 @@ +# core/component/directives/aria/roles-engines/dialog + +This module provides an engine for `v-aria` directive. + +The engine to set `dialog` role attribute. +For more information go to [`https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/dialog_role`]. + +Expects `iOpen` trait to be realized. + + +## Usage + +``` +< &__foo v-aria:dialog + +``` diff --git a/src/core/component/directives/aria/roles-engines/dialog.ts b/src/core/component/directives/aria/roles-engines/dialog/index.ts similarity index 56% rename from src/core/component/directives/aria/roles-engines/dialog.ts rename to src/core/component/directives/aria/roles-engines/dialog/index.ts index beb96370a6..e1652ea13c 100644 --- a/src/core/component/directives/aria/roles-engines/dialog.ts +++ b/src/core/component/directives/aria/roles-engines/dialog/index.ts @@ -6,21 +6,18 @@ * https://github.com/V4Fire/Client/blob/master/LICENSE */ -import AriaRoleEngine from 'core/component/directives/aria/interface'; +import { AriaRoleEngine } from 'core/component/directives/aria/roles-engines/interface'; import iOpen from 'traits/i-open/i-open'; -export default class DialogEngine extends AriaRoleEngine { +export class DialogEngine extends AriaRoleEngine { /** * Sets base aria attributes for current role */ init(): void { - const - {el, vnode} = this.options; + this.el.setAttribute('role', 'dialog'); + this.el.setAttribute('aria-modal', 'true'); - el.setAttribute('role', 'dialog'); - el.setAttribute('aria-modal', 'true'); - - if (!iOpen.is(vnode.fakeContext)) { + if (!iOpen.is(this.ctx)) { Object.throw('Dialog aria directive expects the component to realize iOpen interface'); } } diff --git a/src/core/component/directives/aria/roles-engines/index.ts b/src/core/component/directives/aria/roles-engines/index.ts index 7e8e48a6f5..9fdac42030 100644 --- a/src/core/component/directives/aria/roles-engines/index.ts +++ b/src/core/component/directives/aria/roles-engines/index.ts @@ -6,13 +6,15 @@ * https://github.com/V4Fire/Client/blob/master/LICENSE */ -export { default as dialog } from 'core/component/directives/aria/roles-engines/dialog'; -export { default as tablist } from 'core/component/directives/aria/roles-engines/tablist'; -export { default as tab } from 'core/component/directives/aria/roles-engines/tab'; -export { default as tabpanel } from 'core/component/directives/aria/roles-engines/tabpanel'; -export { default as controls } from 'core/component/directives/aria/roles-engines/controls'; -export { default as combobox } from 'core/component/directives/aria/roles-engines/combobox'; -export { default as listbox } from 'core/component/directives/aria/roles-engines/listbox'; -export { default as option } from 'core/component/directives/aria/roles-engines/option'; -export { default as tree } from 'core/component/directives/aria/roles-engines/tree'; -export { default as treeitem } from 'core/component/directives/aria/roles-engines/treeitem'; +export * from 'core/component/directives/aria/roles-engines/dialog'; +export * from 'core/component/directives/aria/roles-engines/tablist'; +export * from 'core/component/directives/aria/roles-engines/tab'; +export * from 'core/component/directives/aria/roles-engines/tabpanel'; +export * from 'core/component/directives/aria/roles-engines/controls'; +export * from 'core/component/directives/aria/roles-engines/combobox'; +export * from 'core/component/directives/aria/roles-engines/listbox'; +export * from 'core/component/directives/aria/roles-engines/option'; +export * from 'core/component/directives/aria/roles-engines/tree'; +export * from 'core/component/directives/aria/roles-engines/treeitem'; + +export { AriaRoleEngine, EngineOptions, EventNames } from 'core/component/directives/aria/roles-engines/interface'; diff --git a/src/core/component/directives/aria/roles-engines/interface.ts b/src/core/component/directives/aria/roles-engines/interface.ts index 8d2db1ab10..06a5b3ac87 100644 --- a/src/core/component/directives/aria/roles-engines/interface.ts +++ b/src/core/component/directives/aria/roles-engines/interface.ts @@ -1,38 +1,66 @@ -export interface TabParams { - preSelected: boolean; - isFirst: boolean; - isActive: boolean; - orientation: string; - changeEvent(cb: Function): void; -} +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ -export interface TablistParams { - isMultiple: boolean; - orientation: string; -} +import type Async from 'core/async'; +import type iAccess from 'traits/i-access/i-access'; +import type iBlock from 'super/i-block/i-block'; + +export abstract class AriaRoleEngine { + /** + * Element on which the directive is set + */ + readonly el: HTMLElement; + + /** + * Component on which the directive is set + */ + readonly ctx: CanUndef; -export interface TreeParams { - isRoot: boolean; - orientation: string; - changeEvent(cb: Function): void; + /** + * Directive passed modifiers + */ + readonly modifiers: CanUndef>; + + /** + * Async instance + */ + async: CanUndef; + + constructor({el, ctx, modifiers}: EngineOptions) { + this.el = el; + this.ctx = ctx; + this.modifiers = modifiers; + } + + abstract init(): void; } -export interface TreeitemParams { - isRootFirstItem: boolean; - isExpandable: boolean; - isExpanded: boolean; - orientation: string; - rootElement: CanUndef; - toggleFold(el: Element, value?: boolean): void; +export interface EngineOptions { + el: HTMLElement; + modifiers: CanUndef>; + params: DictionaryType; + ctx: iBlock & iAccess; } -export interface ComboboxParams { - isMultiple: boolean; - changeEvent(cb: Function): void; - openEvent(cb: Function): void; - closeEvent(cb: Function): void; +export type EventBinder = (cb: Function) => void; + +export const enum KeyCodes { + ENTER = 'Enter', + END = 'End', + HOME = 'Home', + LEFT = 'ArrowLeft', + UP = 'ArrowUp', + RIGHT = 'ArrowRight', + DOWN = 'ArrowDown' } -export interface ControlsParams { - for: CanArray | Array<[string, string]>; +export enum EventNames { + '@open' = 'onOpen', + '@close' = 'onClose', + '@change' = 'onChange' } diff --git a/src/core/component/directives/aria/roles-engines/listbox/CHANGELOG.md b/src/core/component/directives/aria/roles-engines/listbox/CHANGELOG.md new file mode 100644 index 0000000000..ba2980fb55 --- /dev/null +++ b/src/core/component/directives/aria/roles-engines/listbox/CHANGELOG.md @@ -0,0 +1,16 @@ +Changelog +========= + +> **Tags:** +> - :boom: [Breaking Change] +> - :rocket: [New Feature] +> - :bug: [Bug Fix] +> - :memo: [Documentation] +> - :house: [Internal] +> - :nail_care: [Polish] + +## v?.??.? (2022-0?-??) + +#### :rocket: New Feature + +* Initial release diff --git a/src/core/component/directives/aria/roles-engines/listbox/README.md b/src/core/component/directives/aria/roles-engines/listbox/README.md new file mode 100644 index 0000000000..fdd53494f0 --- /dev/null +++ b/src/core/component/directives/aria/roles-engines/listbox/README.md @@ -0,0 +1,14 @@ +# core/component/directives/aria/roles-engines/listbox + +This module provides an engine for `v-aria` directive. + +The engine to set `listbox` role attribute. +For more information go to [`https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/listbox_role`]. +For recommendations how to make accessible widget go to [`https://www.w3.org/WAI/ARIA/apg/patterns/listbox/`]. + +## Usage + +``` +< &__foo v-aria:listbox = {...} + +``` diff --git a/src/core/component/directives/aria/roles-engines/listbox.ts b/src/core/component/directives/aria/roles-engines/listbox/index.ts similarity index 50% rename from src/core/component/directives/aria/roles-engines/listbox.ts rename to src/core/component/directives/aria/roles-engines/listbox/index.ts index 9bfd9acec3..7d451389d5 100644 --- a/src/core/component/directives/aria/roles-engines/listbox.ts +++ b/src/core/component/directives/aria/roles-engines/listbox/index.ts @@ -6,17 +6,14 @@ * https://github.com/V4Fire/Client/blob/master/LICENSE */ -import AriaRoleEngine from 'core/component/directives/aria/interface'; +import { AriaRoleEngine } from 'core/component/directives/aria/roles-engines/interface'; -export default class ListboxEngine extends AriaRoleEngine { +export class ListboxEngine extends AriaRoleEngine { /** * Sets base aria attributes for current role */ init(): void { - const - {el} = this.options; - - el.setAttribute('role', 'listbox'); - el.setAttribute('tabindex', '-1'); + this.el.setAttribute('role', 'listbox'); + this.el.setAttribute('tabindex', '-1'); } } diff --git a/src/core/component/directives/aria/roles-engines/option.ts b/src/core/component/directives/aria/roles-engines/option.ts deleted file mode 100644 index 3b30bfc48b..0000000000 --- a/src/core/component/directives/aria/roles-engines/option.ts +++ /dev/null @@ -1,34 +0,0 @@ -/*! - * V4Fire Client Core - * https://github.com/V4Fire/Client - * - * Released under the MIT license - * https://github.com/V4Fire/Client/blob/master/LICENSE - */ - -import AriaRoleEngine from 'core/component/directives/aria/interface'; - -export default class OptionEngine extends AriaRoleEngine { - /** - * Sets base aria attributes for current role - */ - init(): void { - const - {el} = this.options, - {value: {preSelected}} = this.options.binding; - - el.setAttribute('role', 'option'); - el.setAttribute('aria-selected', String(preSelected)); - } - - /** - * Handler: selected option changes - * @param isSelected - */ - protected onChange(isSelected: boolean): void { - const - {el} = this.options; - - el.setAttribute('aria-selected', String(isSelected)); - } -} diff --git a/src/core/component/directives/aria/roles-engines/option/CHANGELOG.md b/src/core/component/directives/aria/roles-engines/option/CHANGELOG.md new file mode 100644 index 0000000000..ba2980fb55 --- /dev/null +++ b/src/core/component/directives/aria/roles-engines/option/CHANGELOG.md @@ -0,0 +1,16 @@ +Changelog +========= + +> **Tags:** +> - :boom: [Breaking Change] +> - :rocket: [New Feature] +> - :bug: [Bug Fix] +> - :memo: [Documentation] +> - :house: [Internal] +> - :nail_care: [Polish] + +## v?.??.? (2022-0?-??) + +#### :rocket: New Feature + +* Initial release diff --git a/src/core/component/directives/aria/roles-engines/option/README.md b/src/core/component/directives/aria/roles-engines/option/README.md new file mode 100644 index 0000000000..cdf2fdcf28 --- /dev/null +++ b/src/core/component/directives/aria/roles-engines/option/README.md @@ -0,0 +1,13 @@ +# core/component/directives/aria/roles-engines/option + +This module provides an engine for `v-aria` directive. + +The engine to set `option` role attribute. +For more information go to [`https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/option_role`]. + +## Usage + +``` +< &__foo v-aria:option + +``` diff --git a/src/core/component/directives/aria/roles-engines/option/index.ts b/src/core/component/directives/aria/roles-engines/option/index.ts new file mode 100644 index 0000000000..566d2850ab --- /dev/null +++ b/src/core/component/directives/aria/roles-engines/option/index.ts @@ -0,0 +1,39 @@ +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ + +import type { OptionParams } from 'core/component/directives/aria/roles-engines/option/interface'; +import { AriaRoleEngine, EngineOptions } from 'core/component/directives/aria/roles-engines/interface'; + +export class OptionEngine extends AriaRoleEngine { + /** + * Engine params + */ + params: OptionParams; + + constructor(options: EngineOptions) { + super(options); + + this.params = options.params; + } + + /** + * Sets base aria attributes for current role + */ + init(): void { + this.el.setAttribute('role', 'option'); + this.el.setAttribute('aria-selected', String(this.params.isSelected)); + } + + /** + * Handler: selected option changes + * @param isSelected + */ + protected onChange(isSelected: boolean): void { + this.el.setAttribute('aria-selected', String(isSelected)); + } +} diff --git a/src/core/component/directives/aria/roles-engines/option/interface.ts b/src/core/component/directives/aria/roles-engines/option/interface.ts new file mode 100644 index 0000000000..6c3774f6e4 --- /dev/null +++ b/src/core/component/directives/aria/roles-engines/option/interface.ts @@ -0,0 +1,14 @@ +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ + +import type { EventBinder } from 'core/component/directives/aria/roles-engines/interface'; + +export interface OptionParams { + isSelected: boolean; + '@change': EventBinder; +} diff --git a/src/core/component/directives/aria/roles-engines/tab/CHANGELOG.md b/src/core/component/directives/aria/roles-engines/tab/CHANGELOG.md new file mode 100644 index 0000000000..ba2980fb55 --- /dev/null +++ b/src/core/component/directives/aria/roles-engines/tab/CHANGELOG.md @@ -0,0 +1,16 @@ +Changelog +========= + +> **Tags:** +> - :boom: [Breaking Change] +> - :rocket: [New Feature] +> - :bug: [Bug Fix] +> - :memo: [Documentation] +> - :house: [Internal] +> - :nail_care: [Polish] + +## v?.??.? (2022-0?-??) + +#### :rocket: New Feature + +* Initial release diff --git a/src/core/component/directives/aria/roles-engines/tab/README.md b/src/core/component/directives/aria/roles-engines/tab/README.md new file mode 100644 index 0000000000..0395e3bad9 --- /dev/null +++ b/src/core/component/directives/aria/roles-engines/tab/README.md @@ -0,0 +1,27 @@ +# core/component/directives/aria/roles-engines/tab + +This module provides an engine for `v-aria` directive. + +The engine to set `tab` role attribute. +For more information go to [`https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/tab_role`]. +For recommendations how to make accessible widget go to [`https://www.w3.org/WAI/ARIA/apg/patterns/tabpanel/`]. + +## Usage + +``` +< &__foo v-aria:tab = {...} + +``` + +## How to use + +Tabs expect the `controls` role engine to be added in addition. ID passed to `controls` engine should be the id of the element with role `tabpanel`. + +Example: +``` +< button v-aria:tab | v-aria:controls = {for: 'id1'} + +< v-aria:tabpanel = {labelledby: 'id2'} | :id = 'id1' + < span :id = 'id2' + // content +``` diff --git a/src/core/component/directives/aria/roles-engines/tab.ts b/src/core/component/directives/aria/roles-engines/tab/index.ts similarity index 69% rename from src/core/component/directives/aria/roles-engines/tab.ts rename to src/core/component/directives/aria/roles-engines/tab/index.ts index ab2d0b3309..71c58da143 100644 --- a/src/core/component/directives/aria/roles-engines/tab.ts +++ b/src/core/component/directives/aria/roles-engines/tab/index.ts @@ -9,28 +9,19 @@ * Copyright © [2022] W3C® (MIT, ERCIM, Keio, Beihang). */ -import AriaRoleEngine, { DirectiveOptions, keyCodes } from 'core/component/directives/aria/interface'; +import type { TabParams } from 'core/component/directives/aria/roles-engines/tab/interface'; +import { AriaRoleEngine, EngineOptions, KeyCodes } from 'core/component/directives/aria/roles-engines/interface'; -import type { TabParams } from 'core/component/directives/aria/roles-engines/interface'; -import type iAccess from 'traits/i-access/i-access'; -import type iBlock from 'super/i-block/i-block'; - -export default class TabEngine extends AriaRoleEngine { +export class TabEngine extends AriaRoleEngine { /** - * Passed directive params + * Engine params */ params: TabParams; - /** - * Component instance - */ - ctx: iAccess & iBlock; - - constructor(options: DirectiveOptions) { + constructor(options: EngineOptions) { super(options); - this.params = this.options.binding.value; - this.ctx = Object.cast(this.options.vnode.fakeContext); + this.params = options.params; } /** @@ -38,18 +29,18 @@ export default class TabEngine extends AriaRoleEngine { */ init(): void { const - {el} = this.options, - {isFirst, preSelected} = this.params; + {el} = this, + {isFirst, isSelected, hasDefaultSelectedTabs} = this.params; el.setAttribute('role', 'tab'); - el.setAttribute('aria-selected', String(this.params.isActive)); + el.setAttribute('aria-selected', String(isSelected)); - if (isFirst && !preSelected) { + if (isFirst && !hasDefaultSelectedTabs) { if (el.tabIndex < 0) { el.setAttribute('tabindex', '0'); } - } else if (preSelected && this.params.isActive) { + } else if (hasDefaultSelectedTabs && isSelected) { if (el.tabIndex < 0) { el.setAttribute('tabindex', '0'); } @@ -68,7 +59,7 @@ export default class TabEngine extends AriaRoleEngine { */ protected moveFocusToFirstTab(): void { const - firstTab = >this.ctx.findFocusableElement(); + firstTab = this.ctx?.findFocusableElement(); firstTab?.focus(); } @@ -78,7 +69,11 @@ export default class TabEngine extends AriaRoleEngine { */ protected moveFocusToLastTab(): void { const - tabs = >this.ctx.findAllFocusableElements(); + tabs = this.ctx?.findAllFocusableElements(); + + if (tabs == null) { + return; + } let lastTab: CanUndef; @@ -96,7 +91,7 @@ export default class TabEngine extends AriaRoleEngine { */ protected moveFocus(step: 1 | -1): void { const - focusable = >this.ctx.getNextFocusableElement(step); + focusable = this.ctx?.getNextFocusableElement(step); focusable?.focus(); } @@ -107,7 +102,7 @@ export default class TabEngine extends AriaRoleEngine { */ protected onChange(active: Element | NodeListOf): void { const - {el} = this.options; + {el} = this; function setAttributes(isSelected: boolean) { el.setAttribute('aria-selected', String(isSelected)); @@ -134,7 +129,7 @@ export default class TabEngine extends AriaRoleEngine { isVertical = this.params.orientation === 'vertical'; switch (evt.key) { - case keyCodes.LEFT: + case KeyCodes.LEFT: if (isVertical) { return; } @@ -142,7 +137,7 @@ export default class TabEngine extends AriaRoleEngine { this.moveFocus(-1); break; - case keyCodes.UP: + case KeyCodes.UP: if (isVertical) { this.moveFocus(-1); break; @@ -150,7 +145,7 @@ export default class TabEngine extends AriaRoleEngine { return; - case keyCodes.RIGHT: + case KeyCodes.RIGHT: if (isVertical) { return; } @@ -158,7 +153,7 @@ export default class TabEngine extends AriaRoleEngine { this.moveFocus(1); break; - case keyCodes.DOWN: + case KeyCodes.DOWN: if (isVertical) { this.moveFocus(1); break; @@ -166,11 +161,11 @@ export default class TabEngine extends AriaRoleEngine { return; - case keyCodes.HOME: + case KeyCodes.HOME: this.moveFocusToFirstTab(); break; - case keyCodes.END: + case KeyCodes.END: this.moveFocusToLastTab(); break; diff --git a/src/core/component/directives/aria/roles-engines/tab/interface.ts b/src/core/component/directives/aria/roles-engines/tab/interface.ts new file mode 100644 index 0000000000..c75904fcd1 --- /dev/null +++ b/src/core/component/directives/aria/roles-engines/tab/interface.ts @@ -0,0 +1,17 @@ +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ + +import type { EventBinder } from 'core/component/directives/aria/roles-engines/interface'; + +export interface TabParams { + isFirst: boolean; + isSelected: boolean; + hasDefaultSelectedTabs: boolean; + orientation: string; + '@change': EventBinder; +} diff --git a/src/core/component/directives/aria/roles-engines/tablist/CHANGELOG.md b/src/core/component/directives/aria/roles-engines/tablist/CHANGELOG.md new file mode 100644 index 0000000000..ba2980fb55 --- /dev/null +++ b/src/core/component/directives/aria/roles-engines/tablist/CHANGELOG.md @@ -0,0 +1,16 @@ +Changelog +========= + +> **Tags:** +> - :boom: [Breaking Change] +> - :rocket: [New Feature] +> - :bug: [Bug Fix] +> - :memo: [Documentation] +> - :house: [Internal] +> - :nail_care: [Polish] + +## v?.??.? (2022-0?-??) + +#### :rocket: New Feature + +* Initial release diff --git a/src/core/component/directives/aria/roles-engines/tablist/README.md b/src/core/component/directives/aria/roles-engines/tablist/README.md new file mode 100644 index 0000000000..078d4fd7e6 --- /dev/null +++ b/src/core/component/directives/aria/roles-engines/tablist/README.md @@ -0,0 +1,14 @@ +# core/component/directives/aria/roles-engines/tablist + +This module provides an engine for `v-aria` directive. + +The engine to set `tablist` role attribute. +For more information go to [`https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/tablist_role`]. +For recommendations how to make accessible widget go to [`https://www.w3.org/WAI/ARIA/apg/patterns/tabpanel/`]. + +## Usage + +``` +< &__foo v-aria:tablist = {...} + +``` diff --git a/src/core/component/directives/aria/roles-engines/tablist.ts b/src/core/component/directives/aria/roles-engines/tablist/index.ts similarity index 59% rename from src/core/component/directives/aria/roles-engines/tablist.ts rename to src/core/component/directives/aria/roles-engines/tablist/index.ts index b5c4653cd4..74ba0e4970 100644 --- a/src/core/component/directives/aria/roles-engines/tablist.ts +++ b/src/core/component/directives/aria/roles-engines/tablist/index.ts @@ -6,17 +6,27 @@ * https://github.com/V4Fire/Client/blob/master/LICENSE */ -import AriaRoleEngine from 'core/component/directives/aria/interface'; -import type { TablistParams } from 'core/component/directives/aria/roles-engines/interface'; +import type { TablistParams } from 'core/component/directives/aria/roles-engines/tablist/interface'; +import { AriaRoleEngine, EngineOptions } from 'core/component/directives/aria/roles-engines/interface'; + +export class TablistEngine extends AriaRoleEngine { + /** + * Engine params + */ + params: TablistParams; + + constructor(options: EngineOptions) { + super(options); + + this.params = options.params; + } -export default class TablistEngine extends AriaRoleEngine { /** * Sets base aria attributes for current role */ init(): void { const - {el, binding} = this.options, - params: TablistParams = binding.value; + {el, params} = this; el.setAttribute('role', 'tablist'); diff --git a/src/core/component/directives/aria/roles-engines/tablist/interface.ts b/src/core/component/directives/aria/roles-engines/tablist/interface.ts new file mode 100644 index 0000000000..ec88e5a93a --- /dev/null +++ b/src/core/component/directives/aria/roles-engines/tablist/interface.ts @@ -0,0 +1,12 @@ +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ + +export interface TablistParams { + isMultiple: boolean; + orientation: string; +} diff --git a/src/core/component/directives/aria/roles-engines/tabpanel/CHANGELOG.md b/src/core/component/directives/aria/roles-engines/tabpanel/CHANGELOG.md new file mode 100644 index 0000000000..ba2980fb55 --- /dev/null +++ b/src/core/component/directives/aria/roles-engines/tabpanel/CHANGELOG.md @@ -0,0 +1,16 @@ +Changelog +========= + +> **Tags:** +> - :boom: [Breaking Change] +> - :rocket: [New Feature] +> - :bug: [Bug Fix] +> - :memo: [Documentation] +> - :house: [Internal] +> - :nail_care: [Polish] + +## v?.??.? (2022-0?-??) + +#### :rocket: New Feature + +* Initial release diff --git a/src/core/component/directives/aria/roles-engines/tabpanel/README.md b/src/core/component/directives/aria/roles-engines/tabpanel/README.md new file mode 100644 index 0000000000..a60f4f322d --- /dev/null +++ b/src/core/component/directives/aria/roles-engines/tabpanel/README.md @@ -0,0 +1,25 @@ +# core/component/directives/aria/roles-engines/tabpanel + +This module provides an engine for `v-aria` directive. + +The engine to set `tabpanel` role attribute. +For more information go to [`https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/tabpanel_role`]. +For recommendations how to make accessible widget go to [`https://www.w3.org/WAI/ARIA/apg/patterns/tabpanel/`]. + +## Usage + +``` +< &__foo v-aria:tabpanel = {...} + +``` + +## How to use + +Expects `label` or `labelledby` params to be passed. + +Example: +``` +< v-aria:tabpanel = {labelledby: 'id1'} + < span :id = 'id1' + // content +``` diff --git a/src/core/component/directives/aria/roles-engines/tabpanel.ts b/src/core/component/directives/aria/roles-engines/tabpanel/index.ts similarity index 73% rename from src/core/component/directives/aria/roles-engines/tabpanel.ts rename to src/core/component/directives/aria/roles-engines/tabpanel/index.ts index ca3537088c..f49f2661ba 100644 --- a/src/core/component/directives/aria/roles-engines/tabpanel.ts +++ b/src/core/component/directives/aria/roles-engines/tabpanel/index.ts @@ -6,19 +6,18 @@ * https://github.com/V4Fire/Client/blob/master/LICENSE */ -import AriaRoleEngine from 'core/component/directives/aria/interface'; +import { AriaRoleEngine } from 'core/component/directives/aria/roles-engines/interface'; -export default class TabpanelEngine extends AriaRoleEngine { +export class TabpanelEngine extends AriaRoleEngine { /** * Sets base aria attributes for current role */ init(): void { const - {el} = this.options; + {el} = this; if (!el.hasAttribute('aria-label') && !el.hasAttribute('aria-labelledby')) { Object.throw('Tabpanel aria directive expects "label" or "labelledby" value to be passed'); - return; } el.setAttribute('role', 'tabpanel'); diff --git a/src/core/component/directives/aria/roles-engines/tree/CHANGELOG.md b/src/core/component/directives/aria/roles-engines/tree/CHANGELOG.md new file mode 100644 index 0000000000..ba2980fb55 --- /dev/null +++ b/src/core/component/directives/aria/roles-engines/tree/CHANGELOG.md @@ -0,0 +1,16 @@ +Changelog +========= + +> **Tags:** +> - :boom: [Breaking Change] +> - :rocket: [New Feature] +> - :bug: [Bug Fix] +> - :memo: [Documentation] +> - :house: [Internal] +> - :nail_care: [Polish] + +## v?.??.? (2022-0?-??) + +#### :rocket: New Feature + +* Initial release diff --git a/src/core/component/directives/aria/roles-engines/tree/README.md b/src/core/component/directives/aria/roles-engines/tree/README.md new file mode 100644 index 0000000000..f18c69c773 --- /dev/null +++ b/src/core/component/directives/aria/roles-engines/tree/README.md @@ -0,0 +1,15 @@ +# core/component/directives/aria/roles-engines/tree + +This module provides an engine for `v-aria` directive. + +The engine to set `tree` role attribute. +For more information go to [`https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/tree_role`]. + +For recommendations how to make accessible widget go to [`https://www.w3.org/WAI/ARIA/apg/patterns/treeview/`]. + +## Usage + +``` +< &__foo v-aria:tree = {...} + +``` diff --git a/src/core/component/directives/aria/roles-engines/tree.ts b/src/core/component/directives/aria/roles-engines/tree/index.ts similarity index 52% rename from src/core/component/directives/aria/roles-engines/tree.ts rename to src/core/component/directives/aria/roles-engines/tree/index.ts index 3b8986e196..4d94d71be4 100644 --- a/src/core/component/directives/aria/roles-engines/tree.ts +++ b/src/core/component/directives/aria/roles-engines/tree/index.ts @@ -6,19 +6,19 @@ * https://github.com/V4Fire/Client/blob/master/LICENSE */ -import AriaRoleEngine, { DirectiveOptions } from 'core/component/directives/aria/interface'; -import type { TreeParams } from 'core/component/directives/aria/roles-engines/interface'; +import type { TreeParams } from 'core/component/directives/aria/roles-engines/tree/interface'; +import { AriaRoleEngine, EngineOptions } from 'core/component/directives/aria/roles-engines/interface'; -export default class TreeEngine extends AriaRoleEngine { +export class TreeEngine extends AriaRoleEngine { /** - * Passed directive params + * Engine params */ params: TreeParams; - constructor(options: DirectiveOptions) { + constructor(options: EngineOptions) { super(options); - this.params = options.binding.value; + this.params = options.params; } /** @@ -26,13 +26,12 @@ export default class TreeEngine extends AriaRoleEngine { */ init(): void { const - {el} = this.options, - {orientation} = this.params; + {orientation, isRoot} = this.params; this.setRootRole(); - if (orientation === 'horizontal' && this.params.isRoot) { - el.setAttribute('aria-orientation', orientation); + if (orientation === 'horizontal' && isRoot) { + this.el.setAttribute('aria-orientation', orientation); } } @@ -40,10 +39,7 @@ export default class TreeEngine extends AriaRoleEngine { * Sets the role to the element depending on whether the tree is root or nested */ protected setRootRole(): void { - const - {el} = this.options; - - el.setAttribute('role', this.params.isRoot ? 'tree' : 'group'); + this.el.setAttribute('role', this.params.isRoot ? 'tree' : 'group'); } /** @@ -51,7 +47,7 @@ export default class TreeEngine extends AriaRoleEngine { * @param el * @param isFolded */ - protected onChange(el: HTMLElement, isFolded: boolean): void { + protected onChange(el: Element, isFolded: boolean): void { el.setAttribute('aria-expanded', String(!isFolded)); } } diff --git a/src/core/component/directives/aria/roles-engines/tree/interface.ts b/src/core/component/directives/aria/roles-engines/tree/interface.ts new file mode 100644 index 0000000000..150df682b1 --- /dev/null +++ b/src/core/component/directives/aria/roles-engines/tree/interface.ts @@ -0,0 +1,15 @@ +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ + +import type { EventBinder } from 'core/component/directives/aria/roles-engines/interface'; + +export interface TreeParams { + isRoot: boolean; + orientation: string; + '@change': EventBinder; +} diff --git a/src/core/component/directives/aria/roles-engines/treeitem/CHANGELOG.md b/src/core/component/directives/aria/roles-engines/treeitem/CHANGELOG.md new file mode 100644 index 0000000000..ba2980fb55 --- /dev/null +++ b/src/core/component/directives/aria/roles-engines/treeitem/CHANGELOG.md @@ -0,0 +1,16 @@ +Changelog +========= + +> **Tags:** +> - :boom: [Breaking Change] +> - :rocket: [New Feature] +> - :bug: [Bug Fix] +> - :memo: [Documentation] +> - :house: [Internal] +> - :nail_care: [Polish] + +## v?.??.? (2022-0?-??) + +#### :rocket: New Feature + +* Initial release diff --git a/src/core/component/directives/aria/roles-engines/treeitem/README.md b/src/core/component/directives/aria/roles-engines/treeitem/README.md new file mode 100644 index 0000000000..7e59a5b0bd --- /dev/null +++ b/src/core/component/directives/aria/roles-engines/treeitem/README.md @@ -0,0 +1,17 @@ +# core/component/directives/aria/roles-engines/treeitem + +This module provides an engine for `v-aria` directive. + +The engine to set `treeitem` role attribute. +For more information go to [`https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/treeitem_role`]. + +For recommendations how to make accessible widget go to [`https://www.w3.org/WAI/ARIA/apg/patterns/treeview/`]. + +Expects `iAccess` trait to be realized. + +## Usage + +``` +< &__foo v-aria:treeitem = {...} + +``` diff --git a/src/core/component/directives/aria/roles-engines/treeitem.ts b/src/core/component/directives/aria/roles-engines/treeitem/index.ts similarity index 72% rename from src/core/component/directives/aria/roles-engines/treeitem.ts rename to src/core/component/directives/aria/roles-engines/treeitem/index.ts index 9215388542..f2de0c1c60 100644 --- a/src/core/component/directives/aria/roles-engines/treeitem.ts +++ b/src/core/component/directives/aria/roles-engines/treeitem/index.ts @@ -9,38 +9,25 @@ * Copyright © [2022] W3C® (MIT, ERCIM, Keio, Beihang). */ -import AriaRoleEngine, { DirectiveOptions, keyCodes } from 'core/component/directives/aria/interface'; import iAccess from 'traits/i-access/i-access'; -import type { TreeitemParams } from 'core/component/directives/aria/roles-engines/interface'; -import type iBlock from 'super/i-block/i-block'; +import type { TreeitemParams } from 'core/component/directives/aria/roles-engines/treeitem/interface'; +import { AriaRoleEngine, KeyCodes, EngineOptions } from 'core/component/directives/aria/roles-engines/interface'; -export default class TreeItemEngine extends AriaRoleEngine { +export class TreeitemEngine extends AriaRoleEngine { /** - * Passed directive params + * Engine params */ params: TreeitemParams; - /** - * Component instance - */ - ctx: iAccess & iBlock['unsafe']; - - /** - * Element with current directive - */ - el: HTMLElement; - - constructor(options: DirectiveOptions) { + constructor(options: EngineOptions) { super(options); - if (!iAccess.is(options.vnode.fakeContext)) { + if (!iAccess.is(this.ctx)) { Object.throw('Treeitem aria directive expects the component to realize iAccess interface'); } - this.ctx = Object.cast(options.vnode.fakeContext); - this.el = this.options.el; - this.params = this.options.binding.value; + this.params = options.params; } /** @@ -50,11 +37,11 @@ export default class TreeItemEngine extends AriaRoleEngine { this.async?.on(this.el, 'keydown', this.onKeyDown.bind(this)); const - isMuted = this.ctx.removeAllFromTabSequence(this.el); + isMuted = this.ctx?.removeAllFromTabSequence(this.el); - if (this.params.isRootFirstItem) { + if (this.params.isFirstRootItem) { if (isMuted) { - this.ctx.restoreAllToTabSequence(this.el); + this.ctx?.restoreAllToTabSequence(this.el); } else { this.el.tabIndex = 0; @@ -63,7 +50,7 @@ export default class TreeItemEngine extends AriaRoleEngine { this.el.setAttribute('role', 'treeitem'); - this.ctx.$nextTick(() => { + this.ctx?.$nextTick(() => { if (this.params.isExpandable) { this.el.setAttribute('aria-expanded', String(this.params.isExpanded)); } @@ -75,8 +62,8 @@ export default class TreeItemEngine extends AriaRoleEngine { * @param el */ protected focusNext(el: HTMLElement): void { - this.ctx.removeAllFromTabSequence(this.el); - this.ctx.restoreAllToTabSequence(el); + this.ctx?.removeAllFromTabSequence(this.el); + this.ctx?.restoreAllToTabSequence(el); el.focus(); } @@ -87,7 +74,7 @@ export default class TreeItemEngine extends AriaRoleEngine { */ protected moveFocus(step: 1 | -1): void { const - nextEl = >this.ctx.getNextFocusableElement(step); + nextEl = this.ctx?.getNextFocusableElement(step); if (nextEl != null) { this.focusNext(nextEl); @@ -128,7 +115,7 @@ export default class TreeItemEngine extends AriaRoleEngine { } const - focusableParent = >this.ctx.findFocusableElement(parent); + focusableParent = this.ctx?.findFocusableElement(parent); if (focusableParent != null) { this.focusNext(focusableParent); @@ -140,7 +127,7 @@ export default class TreeItemEngine extends AriaRoleEngine { */ protected setFocusToFirstItem(): void { const - firstItem = >this.ctx.findFocusableElement(this.params.rootElement); + firstItem = this.ctx?.findFocusableElement(this.params.rootElement); if (firstItem != null) { this.focusNext(firstItem); @@ -152,7 +139,7 @@ export default class TreeItemEngine extends AriaRoleEngine { */ protected setFocusToLastItem(): void { const - items = >this.ctx.findAllFocusableElements(this.params.rootElement); + items = >this.ctx?.findAllFocusableElements(this.params.rootElement); let lastItem: CanUndef; @@ -200,7 +187,7 @@ export default class TreeItemEngine extends AriaRoleEngine { }; switch (e.key) { - case keyCodes.UP: + case KeyCodes.UP: if (isHorizontal) { close(); break; @@ -209,7 +196,7 @@ export default class TreeItemEngine extends AriaRoleEngine { this.moveFocus(-1); break; - case keyCodes.DOWN: + case KeyCodes.DOWN: if (isHorizontal) { open(); break; @@ -218,7 +205,7 @@ export default class TreeItemEngine extends AriaRoleEngine { this.moveFocus(1); break; - case keyCodes.RIGHT: + case KeyCodes.RIGHT: if (isHorizontal) { this.moveFocus(1); break; @@ -227,7 +214,7 @@ export default class TreeItemEngine extends AriaRoleEngine { open(); break; - case keyCodes.LEFT: + case KeyCodes.LEFT: if (isHorizontal) { this.moveFocus(-1); break; @@ -236,15 +223,15 @@ export default class TreeItemEngine extends AriaRoleEngine { close(); break; - case keyCodes.ENTER: + case KeyCodes.ENTER: this.params.toggleFold(this.el); break; - case keyCodes.HOME: + case KeyCodes.HOME: this.setFocusToFirstItem(); break; - case keyCodes.END: + case KeyCodes.END: this.setFocusToLastItem(); break; diff --git a/src/core/component/directives/aria/roles-engines/treeitem/interface.ts b/src/core/component/directives/aria/roles-engines/treeitem/interface.ts new file mode 100644 index 0000000000..38bfb24418 --- /dev/null +++ b/src/core/component/directives/aria/roles-engines/treeitem/interface.ts @@ -0,0 +1,16 @@ +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ + +export interface TreeitemParams { + isFirstRootItem: boolean; + isExpandable: boolean; + isExpanded: boolean; + orientation: string; + rootElement: CanUndef; + toggleFold(el: Element, value?: boolean): void; +} diff --git a/src/core/component/render-function/CHANGELOG.md b/src/core/component/render-function/CHANGELOG.md index 1c8c43aa5d..b80bbe6ed6 100644 --- a/src/core/component/render-function/CHANGELOG.md +++ b/src/core/component/render-function/CHANGELOG.md @@ -9,6 +9,12 @@ Changelog > - :house: [Internal] > - :nail_care: [Polish] +## v3.??.? (2022-??-??) + +#### :bug: Bug Fix + +* Fixed `v-attrs` regexp for parsing incoming modifiers + ## v3.12.1 (2021-11-26) #### :bug: Bug Fix diff --git a/src/form/b-select/b-select.ts b/src/form/b-select/b-select.ts index d48147cf25..0b39a564fb 100644 --- a/src/form/b-select/b-select.ts +++ b/src/form/b-select/b-select.ts @@ -954,18 +954,18 @@ class bSelect extends iInputText implements iOpenToggle, iItems { const comboboxConfig = { isMultiple: this.multiple, - changeEvent: (cb) => this.localEmitter.on(event, ({link}) => cb(link)), - closeEvent: (cb) => this.on('close', cb), - openEvent: (cb) => this.on('open', () => { + '@change': (cb) => this.localEmitter.on(event, ({link}) => cb(link)), + '@close': (cb) => this.on('close', cb), + '@open': (cb) => this.on('open', () => { void this.$nextTick(() => cb(this.selectedElement)); }) }; const optionConfig = { - get preSelected() { + get isSelected() { return isSelected(); }, - changeEvent: (cb) => this.on('actionChange', () => cb(isSelected())) + '@change': (cb) => this.on('actionChange', () => cb(isSelected())) }; switch (role) { diff --git a/src/traits/i-access/i-access.ts b/src/traits/i-access/i-access.ts index d49cadad51..37d03c86f9 100644 --- a/src/traits/i-access/i-access.ts +++ b/src/traits/i-access/i-access.ts @@ -211,10 +211,10 @@ export default abstract class iAccess { removedElems = intoIter(ctx.querySelectorAll('[data-tabindex]')); if (el?.hasAttribute('data-tabindex')) { - removedElems = sequence(removedElems, intoIter([el])); + removedElems = sequence(removedElems, intoIter([el])); } - for (const elem of >removedElems) { + for (const elem of removedElems) { const originalTabIndex = elem.getAttribute('data-tabindex'); @@ -261,7 +261,7 @@ export default abstract class iAccess { /** @see [[iAccess.findFocusableElement]] */ static findFocusableElement: AddSelf = - (component, el?): CanUndef => { + (component, el?) => { const ctx = el ?? component.$el, focusableElems = this.findAllFocusableElements(component, ctx); @@ -284,7 +284,7 @@ export default abstract class iAccess { focusableIter = intoIter(focusableElems ?? []); if (ctx?.matches(FOCUSABLE_SELECTOR)) { - focusableIter = sequence(focusableIter, intoIter([el])); + focusableIter = sequence(focusableIter, intoIter([el])); } function* createFocusableWithoutDisabled(iter: IterableIterator): IterableIterator { @@ -404,7 +404,7 @@ export default abstract class iAccess { * @param step * @param el - a context to search, if not set, document will be used */ - getNextFocusableElement(step: 1 | -1, el?: Element): CanUndef { + getNextFocusableElement(step: 1 | -1, el?: T): CanUndef { return Object.throw(); } @@ -412,7 +412,7 @@ export default abstract class iAccess { * Find focusable element except disabled ones * @param el - a context to search, if not set, component will be used */ - findFocusableElement(el?: Element): CanUndef { + findFocusableElement(el?: T): CanUndef { return Object.throw(); } @@ -420,7 +420,7 @@ export default abstract class iAccess { * Find all focusable elements except disabled ones. Search includes the specified element * @param el - a context to search, if not set, component will be used */ - findAllFocusableElements(el?: Element): IterableIterator> { + findAllFocusableElements(el?: T): IterableIterator> { return Object.throw(); } } From 017d1371b51241df9759881ba3eef4ae6ca016e1 Mon Sep 17 00:00:00 2001 From: Aleksandr Bunin Date: Wed, 10 Aug 2022 11:25:02 +0300 Subject: [PATCH 023/185] refactoring tests folders --- .../aria/{ => roles-engines/combobox}/test/unit/combobox.ts | 0 .../aria/{ => roles-engines/controls}/test/unit/controls.ts | 0 .../aria/{ => roles-engines/dialog}/test/unit/dialog.ts | 0 .../aria/{ => roles-engines/listbox}/test/unit/listbox.ts | 0 .../aria/{ => roles-engines/option}/test/unit/option.ts | 0 .../directives/aria/{ => roles-engines/tab}/test/unit/tab.ts | 0 .../aria/{ => roles-engines/tablist}/test/unit/tablist.ts | 0 .../aria/{ => roles-engines/tabpanel}/test/unit/tabpanel.ts | 0 .../directives/aria/{ => roles-engines/tree}/test/unit/tree.ts | 0 .../aria/{ => roles-engines/treeitem}/test/unit/treeitem.ts | 0 10 files changed, 0 insertions(+), 0 deletions(-) rename src/core/component/directives/aria/{ => roles-engines/combobox}/test/unit/combobox.ts (100%) rename src/core/component/directives/aria/{ => roles-engines/controls}/test/unit/controls.ts (100%) rename src/core/component/directives/aria/{ => roles-engines/dialog}/test/unit/dialog.ts (100%) rename src/core/component/directives/aria/{ => roles-engines/listbox}/test/unit/listbox.ts (100%) rename src/core/component/directives/aria/{ => roles-engines/option}/test/unit/option.ts (100%) rename src/core/component/directives/aria/{ => roles-engines/tab}/test/unit/tab.ts (100%) rename src/core/component/directives/aria/{ => roles-engines/tablist}/test/unit/tablist.ts (100%) rename src/core/component/directives/aria/{ => roles-engines/tabpanel}/test/unit/tabpanel.ts (100%) rename src/core/component/directives/aria/{ => roles-engines/tree}/test/unit/tree.ts (100%) rename src/core/component/directives/aria/{ => roles-engines/treeitem}/test/unit/treeitem.ts (100%) diff --git a/src/core/component/directives/aria/test/unit/combobox.ts b/src/core/component/directives/aria/roles-engines/combobox/test/unit/combobox.ts similarity index 100% rename from src/core/component/directives/aria/test/unit/combobox.ts rename to src/core/component/directives/aria/roles-engines/combobox/test/unit/combobox.ts diff --git a/src/core/component/directives/aria/test/unit/controls.ts b/src/core/component/directives/aria/roles-engines/controls/test/unit/controls.ts similarity index 100% rename from src/core/component/directives/aria/test/unit/controls.ts rename to src/core/component/directives/aria/roles-engines/controls/test/unit/controls.ts diff --git a/src/core/component/directives/aria/test/unit/dialog.ts b/src/core/component/directives/aria/roles-engines/dialog/test/unit/dialog.ts similarity index 100% rename from src/core/component/directives/aria/test/unit/dialog.ts rename to src/core/component/directives/aria/roles-engines/dialog/test/unit/dialog.ts diff --git a/src/core/component/directives/aria/test/unit/listbox.ts b/src/core/component/directives/aria/roles-engines/listbox/test/unit/listbox.ts similarity index 100% rename from src/core/component/directives/aria/test/unit/listbox.ts rename to src/core/component/directives/aria/roles-engines/listbox/test/unit/listbox.ts diff --git a/src/core/component/directives/aria/test/unit/option.ts b/src/core/component/directives/aria/roles-engines/option/test/unit/option.ts similarity index 100% rename from src/core/component/directives/aria/test/unit/option.ts rename to src/core/component/directives/aria/roles-engines/option/test/unit/option.ts diff --git a/src/core/component/directives/aria/test/unit/tab.ts b/src/core/component/directives/aria/roles-engines/tab/test/unit/tab.ts similarity index 100% rename from src/core/component/directives/aria/test/unit/tab.ts rename to src/core/component/directives/aria/roles-engines/tab/test/unit/tab.ts diff --git a/src/core/component/directives/aria/test/unit/tablist.ts b/src/core/component/directives/aria/roles-engines/tablist/test/unit/tablist.ts similarity index 100% rename from src/core/component/directives/aria/test/unit/tablist.ts rename to src/core/component/directives/aria/roles-engines/tablist/test/unit/tablist.ts diff --git a/src/core/component/directives/aria/test/unit/tabpanel.ts b/src/core/component/directives/aria/roles-engines/tabpanel/test/unit/tabpanel.ts similarity index 100% rename from src/core/component/directives/aria/test/unit/tabpanel.ts rename to src/core/component/directives/aria/roles-engines/tabpanel/test/unit/tabpanel.ts diff --git a/src/core/component/directives/aria/test/unit/tree.ts b/src/core/component/directives/aria/roles-engines/tree/test/unit/tree.ts similarity index 100% rename from src/core/component/directives/aria/test/unit/tree.ts rename to src/core/component/directives/aria/roles-engines/tree/test/unit/tree.ts diff --git a/src/core/component/directives/aria/test/unit/treeitem.ts b/src/core/component/directives/aria/roles-engines/treeitem/test/unit/treeitem.ts similarity index 100% rename from src/core/component/directives/aria/test/unit/treeitem.ts rename to src/core/component/directives/aria/roles-engines/treeitem/test/unit/treeitem.ts From 51b22103bfb56ab448f1e435f17209aaf524c253 Mon Sep 17 00:00:00 2001 From: Aleksandr Bunin Date: Wed, 10 Aug 2022 13:08:30 +0300 Subject: [PATCH 024/185] refactor --- .../directives/aria/roles-engines/combobox/interface.ts | 8 ++++---- .../component/directives/aria/roles-engines/interface.ts | 2 +- .../directives/aria/roles-engines/option/interface.ts | 4 ++-- .../directives/aria/roles-engines/tab/interface.ts | 4 ++-- .../directives/aria/roles-engines/tree/interface.ts | 4 ++-- 5 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/core/component/directives/aria/roles-engines/combobox/interface.ts b/src/core/component/directives/aria/roles-engines/combobox/interface.ts index 7675f2fbc4..b34ccb2270 100644 --- a/src/core/component/directives/aria/roles-engines/combobox/interface.ts +++ b/src/core/component/directives/aria/roles-engines/combobox/interface.ts @@ -6,11 +6,11 @@ * https://github.com/V4Fire/Client/blob/master/LICENSE */ -import type { EventBinder } from 'core/component/directives/aria/roles-engines/interface'; +import type { HandlerAttachment } from 'core/component/directives/aria/roles-engines/interface'; export interface ComboboxParams { isMultiple: boolean; - '@change': EventBinder; - '@open': EventBinder; - '@close': EventBinder; + '@change': HandlerAttachment; + '@open': HandlerAttachment; + '@close': HandlerAttachment; } diff --git a/src/core/component/directives/aria/roles-engines/interface.ts b/src/core/component/directives/aria/roles-engines/interface.ts index 06a5b3ac87..9a987a3689 100644 --- a/src/core/component/directives/aria/roles-engines/interface.ts +++ b/src/core/component/directives/aria/roles-engines/interface.ts @@ -47,7 +47,7 @@ export interface EngineOptions { ctx: iBlock & iAccess; } -export type EventBinder = (cb: Function) => void; +export type HandlerAttachment = (cb: Function) => void; export const enum KeyCodes { ENTER = 'Enter', diff --git a/src/core/component/directives/aria/roles-engines/option/interface.ts b/src/core/component/directives/aria/roles-engines/option/interface.ts index 6c3774f6e4..4e90c740ae 100644 --- a/src/core/component/directives/aria/roles-engines/option/interface.ts +++ b/src/core/component/directives/aria/roles-engines/option/interface.ts @@ -6,9 +6,9 @@ * https://github.com/V4Fire/Client/blob/master/LICENSE */ -import type { EventBinder } from 'core/component/directives/aria/roles-engines/interface'; +import type { HandlerAttachment } from 'core/component/directives/aria/roles-engines/interface'; export interface OptionParams { isSelected: boolean; - '@change': EventBinder; + '@change': HandlerAttachment; } diff --git a/src/core/component/directives/aria/roles-engines/tab/interface.ts b/src/core/component/directives/aria/roles-engines/tab/interface.ts index c75904fcd1..e42c5bdc11 100644 --- a/src/core/component/directives/aria/roles-engines/tab/interface.ts +++ b/src/core/component/directives/aria/roles-engines/tab/interface.ts @@ -6,12 +6,12 @@ * https://github.com/V4Fire/Client/blob/master/LICENSE */ -import type { EventBinder } from 'core/component/directives/aria/roles-engines/interface'; +import type { HandlerAttachment } from 'core/component/directives/aria/roles-engines/interface'; export interface TabParams { isFirst: boolean; isSelected: boolean; hasDefaultSelectedTabs: boolean; orientation: string; - '@change': EventBinder; + '@change': HandlerAttachment; } diff --git a/src/core/component/directives/aria/roles-engines/tree/interface.ts b/src/core/component/directives/aria/roles-engines/tree/interface.ts index 150df682b1..44b0e9cf3f 100644 --- a/src/core/component/directives/aria/roles-engines/tree/interface.ts +++ b/src/core/component/directives/aria/roles-engines/tree/interface.ts @@ -6,10 +6,10 @@ * https://github.com/V4Fire/Client/blob/master/LICENSE */ -import type { EventBinder } from 'core/component/directives/aria/roles-engines/interface'; +import type { HandlerAttachment } from 'core/component/directives/aria/roles-engines/interface'; export interface TreeParams { isRoot: boolean; orientation: string; - '@change': EventBinder; + '@change': HandlerAttachment; } From b63e776cfef91ea30cbade9ffbfcc36bc6787129 Mon Sep 17 00:00:00 2001 From: Aleksandr Bunin Date: Fri, 12 Aug 2022 12:49:43 +0300 Subject: [PATCH 025/185] refactoring --- .../component/directives/aria/aria-setter.ts | 34 +++--- .../aria/roles-engines/combobox/index.ts | 10 +- .../aria/roles-engines/combobox/interface.ts | 4 +- .../combobox/test/unit/combobox.ts | 3 +- .../aria/roles-engines/controls/index.ts | 4 +- .../controls/test/unit/controls.ts | 3 +- .../roles-engines/dialog/test/unit/dialog.ts | 3 +- .../directives/aria/roles-engines/index.ts | 2 +- .../aria/roles-engines/interface.ts | 43 +++++--- .../listbox/test/unit/listbox.ts | 3 +- .../aria/roles-engines/option/index.ts | 4 +- .../roles-engines/option/test/unit/option.ts | 3 +- .../aria/roles-engines/tab/index.ts | 12 +- .../aria/roles-engines/tab/test/unit/tab.ts | 3 +- .../aria/roles-engines/tablist/index.ts | 4 +- .../tablist/test/unit/tablist.ts | 3 +- .../tabpanel/test/unit/tabpanel.ts | 3 +- .../aria/roles-engines/tree/index.ts | 4 +- .../aria/roles-engines/tree/test/unit/tree.ts | 3 +- .../aria/roles-engines/treeitem/README.md | 10 ++ .../aria/roles-engines/treeitem/index.ts | 10 +- .../treeitem/test/unit/treeitem.ts | 103 ++++++++++-------- .../directives/aria/test/unit/simple.ts | 3 +- src/traits/i-access/i-access.ts | 6 +- 24 files changed, 164 insertions(+), 116 deletions(-) diff --git a/src/core/component/directives/aria/aria-setter.ts b/src/core/component/directives/aria/aria-setter.ts index 05d62f9549..a88f7a9bfe 100644 --- a/src/core/component/directives/aria/aria-setter.ts +++ b/src/core/component/directives/aria/aria-setter.ts @@ -6,14 +6,12 @@ * https://github.com/V4Fire/Client/blob/master/LICENSE */ -import * as ariaRoles from 'core/component/directives/aria/roles-engines'; import Async from 'core/async'; - import type iBlock from 'super/i-block/i-block'; -import type iAccess from 'traits/i-access/i-access'; +import * as ariaRoles from 'core/component/directives/aria/roles-engines'; import type { DirectiveOptions } from 'core/component/directives/aria/interface'; -import { AriaRoleEngine, EngineOptions, EventNames } from 'core/component/directives/aria/roles-engines'; +import type { AriaRoleEngine, EngineOptions } from 'core/component/directives/aria/roles-engines'; /** * Class-helper for making base operations for the directive @@ -39,10 +37,6 @@ export default class AriaSetter { this.async = new Async(); this.setAriaRole(); - if (this.role != null) { - this.role.async = this.async; - } - this.init(); } @@ -57,7 +51,7 @@ export default class AriaSetter { } /** - * Runs on update directive hook. Removes listeners from component if the component is Functional + * Runs on update directive hook. Removes listeners from component if the component is Functional. */ update(): void { const @@ -69,7 +63,7 @@ export default class AriaSetter { } /** - * Runs on unbind directive hook. Clears the Async instance + * Runs on unbind directive hook. Clears the Async instance. */ destroy(): void { this.async.clearAll(); @@ -104,7 +98,7 @@ export default class AriaSetter { /** * Creates a dictionary with engine options */ - protected createRoleOptions(): EngineOptions { + protected createRoleOptions(): EngineOptions { const {el, binding, vnode} = this.options, {value, modifiers} = binding; @@ -113,7 +107,8 @@ export default class AriaSetter { el, modifiers, params: value, - ctx: Object.cast(vnode.fakeContext) + ctx: Object.cast(vnode.fakeContext), + async: this.async }; } @@ -156,7 +151,7 @@ export default class AriaSetter { /** * Sets handlers for the base role events: open, close, change. - * Expects the passed into directive specified event properties to be Function, Promise or String + * Expects the passed into directive specified event properties to be Function, Promise or String. */ protected addEventHandlers(): void { if (this.role == null) { @@ -166,11 +161,20 @@ export default class AriaSetter { const params = this.options.binding.value; + const + getCallbackName = (key: string) => `on-${key.slice(1)}`.camelize(false); + for (const key in params) { - if (key in EventNames) { + if (key.startsWith('@')) { + const + callbackName = getCallbackName(key); + + if (!Object.isFunction(this.role[callbackName])) { + Object.throw('Aria role engine does not contains event handler for passed event name or the type of engine\'s property is not a function'); + } const - callback = this.role[EventNames[key]].bind(this.role), + callback = this.role[callbackName].bind(this.role), property = params[key]; if (Object.isFunction(property)) { diff --git a/src/core/component/directives/aria/roles-engines/combobox/index.ts b/src/core/component/directives/aria/roles-engines/combobox/index.ts index ad703aa2f3..456714dfca 100644 --- a/src/core/component/directives/aria/roles-engines/combobox/index.ts +++ b/src/core/component/directives/aria/roles-engines/combobox/index.ts @@ -6,6 +6,9 @@ * https://github.com/V4Fire/Client/blob/master/LICENSE */ +import type iAccess from 'traits/i-access/i-access'; +import type { ComponentInterface } from 'super/i-block/i-block'; + import type { ComboboxParams } from 'core/component/directives/aria/roles-engines/combobox/interface'; import { AriaRoleEngine, EngineOptions } from 'core/component/directives/aria/roles-engines/interface'; @@ -13,14 +16,17 @@ export class ComboboxEngine extends AriaRoleEngine { /** * Engine params */ - params: ComboboxParams; + override params: ComboboxParams; /** * First focusable element inside the element with directive or this element if there is no focusable inside */ override el: HTMLElement; - constructor(options: EngineOptions) { + /** @see [[AriaRoleEngine.Ctx]] */ + override Ctx!: ComponentInterface & iAccess; + + constructor(options: EngineOptions) { super(options); const diff --git a/src/core/component/directives/aria/roles-engines/combobox/interface.ts b/src/core/component/directives/aria/roles-engines/combobox/interface.ts index b34ccb2270..a319311e58 100644 --- a/src/core/component/directives/aria/roles-engines/combobox/interface.ts +++ b/src/core/component/directives/aria/roles-engines/combobox/interface.ts @@ -6,9 +6,9 @@ * https://github.com/V4Fire/Client/blob/master/LICENSE */ -import type { HandlerAttachment } from 'core/component/directives/aria/roles-engines/interface'; +import type { AbstractParams, HandlerAttachment } from 'core/component/directives/aria/roles-engines/interface'; -export interface ComboboxParams { +export interface ComboboxParams extends AbstractParams { isMultiple: boolean; '@change': HandlerAttachment; '@open': HandlerAttachment; diff --git a/src/core/component/directives/aria/roles-engines/combobox/test/unit/combobox.ts b/src/core/component/directives/aria/roles-engines/combobox/test/unit/combobox.ts index 8b99af6f76..1e92ffca7c 100644 --- a/src/core/component/directives/aria/roles-engines/combobox/test/unit/combobox.ts +++ b/src/core/component/directives/aria/roles-engines/combobox/test/unit/combobox.ts @@ -1,5 +1,3 @@ -// @ts-check - /*! * V4Fire Client Core * https://github.com/V4Fire/Client @@ -7,6 +5,7 @@ * Released under the MIT license * https://github.com/V4Fire/Client/blob/master/LICENSE */ + import type { JSHandle, Page } from 'playwright'; import type iBlock from 'super/i-block/i-block'; diff --git a/src/core/component/directives/aria/roles-engines/controls/index.ts b/src/core/component/directives/aria/roles-engines/controls/index.ts index f106754ddd..7fed253895 100644 --- a/src/core/component/directives/aria/roles-engines/controls/index.ts +++ b/src/core/component/directives/aria/roles-engines/controls/index.ts @@ -13,9 +13,9 @@ export class ControlsEngine extends AriaRoleEngine { /** * Engine params */ - params: ControlsParams; + override params: ControlsParams; - constructor(options: EngineOptions) { + constructor(options: EngineOptions) { super(options); this.params = options.params; diff --git a/src/core/component/directives/aria/roles-engines/controls/test/unit/controls.ts b/src/core/component/directives/aria/roles-engines/controls/test/unit/controls.ts index 5d014fe155..b43c742542 100644 --- a/src/core/component/directives/aria/roles-engines/controls/test/unit/controls.ts +++ b/src/core/component/directives/aria/roles-engines/controls/test/unit/controls.ts @@ -1,5 +1,3 @@ -// @ts-check - /*! * V4Fire Client Core * https://github.com/V4Fire/Client @@ -7,6 +5,7 @@ * Released under the MIT license * https://github.com/V4Fire/Client/blob/master/LICENSE */ + import type { JSHandle, Page } from 'playwright'; import type iBlock from 'super/i-block/i-block'; diff --git a/src/core/component/directives/aria/roles-engines/dialog/test/unit/dialog.ts b/src/core/component/directives/aria/roles-engines/dialog/test/unit/dialog.ts index f1f2c031f1..5031e0debe 100644 --- a/src/core/component/directives/aria/roles-engines/dialog/test/unit/dialog.ts +++ b/src/core/component/directives/aria/roles-engines/dialog/test/unit/dialog.ts @@ -1,5 +1,3 @@ -// @ts-check - /*! * V4Fire Client Core * https://github.com/V4Fire/Client @@ -7,6 +5,7 @@ * Released under the MIT license * https://github.com/V4Fire/Client/blob/master/LICENSE */ + import type { JSHandle, Page } from 'playwright'; import type iBlock from 'super/i-block/i-block'; diff --git a/src/core/component/directives/aria/roles-engines/index.ts b/src/core/component/directives/aria/roles-engines/index.ts index 9fdac42030..cbb9f1bbce 100644 --- a/src/core/component/directives/aria/roles-engines/index.ts +++ b/src/core/component/directives/aria/roles-engines/index.ts @@ -17,4 +17,4 @@ export * from 'core/component/directives/aria/roles-engines/option'; export * from 'core/component/directives/aria/roles-engines/tree'; export * from 'core/component/directives/aria/roles-engines/treeitem'; -export { AriaRoleEngine, EngineOptions, EventNames } from 'core/component/directives/aria/roles-engines/interface'; +export { AriaRoleEngine, EngineOptions } from 'core/component/directives/aria/roles-engines/interface'; diff --git a/src/core/component/directives/aria/roles-engines/interface.ts b/src/core/component/directives/aria/roles-engines/interface.ts index 9a987a3689..26f1e50576 100644 --- a/src/core/component/directives/aria/roles-engines/interface.ts +++ b/src/core/component/directives/aria/roles-engines/interface.ts @@ -7,10 +7,19 @@ */ import type Async from 'core/async'; -import type iAccess from 'traits/i-access/i-access'; -import type iBlock from 'super/i-block/i-block'; +import type { ComponentInterface } from 'super/i-block/i-block'; export abstract class AriaRoleEngine { + /** + * Type: directive passed params + */ + readonly Params!: AbstractParams; + + /** + * Type: component on which the directive is set + */ + readonly Ctx!: ComponentInterface; + /** * Element on which the directive is set */ @@ -19,32 +28,42 @@ export abstract class AriaRoleEngine { /** * Component on which the directive is set */ - readonly ctx: CanUndef; + readonly ctx?: this['Ctx']; /** * Directive passed modifiers */ - readonly modifiers: CanUndef>; + readonly modifiers?: Dictionary; + + /** + * Directive passed params + */ + readonly params: this['Params']; /** * Async instance */ async: CanUndef; - constructor({el, ctx, modifiers}: EngineOptions) { + constructor({el, ctx, modifiers, params, async}: EngineOptions) { this.el = el; this.ctx = ctx; this.modifiers = modifiers; + this.params = params; + this.async = async; } abstract init(): void; } -export interface EngineOptions { +export interface AbstractParams {} + +export interface EngineOptions

{ el: HTMLElement; - modifiers: CanUndef>; - params: DictionaryType; - ctx: iBlock & iAccess; + ctx?: C; + modifiers?: Dictionary; + params: P; + async: Async; } export type HandlerAttachment = (cb: Function) => void; @@ -58,9 +77,3 @@ export const enum KeyCodes { RIGHT = 'ArrowRight', DOWN = 'ArrowDown' } - -export enum EventNames { - '@open' = 'onOpen', - '@close' = 'onClose', - '@change' = 'onChange' -} diff --git a/src/core/component/directives/aria/roles-engines/listbox/test/unit/listbox.ts b/src/core/component/directives/aria/roles-engines/listbox/test/unit/listbox.ts index ff654c727b..d4fd92d3cb 100644 --- a/src/core/component/directives/aria/roles-engines/listbox/test/unit/listbox.ts +++ b/src/core/component/directives/aria/roles-engines/listbox/test/unit/listbox.ts @@ -1,5 +1,3 @@ -// @ts-check - /*! * V4Fire Client Core * https://github.com/V4Fire/Client @@ -7,6 +5,7 @@ * Released under the MIT license * https://github.com/V4Fire/Client/blob/master/LICENSE */ + import type { JSHandle, Page } from 'playwright'; import type iBlock from 'super/i-block/i-block'; diff --git a/src/core/component/directives/aria/roles-engines/option/index.ts b/src/core/component/directives/aria/roles-engines/option/index.ts index 566d2850ab..49322476a5 100644 --- a/src/core/component/directives/aria/roles-engines/option/index.ts +++ b/src/core/component/directives/aria/roles-engines/option/index.ts @@ -13,9 +13,9 @@ export class OptionEngine extends AriaRoleEngine { /** * Engine params */ - params: OptionParams; + override params: OptionParams; - constructor(options: EngineOptions) { + constructor(options: EngineOptions) { super(options); this.params = options.params; diff --git a/src/core/component/directives/aria/roles-engines/option/test/unit/option.ts b/src/core/component/directives/aria/roles-engines/option/test/unit/option.ts index 0b24a690fb..f4b912d6d0 100644 --- a/src/core/component/directives/aria/roles-engines/option/test/unit/option.ts +++ b/src/core/component/directives/aria/roles-engines/option/test/unit/option.ts @@ -1,5 +1,3 @@ -// @ts-check - /*! * V4Fire Client Core * https://github.com/V4Fire/Client @@ -7,6 +5,7 @@ * Released under the MIT license * https://github.com/V4Fire/Client/blob/master/LICENSE */ + import type { JSHandle, Page } from 'playwright'; import type iBlock from 'super/i-block/i-block'; diff --git a/src/core/component/directives/aria/roles-engines/tab/index.ts b/src/core/component/directives/aria/roles-engines/tab/index.ts index 71c58da143..43429b08cb 100644 --- a/src/core/component/directives/aria/roles-engines/tab/index.ts +++ b/src/core/component/directives/aria/roles-engines/tab/index.ts @@ -9,6 +9,9 @@ * Copyright © [2022] W3C® (MIT, ERCIM, Keio, Beihang). */ +import type iBlock from 'super/i-block/i-block'; +import type iAccess from 'traits/i-access/i-access'; + import type { TabParams } from 'core/component/directives/aria/roles-engines/tab/interface'; import { AriaRoleEngine, EngineOptions, KeyCodes } from 'core/component/directives/aria/roles-engines/interface'; @@ -16,9 +19,12 @@ export class TabEngine extends AriaRoleEngine { /** * Engine params */ - params: TabParams; + override params: TabParams; + + /** @see [[AriaRoleEngine.ctx]] */ + override ctx?: iBlock & iAccess; - constructor(options: EngineOptions) { + constructor(options: EngineOptions) { super(options); this.params = options.params; @@ -91,7 +97,7 @@ export class TabEngine extends AriaRoleEngine { */ protected moveFocus(step: 1 | -1): void { const - focusable = this.ctx?.getNextFocusableElement(step); + focusable = this.ctx?.getNextFocusableElement(step); focusable?.focus(); } diff --git a/src/core/component/directives/aria/roles-engines/tab/test/unit/tab.ts b/src/core/component/directives/aria/roles-engines/tab/test/unit/tab.ts index f2f1d8a31a..ee1725eb04 100644 --- a/src/core/component/directives/aria/roles-engines/tab/test/unit/tab.ts +++ b/src/core/component/directives/aria/roles-engines/tab/test/unit/tab.ts @@ -1,5 +1,3 @@ -// @ts-check - /*! * V4Fire Client Core * https://github.com/V4Fire/Client @@ -7,6 +5,7 @@ * Released under the MIT license * https://github.com/V4Fire/Client/blob/master/LICENSE */ + import type { JSHandle, Page } from 'playwright'; import type iBlock from 'super/i-block/i-block'; diff --git a/src/core/component/directives/aria/roles-engines/tablist/index.ts b/src/core/component/directives/aria/roles-engines/tablist/index.ts index 74ba0e4970..83451227bd 100644 --- a/src/core/component/directives/aria/roles-engines/tablist/index.ts +++ b/src/core/component/directives/aria/roles-engines/tablist/index.ts @@ -13,9 +13,9 @@ export class TablistEngine extends AriaRoleEngine { /** * Engine params */ - params: TablistParams; + override params: TablistParams; - constructor(options: EngineOptions) { + constructor(options: EngineOptions) { super(options); this.params = options.params; diff --git a/src/core/component/directives/aria/roles-engines/tablist/test/unit/tablist.ts b/src/core/component/directives/aria/roles-engines/tablist/test/unit/tablist.ts index e688c633bc..fd4872facb 100644 --- a/src/core/component/directives/aria/roles-engines/tablist/test/unit/tablist.ts +++ b/src/core/component/directives/aria/roles-engines/tablist/test/unit/tablist.ts @@ -1,5 +1,3 @@ -// @ts-check - /*! * V4Fire Client Core * https://github.com/V4Fire/Client @@ -7,6 +5,7 @@ * Released under the MIT license * https://github.com/V4Fire/Client/blob/master/LICENSE */ + import type { JSHandle, Page } from 'playwright'; import type iBlock from 'super/i-block/i-block'; diff --git a/src/core/component/directives/aria/roles-engines/tabpanel/test/unit/tabpanel.ts b/src/core/component/directives/aria/roles-engines/tabpanel/test/unit/tabpanel.ts index f99e3050d1..72e2422067 100644 --- a/src/core/component/directives/aria/roles-engines/tabpanel/test/unit/tabpanel.ts +++ b/src/core/component/directives/aria/roles-engines/tabpanel/test/unit/tabpanel.ts @@ -1,5 +1,3 @@ -// @ts-check - /*! * V4Fire Client Core * https://github.com/V4Fire/Client @@ -7,6 +5,7 @@ * Released under the MIT license * https://github.com/V4Fire/Client/blob/master/LICENSE */ + import type { JSHandle, Page } from 'playwright'; import type iBlock from 'super/i-block/i-block'; diff --git a/src/core/component/directives/aria/roles-engines/tree/index.ts b/src/core/component/directives/aria/roles-engines/tree/index.ts index 4d94d71be4..483901f2f7 100644 --- a/src/core/component/directives/aria/roles-engines/tree/index.ts +++ b/src/core/component/directives/aria/roles-engines/tree/index.ts @@ -13,9 +13,9 @@ export class TreeEngine extends AriaRoleEngine { /** * Engine params */ - params: TreeParams; + override params: TreeParams; - constructor(options: EngineOptions) { + constructor(options: EngineOptions) { super(options); this.params = options.params; diff --git a/src/core/component/directives/aria/roles-engines/tree/test/unit/tree.ts b/src/core/component/directives/aria/roles-engines/tree/test/unit/tree.ts index 5f76193047..e040da54ed 100644 --- a/src/core/component/directives/aria/roles-engines/tree/test/unit/tree.ts +++ b/src/core/component/directives/aria/roles-engines/tree/test/unit/tree.ts @@ -1,5 +1,3 @@ -// @ts-check - /*! * V4Fire Client Core * https://github.com/V4Fire/Client @@ -7,6 +5,7 @@ * Released under the MIT license * https://github.com/V4Fire/Client/blob/master/LICENSE */ + import type { JSHandle, Page } from 'playwright'; import type iBlock from 'super/i-block/i-block'; diff --git a/src/core/component/directives/aria/roles-engines/treeitem/README.md b/src/core/component/directives/aria/roles-engines/treeitem/README.md index 7e59a5b0bd..3c262ae318 100644 --- a/src/core/component/directives/aria/roles-engines/treeitem/README.md +++ b/src/core/component/directives/aria/roles-engines/treeitem/README.md @@ -15,3 +15,13 @@ Expects `iAccess` trait to be realized. < &__foo v-aria:treeitem = {...} ``` + +## Adding new role engines +When creating a new role engine which handles some components events the contract of passed params types and naming should be respected. + +The name of handlers in engine should be like `onChange`, `onOpen`, etc. +The name of property in passed params should be like `@change`, `@open`, etc. +Types of the property on passed params could be: +- `Function` that accepts callback parameter; +- `Promise`, so the handler will be passed in `.then` method; +- `String` that is the name of component's event, so the handler will be added as a listener to this event. diff --git a/src/core/component/directives/aria/roles-engines/treeitem/index.ts b/src/core/component/directives/aria/roles-engines/treeitem/index.ts index f2de0c1c60..578b2a897a 100644 --- a/src/core/component/directives/aria/roles-engines/treeitem/index.ts +++ b/src/core/component/directives/aria/roles-engines/treeitem/index.ts @@ -10,6 +10,7 @@ */ import iAccess from 'traits/i-access/i-access'; +import type iBlock from 'super/i-block/i-block'; import type { TreeitemParams } from 'core/component/directives/aria/roles-engines/treeitem/interface'; import { AriaRoleEngine, KeyCodes, EngineOptions } from 'core/component/directives/aria/roles-engines/interface'; @@ -18,9 +19,12 @@ export class TreeitemEngine extends AriaRoleEngine { /** * Engine params */ - params: TreeitemParams; + override params: TreeitemParams; - constructor(options: EngineOptions) { + /** @see [[AriaRoleEngine.ctx]] */ + override ctx?: iBlock & iAccess; + + constructor(options: EngineOptions) { super(options); if (!iAccess.is(this.ctx)) { @@ -74,7 +78,7 @@ export class TreeitemEngine extends AriaRoleEngine { */ protected moveFocus(step: 1 | -1): void { const - nextEl = this.ctx?.getNextFocusableElement(step); + nextEl = this.ctx?.getNextFocusableElement(step); if (nextEl != null) { this.focusNext(nextEl); diff --git a/src/core/component/directives/aria/roles-engines/treeitem/test/unit/treeitem.ts b/src/core/component/directives/aria/roles-engines/treeitem/test/unit/treeitem.ts index 58b3b56b50..00bde5251e 100644 --- a/src/core/component/directives/aria/roles-engines/treeitem/test/unit/treeitem.ts +++ b/src/core/component/directives/aria/roles-engines/treeitem/test/unit/treeitem.ts @@ -1,5 +1,3 @@ -// @ts-check - /*! * V4Fire Client Core * https://github.com/V4Fire/Client @@ -7,6 +5,7 @@ * Released under the MIT license * https://github.com/V4Fire/Client/blob/master/LICENSE */ + import type { JSHandle, Page } from 'playwright'; import type iBlock from 'super/i-block/i-block'; @@ -62,40 +61,48 @@ test.describe('v-aria:treeitem', () => { items = ctx.unsafe.block.elements('node'), labels = document.querySelectorAll('label'); - const res: any[] = []; + const + res: Array> = []; + + const + eq = (index: number) => document.activeElement?.id === labels[index].getAttribute('for'), + att = (): Nullable => items[1].getAttribute('aria-expanded'), + dis = (key: string) => document.activeElement?.dispatchEvent( + new KeyboardEvent('keydown', {key, bubbles: true}) + ); input?.focus(); input?.dispatchEvent(new KeyboardEvent('keydown', {key: 'ArrowDown', bubbles: true})); - res.push(document.activeElement?.id === labels[1].getAttribute('for')); + res.push(eq(1)); - document.activeElement?.dispatchEvent(new KeyboardEvent('keydown', {key: 'ArrowUp', bubbles: true})); - res.push(document.activeElement?.id === labels[0].getAttribute('for')); + dis('ArrowUp'); + res.push(eq(0)); - document.activeElement?.dispatchEvent(new KeyboardEvent('keydown', {key: 'ArrowDown', bubbles: true})); - document.activeElement?.dispatchEvent(new KeyboardEvent('keydown', {key: 'Enter', bubbles: true})); - res.push(items[1].getAttribute('aria-expanded')); + dis('ArrowDown'); + dis('Enter'); + res.push(att()); - document.activeElement?.dispatchEvent(new KeyboardEvent('keydown', {key: 'Enter', bubbles: true})); - res.push(items[1].getAttribute('aria-expanded')); + dis('Enter'); + res.push(att()); - document.activeElement?.dispatchEvent(new KeyboardEvent('keydown', {key: 'ArrowRight', bubbles: true})); - res.push(items[1].getAttribute('aria-expanded')); + dis('ArrowRight'); + res.push(att()); - document.activeElement?.dispatchEvent(new KeyboardEvent('keydown', {key: 'ArrowRight', bubbles: true})); - res.push(document.activeElement?.id === labels[2].getAttribute('for')); + dis('ArrowRight'); + res.push(eq(2)); - document.activeElement?.dispatchEvent(new KeyboardEvent('keydown', {key: 'ArrowLeft', bubbles: true})); - res.push(document.activeElement?.id === labels[1].getAttribute('for')); + dis('ArrowLeft'); + res.push(eq(1)); - document.activeElement?.dispatchEvent(new KeyboardEvent('keydown', {key: 'ArrowLeft', bubbles: true})); - res.push(items[1].getAttribute('aria-expanded')); + dis('ArrowLeft'); + res.push(att()); - document.activeElement?.dispatchEvent(new KeyboardEvent('keydown', {key: 'Home', bubbles: true})); - res.push(document.activeElement?.id === labels[0].getAttribute('for')); + dis('Home'); + res.push(eq(0)); - document.activeElement?.dispatchEvent(new KeyboardEvent('keydown', {key: 'End', bubbles: true})); - res.push(document.activeElement?.id === labels[3].getAttribute('for')); + dis('End'); + res.push(eq(3)); return res; }) @@ -118,40 +125,48 @@ test.describe('v-aria:treeitem', () => { items = ctx.unsafe.block.elements('node'), labels = document.querySelectorAll('label'); - const res: Array> = []; + const + res: Array> = []; + + const + eq = (index: number) => document.activeElement?.id === labels[index].getAttribute('for'), + att = (): Nullable => items[1].getAttribute('aria-expanded'), + dis = (key: string) => document.activeElement?.dispatchEvent( + new KeyboardEvent('keydown', {key, bubbles: true}) + ); input?.focus(); input?.dispatchEvent(new KeyboardEvent('keydown', {key: 'ArrowRight', bubbles: true})); - res.push(document.activeElement?.id === labels[1].getAttribute('for')); + res.push(eq(1)); - document.activeElement?.dispatchEvent(new KeyboardEvent('keydown', {key: 'ArrowLeft', bubbles: true})); - res.push(document.activeElement?.id === labels[0].getAttribute('for')); + dis('ArrowLeft'); + res.push(eq(0)); - document.activeElement?.dispatchEvent(new KeyboardEvent('keydown', {key: 'ArrowRight', bubbles: true})); - document.activeElement?.dispatchEvent(new KeyboardEvent('keydown', {key: 'Enter', bubbles: true})); - res.push(items[1].getAttribute('aria-expanded')); + dis('ArrowRight'); + dis('Enter'); + res.push(att()); - document.activeElement?.dispatchEvent(new KeyboardEvent('keydown', {key: 'Enter', bubbles: true})); - res.push(items[1].getAttribute('aria-expanded')); + dis('Enter'); + res.push(att()); - document.activeElement?.dispatchEvent(new KeyboardEvent('keydown', {key: 'ArrowDown', bubbles: true})); - res.push(items[1].getAttribute('aria-expanded')); + dis('ArrowDown'); + res.push(att()); - document.activeElement?.dispatchEvent(new KeyboardEvent('keydown', {key: 'ArrowDown', bubbles: true})); - res.push(document.activeElement?.id === labels[2].getAttribute('for')); + dis('ArrowDown'); + res.push(eq(2)); - document.activeElement?.dispatchEvent(new KeyboardEvent('keydown', {key: 'ArrowUp', bubbles: true})); - res.push(document.activeElement?.id === labels[1].getAttribute('for')); + dis('ArrowUp'); + res.push(eq(1)); - document.activeElement?.dispatchEvent(new KeyboardEvent('keydown', {key: 'ArrowUp', bubbles: true})); - res.push(items[1].getAttribute('aria-expanded')); + dis('ArrowUp'); + res.push(att()); - document.activeElement?.dispatchEvent(new KeyboardEvent('keydown', {key: 'Home', bubbles: true})); - res.push(document.activeElement?.id === labels[0].getAttribute('for')); + dis('Home'); + res.push(eq(0)); - document.activeElement?.dispatchEvent(new KeyboardEvent('keydown', {key: 'End', bubbles: true})); - res.push(document.activeElement?.id === labels[3].getAttribute('for')); + dis('End'); + res.push(eq(3)); return res; }) diff --git a/src/core/component/directives/aria/test/unit/simple.ts b/src/core/component/directives/aria/test/unit/simple.ts index 649d116a97..ff9cab8333 100644 --- a/src/core/component/directives/aria/test/unit/simple.ts +++ b/src/core/component/directives/aria/test/unit/simple.ts @@ -1,5 +1,3 @@ -// @ts-check - /*! * V4Fire Client Core * https://github.com/V4Fire/Client @@ -7,6 +5,7 @@ * Released under the MIT license * https://github.com/V4Fire/Client/blob/master/LICENSE */ + import type { JSHandle, Page } from 'playwright'; import type iBlock from 'super/i-block/i-block'; diff --git a/src/traits/i-access/i-access.ts b/src/traits/i-access/i-access.ts index 37d03c86f9..c49aa0bdec 100644 --- a/src/traits/i-access/i-access.ts +++ b/src/traits/i-access/i-access.ts @@ -231,7 +231,7 @@ export default abstract class iAccess { /** @see [[iAccess.getNextFocusableElement]] */ static getNextFocusableElement: AddSelf = - (component, step, el?): CanUndef => { + (component, step, el?): CanUndef => { if (document.activeElement == null) { return; } @@ -404,7 +404,7 @@ export default abstract class iAccess { * @param step * @param el - a context to search, if not set, document will be used */ - getNextFocusableElement(step: 1 | -1, el?: T): CanUndef { + getNextFocusableElement(step: 1 | -1, el?: T): CanUndef { return Object.throw(); } @@ -417,7 +417,7 @@ export default abstract class iAccess { } /** - * Find all focusable elements except disabled ones. Search includes the specified element + * Find all focusable elements except disabled ones. Search includes the specified element. * @param el - a context to search, if not set, component will be used */ findAllFocusableElements(el?: T): IterableIterator> { From 93c203caa2db1daee15350c28f90f85d8076bc06 Mon Sep 17 00:00:00 2001 From: Aleksandr Bunin Date: Mon, 15 Aug 2022 11:13:37 +0300 Subject: [PATCH 026/185] upd deps --- package-lock.json | 115 ++++++++++++++++++++++++++++++++-------------- 1 file changed, 80 insertions(+), 35 deletions(-) diff --git a/package-lock.json b/package-lock.json index b25bce8d73..1f1f7219b4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -36,7 +36,7 @@ "@types/jasmine": "3.10.3", "@types/semver": "7.3.10", "@types/webpack": "5.28.0", - "@v4fire/core": "3.86.2", + "@v4fire/core": "3.87.0", "@v4fire/linters": "1.9.0", "husky": "7.0.4", "nyc": "15.1.0", @@ -121,7 +121,7 @@ "webpack-cli": "4.9.2" }, "peerDependencies": { - "@v4fire/core": "^3.86.2", + "@v4fire/core": "^3.87.0", "webpack": "^5.70.0" } }, @@ -4430,9 +4430,9 @@ } }, "node_modules/@v4fire/core": { - "version": "3.86.2", - "resolved": "https://registry.npmjs.org/@v4fire/core/-/core-3.86.2.tgz", - "integrity": "sha512-Xu/SKvKkV/XBT+CRXkOVak1KMelBNZseQY9Kh2HD5ZUZ3tyuYfyr5NtwEzCY3rVg+ouh716lQ4s5WFg1u7MJRQ==", + "version": "3.87.0", + "resolved": "https://registry.npmjs.org/@v4fire/core/-/core-3.87.0.tgz", + "integrity": "sha512-NbfSuOWxMldX6IsJ3vuklPB5h13O0Idxnjfwd0j3zCQk2x429TIlCXZuFOJsTdYu6eCBlUc2hfwO1BNtktOHCA==", "dev": true, "dependencies": { "@swc/core": "1.2.153", @@ -4497,7 +4497,7 @@ "through2": "4.0.2", "tsc-alias": "1.6.1", "tsconfig": "7.0.0", - "typedoc": "0.22.12", + "typedoc": "0.22.13", "typescript": "4.6.2", "upath": "2.0.1" } @@ -17089,9 +17089,9 @@ } }, "node_modules/jsonc-parser": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.0.0.tgz", - "integrity": "sha512-fQzRfAbIBnR0IQvftw9FJveWiHp72Fg20giDrHz6TdfB12UH/uue0D3hm57UB5KgAVuniLMCaS8P1IMj9NR7cA==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.1.0.tgz", + "integrity": "sha512-DRf0QjnNeCUds3xTjKlQQ3DpJD51GvDjJfnxUVWg6PZTo2otSm+slzNAxU/35hF8/oJIKoG9slq30JYOsF2azg==", "dev": true, "optional": true }, @@ -17975,9 +17975,9 @@ } }, "node_modules/marked": { - "version": "4.0.17", - "resolved": "https://registry.npmjs.org/marked/-/marked-4.0.17.tgz", - "integrity": "sha512-Wfk0ATOK5iPxM4ptrORkFemqroz0ZDxp5MWfYA7H/F+wO17NRWV5Ypxi6p3g2Xmw2bKeiYOl6oVnLHKxBA0VhA==", + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/marked/-/marked-4.0.18.tgz", + "integrity": "sha512-wbLDJ7Zh0sqA0Vdg6aqlbT+yPxqLblpAZh1mK2+AO2twQkPywvvqQNfEPVwSSRjZ7dZcdeVBIAgiO7MMp3Dszw==", "dev": true, "optional": true, "bin": { @@ -26397,17 +26397,17 @@ } }, "node_modules/typedoc": { - "version": "0.22.12", - "resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.22.12.tgz", - "integrity": "sha512-FcyC+YuaOpr3rB9QwA1IHOi9KnU2m50sPJW5vcNRPCIdecp+3bFkh7Rq5hBU1Fyn29UR2h4h/H7twZHWDhL0sw==", + "version": "0.22.13", + "resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.22.13.tgz", + "integrity": "sha512-NHNI7Dr6JHa/I3+c62gdRNXBIyX7P33O9TafGLd07ur3MqzcKgwTvpg18EtvCLHJyfeSthAtCLpM7WkStUmDuQ==", "dev": true, "optional": true, "dependencies": { "glob": "^7.2.0", "lunr": "^2.3.9", - "marked": "^4.0.10", - "minimatch": "^3.0.4", - "shiki": "^0.10.0" + "marked": "^4.0.12", + "minimatch": "^5.0.1", + "shiki": "^0.10.1" }, "bin": { "typedoc": "bin/typedoc" @@ -26416,7 +26416,30 @@ "node": ">= 12.10.0" }, "peerDependencies": { - "typescript": "4.0.x || 4.1.x || 4.2.x || 4.3.x || 4.4.x || 4.5.x" + "typescript": "4.0.x || 4.1.x || 4.2.x || 4.3.x || 4.4.x || 4.5.x || 4.6.x" + } + }, + "node_modules/typedoc/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "optional": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/typedoc/node_modules/minimatch": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.0.tgz", + "integrity": "sha512-9TPBGGak4nHfGZsPBohm9AWg6NoT7QTCehS3BIJABslyZbzxfV78QM2Y6+i741OPZIafFAaiiEMh5OyIrJPgtg==", + "dev": true, + "optional": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" } }, "node_modules/typescript": { @@ -31086,9 +31109,9 @@ } }, "@v4fire/core": { - "version": "3.86.2", - "resolved": "https://registry.npmjs.org/@v4fire/core/-/core-3.86.2.tgz", - "integrity": "sha512-Xu/SKvKkV/XBT+CRXkOVak1KMelBNZseQY9Kh2HD5ZUZ3tyuYfyr5NtwEzCY3rVg+ouh716lQ4s5WFg1u7MJRQ==", + "version": "3.87.0", + "resolved": "https://registry.npmjs.org/@v4fire/core/-/core-3.87.0.tgz", + "integrity": "sha512-NbfSuOWxMldX6IsJ3vuklPB5h13O0Idxnjfwd0j3zCQk2x429TIlCXZuFOJsTdYu6eCBlUc2hfwO1BNtktOHCA==", "dev": true, "requires": { "@babel/core": "7.17.5", @@ -31147,7 +31170,7 @@ "tsconfig": "7.0.0", "tsconfig-paths": "3.13.0", "tslib": "2.3.1", - "typedoc": "0.22.12", + "typedoc": "0.22.13", "typescript": "4.6.2", "upath": "2.0.1", "w3c-xmlserializer": "2.0.0" @@ -41058,9 +41081,9 @@ } }, "jsonc-parser": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.0.0.tgz", - "integrity": "sha512-fQzRfAbIBnR0IQvftw9FJveWiHp72Fg20giDrHz6TdfB12UH/uue0D3hm57UB5KgAVuniLMCaS8P1IMj9NR7cA==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.1.0.tgz", + "integrity": "sha512-DRf0QjnNeCUds3xTjKlQQ3DpJD51GvDjJfnxUVWg6PZTo2otSm+slzNAxU/35hF8/oJIKoG9slq30JYOsF2azg==", "dev": true, "optional": true }, @@ -41802,9 +41825,9 @@ } }, "marked": { - "version": "4.0.17", - "resolved": "https://registry.npmjs.org/marked/-/marked-4.0.17.tgz", - "integrity": "sha512-Wfk0ATOK5iPxM4ptrORkFemqroz0ZDxp5MWfYA7H/F+wO17NRWV5Ypxi6p3g2Xmw2bKeiYOl6oVnLHKxBA0VhA==", + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/marked/-/marked-4.0.18.tgz", + "integrity": "sha512-wbLDJ7Zh0sqA0Vdg6aqlbT+yPxqLblpAZh1mK2+AO2twQkPywvvqQNfEPVwSSRjZ7dZcdeVBIAgiO7MMp3Dszw==", "dev": true, "optional": true }, @@ -48384,17 +48407,39 @@ } }, "typedoc": { - "version": "0.22.12", - "resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.22.12.tgz", - "integrity": "sha512-FcyC+YuaOpr3rB9QwA1IHOi9KnU2m50sPJW5vcNRPCIdecp+3bFkh7Rq5hBU1Fyn29UR2h4h/H7twZHWDhL0sw==", + "version": "0.22.13", + "resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.22.13.tgz", + "integrity": "sha512-NHNI7Dr6JHa/I3+c62gdRNXBIyX7P33O9TafGLd07ur3MqzcKgwTvpg18EtvCLHJyfeSthAtCLpM7WkStUmDuQ==", "dev": true, "optional": true, "requires": { "glob": "^7.2.0", "lunr": "^2.3.9", - "marked": "^4.0.10", - "minimatch": "^3.0.4", - "shiki": "^0.10.0" + "marked": "^4.0.12", + "minimatch": "^5.0.1", + "shiki": "^0.10.1" + }, + "dependencies": { + "brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "optional": true, + "requires": { + "balanced-match": "^1.0.0" + } + }, + "minimatch": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.0.tgz", + "integrity": "sha512-9TPBGGak4nHfGZsPBohm9AWg6NoT7QTCehS3BIJABslyZbzxfV78QM2Y6+i741OPZIafFAaiiEMh5OyIrJPgtg==", + "dev": true, + "optional": true, + "requires": { + "brace-expansion": "^2.0.1" + } + } } }, "typescript": { From 18ce98f98fd7f39c4bb6ea5bd57a9f34e68ac3ef Mon Sep 17 00:00:00 2001 From: Aleksandr Bunin Date: Mon, 15 Aug 2022 14:02:15 +0300 Subject: [PATCH 027/185] add AccessibleElement type --- index.d.ts | 2 ++ .../aria/roles-engines/combobox/index.ts | 2 +- .../aria/roles-engines/tab/index.ts | 6 ++--- .../aria/roles-engines/treeitem/index.ts | 10 ++++--- src/traits/i-access/i-access.ts | 26 ++++++++++--------- 5 files changed, 27 insertions(+), 19 deletions(-) diff --git a/index.d.ts b/index.d.ts index 81ccb3bde4..22949f9f7b 100644 --- a/index.d.ts +++ b/index.d.ts @@ -215,3 +215,5 @@ interface TouchGesturePoint extends Partial { x: number; y: number; } + +type AccessibleElement = Element & HTMLOrSVGElement; diff --git a/src/core/component/directives/aria/roles-engines/combobox/index.ts b/src/core/component/directives/aria/roles-engines/combobox/index.ts index 456714dfca..708fab3488 100644 --- a/src/core/component/directives/aria/roles-engines/combobox/index.ts +++ b/src/core/component/directives/aria/roles-engines/combobox/index.ts @@ -32,7 +32,7 @@ export class ComboboxEngine extends AriaRoleEngine { const {el} = this; - this.el = this.ctx?.findFocusableElement() ?? el; + this.el = this.ctx?.findFocusableElement() ?? el; this.params = options.params; } diff --git a/src/core/component/directives/aria/roles-engines/tab/index.ts b/src/core/component/directives/aria/roles-engines/tab/index.ts index 43429b08cb..4f866e7bb0 100644 --- a/src/core/component/directives/aria/roles-engines/tab/index.ts +++ b/src/core/component/directives/aria/roles-engines/tab/index.ts @@ -65,7 +65,7 @@ export class TabEngine extends AriaRoleEngine { */ protected moveFocusToFirstTab(): void { const - firstTab = this.ctx?.findFocusableElement(); + firstTab = this.ctx?.findFocusableElement(); firstTab?.focus(); } @@ -75,14 +75,14 @@ export class TabEngine extends AriaRoleEngine { */ protected moveFocusToLastTab(): void { const - tabs = this.ctx?.findAllFocusableElements(); + tabs = this.ctx?.findAllFocusableElements(); if (tabs == null) { return; } let - lastTab: CanUndef; + lastTab: CanUndef; for (const tab of tabs) { lastTab = tab; diff --git a/src/core/component/directives/aria/roles-engines/treeitem/index.ts b/src/core/component/directives/aria/roles-engines/treeitem/index.ts index 578b2a897a..995317384d 100644 --- a/src/core/component/directives/aria/roles-engines/treeitem/index.ts +++ b/src/core/component/directives/aria/roles-engines/treeitem/index.ts @@ -65,7 +65,7 @@ export class TreeitemEngine extends AriaRoleEngine { * Changes focus from the current focused element to the passed one * @param el */ - protected focusNext(el: HTMLElement): void { + protected focusNext(el: AccessibleElement): void { this.ctx?.removeAllFromTabSequence(this.el); this.ctx?.restoreAllToTabSequence(el); @@ -143,10 +143,14 @@ export class TreeitemEngine extends AriaRoleEngine { */ protected setFocusToLastItem(): void { const - items = >this.ctx?.findAllFocusableElements(this.params.rootElement); + items = this.ctx?.findAllFocusableElements(this.params.rootElement); + + if (items == null) { + return; + } let - lastItem: CanUndef; + lastItem: CanUndef; for (const item of items) { if (item.offsetWidth > 0 || item.offsetHeight > 0) { diff --git a/src/traits/i-access/i-access.ts b/src/traits/i-access/i-access.ts index c49aa0bdec..0d953e5a82 100644 --- a/src/traits/i-access/i-access.ts +++ b/src/traits/i-access/i-access.ts @@ -180,7 +180,7 @@ export default abstract class iAccess { areElementsRemoved = false; const - focusableElems = >this.findAllFocusableElements(component, ctx); + focusableElems = this.findAllFocusableElements(component, ctx); for (const focusableEl of focusableElems) { if (!focusableEl.hasAttribute('data-tabindex')) { @@ -231,7 +231,7 @@ export default abstract class iAccess { /** @see [[iAccess.getNextFocusableElement]] */ static getNextFocusableElement: AddSelf = - (component, step, el?): CanUndef => { + (component, step, el?): CanUndef => { if (document.activeElement == null) { return; } @@ -239,7 +239,7 @@ export default abstract class iAccess { const ctx = el ?? document.documentElement, focusableElems = >this.findAllFocusableElements(component, ctx), - visibleFocusable: HTMLElement[] = []; + visibleFocusable: AccessibleElement[] = []; for (const focusableEl of focusableElems) { if ( @@ -252,7 +252,7 @@ export default abstract class iAccess { } const - index = visibleFocusable.indexOf(document.activeElement); + index = visibleFocusable.indexOf(document.activeElement); if (index > -1) { return visibleFocusable[index + step]; @@ -261,13 +261,13 @@ export default abstract class iAccess { /** @see [[iAccess.findFocusableElement]] */ static findFocusableElement: AddSelf = - (component, el?) => { + (component, el?): CanUndef => { const - ctx = el ?? component.$el, + ctx = el ?? component.$el, focusableElems = this.findAllFocusableElements(component, ctx); for (const focusableEl of focusableElems) { - if (!focusableEl?.hasAttribute('disabled')) { + if (!focusableEl.hasAttribute('disabled')) { return focusableEl; } } @@ -275,7 +275,7 @@ export default abstract class iAccess { /** @see [[iAccess.findAllFocusableElements]] */ static findAllFocusableElements: AddSelf = - (component, el?): IterableIterator> => { + (component, el?): IterableIterator => { const ctx = el ?? component.$el, focusableElems = ctx?.querySelectorAll(FOCUSABLE_SELECTOR); @@ -287,7 +287,9 @@ export default abstract class iAccess { focusableIter = sequence(focusableIter, intoIter([el])); } - function* createFocusableWithoutDisabled(iter: IterableIterator): IterableIterator { + function* createFocusableWithoutDisabled( + iter: IterableIterator + ): IterableIterator { for (const iterEl of iter) { if (!iterEl.hasAttribute('disabled')) { yield iterEl; @@ -404,7 +406,7 @@ export default abstract class iAccess { * @param step * @param el - a context to search, if not set, document will be used */ - getNextFocusableElement(step: 1 | -1, el?: T): CanUndef { + getNextFocusableElement(step: 1 | -1, el?: T): CanUndef { return Object.throw(); } @@ -412,7 +414,7 @@ export default abstract class iAccess { * Find focusable element except disabled ones * @param el - a context to search, if not set, component will be used */ - findFocusableElement(el?: T): CanUndef { + findFocusableElement(el?: T): CanUndef { return Object.throw(); } @@ -420,7 +422,7 @@ export default abstract class iAccess { * Find all focusable elements except disabled ones. Search includes the specified element. * @param el - a context to search, if not set, component will be used */ - findAllFocusableElements(el?: T): IterableIterator> { + findAllFocusableElements(el?: T): IterableIterator { return Object.throw(); } } From 3932ca7cee26822ff9559b6e48648fbbe6ac1fde Mon Sep 17 00:00:00 2001 From: kobezzza Date: Tue, 16 Aug 2022 10:34:05 +0300 Subject: [PATCH 028/185] refactor: small refactoring of new properties --- src/base/b-list/b-list.ts | 60 +++++++++++++++++++-------------------- src/base/b-tree/b-tree.ts | 47 +++++++++++++++--------------- 2 files changed, 53 insertions(+), 54 deletions(-) diff --git a/src/base/b-list/b-list.ts b/src/base/b-list/b-list.ts index 90fae79c76..8186ebfff7 100644 --- a/src/base/b-list/b-list.ts +++ b/src/base/b-list/b-list.ts @@ -36,8 +36,7 @@ export * from 'base/b-list/interface'; export const $$ = symbolGenerator(); -interface bList extends Trait { -} +interface bList extends Trait {} /** * Component to create a list of tabs/links @@ -264,14 +263,6 @@ class bList extends iData implements iVisible, iWidth, iItems, iAccess { protected activeStore!: this['Active']; - /** - * True if the component is used as a tablist - */ - @computed({dependencies: ['items']}) - protected get isTablist(): boolean { - return this.items.some((el) => el.href === undefined); - } - /** * A link to the active item element. * If the component is switched to the `multiple` mode, the getter will return an array of elements. @@ -303,6 +294,14 @@ class bList extends iData implements iVisible, iWidth, iItems, iAccess { }); } + /** + * True if the component is used as a tablist + */ + @computed({dependencies: ['items']}) + protected get isTablist(): boolean { + return this.items.some((el) => el.href === undefined); + } + /** * Returns true if the specified value is active * @param value @@ -712,40 +711,39 @@ class bList extends iData implements iVisible, iWidth, iItems, iAccess { const isActive = this.isActive.bind(this, item?.value); - const bindChangeEvent = (cb: Function) => { - this.on('change', () => { - if (Object.isSet(this.active)) { - cb(this.block?.elements('link', {active: true})); - - } else { - cb(this.block?.element('link', {active: true})); - } - }); - }; - const tablistConfig = { isMultiple: this.multiple, orientation: this.orientation }; const tabConfig = { - hasDefaultSelectedTabs: this.active != null, - isFirst: i === 0, orientation: this.orientation, - '@change': bindChangeEvent, + + isFirst: i === 0, + hasDefaultSelectedTabs: this.active != null, get isSelected() { return isActive(); - } + }, + + '@change': bindChangeEvent.bind(this) }; switch (role) { - case 'tablist': - return tablistConfig; - case 'tab': - return tabConfig; - default: - return {}; + case 'tablist': return tablistConfig; + case 'tab': return tabConfig; + default: return {}; + } + + function bindChangeEvent(this: bList, cb: Function) { + this.on('change', () => { + if (Object.isSet(this.active)) { + cb(this.block?.elements('link', {active: true})); + + } else { + cb(this.block?.element('link', {active: true})); + } + }); } } diff --git a/src/base/b-tree/b-tree.ts b/src/base/b-tree/b-tree.ts index 013cda0154..6212c0eabc 100644 --- a/src/base/b-tree/b-tree.ts +++ b/src/base/b-tree/b-tree.ts @@ -261,8 +261,8 @@ class bTree extends iData implements iItems, iAccess { * Returns a dictionary with configurations for the `v-aria` directive used as a treeitem * * @param role - * @param item - tab item data - * @param i - tab item position index + * @param item - tree item data + * @param i - tree item position index */ protected getAriaConfig(role: 'treeitem', item: this['Item'], i: number): Dictionary @@ -271,21 +271,6 @@ class bTree extends iData implements iItems, iAccess { getFoldedMod = this.getFoldedModById.bind(this, item?.id), root = () => this.top?.$el ?? this.$el; - const toggleFold = (target: HTMLElement, value?: boolean): void => { - const - mod = this.block?.getElMod(target, 'node', 'folded'); - - if (mod == null) { - return; - } - - const - newVal = value ? value : mod === 'false'; - - this.block?.setElMod(target, 'node', 'folded', newVal); - this.emit('fold', target, item, newVal); - }; - const treeConfig = { isRoot: this.top == null, orientation: this.orientation, @@ -295,13 +280,8 @@ class bTree extends iData implements iItems, iAccess { }; const treeitemConfig = { - isFirstRootItem: this.top == null && i === 0, orientation: this.orientation, - toggleFold, - - get rootElement() { - return root(); - }, + isFirstRootItem: this.top == null && i === 0, get isExpanded() { return getFoldedMod() === 'false'; @@ -309,6 +289,12 @@ class bTree extends iData implements iItems, iAccess { get isExpandable() { return item?.children != null; + }, + + toggleFold: toggleFold.bind(this), + + get rootElement() { + return root(); } }; @@ -317,6 +303,21 @@ class bTree extends iData implements iItems, iAccess { case 'treeitem': return treeitemConfig; default: return {}; } + + function toggleFold(this: bTree, target: HTMLElement, value?: boolean) { + const + mod = this.block?.getElMod(target, 'node', 'folded'); + + if (mod == null) { + return; + } + + const + newVal = value ? value : mod === 'false'; + + this.block?.setElMod(target, 'node', 'folded', newVal); + this.emit('fold', target, item, newVal); + } } /** From ee0135a40629ef2bc357385eaf06988f93a39b35 Mon Sep 17 00:00:00 2001 From: kobezzza Date: Tue, 16 Aug 2022 10:34:11 +0300 Subject: [PATCH 029/185] chore: updated changelog --- src/base/b-list/CHANGELOG.md | 9 +++++---- src/base/b-tree/CHANGELOG.md | 8 +++++--- src/base/b-window/CHANGELOG.md | 7 +++---- src/form/b-checkbox/CHANGELOG.md | 2 +- src/form/b-select/CHANGELOG.md | 2 +- src/traits/i-access/CHANGELOG.md | 2 +- src/traits/i-open/CHANGELOG.md | 2 +- 7 files changed, 17 insertions(+), 15 deletions(-) diff --git a/src/base/b-list/CHANGELOG.md b/src/base/b-list/CHANGELOG.md index bd365587c4..f4399e1a93 100644 --- a/src/base/b-list/CHANGELOG.md +++ b/src/base/b-list/CHANGELOG.md @@ -9,16 +9,17 @@ Changelog > - :house: [Internal] > - :nail_care: [Polish] -## v3.0.0-rc.??? (2022-??-??) +## v3.?.? (2022-??-??) #### :rocket: New Feature -* Added a new directive `v-aria` * Added a new prop `orientation` -* Added `isTablist` -* Added `getAriaConfig` * Now the component derives `iAccess` +#### :house: Internal + +* Improved component accessibility + ## v3.0.0-rc.211 (2021-07-21) #### :boom: Breaking Change diff --git a/src/base/b-tree/CHANGELOG.md b/src/base/b-tree/CHANGELOG.md index 9aeec86d84..51d3724336 100644 --- a/src/base/b-tree/CHANGELOG.md +++ b/src/base/b-tree/CHANGELOG.md @@ -9,15 +9,17 @@ Changelog > - :house: [Internal] > - :nail_care: [Polish] -## v3.0.0-rc.??? (2022-??-??) +## v3.?.? (2022-??-??) #### :rocket: New Feature -* Added a new directive `v-aria` * Added a new prop `orientation` -* Added `getAriaConfig` * Now the component derives `iAccess` +#### :house: Internal + +* Improved component accessibility + ## v3.0.0-rc.164 (2021-03-22) #### :house: Internal diff --git a/src/base/b-window/CHANGELOG.md b/src/base/b-window/CHANGELOG.md index 4fe7a2cff7..32ca2a33f5 100644 --- a/src/base/b-window/CHANGELOG.md +++ b/src/base/b-window/CHANGELOG.md @@ -9,12 +9,11 @@ Changelog > - :house: [Internal] > - :nail_care: [Polish] -## v3.?.? (2022-0?-??) +## v3.?.? (2022-??-??) -#### :rocket: New Feature +#### :house: Internal -* Added a new directive `v-aria` -* Added a new directive `v-id` +* Improved component accessibility ## v3.0.0-rc.211 (2021-07-21) diff --git a/src/form/b-checkbox/CHANGELOG.md b/src/form/b-checkbox/CHANGELOG.md index 5a05e47de2..38d8ea8b5c 100644 --- a/src/form/b-checkbox/CHANGELOG.md +++ b/src/form/b-checkbox/CHANGELOG.md @@ -9,7 +9,7 @@ Changelog > - :house: [Internal] > - :nail_care: [Polish] -## v3.0.0-rc.??? (2022-??-??) +## v3.?.? (2022-??-??) #### :bug: Bug Fix diff --git a/src/form/b-select/CHANGELOG.md b/src/form/b-select/CHANGELOG.md index 334f6805f1..f336cc6855 100644 --- a/src/form/b-select/CHANGELOG.md +++ b/src/form/b-select/CHANGELOG.md @@ -9,7 +9,7 @@ Changelog > - :house: [Internal] > - :nail_care: [Polish] -## v3.0.0-rc.??? (2022-??-??) +## v3.?.? (2022-??-??) #### :rocket: New Feature diff --git a/src/traits/i-access/CHANGELOG.md b/src/traits/i-access/CHANGELOG.md index f4cab58731..5823c3f158 100644 --- a/src/traits/i-access/CHANGELOG.md +++ b/src/traits/i-access/CHANGELOG.md @@ -9,7 +9,7 @@ Changelog > - :house: [Internal] > - :nail_care: [Polish] -## v3.0.0-rc.??? (2022-??-??) +## v3.?.? (2022-??-??) #### :rocket: New Feature diff --git a/src/traits/i-open/CHANGELOG.md b/src/traits/i-open/CHANGELOG.md index 7f2f3680c6..3473a34026 100644 --- a/src/traits/i-open/CHANGELOG.md +++ b/src/traits/i-open/CHANGELOG.md @@ -9,7 +9,7 @@ Changelog > - :house: [Internal] > - :nail_care: [Polish] -## v3.0.0-rc.??? (2022-??-??) +## v3.?.? (2022-??-??) #### :rocket: New Feature From 47d1cbe84ffbb518a302d4c9a8f6ab744a9fc757 Mon Sep 17 00:00:00 2001 From: kobezzza Date: Tue, 16 Aug 2022 11:11:40 +0300 Subject: [PATCH 030/185] fix: added support for updating directive value --- src/core/component/directives/id/index.ts | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/src/core/component/directives/id/index.ts b/src/core/component/directives/id/index.ts index 7e1c4a51ab..49e1214828 100644 --- a/src/core/component/directives/id/index.ts +++ b/src/core/component/directives/id/index.ts @@ -17,16 +17,28 @@ import type iBlock from 'super/i-block/i-block'; ComponentEngine.directive('id', { inserted(el: HTMLElement, binding: VNodeDirective, vnode: VNode): void { const - ctx = Object.cast(vnode.fakeContext), - {modifiers: mod} = binding; + ctx = Object.cast(vnode.fakeContext); - if (mod?.preserve != null && el.hasAttribute('id')) { + if (el.hasAttribute('id') && binding.modifiers?.preserve != null) { + el.setAttribute('data-v-id-preserve', 'true'); return; } + el.setAttribute('id', ctx.dom.getId(binding.value)); + }, + + update(el: HTMLElement, binding: VNodeDirective, vnode: VNode) { const - id = ctx.dom.getId(binding.value); + ctx = Object.cast(vnode.fakeContext); + + if (el.hasAttribute('data-v-id-preserve')) { + return; + } + + el.setAttribute('id', ctx.dom.getId(binding.value)); + }, - el.setAttribute('id', id); + unbind(el: HTMLElement) { + el.removeAttribute('data-v-id-preserve'); } }); From c7251efc0d5afa24061a9afae3234a7adb9094f6 Mon Sep 17 00:00:00 2001 From: kobezzza Date: Tue, 16 Aug 2022 11:11:59 +0300 Subject: [PATCH 031/185] doc: improved doc --- src/core/component/directives/id/README.md | 42 +++++++++++++++++----- 1 file changed, 33 insertions(+), 9 deletions(-) diff --git a/src/core/component/directives/id/README.md b/src/core/component/directives/id/README.md index 58c7b90465..8f53efd3dd 100644 --- a/src/core/component/directives/id/README.md +++ b/src/core/component/directives/id/README.md @@ -1,26 +1,50 @@ -# core/component/directives/aria +# core/component/directives/id -This module provides a directive for easy adding of id attribute. +This module provides a directive to easily add an id attribute to an element. -## Usage +``` +< div v-id = 'title' +``` + +## Why is this directive needed? +A page cannot have two or more elements with the same id attribute at the same time. But when we design or edit a +component markup and add such an attribute, we cannot be sure that the name is not already used by other components. +To solve this problem, any component has the `dom.getId` method. + +``` +< div :id = dom.getId('title') ``` -< &__foo v-id = 'title' + +This method returns the passed identifier, plus the unique ID of the component within which the method is called. +Thus, we only need to guarantee the uniqueness of the identifier within one template, and not all components. +The problem is solved, but now our template has become more "dirty" due to the addition of syntactic noise with the +method call and other stuff. This directive just solves this problem. +``` +< div v-id = 'title' ``` The same as -``` -< &__foo :id = dom.getId('title') +``` +< div :id = dom.getId('title') ``` ## Modifiers -1. `preserve` means that if there is already an id attribute on the element, -the directive will left it and will not set another one +### preserve +This modifier means that if the element already has an id attribute, then the directive will leave it and won't overwrite it + +``` +< div id = my-div1 | v-id = 'title1' +< div id = my-div2 | v-id.preserve = 'title2' ``` -< &__foo v-id.preserve = 'title' +Will turn into + +```html +

+
``` From 305a2ddf58edff1a1cc7b4e45c9395a0ba5fdf37 Mon Sep 17 00:00:00 2001 From: kobezzza Date: Tue, 16 Aug 2022 11:12:58 +0300 Subject: [PATCH 032/185] chore: updated changelog --- src/core/component/directives/aria/CHANGELOG.md | 2 +- .../directives/aria/roles-engines/combobox/CHANGELOG.md | 2 +- .../directives/aria/roles-engines/controls/CHANGELOG.md | 2 +- .../component/directives/aria/roles-engines/dialog/CHANGELOG.md | 2 +- .../directives/aria/roles-engines/listbox/CHANGELOG.md | 2 +- .../component/directives/aria/roles-engines/option/CHANGELOG.md | 2 +- .../component/directives/aria/roles-engines/tab/CHANGELOG.md | 2 +- .../directives/aria/roles-engines/tablist/CHANGELOG.md | 2 +- .../directives/aria/roles-engines/tabpanel/CHANGELOG.md | 2 +- .../component/directives/aria/roles-engines/tree/CHANGELOG.md | 2 +- .../directives/aria/roles-engines/treeitem/CHANGELOG.md | 2 +- src/core/component/directives/id/CHANGELOG.md | 2 +- 12 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/core/component/directives/aria/CHANGELOG.md b/src/core/component/directives/aria/CHANGELOG.md index ba2980fb55..6e314edf2d 100644 --- a/src/core/component/directives/aria/CHANGELOG.md +++ b/src/core/component/directives/aria/CHANGELOG.md @@ -9,7 +9,7 @@ Changelog > - :house: [Internal] > - :nail_care: [Polish] -## v?.??.? (2022-0?-??) +## v3.?.? (2022-??-??) #### :rocket: New Feature diff --git a/src/core/component/directives/aria/roles-engines/combobox/CHANGELOG.md b/src/core/component/directives/aria/roles-engines/combobox/CHANGELOG.md index ba2980fb55..6e314edf2d 100644 --- a/src/core/component/directives/aria/roles-engines/combobox/CHANGELOG.md +++ b/src/core/component/directives/aria/roles-engines/combobox/CHANGELOG.md @@ -9,7 +9,7 @@ Changelog > - :house: [Internal] > - :nail_care: [Polish] -## v?.??.? (2022-0?-??) +## v3.?.? (2022-??-??) #### :rocket: New Feature diff --git a/src/core/component/directives/aria/roles-engines/controls/CHANGELOG.md b/src/core/component/directives/aria/roles-engines/controls/CHANGELOG.md index ba2980fb55..6e314edf2d 100644 --- a/src/core/component/directives/aria/roles-engines/controls/CHANGELOG.md +++ b/src/core/component/directives/aria/roles-engines/controls/CHANGELOG.md @@ -9,7 +9,7 @@ Changelog > - :house: [Internal] > - :nail_care: [Polish] -## v?.??.? (2022-0?-??) +## v3.?.? (2022-??-??) #### :rocket: New Feature diff --git a/src/core/component/directives/aria/roles-engines/dialog/CHANGELOG.md b/src/core/component/directives/aria/roles-engines/dialog/CHANGELOG.md index ba2980fb55..6e314edf2d 100644 --- a/src/core/component/directives/aria/roles-engines/dialog/CHANGELOG.md +++ b/src/core/component/directives/aria/roles-engines/dialog/CHANGELOG.md @@ -9,7 +9,7 @@ Changelog > - :house: [Internal] > - :nail_care: [Polish] -## v?.??.? (2022-0?-??) +## v3.?.? (2022-??-??) #### :rocket: New Feature diff --git a/src/core/component/directives/aria/roles-engines/listbox/CHANGELOG.md b/src/core/component/directives/aria/roles-engines/listbox/CHANGELOG.md index ba2980fb55..6e314edf2d 100644 --- a/src/core/component/directives/aria/roles-engines/listbox/CHANGELOG.md +++ b/src/core/component/directives/aria/roles-engines/listbox/CHANGELOG.md @@ -9,7 +9,7 @@ Changelog > - :house: [Internal] > - :nail_care: [Polish] -## v?.??.? (2022-0?-??) +## v3.?.? (2022-??-??) #### :rocket: New Feature diff --git a/src/core/component/directives/aria/roles-engines/option/CHANGELOG.md b/src/core/component/directives/aria/roles-engines/option/CHANGELOG.md index ba2980fb55..6e314edf2d 100644 --- a/src/core/component/directives/aria/roles-engines/option/CHANGELOG.md +++ b/src/core/component/directives/aria/roles-engines/option/CHANGELOG.md @@ -9,7 +9,7 @@ Changelog > - :house: [Internal] > - :nail_care: [Polish] -## v?.??.? (2022-0?-??) +## v3.?.? (2022-??-??) #### :rocket: New Feature diff --git a/src/core/component/directives/aria/roles-engines/tab/CHANGELOG.md b/src/core/component/directives/aria/roles-engines/tab/CHANGELOG.md index ba2980fb55..6e314edf2d 100644 --- a/src/core/component/directives/aria/roles-engines/tab/CHANGELOG.md +++ b/src/core/component/directives/aria/roles-engines/tab/CHANGELOG.md @@ -9,7 +9,7 @@ Changelog > - :house: [Internal] > - :nail_care: [Polish] -## v?.??.? (2022-0?-??) +## v3.?.? (2022-??-??) #### :rocket: New Feature diff --git a/src/core/component/directives/aria/roles-engines/tablist/CHANGELOG.md b/src/core/component/directives/aria/roles-engines/tablist/CHANGELOG.md index ba2980fb55..6e314edf2d 100644 --- a/src/core/component/directives/aria/roles-engines/tablist/CHANGELOG.md +++ b/src/core/component/directives/aria/roles-engines/tablist/CHANGELOG.md @@ -9,7 +9,7 @@ Changelog > - :house: [Internal] > - :nail_care: [Polish] -## v?.??.? (2022-0?-??) +## v3.?.? (2022-??-??) #### :rocket: New Feature diff --git a/src/core/component/directives/aria/roles-engines/tabpanel/CHANGELOG.md b/src/core/component/directives/aria/roles-engines/tabpanel/CHANGELOG.md index ba2980fb55..6e314edf2d 100644 --- a/src/core/component/directives/aria/roles-engines/tabpanel/CHANGELOG.md +++ b/src/core/component/directives/aria/roles-engines/tabpanel/CHANGELOG.md @@ -9,7 +9,7 @@ Changelog > - :house: [Internal] > - :nail_care: [Polish] -## v?.??.? (2022-0?-??) +## v3.?.? (2022-??-??) #### :rocket: New Feature diff --git a/src/core/component/directives/aria/roles-engines/tree/CHANGELOG.md b/src/core/component/directives/aria/roles-engines/tree/CHANGELOG.md index ba2980fb55..6e314edf2d 100644 --- a/src/core/component/directives/aria/roles-engines/tree/CHANGELOG.md +++ b/src/core/component/directives/aria/roles-engines/tree/CHANGELOG.md @@ -9,7 +9,7 @@ Changelog > - :house: [Internal] > - :nail_care: [Polish] -## v?.??.? (2022-0?-??) +## v3.?.? (2022-??-??) #### :rocket: New Feature diff --git a/src/core/component/directives/aria/roles-engines/treeitem/CHANGELOG.md b/src/core/component/directives/aria/roles-engines/treeitem/CHANGELOG.md index ba2980fb55..6e314edf2d 100644 --- a/src/core/component/directives/aria/roles-engines/treeitem/CHANGELOG.md +++ b/src/core/component/directives/aria/roles-engines/treeitem/CHANGELOG.md @@ -9,7 +9,7 @@ Changelog > - :house: [Internal] > - :nail_care: [Polish] -## v?.??.? (2022-0?-??) +## v3.?.? (2022-??-??) #### :rocket: New Feature diff --git a/src/core/component/directives/id/CHANGELOG.md b/src/core/component/directives/id/CHANGELOG.md index ba2980fb55..6e314edf2d 100644 --- a/src/core/component/directives/id/CHANGELOG.md +++ b/src/core/component/directives/id/CHANGELOG.md @@ -9,7 +9,7 @@ Changelog > - :house: [Internal] > - :nail_care: [Polish] -## v?.??.? (2022-0?-??) +## v3.?.? (2022-??-??) #### :rocket: New Feature From bb13d56d7dbb1d689910335ab1d359bea1515f7f Mon Sep 17 00:00:00 2001 From: kobezzza Date: Tue, 16 Aug 2022 11:17:58 +0300 Subject: [PATCH 033/185] chore: added new test & refactoring --- .../directives/id/test/unit/functional.ts | 22 ++++++++++++------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/src/core/component/directives/id/test/unit/functional.ts b/src/core/component/directives/id/test/unit/functional.ts index 7c992d0aee..d832c3f6dc 100644 --- a/src/core/component/directives/id/test/unit/functional.ts +++ b/src/core/component/directives/id/test/unit/functional.ts @@ -7,6 +7,7 @@ * Released under the MIT license * https://github.com/V4Fire/Client/blob/master/LICENSE */ + import type { JSHandle, Page } from 'playwright'; import type bDummy from 'dummies/b-dummy/b-dummy'; @@ -18,16 +19,25 @@ test.describe('v-id', () => { await demoPage.goto(); }); - test('id is added', async ({page}) => { - const target = await init(page); - const id = await target.evaluate((ctx) => ctx.$root.unsafe.dom.getId('dummy')); + test('should add an id to the element', async ({page}) => { + const + target = await init(page), + id = await target.evaluate((ctx) => ctx.$root.unsafe.dom.getId('dummy')); test.expect( await target.evaluate((ctx) => ctx.$el?.id) ).toBe(id); }); - test('preserve mod', async ({page}) => { + test('should not preserve the original element id', async ({page}) => { + const target = await init(page, {'v-id': 'dummy', id: 'foo'}); + + test.expect( + await target.evaluate((ctx) => ctx.$el?.id) + ).toBe('dummy'); + }); + + test('should preserve the original element id', async ({page}) => { const target = await init(page, {'v-id.preserve': 'dummy', id: 'foo'}); test.expect( @@ -35,10 +45,6 @@ test.describe('v-id', () => { ).toBe('foo'); }); - /** - * @param page - * @param attrs - */ async function init(page: Page, attrs: Dictionary = {}): Promise> { return Component.createComponent(page, 'b-dummy', { attrs: {'v-id': 'dummy', ...attrs} From ae971081ab2868ec7c2e540668131078d16297ac Mon Sep 17 00:00:00 2001 From: kobezzza Date: Tue, 16 Aug 2022 11:45:37 +0300 Subject: [PATCH 034/185] refactor: better naming --- src/traits/i-open/i-open.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/traits/i-open/i-open.ts b/src/traits/i-open/i-open.ts index 93ef25a1a6..efce316887 100644 --- a/src/traits/i-open/i-open.ts +++ b/src/traits/i-open/i-open.ts @@ -135,7 +135,7 @@ export default abstract class iOpen { } /** - * Checks if the component realize current trait + * Checks if the passed object realize the current trait * @param obj */ static is(obj: unknown): obj is iOpen { @@ -143,8 +143,8 @@ export default abstract class iOpen { return false; } - const dict = Object.cast(obj); - return Object.isFunction(dict.open) && Object.isFunction(dict.close); + const unsafe = Object.cast(obj); + return Object.isFunction(unsafe.open) && Object.isFunction(unsafe.close); } /** From 3422e050bbda53c52622e6df264af565821571a0 Mon Sep 17 00:00:00 2001 From: kobezzza Date: Tue, 16 Aug 2022 11:49:23 +0300 Subject: [PATCH 035/185] chore: updated changelog --- src/traits/i-open/CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/traits/i-open/CHANGELOG.md b/src/traits/i-open/CHANGELOG.md index 3473a34026..68b7bf8e06 100644 --- a/src/traits/i-open/CHANGELOG.md +++ b/src/traits/i-open/CHANGELOG.md @@ -13,7 +13,7 @@ Changelog #### :rocket: New Feature -* Added `is` +* Added a new static method `is` ## v3.0.0-rc.184 (2021-05-12) From d13f3a78d1a0e022064e7f530a00455e0884f689 Mon Sep 17 00:00:00 2001 From: kobezzza Date: Tue, 16 Aug 2022 11:49:39 +0300 Subject: [PATCH 036/185] doc: added doc for new helpers --- src/traits/i-open/README.md | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/src/traits/i-open/README.md b/src/traits/i-open/README.md index 73d15dc17a..6600e47bcc 100644 --- a/src/traits/i-open/README.md +++ b/src/traits/i-open/README.md @@ -133,7 +133,23 @@ export default class bButton implements iOpen { ## Helpers -The trait provides a bunch of helper functions to initialize event listeners. +The trait provides a bunch of helper functions to work with it. + +### is + +Checks if the passed object realize the current trait. + +```typescript +import iOpen from 'traits/i-open/i-open'; + +export default class bButton { + created() { + if (iOpen.is(this)) { + this.open(); + } + } +} +``` ### initCloseHelpers From 0750ae632c2160558a59de5433d72865a6e3bb40 Mon Sep 17 00:00:00 2001 From: kobezzza Date: Wed, 17 Aug 2022 08:50:01 +0300 Subject: [PATCH 037/185] refactor: moved `is` upper --- src/traits/i-open/i-open.ts | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/src/traits/i-open/i-open.ts b/src/traits/i-open/i-open.ts index efce316887..adaabb2cff 100644 --- a/src/traits/i-open/i-open.ts +++ b/src/traits/i-open/i-open.ts @@ -66,6 +66,19 @@ export default abstract class iOpen { } }; + /** + * Checks if the passed object realize the current trait + * @param obj + */ + static is(obj: unknown): obj is iOpen { + if (Object.isPrimitive(obj)) { + return false; + } + + const unsafe = Object.cast(obj); + return Object.isFunction(unsafe.open) && Object.isFunction(unsafe.close); + } + /** * Initialize default event listeners to close a component by a keyboard or mouse * @@ -134,19 +147,6 @@ export default abstract class iOpen { }); } - /** - * Checks if the passed object realize the current trait - * @param obj - */ - static is(obj: unknown): obj is iOpen { - if (Object.isPrimitive(obj)) { - return false; - } - - const unsafe = Object.cast(obj); - return Object.isFunction(unsafe.open) && Object.isFunction(unsafe.close); - } - /** * Opens the component * @param args From 09072bd6430779547d4113f02ba7e5ccfc6799bb Mon Sep 17 00:00:00 2001 From: kobezzza Date: Wed, 17 Aug 2022 08:51:43 +0300 Subject: [PATCH 038/185] refactor: better doc & review new methods --- src/traits/i-access/i-access.ts | 186 ++++++++++++++++---------------- 1 file changed, 94 insertions(+), 92 deletions(-) diff --git a/src/traits/i-access/i-access.ts b/src/traits/i-access/i-access.ts index 0d953e5a82..f4fb85d585 100644 --- a/src/traits/i-access/i-access.ts +++ b/src/traits/i-access/i-access.ts @@ -14,8 +14,9 @@ */ import SyncPromise from 'core/promise/sync'; -import { sequence } from 'core/iter/combinators'; + import { intoIter } from 'core/iter'; +import { sequence } from 'core/iter/combinators'; import type iBlock from 'super/i-block/i-block'; import type { ModsDecl, ModEvent } from 'super/i-block/i-block'; @@ -54,6 +55,19 @@ export default abstract class iAccess { static blur: AddSelf = (component) => SyncPromise.resolve(component.setMod('focused', false)); + /** + * Checks if the component realize current trait + * @param obj + */ + static is(obj: unknown): obj is iAccess { + if (Object.isPrimitive(obj)) { + return false; + } + + const dict = Object.cast(obj); + return Object.isFunction(dict.removeAllFromTabSequence) && Object.isFunction(dict.getNextFocusableElement); + } + /** * Returns true if the component in focus * @param component @@ -168,26 +182,23 @@ export default abstract class iAccess { /** @see [[iAccess.removeAllFromTabSequence]] */ static removeAllFromTabSequence: AddSelf = - (component, el?): boolean => { - const - ctx = el ?? component.$el; - - if (ctx == null) { - return false; - } - + (component, searchCtx = component.$el): boolean => { let areElementsRemoved = false; + if (searchCtx == null) { + return areElementsRemoved; + } + const - focusableElems = this.findAllFocusableElements(component, ctx); + focusableEls = this.findAllFocusableElements(component, searchCtx); - for (const focusableEl of focusableElems) { - if (!focusableEl.hasAttribute('data-tabindex')) { - focusableEl.setAttribute('data-tabindex', String(focusableEl.tabIndex)); + for (const el of focusableEls) { + if (!el.hasAttribute('data-tabindex')) { + el.setAttribute('data-tabindex', String(el.tabIndex)); } - focusableEl.tabIndex = -1; + el.tabIndex = -1; areElementsRemoved = true; } @@ -196,32 +207,28 @@ export default abstract class iAccess { /** @see [[iAccess.restoreAllToTabSequence]] */ static restoreAllToTabSequence: AddSelf = - (component, el?): boolean => { - const - ctx = el ?? component.$el; - - if (ctx == null) { - return false; - } - + (component, searchCtx = component.$el): boolean => { let areElementsRestored = false; + if (searchCtx == null) { + return areElementsRestored; + } + let - removedElems = intoIter(ctx.querySelectorAll('[data-tabindex]')); + removedEls = intoIter(searchCtx.querySelectorAll('[data-tabindex]')); - if (el?.hasAttribute('data-tabindex')) { - removedElems = sequence(removedElems, intoIter([el])); + if (searchCtx.hasAttribute('data-tabindex')) { + removedEls = sequence(removedEls, intoIter([searchCtx])); } - for (const elem of removedElems) { + for (const elem of removedEls) { const originalTabIndex = elem.getAttribute('data-tabindex'); if (originalTabIndex != null) { elem.tabIndex = Number(originalTabIndex); elem.removeAttribute('data-tabindex'); - areElementsRestored = true; } } @@ -231,74 +238,63 @@ export default abstract class iAccess { /** @see [[iAccess.getNextFocusableElement]] */ static getNextFocusableElement: AddSelf = - (component, step, el?): CanUndef => { + (component, step, searchCtx = document.documentElement): AccessibleElement | null => { if (document.activeElement == null) { - return; + return null; } const - ctx = el ?? document.documentElement, - focusableElems = >this.findAllFocusableElements(component, ctx), - visibleFocusable: AccessibleElement[] = []; + focusableEls = >this.findAllFocusableElements(component, searchCtx), + visibleFocusableEls: AccessibleElement[] = []; - for (const focusableEl of focusableElems) { + for (const el of focusableEls) { if ( - focusableEl.offsetWidth > 0 || - focusableEl.offsetHeight > 0 || - focusableEl === document.activeElement + el.offsetWidth > 0 || + el.offsetHeight > 0 || + el === document.activeElement ) { - visibleFocusable.push(focusableEl); + visibleFocusableEls.push(el); } } const - index = visibleFocusable.indexOf(document.activeElement); + index = visibleFocusableEls.indexOf(document.activeElement); if (index > -1) { - return visibleFocusable[index + step]; + return visibleFocusableEls[index + step] ?? null; } + + return null; }; /** @see [[iAccess.findFocusableElement]] */ static findFocusableElement: AddSelf = - (component, el?): CanUndef => { + (component, searchCtx?): AccessibleElement | null => { const - ctx = el ?? component.$el, - focusableElems = this.findAllFocusableElements(component, ctx); + search = this.findAllFocusableElements(component, searchCtx).next(); - for (const focusableEl of focusableElems) { - if (!focusableEl.hasAttribute('disabled')) { - return focusableEl; - } + if (search.done) { + return null; } + + return search.value; }; /** @see [[iAccess.findAllFocusableElements]] */ static findAllFocusableElements: AddSelf = - (component, el?): IterableIterator => { + (component, searchCtx = component.$el): IterableIterator => { const - ctx = el ?? component.$el, - focusableElems = ctx?.querySelectorAll(FOCUSABLE_SELECTOR); + accessibleEls = searchCtx?.querySelectorAll(FOCUSABLE_SELECTOR); let - focusableIter = intoIter(focusableElems ?? []); - - if (ctx?.matches(FOCUSABLE_SELECTOR)) { - focusableIter = sequence(focusableIter, intoIter([el])); - } + searchIter = intoIter(accessibleEls ?? []); - function* createFocusableWithoutDisabled( - iter: IterableIterator - ): IterableIterator { - for (const iterEl of iter) { - if (!iterEl.hasAttribute('disabled')) { - yield iterEl; - } - } + if (searchCtx?.matches(FOCUSABLE_SELECTOR)) { + searchIter = sequence(searchIter, intoIter([searchCtx])); } const - focusableWithoutDisabled = createFocusableWithoutDisabled(focusableIter); + focusableWithoutDisabled = filterDisabledElements(searchIter); return { [Symbol.iterator]() { @@ -307,20 +303,17 @@ export default abstract class iAccess { next: focusableWithoutDisabled.next.bind(focusableWithoutDisabled) }; - }; - /** - * Checks if the component realize current trait - * @param obj - */ - static is(obj: unknown): obj is iAccess { - if (Object.isPrimitive(obj)) { - return false; - } - - const dict = Object.cast(obj); - return Object.isFunction(dict.removeAllFromTabSequence) && Object.isFunction(dict.getNextFocusableElement); - } + function* filterDisabledElements( + iter: IterableIterator + ): IterableIterator { + for (const el of iter) { + if (!el.hasAttribute('disabled')) { + yield el; + } + } + } + }; /** * A Boolean attribute which, if present, indicates that the component should automatically @@ -383,46 +376,55 @@ export default abstract class iAccess { * Removes all children of the specified element that can be focused from the Tab toggle sequence. * In effect, these elements are set to -1 for the tabindex attribute. * - * @param el - a context to search, if not set, the root element of the component will be used + * @param [searchCtx] - a context to search, if not set, the component root element will be used */ - removeAllFromTabSequence(el?: Element): boolean { + removeAllFromTabSequence(searchCtx?: Element): boolean { return Object.throw(); } /** - * Reverts all children of the specified element that can be focused to the Tab toggle sequence. - * This method is used to restore the state of elements to the state - * they had before 'removeAllFromTabSequence' was applied. + * Restores all children of the specified element that can be focused to the Tab toggle sequence. + * This method is used to restore the state of elements to the state they had before `removeAllFromTabSequence` was + * applied. * - * @param el - a context to search, if not set, the root element of the component will be used + * @param [searchCtx] - a context to search, if not set, the component root element will be used */ - restoreAllToTabSequence(el?: Element): boolean { + restoreAllToTabSequence(searchCtx?: Element): boolean { return Object.throw(); } /** - * Gets a next or previous focusable element via the step parameter from the current focused element + * Returns the next (or previous) element to which focus will be switched by pressing Tab. + * The method takes a "step" parameter, i.e. you can control the Tab sequence direction. For example, + * by setting the step to `-1` you will get an element that will be switched to focus by pressing Shift+Tab. * * @param step - * @param el - a context to search, if not set, document will be used + * @param [searchCtx] - a context to search, if not set, document will be used */ - getNextFocusableElement(step: 1 | -1, el?: T): CanUndef { + getNextFocusableElement( + step: 1 | -1, + searchCtx?: Element + ): T | null { return Object.throw(); } /** - * Find focusable element except disabled ones - * @param el - a context to search, if not set, component will be used + * Finds the first non-disabled focusable element from the passed context to search and returns it. + * The element that is the search context is also taken into account in the search. + * + * @param [searchCtx] - a context to search, if not set, the component root element will be used */ - findFocusableElement(el?: T): CanUndef { + findFocusableElement(searchCtx?: Element): T | null { return Object.throw(); } /** - * Find all focusable elements except disabled ones. Search includes the specified element. - * @param el - a context to search, if not set, component will be used + * Finds all non-disabled focusable elements and returns an iterator with the found ones. + * The element that is the search context is also taken into account in the search. + * + * @param [searchCtx] - a context to search, if not set, the component root element will be used */ - findAllFocusableElements(el?: T): IterableIterator { + findAllFocusableElements(searchCtx?: Element): IterableIterator { return Object.throw(); } } From 7d73e055ce55a14c716173ce2830e8d471c460f1 Mon Sep 17 00:00:00 2001 From: Aleksandr Bunin Date: Tue, 16 Aug 2022 12:55:51 +0300 Subject: [PATCH 039/185] fix test --- src/core/component/directives/id/test/unit/functional.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/core/component/directives/id/test/unit/functional.ts b/src/core/component/directives/id/test/unit/functional.ts index d832c3f6dc..9f1a0108c9 100644 --- a/src/core/component/directives/id/test/unit/functional.ts +++ b/src/core/component/directives/id/test/unit/functional.ts @@ -30,11 +30,13 @@ test.describe('v-id', () => { }); test('should not preserve the original element id', async ({page}) => { - const target = await init(page, {'v-id': 'dummy', id: 'foo'}); + const + target = await init(page), + id = await target.evaluate((ctx) => ctx.$root.unsafe.dom.getId('dummy')); test.expect( await target.evaluate((ctx) => ctx.$el?.id) - ).toBe('dummy'); + ).toBe(id); }); test('should preserve the original element id', async ({page}) => { From d614efa1e7ac58cb464a607dbb9c5fcacbb50194 Mon Sep 17 00:00:00 2001 From: Aleksandr Bunin Date: Tue, 16 Aug 2022 13:13:12 +0300 Subject: [PATCH 040/185] fix getFoldedMod usage --- src/base/b-tree/b-tree.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/base/b-tree/b-tree.ts b/src/base/b-tree/b-tree.ts index 6212c0eabc..b716069e3b 100644 --- a/src/base/b-tree/b-tree.ts +++ b/src/base/b-tree/b-tree.ts @@ -268,7 +268,7 @@ class bTree extends iData implements iItems, iAccess { protected getAriaConfig(role: 'tree' | 'treeitem', item?: this['Item'], i?: number): Dictionary { const - getFoldedMod = this.getFoldedModById.bind(this, item?.id), + getFoldedMod = this.getFoldedModById.bind(this, item?.id ?? ''), root = () => this.top?.$el ?? this.$el; const treeConfig = { From e74a13400897bf218a2813cac290123b17c3b2ac Mon Sep 17 00:00:00 2001 From: Aleksandr Bunin Date: Tue, 16 Aug 2022 15:08:17 +0300 Subject: [PATCH 041/185] refactoring getFoldedMod --- src/base/b-tree/b-tree.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/base/b-tree/b-tree.ts b/src/base/b-tree/b-tree.ts index b716069e3b..5cf73f1c29 100644 --- a/src/base/b-tree/b-tree.ts +++ b/src/base/b-tree/b-tree.ts @@ -31,7 +31,8 @@ export * from 'base/b-tree/interface'; export const $$ = symbolGenerator(); -interface bTree extends Trait {} +interface bTree extends Trait { +} /** * Component to render tree of any elements @@ -267,8 +268,11 @@ class bTree extends iData implements iItems, iAccess { protected getAriaConfig(role: 'treeitem', item: this['Item'], i: number): Dictionary protected getAriaConfig(role: 'tree' | 'treeitem', item?: this['Item'], i?: number): Dictionary { + const getFoldedMod = (item?.id != null) ? + this.getFoldedModById.bind(this, item.id) : + () => ''; + const - getFoldedMod = this.getFoldedModById.bind(this, item?.id ?? ''), root = () => this.top?.$el ?? this.$el; const treeConfig = { From 0b80f3979844b50372c5fe553dffdd68b6ab81eb Mon Sep 17 00:00:00 2001 From: kobezzza Date: Wed, 17 Aug 2022 09:08:22 +0300 Subject: [PATCH 042/185] chore: fixed jsdoc --- src/traits/i-access/i-access.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/traits/i-access/i-access.ts b/src/traits/i-access/i-access.ts index f4fb85d585..c3860c74d5 100644 --- a/src/traits/i-access/i-access.ts +++ b/src/traits/i-access/i-access.ts @@ -56,7 +56,7 @@ export default abstract class iAccess { (component) => SyncPromise.resolve(component.setMod('focused', false)); /** - * Checks if the component realize current trait + * Checks if the passed object realize the current trait * @param obj */ static is(obj: unknown): obj is iAccess { From fa32fa369921f0638c42ea4190f78a6f9efd414f Mon Sep 17 00:00:00 2001 From: kobezzza Date: Wed, 17 Aug 2022 09:13:53 +0300 Subject: [PATCH 043/185] :art: --- src/base/b-tree/b-tree.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/base/b-tree/b-tree.ts b/src/base/b-tree/b-tree.ts index 5cf73f1c29..1109d07416 100644 --- a/src/base/b-tree/b-tree.ts +++ b/src/base/b-tree/b-tree.ts @@ -31,8 +31,7 @@ export * from 'base/b-tree/interface'; export const $$ = symbolGenerator(); -interface bTree extends Trait { -} +interface bTree extends Trait {} /** * Component to render tree of any elements From 09eca92f74acacba9fc4c9d46f45666fecf23a5c Mon Sep 17 00:00:00 2001 From: kobezzza Date: Wed, 17 Aug 2022 09:16:01 +0300 Subject: [PATCH 044/185] :art: --- src/super/i-input/i-input.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/super/i-input/i-input.ts b/src/super/i-input/i-input.ts index 309d412ebc..bf00c700c0 100644 --- a/src/super/i-input/i-input.ts +++ b/src/super/i-input/i-input.ts @@ -15,8 +15,8 @@ import symbolGenerator from 'core/symbol'; import SyncPromise from 'core/promise/sync'; import { Option } from 'core/prelude/structures'; - import { derive } from 'core/functools/trait'; + import iAccess from 'traits/i-access/i-access'; import iVisible from 'traits/i-visible/i-visible'; From df6aa320b46686906c22f2c9aebe76530a4b32fd Mon Sep 17 00:00:00 2001 From: kobezzza Date: Wed, 17 Aug 2022 09:17:26 +0300 Subject: [PATCH 045/185] chore: updated version wildcard --- src/super/i-input/CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/super/i-input/CHANGELOG.md b/src/super/i-input/CHANGELOG.md index 672b1738a9..c97e0a31ae 100644 --- a/src/super/i-input/CHANGELOG.md +++ b/src/super/i-input/CHANGELOG.md @@ -9,7 +9,7 @@ Changelog > - :house: [Internal] > - :nail_care: [Polish] -## v3.0.0-rc.??? (2021-??-??) +## ## v3.?.? (2022-??-??) #### :rocket: New Feature From cce081608801ec966a9c68f36658414c2c8d297b Mon Sep 17 00:00:00 2001 From: kobezzza Date: Wed, 17 Aug 2022 09:27:25 +0300 Subject: [PATCH 046/185] refactor: simple refactoring of new methods --- src/form/b-select/b-select.ts | 84 +++++++++++++++++------------------ 1 file changed, 40 insertions(+), 44 deletions(-) diff --git a/src/form/b-select/b-select.ts b/src/form/b-select/b-select.ts index 0b39a564fb..0c4424a2b4 100644 --- a/src/form/b-select/b-select.ts +++ b/src/form/b-select/b-select.ts @@ -865,7 +865,6 @@ class bSelect extends iInputText implements iOpenToggle, iItems { protected override initModEvents(): void { super.initModEvents(); - iOpenToggle.initModEvents(this); this.sync.mod('native', 'native', Boolean); @@ -873,6 +872,46 @@ class bSelect extends iInputText implements iOpenToggle, iItems { this.sync.mod('opened', 'multiple', Boolean); } + /** + * Returns a dictionary with configurations for the `v-aria` directive used as a combobox + * @param role + */ + protected getAriaConfig(role: 'combobox'): Dictionary; + + /** + * Returns a dictionary with configurations for the `v-aria` directive used as an option + * + * @param role + * @param item - option item data + */ + protected getAriaConfig(role: 'option', item: this['Item']): Dictionary; + + protected getAriaConfig(role: 'combobox' | 'option', item?: this['Item']): Dictionary { + const + isSelected = this.isSelected.bind(this, item?.value); + + const comboboxConfig = { + isMultiple: this.multiple, + '@change': (cb) => this.localEmitter.on('el.mod.set.*.marked.*', ({link}) => cb(link)), + '@close': (cb) => this.on('close', cb), + '@open': (cb) => this.on('open', () => this.$nextTick(() => cb(this.selectedElement))) + }; + + const optionConfig = { + get isSelected() { + return isSelected(); + }, + + '@change': (cb) => this.on('actionChange', () => cb(isSelected())) + }; + + switch (role) { + case 'combobox': return comboboxConfig; + case 'option': return optionConfig; + default: return {}; + } + } + protected override beforeDestroy(): void { super.beforeDestroy(); @@ -932,49 +971,6 @@ class bSelect extends iInputText implements iOpenToggle, iItems { return false; } - /** - * Returns a dictionary with options for aria directive for combobox role - * @param role - */ - protected getAriaConfig(role: 'combobox'): Dictionary; - - /** - * Returns a dictionary with options for aria directive for option role - * - * @param role - * @param item - */ - protected getAriaConfig(role: 'option', item: this['Item']): Dictionary; - - protected getAriaConfig(role: 'combobox' | 'option', item?: this['Item']): Dictionary { - const - event = 'el.mod.set.*.marked.*', - isSelected = this.isSelected.bind(this, item?.value); - - const - comboboxConfig = { - isMultiple: this.multiple, - '@change': (cb) => this.localEmitter.on(event, ({link}) => cb(link)), - '@close': (cb) => this.on('close', cb), - '@open': (cb) => this.on('open', () => { - void this.$nextTick(() => cb(this.selectedElement)); - }) - }; - - const optionConfig = { - get isSelected() { - return isSelected(); - }, - '@change': (cb) => this.on('actionChange', () => cb(isSelected())) - }; - - switch (role) { - case 'combobox': return comboboxConfig; - case 'option': return optionConfig; - default: return {}; - } - } - /** * Handler: typing text into a helper text input to search select options * From d276688542d092a9cbe1ef3a389dc19119ece8f3 Mon Sep 17 00:00:00 2001 From: kobezzza Date: Wed, 17 Aug 2022 09:34:02 +0300 Subject: [PATCH 047/185] chore: updated changelog --- src/form/b-select/CHANGELOG.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/form/b-select/CHANGELOG.md b/src/form/b-select/CHANGELOG.md index f336cc6855..38f06bd26d 100644 --- a/src/form/b-select/CHANGELOG.md +++ b/src/form/b-select/CHANGELOG.md @@ -13,14 +13,15 @@ Changelog #### :rocket: New Feature -* Added a new directive `v-aria` -* Added a new directive `v-id` -* Added `getAriaConfig` * Now the component derives `iAccess` #### :bug: Bug Fix -* Fixed the component to emit `iOpen` events +* Fixed a bug due to which the component did not emit `iOpen` events + +#### :house: Internal + +* Improved component accessibility ## v3.5.3 (2021-10-06) From 0ecfe4a734b6f73107d84c5fac4deb98fdbaae0e Mon Sep 17 00:00:00 2001 From: kobezzza Date: Wed, 17 Aug 2022 14:06:45 +0300 Subject: [PATCH 048/185] doc: improved doc --- src/core/component/directives/aria/README.md | 113 +++++++++++++++---- 1 file changed, 92 insertions(+), 21 deletions(-) diff --git a/src/core/component/directives/aria/README.md b/src/core/component/directives/aria/README.md index aabae564b4..df3a69195f 100644 --- a/src/core/component/directives/aria/README.md +++ b/src/core/component/directives/aria/README.md @@ -1,39 +1,110 @@ # core/component/directives/aria -This module provides a directive to add aria attributes and logic to elements through single API. +This module provides a directive to add aria attributes and logic to elements through a single API. -## Usage +``` +< div v-aria = {labelledby: dom.getId('title')} + +/// The same as + +< div :aria-labelledby = dom.getId('title') +``` + +The [Aria](https://www.w3.org/TR/wai-aria) specification consists of a set of entities called roles. +For example, [Tablist](https://www.w3.org/TR/wai-aria/#tablist) or [Combobox](https://www.w3.org/TR/wai-aria/#combobox). +Therefore, the directive also consists of many engines, each of which implements a particular role. + +``` +< div v-aria:combobox = {...} +``` + +## Why is this directive needed? + +Accessibility is an important part of a modern web application. +However, the implementation of a particular role can be quite a challenge, due to the presence of a large number of nuances. +On the other hand, there are many unrelated components that can logically implement the same ARIA role. +Therefore, we need the ability to share this code between components and not enforce coupling between them. +In addition, ARIA roles are heavily DOM bound, so we need the ability to inject in the component markup. +It turns out that using the directive is the most optimal solution for this task. + +## List of supported roles + +All roles supported by the directive are located in the `roles` sub-folder. + +Each role is named after the appropriate name from the ARIA specification. +Each role can accept its own set of options, which are described in its documentation. + +* [Combobox](https://www.w3.org/TR/wai-aria/#combobox) +* [Dialog](https://www.w3.org/TR/wai-aria/#dialog) +* [Listbox](https://www.w3.org/TR/wai-aria/#listbox) +* [Option](https://www.w3.org/TR/wai-aria/#option) +* [Tab](https://www.w3.org/TR/wai-aria/#tab) +* [Tablist](https://www.w3.org/TR/wai-aria/#tablist) +* [Tabpanel](https://www.w3.org/TR/wai-aria/#tabpanel) +* [Tree](https://www.w3.org/TR/wai-aria/#tree) +* [Treeitem](https://www.w3.org/TR/wai-aria/#treeitem) +* [Controls](https://www.w3.org/TR/wai-aria/#aria-controls) + +## Available options + +### [label] + +Defines a string value that labels the current element. +See [this](https://www.w3.org/TR/wai-aria/#aria-label) for more information. ``` -< &__foo v-aria.#bla +< input type = text | v-aria = {labelledby: 'Billing Name'} +``` + +### [labelledby] + +Identifies the element (or elements) that labels the current element. +See [this](https://www.w3.org/TR/wai-aria/#aria-labelledby) for more information. -< &__foo v-aria = {label: 'title'} ``` +< #billing + Billing + +< #name + Name -## Available modifiers: +< input type = text | v-aria = {labelledby: 'billing name'} -- .#[string] (ex. '.#title') +< #address + Address -Example +< input type = text | v-aria = {labelledby: 'billing address'} ``` -< v-aria.#title -the same as -< v-aria = {labelledby: dom.getId('title')} +### [description] + +Defines a string value that describes or annotates the current element. +See [this](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes/aria-description) for more information. + +### [describedby] + +Identifies the element (or elements) that describes the object. +See [this](https://www.w3.org/TR/wai-aria/#aria-describedby) for more information. + ``` +< button v-aria = {describedby: 'trash-desc'} + Move to trash -## Available values: -Parameters passed to the directive are expected to always be object type. Any directive handle common keys: -- label -Expects string as 'title' to the specified element +< p#trash-desc + Items in the trash will be permanently removed after 30 days. +``` -- labelledby -Expects string as an id of the element. This element is a 'title' of to the specified element +## Modifiers -- description -Expects string as expanded 'description' to the specified element +### `#` -- describedby -Expects string as an id of the element. This element is an expanded 'description' to the specified element +This modifier represents a snippet for more convenient setting of the `labelledby` attribute. +Note that the identifier passed in this way is automatically associated with the component within which the directive is used. -Also, there are specific role keys. For info go to [`core/component/directives/role-engines/`](core/component/directives/role-engines/). +``` +< div v-aria.#title + +/// The same as + +< div v-aria = {labelledby: dom.getId('title')} +``` From 072f904d938c3dbc2946bdb730fff9015ac3f25a Mon Sep 17 00:00:00 2001 From: kobezzza Date: Wed, 17 Aug 2022 14:07:08 +0300 Subject: [PATCH 049/185] chore: updated versrion wildcard --- src/core/component/render-function/CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/component/render-function/CHANGELOG.md b/src/core/component/render-function/CHANGELOG.md index b80bbe6ed6..78b125050e 100644 --- a/src/core/component/render-function/CHANGELOG.md +++ b/src/core/component/render-function/CHANGELOG.md @@ -9,7 +9,7 @@ Changelog > - :house: [Internal] > - :nail_care: [Polish] -## v3.??.? (2022-??-??) +## v3.?.? (2022-??-??) #### :bug: Bug Fix From f9b2feeeebdb00925a9a3e8dc9865a9e0e993043 Mon Sep 17 00:00:00 2001 From: kobezzza Date: Wed, 17 Aug 2022 14:12:27 +0300 Subject: [PATCH 050/185] doc: added a new example --- src/core/component/directives/aria/README.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/core/component/directives/aria/README.md b/src/core/component/directives/aria/README.md index df3a69195f..375596c8c0 100644 --- a/src/core/component/directives/aria/README.md +++ b/src/core/component/directives/aria/README.md @@ -81,6 +81,14 @@ See [this](https://www.w3.org/TR/wai-aria/#aria-labelledby) for more information Defines a string value that describes or annotates the current element. See [this](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes/aria-description) for more information. +``` +< div role = application | v-aria = {label: 'calendar', decription: 'Game schedule for the Boston Red Sox 2021 Season'} + < h1 + Red Sox 2021 + + ... +``` + ### [describedby] Identifies the element (or elements) that describes the object. From f50dba1355bbe7b4f5293f682302e9287f333c7a Mon Sep 17 00:00:00 2001 From: Aleksandr Bunin Date: Wed, 17 Aug 2022 13:38:16 +0300 Subject: [PATCH 051/185] fix iAccess --- .../aria/roles-engines/tab/index.ts | 2 +- .../aria/roles-engines/treeitem/index.ts | 6 +- src/traits/i-access/CHANGELOG.md | 10 +- src/traits/i-access/README.md | 104 +++++++++++++++++- src/traits/i-access/i-access.ts | 34 +++--- 5 files changed, 127 insertions(+), 29 deletions(-) diff --git a/src/core/component/directives/aria/roles-engines/tab/index.ts b/src/core/component/directives/aria/roles-engines/tab/index.ts index 4f866e7bb0..2cbe0d3be5 100644 --- a/src/core/component/directives/aria/roles-engines/tab/index.ts +++ b/src/core/component/directives/aria/roles-engines/tab/index.ts @@ -75,7 +75,7 @@ export class TabEngine extends AriaRoleEngine { */ protected moveFocusToLastTab(): void { const - tabs = this.ctx?.findAllFocusableElements(); + tabs = this.ctx?.findFocusableElements(); if (tabs == null) { return; diff --git a/src/core/component/directives/aria/roles-engines/treeitem/index.ts b/src/core/component/directives/aria/roles-engines/treeitem/index.ts index 995317384d..a02a0e6905 100644 --- a/src/core/component/directives/aria/roles-engines/treeitem/index.ts +++ b/src/core/component/directives/aria/roles-engines/treeitem/index.ts @@ -143,7 +143,7 @@ export class TreeitemEngine extends AriaRoleEngine { */ protected setFocusToLastItem(): void { const - items = this.ctx?.findAllFocusableElements(this.params.rootElement); + items = this.ctx?.findFocusableElements(this.params.rootElement); if (items == null) { return; @@ -153,9 +153,7 @@ export class TreeitemEngine extends AriaRoleEngine { lastItem: CanUndef; for (const item of items) { - if (item.offsetWidth > 0 || item.offsetHeight > 0) { - lastItem = item; - } + lastItem = item; } if (lastItem != null) { diff --git a/src/traits/i-access/CHANGELOG.md b/src/traits/i-access/CHANGELOG.md index 5823c3f158..dda083d6fa 100644 --- a/src/traits/i-access/CHANGELOG.md +++ b/src/traits/i-access/CHANGELOG.md @@ -13,10 +13,12 @@ Changelog #### :rocket: New Feature -* Added `removeAllFromTabSequence` -* Added `restoreAllToTabSequence` -* Added `getNextFocusableElement` -* Added `is` +* Added a new method `removeAllFromTabSequence` +* Added a new method `restoreAllToTabSequence` +* Added a new method `getNextFocusableElement` +* Added a new method `findFocusableElement` +* Added a new method `findFocusableElements` +* Added a new static method `is` ## v3.0.0-rc.211 (2021-07-21) diff --git a/src/traits/i-access/README.md b/src/traits/i-access/README.md index a28a6ad64e..69b4b39305 100644 --- a/src/traits/i-access/README.md +++ b/src/traits/i-access/README.md @@ -80,7 +80,7 @@ The trait specifies a getter to determine when the component in focus or not. ### isFocused -True if the component in focus. +True if the component is in focus. The getter has the default implementation via a static method `iAccess.isFocused`. ```typescript @@ -98,6 +98,21 @@ export default class bButton implements iAccess { The trait specifies a bunch of methods to implement. +### is + +True if the component realize `iAccess` trait. + +```typescript +import iAccess from 'traits/i-access/i-access'; + +export default class bButton implements iAccess { + /** @see iAccess.is */ + is(): boolean { + return iAccess.is(this); + } +} +``` + ### enable Enables the component. @@ -162,6 +177,93 @@ export default class bButton implements iAccess { } ``` +### removeAllFromTabSequence + +Removes all children of the specified element that can be focused from the Tab toggle sequence. +In effect, these elements are set to -1 for the tabindex attribute. +The method has the default implementation. + +```typescript +import iAccess from 'traits/i-access/i-access'; + +export default class bButton implements iAccess { + /** @see iAccess.removeAllFromTabSequence */ + removeAllFromTabSequence(): boolean { + return iAccess.removeAllFromTabSequence(this); + } +} +``` + +### restoreAllToTabSequence + +Restores all children of the specified element that can be focused to the Tab toggle sequence. +This method is used to restore the state of elements to the state they had before `removeAllFromTabSequence` was +applied. +The method has the default implementation. + +```typescript +import iAccess from 'traits/i-access/i-access'; + +export default class bButton implements iAccess { + /** @see iAccess.restoreAllToTabSequence */ + restoreAllToTabSequence(): boolean { + return iAccess.restoreAllToTabSequence(this); + } +} +``` + +### getNextFocusableElement + +Returns the next (or previous) element to which focus will be switched by pressing Tab. +The method takes a "step" parameter, i.e. you can control the Tab sequence direction. For example, +by setting the step to `-1` you will get an element that will be switched to focus by pressing Shift+Tab. +The method has the default implementation. + +```typescript +import iAccess from 'traits/i-access/i-access'; + +export default class bButton implements iAccess { + /** @see iAccess.getNextFocusableElement */ + getNextFocusableElement(): AccessibleElement | null { + return iAccess.getNextFocusableElement(this); + } +} +``` + +### findFocusableElement + +Finds the first non-disabled visible focusable element from the passed context to search and returns it. +The element that is the search context is also taken into account in the search. +The method has the default implementation. + +```typescript +import iAccess from 'traits/i-access/i-access'; + +export default class bButton implements iAccess { + /** @see iAccess.findFocusableElement */ + findFocusableElement(): AccessibleElement | null { + return iAccess.findFocusableElement(this); + } +} +``` + +### findFocusableElements + +Finds all non-disabled visible focusable elements and returns an iterator with the found ones. +The element that is the search context is also taken into account in the search. +The method has the default implementation. + +```typescript +import iAccess from 'traits/i-access/i-access'; + +export default class bButton implements iAccess { + /** @see iAccess.findFocusableElements */ + findFocusableElements(): IterableIterator { + return iAccess.findFocusableElements(this); + } +} +``` + ## Helpers The trait provides a bunch of helper functions to initialize event listeners. diff --git a/src/traits/i-access/i-access.ts b/src/traits/i-access/i-access.ts index c3860c74d5..bbdcfc4a47 100644 --- a/src/traits/i-access/i-access.ts +++ b/src/traits/i-access/i-access.ts @@ -191,7 +191,7 @@ export default abstract class iAccess { } const - focusableEls = this.findAllFocusableElements(component, searchCtx); + focusableEls = this.findFocusableElements(component, searchCtx); for (const el of focusableEls) { if (!el.hasAttribute('data-tabindex')) { @@ -244,18 +244,10 @@ export default abstract class iAccess { } const - focusableEls = >this.findAllFocusableElements(component, searchCtx), - visibleFocusableEls: AccessibleElement[] = []; + focusableEls = this.findFocusableElements(component, searchCtx), + visibleFocusableEls: AccessibleElement[] = [...focusableEls]; - for (const el of focusableEls) { - if ( - el.offsetWidth > 0 || - el.offsetHeight > 0 || - el === document.activeElement - ) { - visibleFocusableEls.push(el); - } - } + visibleFocusableEls.sort((el1, el2) => el2.tabIndex - el1.tabIndex); const index = visibleFocusableEls.indexOf(document.activeElement); @@ -271,7 +263,7 @@ export default abstract class iAccess { static findFocusableElement: AddSelf = (component, searchCtx?): AccessibleElement | null => { const - search = this.findAllFocusableElements(component, searchCtx).next(); + search = this.findFocusableElements(component, searchCtx).next(); if (search.done) { return null; @@ -280,8 +272,8 @@ export default abstract class iAccess { return search.value; }; - /** @see [[iAccess.findAllFocusableElements]] */ - static findAllFocusableElements: AddSelf = + /** @see [[iAccess.findFocusableElements]] */ + static findFocusableElements: AddSelf = (component, searchCtx = component.$el): IterableIterator => { const accessibleEls = searchCtx?.querySelectorAll(FOCUSABLE_SELECTOR); @@ -308,7 +300,11 @@ export default abstract class iAccess { iter: IterableIterator ): IterableIterator { for (const el of iter) { - if (!el.hasAttribute('disabled')) { + if ( + !el.hasAttribute('disabled') || + el.getAttribute('visibility') !== 'hidden' || + el.getAttribute('display') !== 'none' + ) { yield el; } } @@ -409,7 +405,7 @@ export default abstract class iAccess { } /** - * Finds the first non-disabled focusable element from the passed context to search and returns it. + * Finds the first non-disabled visible focusable element from the passed context to search and returns it. * The element that is the search context is also taken into account in the search. * * @param [searchCtx] - a context to search, if not set, the component root element will be used @@ -419,12 +415,12 @@ export default abstract class iAccess { } /** - * Finds all non-disabled focusable elements and returns an iterator with the found ones. + * Finds all non-disabled visible focusable elements and returns an iterator with the found ones. * The element that is the search context is also taken into account in the search. * * @param [searchCtx] - a context to search, if not set, the component root element will be used */ - findAllFocusableElements(searchCtx?: Element): IterableIterator { + findFocusableElements(searchCtx?: Element): IterableIterator { return Object.throw(); } } From 50d805a18a445824548875401aaa7f2985474d5d Mon Sep 17 00:00:00 2001 From: Aleksandr Bunin Date: Wed, 17 Aug 2022 14:29:42 +0300 Subject: [PATCH 052/185] fix changlelog --- src/form/b-checkbox/CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/form/b-checkbox/CHANGELOG.md b/src/form/b-checkbox/CHANGELOG.md index 38d8ea8b5c..c75caea52f 100644 --- a/src/form/b-checkbox/CHANGELOG.md +++ b/src/form/b-checkbox/CHANGELOG.md @@ -15,6 +15,10 @@ Changelog * Added `label` tag with `for` attribute to label and `id` to nativeInput in template +#### :house: Internal + +* Improved component accessibility + ## v3.0.0-rc.199 (2021-06-16) #### :boom: Breaking Change From 0ec1760fe21fe01c2a09a4300d52e6848b07ae66 Mon Sep 17 00:00:00 2001 From: Aleksandr Bunin Date: Wed, 17 Aug 2022 17:15:52 +0300 Subject: [PATCH 053/185] b-list doc --- src/base/b-list/README.md | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/src/base/b-list/README.md b/src/base/b-list/README.md index b8f089efa9..666afa3c82 100644 --- a/src/base/b-list/README.md +++ b/src/base/b-list/README.md @@ -10,13 +10,13 @@ If you need a more complex layout, provide it via a slot or by using `item/itemP * The component extends [[iData]]. -* The component implements [[iVisible]], [[iWidth]], [[iItems]] traits. +* The component implements [[iAccess]], [[iVisible]], [[iWidth]], [[iItems]] traits. * The component is used as functional if there is no provided the `dataProvider` prop. * The component supports tooltips. -* The component uses `aria` attributes. +* The component is accessible. * By default, the list will be created using `
    ` and `
  • ` tags. @@ -243,6 +243,10 @@ By default, if the component is switched to the `multiple` mode, this value is s Initial additional attributes are provided to an "internal" (native) list tag. +#### [orientation = `horizontal`] + +The component view orientation. + ### Fields #### items @@ -333,3 +337,14 @@ class Test extends iData { } } ``` + +## Accessibility + +If the component is used as a list of tabs it will implement an ARIA role [tablist](https://www.w3.org/TR/wai-aria/#tablist). +All the accessible functionality included in this [widget](https://www.w3.org/WAI/ARIA/apg/patterns/tabpanel/) is supported. + +When the component is used as a list of links it bases on internal HTML semantics of list tags. + +The component includes the following roles: +- [tablist](https://www.w3.org/TR/wai-aria/#tablist) +- [tab](https://www.w3.org/TR/wai-aria/#tab) From 294d3c056ec636e5e8424acb1546f2477842690b Mon Sep 17 00:00:00 2001 From: Aleksandr Bunin Date: Wed, 17 Aug 2022 17:39:28 +0300 Subject: [PATCH 054/185] b-tree doc --- src/base/b-tree/README.md | 25 ++++++++++++++++++++----- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/src/base/b-tree/README.md b/src/base/b-tree/README.md index e7d5947246..0e22c962fd 100644 --- a/src/base/b-tree/README.md +++ b/src/base/b-tree/README.md @@ -6,10 +6,12 @@ This module provides a component to render a recursive list of elements. * The component extends [[iData]]. -* The component implements the [[iItems]] trait. +* The component implements the [[iAccess]], [[iItems]] trait. * By default, the root tag of the component is `
    `. +* The component is accessible. + ## Features * Recursive rendering of any components. @@ -197,11 +199,11 @@ Also, you can see the implemented traits or the parent component. ### Props -### folded +#### [folded = `true`] If true, then all nested elements are folded by default. -### renderFilter +#### [renderFilter] A common filter to render items via `asyncRender`. It is used to optimize the process of rendering items. @@ -210,7 +212,7 @@ It is used to optimize the process of rendering items. < b-tree :item = 'b-checkbox' | :items = listOfItems | :renderFilter = () => async.idle() ``` -### nestedRenderFilter +#### [nestedRenderFilter] A filter to render nested items via `asyncRender`. It is used to optimize the process of rendering child items. @@ -219,10 +221,23 @@ It is used to optimize the process of rendering child items. < b-tree :item = 'b-checkbox' | :items = listOfItems | :nestedRenderFilter = () => async.idle() ``` -### renderChunks +#### [renderChunks = `5`] Number of chunks to render per tick via `asyncRender`. ``` < b-tree :item = 'b-checkbox' | :items = listOfItems | :renderChunks = 3 ``` + +#### [orientation = `horizontal`] + +The component view orientation. + +## Accessibility + +The component implements an ARIA role [tree](https://www.w3.org/TR/wai-aria/#tree). +Only basic accessible functionality (without optional keyboard combinations) included in this [widget](https://www.w3.org/WAI/ARIA/apg/patterns/treeview/) is supported. + +The component includes the following roles: +- [tree](https://www.w3.org/TR/wai-aria/#tree) +- [treeitem](https://www.w3.org/TR/wai-aria/#treeitem) From d969a56b2bd157e0c56b66253bfce975601b2683 Mon Sep 17 00:00:00 2001 From: Aleksandr Bunin Date: Wed, 17 Aug 2022 17:59:38 +0300 Subject: [PATCH 055/185] b-window doc --- src/base/b-window/README.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/base/b-window/README.md b/src/base/b-window/README.md index 66652ec797..0a4fcbd5f1 100644 --- a/src/base/b-window/README.md +++ b/src/base/b-window/README.md @@ -18,6 +18,8 @@ This module provides a component to create a modal window. * By default, the root tag of the component is `
    `. +* The component is accessible. + ## Modifiers | Name | Description | Values | Default | @@ -263,3 +265,8 @@ When opening the window, you can specify at which `stage` the component should s < span @click = $refs.window.open('loading') Open the window ``` + +## Accessibility + +The component implements an ARIA role [dialog](https://www.w3.org/TR/wai-aria/#dialog). +All the accessible functionality included in this [widget](https://www.w3.org/WAI/ARIA/apg/patterns/dialogmodal/) is supported. From 109dc46bb99fc38985341dce3d5f47dd4d6ded62 Mon Sep 17 00:00:00 2001 From: Aleksandr Bunin Date: Wed, 17 Aug 2022 18:02:44 +0300 Subject: [PATCH 056/185] demo changelog --- src/pages/p-v4-components-demo/b-v4-component-demo/CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/p-v4-components-demo/b-v4-component-demo/CHANGELOG.md b/src/pages/p-v4-components-demo/b-v4-component-demo/CHANGELOG.md index edbedc52a3..43fcf88a8d 100644 --- a/src/pages/p-v4-components-demo/b-v4-component-demo/CHANGELOG.md +++ b/src/pages/p-v4-components-demo/b-v4-component-demo/CHANGELOG.md @@ -11,7 +11,7 @@ Changelog ## v3.?.? (2022-0?-??) -#### :rocket: New Feature +#### :house: Internal * Added a new directive `v-id` From 441cfc2d4f2444f90ffcdce55b99197dd08e7974 Mon Sep 17 00:00:00 2001 From: Aleksandr Bunin Date: Wed, 17 Aug 2022 18:18:07 +0300 Subject: [PATCH 057/185] b-select doc --- src/base/b-list/README.md | 3 +++ src/form/b-select/CHANGELOG.md | 2 ++ src/form/b-select/README.md | 12 ++++++++++++ 3 files changed, 17 insertions(+) diff --git a/src/base/b-list/README.md b/src/base/b-list/README.md index 666afa3c82..4160d137a4 100644 --- a/src/base/b-list/README.md +++ b/src/base/b-list/README.md @@ -348,3 +348,6 @@ When the component is used as a list of links it bases on internal HTML semantic The component includes the following roles: - [tablist](https://www.w3.org/TR/wai-aria/#tablist) - [tab](https://www.w3.org/TR/wai-aria/#tab) + +The widget should also include [tabpanel](https://www.w3.org/TR/wai-aria/#tabpanel) role which is a block that contains the content of each tab. +But the component does not provide such block. So the 'connection' with other component should be set with the help of [`v-aria:controls`](core/component/directives/aria/aria-engines/controls/README.md) diff --git a/src/form/b-select/CHANGELOG.md b/src/form/b-select/CHANGELOG.md index 38f06bd26d..ca67cb2ba4 100644 --- a/src/form/b-select/CHANGELOG.md +++ b/src/form/b-select/CHANGELOG.md @@ -23,6 +23,8 @@ Changelog * Improved component accessibility +* Changed keyboard keys handling according to ARIA specification + ## v3.5.3 (2021-10-06) #### :bug: Bug Fix diff --git a/src/form/b-select/README.md b/src/form/b-select/README.md index e6c7fe2d46..01a493279f 100644 --- a/src/form/b-select/README.md +++ b/src/form/b-select/README.md @@ -17,6 +17,8 @@ The select can contain multiple values. * The component has `skeletonMarker`. +* The component is accessible. + ## Modifiers | Name | Description | Values | Default | @@ -384,3 +386,13 @@ Checks that a component value must be filled. size 20px background-image url("assets/my-icon.svg") ``` + +## Accessibility + +The component implements an ARIA role [combobox](https://www.w3.org/TR/wai-aria/#combobox). +Only basic accessible functionality (without optional keyboard combinations) included in this [widget](https://www.w3.org/WAI/ARIA/apg/patterns/combobox/) is supported. + +The component includes the following roles: +- [combobox](https://www.w3.org/TR/wai-aria/#combobox) +- [listbox](https://www.w3.org/TR/wai-aria/#listbox) +- [option](https://www.w3.org/TR/wai-aria/#option) From c4f173b5e87533b9f69d1e82a966bc9052225ea6 Mon Sep 17 00:00:00 2001 From: Aleksandr Bunin Date: Wed, 17 Aug 2022 18:58:06 +0300 Subject: [PATCH 058/185] changlog about v-id --- src/form/b-button/CHANGELOG.md | 4 ++-- .../p-v4-components-demo/b-v4-component-demo/CHANGELOG.md | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/form/b-button/CHANGELOG.md b/src/form/b-button/CHANGELOG.md index 95f35cb6b7..e7f3ee866c 100644 --- a/src/form/b-button/CHANGELOG.md +++ b/src/form/b-button/CHANGELOG.md @@ -11,9 +11,9 @@ Changelog ## v3.?.? (2022-0?-??) -#### :rocket: New Feature +#### :house: Internal -* Added a new directive `v-id` +* Now element's `id` attribute is added with `v-id` directive ## v3.0.0-rc.211 (2021-07-21) diff --git a/src/pages/p-v4-components-demo/b-v4-component-demo/CHANGELOG.md b/src/pages/p-v4-components-demo/b-v4-component-demo/CHANGELOG.md index 43fcf88a8d..a8389d7ed5 100644 --- a/src/pages/p-v4-components-demo/b-v4-component-demo/CHANGELOG.md +++ b/src/pages/p-v4-components-demo/b-v4-component-demo/CHANGELOG.md @@ -13,7 +13,7 @@ Changelog #### :house: Internal -* Added a new directive `v-id` +* Now element's `id` attribute is added with `v-id` directive ## v3.0.0-rc.37 (2020-07-20) From 5c2c245420516dbe5ffa1dd25f4c1bee524da831 Mon Sep 17 00:00:00 2001 From: Aleksandr Bunin Date: Wed, 17 Aug 2022 19:00:47 +0300 Subject: [PATCH 059/185] b-checkbox changlog --- src/form/b-checkbox/CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/form/b-checkbox/CHANGELOG.md b/src/form/b-checkbox/CHANGELOG.md index c75caea52f..e63c710489 100644 --- a/src/form/b-checkbox/CHANGELOG.md +++ b/src/form/b-checkbox/CHANGELOG.md @@ -13,7 +13,7 @@ Changelog #### :bug: Bug Fix -* Added `label` tag with `for` attribute to label and `id` to nativeInput in template +* The `for` attribute has been added to the label to link to the checkbox #### :house: Internal From e3db4b75a1139f6b3328d3dbe960aa18ecd121ea Mon Sep 17 00:00:00 2001 From: Aleksandr Bunin Date: Thu, 18 Aug 2022 01:02:19 +0300 Subject: [PATCH 060/185] add static property params --- .../component/directives/aria/aria-setter.ts | 51 +++++++++++++------ .../aria/roles-engines/combobox/index.ts | 5 ++ .../aria/roles-engines/controls/index.ts | 5 ++ .../aria/roles-engines/interface.ts | 5 ++ .../aria/roles-engines/option/index.ts | 5 ++ .../aria/roles-engines/tab/index.ts | 5 ++ .../aria/roles-engines/tablist/index.ts | 5 ++ .../aria/roles-engines/tree/index.ts | 5 ++ .../aria/roles-engines/treeitem/index.ts | 5 ++ .../directives/aria/test/unit/simple.ts | 26 ++++++---- 10 files changed, 90 insertions(+), 27 deletions(-) diff --git a/src/core/component/directives/aria/aria-setter.ts b/src/core/component/directives/aria/aria-setter.ts index a88f7a9bfe..ffa8002c20 100644 --- a/src/core/component/directives/aria/aria-setter.ts +++ b/src/core/component/directives/aria/aria-setter.ts @@ -9,7 +9,7 @@ import Async from 'core/async'; import type iBlock from 'super/i-block/i-block'; -import * as ariaRoles from 'core/component/directives/aria/roles-engines'; +import * as roles from 'core/component/directives/aria/roles-engines'; import type { DirectiveOptions } from 'core/component/directives/aria/interface'; import type { AriaRoleEngine, EngineOptions } from 'core/component/directives/aria/roles-engines'; @@ -32,6 +32,11 @@ export default class AriaSetter { */ role: CanUndef; + /** + * Role engine params list + */ + roleParams: CanUndef; + constructor(options: DirectiveOptions) { this.options = options; this.async = new Async(); @@ -44,7 +49,8 @@ export default class AriaSetter { * Initiates the base logic of the directive */ init(): void { - this.setAriaLabel(); + this.setAriaLabelledBy(); + this.setAriaAttributes(); this.addEventHandlers(); this.role?.init(); @@ -84,7 +90,8 @@ export default class AriaSetter { engine = this.createEngineName(role), options = this.createRoleOptions(); - this.role = new ariaRoles[engine](options); + this.role = new roles[engine](options); + this.roleParams = roles[engine].params; } /** @@ -113,10 +120,9 @@ export default class AriaSetter { } /** - * Sets aria-label, aria-labelledby, aria-description and aria-describedby attributes to the element - * from directive parameters + * Sets aria-labelledby attribute to the element from directive parameters */ - protected setAriaLabel(): void { + protected setAriaLabelledBy(): void { const {vnode, binding, el} = this.options, {dom} = Object.cast(vnode.fakeContext), @@ -134,18 +140,30 @@ export default class AriaSetter { el.setAttribute('aria-labelledby', id); } - if (params.label != null) { - el.setAttribute('aria-label', params.label); + if (params.labelledby == null) { + return; + } + + if (Object.isArray(params.labelledby)) { + el.setAttribute('aria-labelledby', params.labelledby.join(' ')); - } else if (params.labelledby != null) { + } else { el.setAttribute('aria-labelledby', params.labelledby); } + } - if (params.description != null) { - el.setAttribute('aria-description', params.description); + /** + * Sets aria attributes from passed params except `aria-labelledby` + */ + protected setAriaAttributes(): void { + const + {el, binding} = this.options, + params = binding.value; - } else if (params.describedby != null) { - el.setAttribute('aria-describedby', params.describedby); + for (const key in params) { + if (!this.roleParams?.includes(key) && key !== 'labelledby') { + el.setAttribute(`aria-${key}`, params[key]); + } } } @@ -161,9 +179,6 @@ export default class AriaSetter { const params = this.options.binding.value; - const - getCallbackName = (key: string) => `on-${key.slice(1)}`.camelize(false); - for (const key in params) { if (key.startsWith('@')) { const @@ -191,5 +206,9 @@ export default class AriaSetter { } } } + + function getCallbackName(key: string) { + return `on-${key.slice(1)}`.camelize(false); + } } } diff --git a/src/core/component/directives/aria/roles-engines/combobox/index.ts b/src/core/component/directives/aria/roles-engines/combobox/index.ts index 708fab3488..f9713248bb 100644 --- a/src/core/component/directives/aria/roles-engines/combobox/index.ts +++ b/src/core/component/directives/aria/roles-engines/combobox/index.ts @@ -26,6 +26,11 @@ export class ComboboxEngine extends AriaRoleEngine { /** @see [[AriaRoleEngine.Ctx]] */ override Ctx!: ComponentInterface & iAccess; + /** + * Engine params list + */ + static override params: string[] = ['isMultiple', '@change', '@open', '@close']; + constructor(options: EngineOptions) { super(options); diff --git a/src/core/component/directives/aria/roles-engines/controls/index.ts b/src/core/component/directives/aria/roles-engines/controls/index.ts index 7fed253895..41bcf25dd5 100644 --- a/src/core/component/directives/aria/roles-engines/controls/index.ts +++ b/src/core/component/directives/aria/roles-engines/controls/index.ts @@ -15,6 +15,11 @@ export class ControlsEngine extends AriaRoleEngine { */ override params: ControlsParams; + /** + * Engine params + */ + static override params: string[]; + constructor(options: EngineOptions) { super(options); diff --git a/src/core/component/directives/aria/roles-engines/interface.ts b/src/core/component/directives/aria/roles-engines/interface.ts index 26f1e50576..775c6ff3a0 100644 --- a/src/core/component/directives/aria/roles-engines/interface.ts +++ b/src/core/component/directives/aria/roles-engines/interface.ts @@ -45,6 +45,11 @@ export abstract class AriaRoleEngine { */ async: CanUndef; + /** + * Directive expected params list + */ + static params: string[]; + constructor({el, ctx, modifiers, params, async}: EngineOptions) { this.el = el; this.ctx = ctx; diff --git a/src/core/component/directives/aria/roles-engines/option/index.ts b/src/core/component/directives/aria/roles-engines/option/index.ts index 49322476a5..10fcb4c262 100644 --- a/src/core/component/directives/aria/roles-engines/option/index.ts +++ b/src/core/component/directives/aria/roles-engines/option/index.ts @@ -15,6 +15,11 @@ export class OptionEngine extends AriaRoleEngine { */ override params: OptionParams; + /** + * Engine params list + */ + static override params: string[] = ['isSelected', '@change']; + constructor(options: EngineOptions) { super(options); diff --git a/src/core/component/directives/aria/roles-engines/tab/index.ts b/src/core/component/directives/aria/roles-engines/tab/index.ts index 2cbe0d3be5..513b96cd0a 100644 --- a/src/core/component/directives/aria/roles-engines/tab/index.ts +++ b/src/core/component/directives/aria/roles-engines/tab/index.ts @@ -24,6 +24,11 @@ export class TabEngine extends AriaRoleEngine { /** @see [[AriaRoleEngine.ctx]] */ override ctx?: iBlock & iAccess; + /** + * Engine params list + */ + static override params: string[] = ['isFirst', 'isSelected', 'hasDefaultSelectedTabs', 'orientation', '@change']; + constructor(options: EngineOptions) { super(options); diff --git a/src/core/component/directives/aria/roles-engines/tablist/index.ts b/src/core/component/directives/aria/roles-engines/tablist/index.ts index 83451227bd..5aa6531fba 100644 --- a/src/core/component/directives/aria/roles-engines/tablist/index.ts +++ b/src/core/component/directives/aria/roles-engines/tablist/index.ts @@ -15,6 +15,11 @@ export class TablistEngine extends AriaRoleEngine { */ override params: TablistParams; + /** + * Engine params list + */ + static override params: string[] = ['isMultiple', 'orientation']; + constructor(options: EngineOptions) { super(options); diff --git a/src/core/component/directives/aria/roles-engines/tree/index.ts b/src/core/component/directives/aria/roles-engines/tree/index.ts index 483901f2f7..9bb4cd98e4 100644 --- a/src/core/component/directives/aria/roles-engines/tree/index.ts +++ b/src/core/component/directives/aria/roles-engines/tree/index.ts @@ -15,6 +15,11 @@ export class TreeEngine extends AriaRoleEngine { */ override params: TreeParams; + /** + * Engine params list + */ + static override params: string[] = ['isRoot', 'orientation', '@change']; + constructor(options: EngineOptions) { super(options); diff --git a/src/core/component/directives/aria/roles-engines/treeitem/index.ts b/src/core/component/directives/aria/roles-engines/treeitem/index.ts index a02a0e6905..a96d27a0bb 100644 --- a/src/core/component/directives/aria/roles-engines/treeitem/index.ts +++ b/src/core/component/directives/aria/roles-engines/treeitem/index.ts @@ -24,6 +24,11 @@ export class TreeitemEngine extends AriaRoleEngine { /** @see [[AriaRoleEngine.ctx]] */ override ctx?: iBlock & iAccess; + /** + * Engine params list + */ + static override params: string[] = ['isFirstRootItem', 'isExpandable', 'isExpanded', 'orientation', 'rootElement', 'toggleFold']; + constructor(options: EngineOptions) { super(options); diff --git a/src/core/component/directives/aria/test/unit/simple.ts b/src/core/component/directives/aria/test/unit/simple.ts index ff9cab8333..592cef7b24 100644 --- a/src/core/component/directives/aria/test/unit/simple.ts +++ b/src/core/component/directives/aria/test/unit/simple.ts @@ -17,7 +17,7 @@ test.describe('v-aria', () => { await demoPage.goto(); }); - test('aria-label is added', async ({page}) => { + test('one aria attribute is added', async ({page}) => { const target = await init(page, {'v-aria': {label: 'bla'}}); test.expect( @@ -25,28 +25,32 @@ test.describe('v-aria', () => { ).toBe('bla'); }); - test('aria-labelledby is added', async ({page}) => { - const target = await init(page, {'v-aria': {labelledby: 'bla'}}); + test('two aria attributes are added', async ({page}) => { + const target = await init(page, {'v-aria': {describedby: 'bla', label: 'foo'}}); test.expect( - await target.evaluate((ctx) => ctx.$el?.getAttribute('aria-labelledby')) + await target.evaluate((ctx) => ctx.$el?.getAttribute('aria-describedby')) ).toBe('bla'); + + test.expect( + await target.evaluate((ctx) => ctx.$el?.getAttribute('aria-label')) + ).toBe('foo'); }); - test('aria-description is added', async ({page}) => { - const target = await init(page, {'v-aria': {description: 'bla'}}); + test('aria-labelledby is added by string', async ({page}) => { + const target = await init(page, {'v-aria': {labelledby: 'bla'}}); test.expect( - await target.evaluate((ctx) => ctx.$el?.getAttribute('aria-description')) + await target.evaluate((ctx) => ctx.$el?.getAttribute('aria-labelledby')) ).toBe('bla'); }); - test('aria-describedby is added', async ({page}) => { - const target = await init(page, {'v-aria': {describedby: 'bla'}}); + test('aria-labelledby is added by array', async ({page}) => { + const target = await init(page, {'v-aria': {labelledby: ['bla', 'bar', 'foo']}}); test.expect( - await target.evaluate((ctx) => ctx.$el?.getAttribute('aria-describedby')) - ).toBe('bla'); + await target.evaluate((ctx) => ctx.$el?.getAttribute('aria-labelledby')) + ).toBe('bla bar foo'); }); test('aria-labelledby sugar syntax', async ({page}) => { From 1c22027602b801c66567e66c8251a1d594f3bf16 Mon Sep 17 00:00:00 2001 From: Aleksandr Bunin Date: Thu, 18 Aug 2022 01:02:39 +0300 Subject: [PATCH 061/185] fix iaccess --- src/traits/i-access/i-access.ts | 36 ++++++++++++++++++++++----------- 1 file changed, 24 insertions(+), 12 deletions(-) diff --git a/src/traits/i-access/i-access.ts b/src/traits/i-access/i-access.ts index bbdcfc4a47..45c88979ea 100644 --- a/src/traits/i-access/i-access.ts +++ b/src/traits/i-access/i-access.ts @@ -239,24 +239,32 @@ export default abstract class iAccess { /** @see [[iAccess.getNextFocusableElement]] */ static getNextFocusableElement: AddSelf = (component, step, searchCtx = document.documentElement): AccessibleElement | null => { - if (document.activeElement == null) { + const + {activeElement} = document; + + if (activeElement == null) { return null; } const - focusableEls = this.findFocusableElements(component, searchCtx), - visibleFocusableEls: AccessibleElement[] = [...focusableEls]; + focusableEls = [...this.findFocusableElements(component, searchCtx)], + index = focusableEls.indexOf(activeElement); - visibleFocusableEls.sort((el1, el2) => el2.tabIndex - el1.tabIndex); + if (index < 0) { + return null; + } - const - index = visibleFocusableEls.indexOf(document.activeElement); + if (step > 0) { + const next = focusableEls + .slice(index + 1) + .find((el) => el.tabIndex > 0); - if (index > -1) { - return visibleFocusableEls[index + step] ?? null; + if (next != null) { + return next; + } } - return null; + return focusableEls[index + step] ?? null; }; /** @see [[iAccess.findFocusableElement]] */ @@ -300,10 +308,14 @@ export default abstract class iAccess { iter: IterableIterator ): IterableIterator { for (const el of iter) { + const + rect = el.getBoundingClientRect(); + if ( - !el.hasAttribute('disabled') || - el.getAttribute('visibility') !== 'hidden' || - el.getAttribute('display') !== 'none' + !el.hasAttribute('disabled') && + el.getAttribute('visibility') !== 'hidden' && + el.getAttribute('display') !== 'none' && + rect.height > 0 || rect.width > 0 ) { yield el; } From cfded05a6d39d8a1876d74ca35cf68b619f7cca4 Mon Sep 17 00:00:00 2001 From: Aleksandr Bunin Date: Thu, 18 Aug 2022 16:26:34 +0300 Subject: [PATCH 062/185] fix iaccess --- .../aria/roles-engines/treeitem/index.ts | 7 +++- .../treeitem/test/unit/treeitem.ts | 6 +-- src/traits/i-access/i-access.ts | 37 ++++++++++++------- 3 files changed, 31 insertions(+), 19 deletions(-) diff --git a/src/core/component/directives/aria/roles-engines/treeitem/index.ts b/src/core/component/directives/aria/roles-engines/treeitem/index.ts index a96d27a0bb..cad2c6ce5a 100644 --- a/src/core/component/directives/aria/roles-engines/treeitem/index.ts +++ b/src/core/component/directives/aria/roles-engines/treeitem/index.ts @@ -137,7 +137,7 @@ export class TreeitemEngine extends AriaRoleEngine { protected setFocusToFirstItem(): void { const firstItem = this.ctx?.findFocusableElement(this.params.rootElement); - +debugger; if (firstItem != null) { this.focusNext(firstItem); } @@ -235,7 +235,10 @@ export class TreeitemEngine extends AriaRoleEngine { break; case KeyCodes.ENTER: - this.params.toggleFold(this.el); + if (this.params.isExpandable) { + this.params.toggleFold(this.el); + } + break; case KeyCodes.HOME: diff --git a/src/core/component/directives/aria/roles-engines/treeitem/test/unit/treeitem.ts b/src/core/component/directives/aria/roles-engines/treeitem/test/unit/treeitem.ts index 00bde5251e..f44947bbf7 100644 --- a/src/core/component/directives/aria/roles-engines/treeitem/test/unit/treeitem.ts +++ b/src/core/component/directives/aria/roles-engines/treeitem/test/unit/treeitem.ts @@ -48,8 +48,6 @@ test.describe('v-aria:treeitem', () => { test('keyboard keys handle on vertical orientation', async ({page}) => { const target = await init(page); - await page.waitForSelector('[role="group"]'); - test.expect( await target.evaluate((ctx) => { if (ctx.unsafe.block == null) { @@ -78,6 +76,8 @@ test.describe('v-aria:treeitem', () => { dis('ArrowUp'); res.push(eq(0)); + dis('Enter'); + res.push(items[0].getAttribute('aria-expanded')); dis('ArrowDown'); dis('Enter'); @@ -106,7 +106,7 @@ test.describe('v-aria:treeitem', () => { return res; }) - ).toEqual([true, true, 'true', 'false', 'true', true, true, 'false', true, true]); + ).toEqual([true, true, null, 'true', 'false', 'true', true, true, 'false', true, true]); }); test('keyboard keys handle on horizontal orientation', async ({page}) => { diff --git a/src/traits/i-access/i-access.ts b/src/traits/i-access/i-access.ts index 45c88979ea..fe8593e930 100644 --- a/src/traits/i-access/i-access.ts +++ b/src/traits/i-access/i-access.ts @@ -247,22 +247,18 @@ export default abstract class iAccess { } const - focusableEls = [...this.findFocusableElements(component, searchCtx)], + focusableEls = [...this.findFocusableElements(component, searchCtx, {native: false})], index = focusableEls.indexOf(activeElement); if (index < 0) { return null; } - if (step > 0) { - const next = focusableEls - .slice(index + 1) - .find((el) => el.tabIndex > 0); - - if (next != null) { - return next; + focusableEls.forEach((el) => { + if (el.tabIndex > 0) { + Object.throw('The tab sequence has an element with tabindex more than 0. The sequence would be different in different browsers. It is strongly recommended not to use tabindexes more than 0.'); } - } + }); return focusableEls[index + step] ?? null; }; @@ -282,7 +278,7 @@ export default abstract class iAccess { /** @see [[iAccess.findFocusableElements]] */ static findFocusableElements: AddSelf = - (component, searchCtx = component.$el): IterableIterator => { + (component, searchCtx = component.$el, opts = {native: true}): IterableIterator => { const accessibleEls = searchCtx?.querySelectorAll(FOCUSABLE_SELECTOR); @@ -314,10 +310,16 @@ export default abstract class iAccess { if ( !el.hasAttribute('disabled') && el.getAttribute('visibility') !== 'hidden' && - el.getAttribute('display') !== 'none' && - rect.height > 0 || rect.width > 0 + el.getAttribute('display') !== 'none' ) { - yield el; + if (!opts.native) { + if (rect.height > 0 || rect.width > 0) { + yield el; + } + + } else { + yield el; + } } } } @@ -429,10 +431,17 @@ export default abstract class iAccess { /** * Finds all non-disabled visible focusable elements and returns an iterator with the found ones. * The element that is the search context is also taken into account in the search. + * Also expects a dictionary with option of filtration invisible elements. + * If native property is set to true, the method filters invisible elements by css properties + * `disabled`, `visible` and `display`. + * Native in false also adds the filtration by element's current visibility on the screen. * * @param [searchCtx] - a context to search, if not set, the component root element will be used + * @param [opts] - dictionary with options of elements' visibility filtration, {native: true} by default */ - findFocusableElements(searchCtx?: Element): IterableIterator { + findFocusableElements< + T extends AccessibleElement = AccessibleElement + >(searchCtx?: Element, opts?: {native: boolean}): IterableIterator { return Object.throw(); } } From eb474212f000594ac47d5e1a68118ed0d80685a3 Mon Sep 17 00:00:00 2001 From: Aleksandr Bunin Date: Thu, 18 Aug 2022 17:50:23 +0300 Subject: [PATCH 063/185] refactoring --- src/base/b-list/b-list.ts | 2 + src/base/b-tree/b-tree.ts | 2 + src/base/b-window/b-window.ts | 2 + .../aria/{aria-setter.ts => adapter.ts} | 86 ++++++------------- src/core/component/directives/aria/index.ts | 10 +-- .../aria/roles-engines/combobox/index.ts | 19 ++-- .../aria/roles-engines/controls/index.ts | 22 ++--- .../aria/roles-engines/dialog/index.ts | 4 +- .../aria/roles-engines/interface.ts | 3 + .../aria/roles-engines/listbox/index.ts | 4 +- .../aria/roles-engines/option/index.ts | 22 ++--- .../aria/roles-engines/tab/index.ts | 22 ++--- .../aria/roles-engines/tablist/index.ts | 22 ++--- .../aria/roles-engines/tabpanel/index.ts | 4 +- .../aria/roles-engines/tree/index.ts | 22 ++--- .../aria/roles-engines/treeitem/index.ts | 26 ++---- src/core/component/directives/index.ts | 2 - src/form/b-checkbox/b-checkbox.ts | 2 + src/form/b-select/b-select.ts | 7 +- 19 files changed, 84 insertions(+), 199 deletions(-) rename src/core/component/directives/aria/{aria-setter.ts => adapter.ts} (68%) diff --git a/src/base/b-list/b-list.ts b/src/base/b-list/b-list.ts index 8186ebfff7..5aacacdb28 100644 --- a/src/base/b-list/b-list.ts +++ b/src/base/b-list/b-list.ts @@ -15,6 +15,8 @@ import 'models/demo/list'; //#endif +import 'core/component/directives/aria'; + import symbolGenerator from 'core/symbol'; import SyncPromise from 'core/promise/sync'; diff --git a/src/base/b-tree/b-tree.ts b/src/base/b-tree/b-tree.ts index 1109d07416..8c5fac81a2 100644 --- a/src/base/b-tree/b-tree.ts +++ b/src/base/b-tree/b-tree.ts @@ -15,6 +15,8 @@ import 'models/demo/nested-list'; //#endif +import 'core/component/directives/aria'; + import symbolGenerator from 'core/symbol'; import { derive } from 'core/functools/trait'; diff --git a/src/base/b-window/b-window.ts b/src/base/b-window/b-window.ts index 7712d6f8f5..1cd6c516b3 100644 --- a/src/base/b-window/b-window.ts +++ b/src/base/b-window/b-window.ts @@ -11,6 +11,8 @@ * @packageDocumentation */ +import 'core/component/directives/aria'; + import symbolGenerator from 'core/symbol'; import { derive } from 'core/functools/trait'; diff --git a/src/core/component/directives/aria/aria-setter.ts b/src/core/component/directives/aria/adapter.ts similarity index 68% rename from src/core/component/directives/aria/aria-setter.ts rename to src/core/component/directives/aria/adapter.ts index ffa8002c20..8439be4ed9 100644 --- a/src/core/component/directives/aria/aria-setter.ts +++ b/src/core/component/directives/aria/adapter.ts @@ -14,34 +14,30 @@ import type { DirectiveOptions } from 'core/component/directives/aria/interface' import type { AriaRoleEngine, EngineOptions } from 'core/component/directives/aria/roles-engines'; /** - * Class-helper for making base operations for the directive + * An adapter to create an ARIA role instance based on the passed directive options and to add common attributes */ -export default class AriaSetter { +export default class AriaAdapter { /** - * Aria directive options + * Parameters passed from the directive */ - readonly options: DirectiveOptions; + protected readonly options: DirectiveOptions; - /** - * Async instance for aria directive - */ - readonly async: Async; + /** @see [[Async]] */ + protected readonly async: Async = new Async(); /** - * Role engine instance + * An instance of the associated ARIA role */ - role: CanUndef; + protected role: CanUndef; /** * Role engine params list */ - roleParams: CanUndef; + protected roleParams: CanUndef; constructor(options: DirectiveOptions) { this.options = options; - this.async = new Async(); this.setAriaRole(); - this.init(); } @@ -56,18 +52,6 @@ export default class AriaSetter { this.role?.init(); } - /** - * Runs on update directive hook. Removes listeners from component if the component is Functional. - */ - update(): void { - const - ctx = this.options.vnode.fakeContext; - - if (ctx.isFunctional) { - ctx.off(); - } - } - /** * Runs on unbind directive hook. Clears the Async instance. */ @@ -75,48 +59,35 @@ export default class AriaSetter { this.async.clearAll(); } + protected get ctx(): iBlock['unsafe'] { + return Object.cast(this.options.vnode.fakeContext); + } + /** * If the role was passed as a directive argument sets specified engine */ protected setAriaRole(): CanUndef { const - {arg: role} = this.options.binding; + {el, binding} = this.options, + {value, modifiers, arg: role} = binding; if (role == null) { return; } const - engine = this.createEngineName(role), - options = this.createRoleOptions(); + engine = `${role.capitalize()}Engine`; - this.role = new roles[engine](options); - this.roleParams = roles[engine].params; - } - - /** - * Creates an engine name from a passed parameter - * @param role - */ - protected createEngineName(role: string): string { - return `${role.capitalize()}Engine`; - } - - /** - * Creates a dictionary with engine options - */ - protected createRoleOptions(): EngineOptions { - const - {el, binding, vnode} = this.options, - {value, modifiers} = binding; - - return { + const options: EngineOptions = { el, modifiers, params: value, - ctx: Object.cast(vnode.fakeContext), + ctx: this.ctx, async: this.async }; + + this.role = new roles[engine](options); + this.roleParams = roles[engine].params; } /** @@ -124,8 +95,8 @@ export default class AriaSetter { */ protected setAriaLabelledBy(): void { const - {vnode, binding, el} = this.options, - {dom} = Object.cast(vnode.fakeContext), + {binding, el} = this.options, + {dom} = this.ctx, params = Object.isCustomObject(binding.value) ? binding.value : {}; for (const mod in binding.modifiers) { @@ -182,7 +153,7 @@ export default class AriaSetter { for (const key in params) { if (key.startsWith('@')) { const - callbackName = getCallbackName(key); + callbackName = `on-${key.slice(1)}`.camelize(false); if (!Object.isFunction(this.role[callbackName])) { Object.throw('Aria role engine does not contains event handler for passed event name or the type of engine\'s property is not a function'); @@ -199,16 +170,9 @@ export default class AriaSetter { void property.then(callback); } else if (Object.isString(property)) { - const - ctx = this.options.vnode.fakeContext; - - ctx.on(property, callback); + this.ctx.on(property, callback); } } } - - function getCallbackName(key: string) { - return `on-${key.slice(1)}`.camelize(false); - } } } diff --git a/src/core/component/directives/aria/index.ts b/src/core/component/directives/aria/index.ts index 0155ea4e8c..e50f94f653 100644 --- a/src/core/component/directives/aria/index.ts +++ b/src/core/component/directives/aria/index.ts @@ -12,12 +12,12 @@ */ import { ComponentEngine, VNode, VNodeDirective } from 'core/component/engines'; -import AriaSetter from 'core/component/directives/aria/aria-setter'; +import AriaAdapter from 'core/component/directives/aria/adapter'; export * from 'core/component/directives/aria/interface'; const - ariaInstances = new WeakMap(); + ariaInstances = new WeakMap(); ComponentEngine.directive('aria', { inserted(el: HTMLElement, binding: VNodeDirective, vnode: VNode): void { @@ -28,11 +28,7 @@ ComponentEngine.directive('aria', { return; } - ariaInstances.set(el, new AriaSetter({el, binding, vnode})); - }, - - update(el: HTMLElement) { - ariaInstances.get(el)?.update(); + ariaInstances.set(el, new AriaAdapter({el, binding, vnode})); }, unbind(el: HTMLElement) { diff --git a/src/core/component/directives/aria/roles-engines/combobox/index.ts b/src/core/component/directives/aria/roles-engines/combobox/index.ts index f9713248bb..4f9b89a44b 100644 --- a/src/core/component/directives/aria/roles-engines/combobox/index.ts +++ b/src/core/component/directives/aria/roles-engines/combobox/index.ts @@ -13,22 +13,16 @@ import type { ComboboxParams } from 'core/component/directives/aria/roles-engine import { AriaRoleEngine, EngineOptions } from 'core/component/directives/aria/roles-engines/interface'; export class ComboboxEngine extends AriaRoleEngine { - /** - * Engine params - */ - override params: ComboboxParams; + /** @see [[AriaRoleEngine.Prams]] */ + override Params!: ComboboxParams; - /** - * First focusable element inside the element with directive or this element if there is no focusable inside - */ + /** @see [[AriaRoleEngine.el]] */ override el: HTMLElement; /** @see [[AriaRoleEngine.Ctx]] */ override Ctx!: ComponentInterface & iAccess; - /** - * Engine params list - */ + /** @see [[AriaRoleEngine.params]] */ static override params: string[] = ['isMultiple', '@change', '@open', '@close']; constructor(options: EngineOptions) { @@ -38,12 +32,9 @@ export class ComboboxEngine extends AriaRoleEngine { {el} = this; this.el = this.ctx?.findFocusableElement() ?? el; - this.params = options.params; } - /** - * Sets base aria attributes for current role - */ + /* @inheritDoc */ init(): void { this.el.setAttribute('role', 'combobox'); this.el.setAttribute('aria-expanded', 'false'); diff --git a/src/core/component/directives/aria/roles-engines/controls/index.ts b/src/core/component/directives/aria/roles-engines/controls/index.ts index 41bcf25dd5..f0bb7da1f6 100644 --- a/src/core/component/directives/aria/roles-engines/controls/index.ts +++ b/src/core/component/directives/aria/roles-engines/controls/index.ts @@ -7,28 +7,16 @@ */ import type { ControlsParams } from 'core/component/directives/aria/roles-engines/controls/interface'; -import { AriaRoleEngine, EngineOptions } from 'core/component/directives/aria/roles-engines/interface'; +import { AriaRoleEngine } from 'core/component/directives/aria/roles-engines/interface'; export class ControlsEngine extends AriaRoleEngine { - /** - * Engine params - */ - override params: ControlsParams; + /** @see [[AriaRoleEngine.Params]] */ + override Params!: ControlsParams; - /** - * Engine params - */ + /** @see [[AriaRoleEngine.params]] */ static override params: string[]; - constructor(options: EngineOptions) { - super(options); - - this.params = options.params; - } - - /** - * Sets base aria attributes for current role - */ + /* @inheritDoc */ init(): void { const {ctx, modifiers, el} = this, diff --git a/src/core/component/directives/aria/roles-engines/dialog/index.ts b/src/core/component/directives/aria/roles-engines/dialog/index.ts index e1652ea13c..c3f99369de 100644 --- a/src/core/component/directives/aria/roles-engines/dialog/index.ts +++ b/src/core/component/directives/aria/roles-engines/dialog/index.ts @@ -10,9 +10,7 @@ import { AriaRoleEngine } from 'core/component/directives/aria/roles-engines/int import iOpen from 'traits/i-open/i-open'; export class DialogEngine extends AriaRoleEngine { - /** - * Sets base aria attributes for current role - */ + /* @inheritDoc */ init(): void { this.el.setAttribute('role', 'dialog'); this.el.setAttribute('aria-modal', 'true'); diff --git a/src/core/component/directives/aria/roles-engines/interface.ts b/src/core/component/directives/aria/roles-engines/interface.ts index 775c6ff3a0..ad7a391346 100644 --- a/src/core/component/directives/aria/roles-engines/interface.ts +++ b/src/core/component/directives/aria/roles-engines/interface.ts @@ -58,6 +58,9 @@ export abstract class AriaRoleEngine { this.async = async; } + /** + * Sets base aria attributes for current role + */ abstract init(): void; } diff --git a/src/core/component/directives/aria/roles-engines/listbox/index.ts b/src/core/component/directives/aria/roles-engines/listbox/index.ts index 7d451389d5..42bc7dbc89 100644 --- a/src/core/component/directives/aria/roles-engines/listbox/index.ts +++ b/src/core/component/directives/aria/roles-engines/listbox/index.ts @@ -9,9 +9,7 @@ import { AriaRoleEngine } from 'core/component/directives/aria/roles-engines/interface'; export class ListboxEngine extends AriaRoleEngine { - /** - * Sets base aria attributes for current role - */ + /* @inheritDoc */ init(): void { this.el.setAttribute('role', 'listbox'); this.el.setAttribute('tabindex', '-1'); diff --git a/src/core/component/directives/aria/roles-engines/option/index.ts b/src/core/component/directives/aria/roles-engines/option/index.ts index 10fcb4c262..3a31e1802f 100644 --- a/src/core/component/directives/aria/roles-engines/option/index.ts +++ b/src/core/component/directives/aria/roles-engines/option/index.ts @@ -7,28 +7,16 @@ */ import type { OptionParams } from 'core/component/directives/aria/roles-engines/option/interface'; -import { AriaRoleEngine, EngineOptions } from 'core/component/directives/aria/roles-engines/interface'; +import { AriaRoleEngine } from 'core/component/directives/aria/roles-engines/interface'; export class OptionEngine extends AriaRoleEngine { - /** - * Engine params - */ - override params: OptionParams; + /** @see [[AriaRoleEngine.Params]] */ + override Params!: OptionParams; - /** - * Engine params list - */ + /** @see [[AriaRoleEngine.params]] */ static override params: string[] = ['isSelected', '@change']; - constructor(options: EngineOptions) { - super(options); - - this.params = options.params; - } - - /** - * Sets base aria attributes for current role - */ + /* @inheritDoc */ init(): void { this.el.setAttribute('role', 'option'); this.el.setAttribute('aria-selected', String(this.params.isSelected)); diff --git a/src/core/component/directives/aria/roles-engines/tab/index.ts b/src/core/component/directives/aria/roles-engines/tab/index.ts index 513b96cd0a..6516157e40 100644 --- a/src/core/component/directives/aria/roles-engines/tab/index.ts +++ b/src/core/component/directives/aria/roles-engines/tab/index.ts @@ -13,31 +13,19 @@ import type iBlock from 'super/i-block/i-block'; import type iAccess from 'traits/i-access/i-access'; import type { TabParams } from 'core/component/directives/aria/roles-engines/tab/interface'; -import { AriaRoleEngine, EngineOptions, KeyCodes } from 'core/component/directives/aria/roles-engines/interface'; +import { AriaRoleEngine, KeyCodes } from 'core/component/directives/aria/roles-engines/interface'; export class TabEngine extends AriaRoleEngine { - /** - * Engine params - */ - override params: TabParams; + /** @see [[AriaRoleEngine.Params]] */ + override Params!: TabParams; /** @see [[AriaRoleEngine.ctx]] */ override ctx?: iBlock & iAccess; - /** - * Engine params list - */ + /** @see [[AriaRoleEngine.params]] */ static override params: string[] = ['isFirst', 'isSelected', 'hasDefaultSelectedTabs', 'orientation', '@change']; - constructor(options: EngineOptions) { - super(options); - - this.params = options.params; - } - - /** - * Sets base aria attributes for current role - */ + /* @inheritDoc */ init(): void { const {el} = this, diff --git a/src/core/component/directives/aria/roles-engines/tablist/index.ts b/src/core/component/directives/aria/roles-engines/tablist/index.ts index 5aa6531fba..8f43ad2125 100644 --- a/src/core/component/directives/aria/roles-engines/tablist/index.ts +++ b/src/core/component/directives/aria/roles-engines/tablist/index.ts @@ -7,28 +7,16 @@ */ import type { TablistParams } from 'core/component/directives/aria/roles-engines/tablist/interface'; -import { AriaRoleEngine, EngineOptions } from 'core/component/directives/aria/roles-engines/interface'; +import { AriaRoleEngine } from 'core/component/directives/aria/roles-engines/interface'; export class TablistEngine extends AriaRoleEngine { - /** - * Engine params - */ - override params: TablistParams; + /** @see [[AriaRoleEngine.Params]] */ + override Params!: TablistParams; - /** - * Engine params list - */ + /** @see [[AriaRoleEngine.params]] */ static override params: string[] = ['isMultiple', 'orientation']; - constructor(options: EngineOptions) { - super(options); - - this.params = options.params; - } - - /** - * Sets base aria attributes for current role - */ + /* @inheritDoc */ init(): void { const {el, params} = this; diff --git a/src/core/component/directives/aria/roles-engines/tabpanel/index.ts b/src/core/component/directives/aria/roles-engines/tabpanel/index.ts index f49f2661ba..0eea14ef23 100644 --- a/src/core/component/directives/aria/roles-engines/tabpanel/index.ts +++ b/src/core/component/directives/aria/roles-engines/tabpanel/index.ts @@ -9,9 +9,7 @@ import { AriaRoleEngine } from 'core/component/directives/aria/roles-engines/interface'; export class TabpanelEngine extends AriaRoleEngine { - /** - * Sets base aria attributes for current role - */ + /* @inheritDoc */ init(): void { const {el} = this; diff --git a/src/core/component/directives/aria/roles-engines/tree/index.ts b/src/core/component/directives/aria/roles-engines/tree/index.ts index 9bb4cd98e4..1418d1ecdc 100644 --- a/src/core/component/directives/aria/roles-engines/tree/index.ts +++ b/src/core/component/directives/aria/roles-engines/tree/index.ts @@ -7,28 +7,16 @@ */ import type { TreeParams } from 'core/component/directives/aria/roles-engines/tree/interface'; -import { AriaRoleEngine, EngineOptions } from 'core/component/directives/aria/roles-engines/interface'; +import { AriaRoleEngine } from 'core/component/directives/aria/roles-engines/interface'; export class TreeEngine extends AriaRoleEngine { - /** - * Engine params - */ - override params: TreeParams; + /** @see [[AriaRoleEngine.Params]] */ + override Params!: TreeParams; - /** - * Engine params list - */ + /** @see [[AriaRoleEngine.params]] */ static override params: string[] = ['isRoot', 'orientation', '@change']; - constructor(options: EngineOptions) { - super(options); - - this.params = options.params; - } - - /** - * Sets base aria attributes for current role - */ + /* @inheritDoc */ init(): void { const {orientation, isRoot} = this.params; diff --git a/src/core/component/directives/aria/roles-engines/treeitem/index.ts b/src/core/component/directives/aria/roles-engines/treeitem/index.ts index cad2c6ce5a..4b533cc2fb 100644 --- a/src/core/component/directives/aria/roles-engines/treeitem/index.ts +++ b/src/core/component/directives/aria/roles-engines/treeitem/index.ts @@ -13,36 +13,24 @@ import iAccess from 'traits/i-access/i-access'; import type iBlock from 'super/i-block/i-block'; import type { TreeitemParams } from 'core/component/directives/aria/roles-engines/treeitem/interface'; -import { AriaRoleEngine, KeyCodes, EngineOptions } from 'core/component/directives/aria/roles-engines/interface'; +import { AriaRoleEngine, KeyCodes } from 'core/component/directives/aria/roles-engines/interface'; export class TreeitemEngine extends AriaRoleEngine { - /** - * Engine params - */ - override params: TreeitemParams; + /** @see [[AriaRoleEngine.Params]] */ + override Params!: TreeitemParams; /** @see [[AriaRoleEngine.ctx]] */ override ctx?: iBlock & iAccess; - /** - * Engine params list - */ + /** @see [[AriaRoleEngine.params]] */ static override params: string[] = ['isFirstRootItem', 'isExpandable', 'isExpanded', 'orientation', 'rootElement', 'toggleFold']; - constructor(options: EngineOptions) { - super(options); - + /* @inheritDoc */ + init(): void { if (!iAccess.is(this.ctx)) { Object.throw('Treeitem aria directive expects the component to realize iAccess interface'); } - this.params = options.params; - } - - /** - * Sets base aria attributes for current role - */ - init(): void { this.async?.on(this.el, 'keydown', this.onKeyDown.bind(this)); const @@ -137,7 +125,7 @@ export class TreeitemEngine extends AriaRoleEngine { protected setFocusToFirstItem(): void { const firstItem = this.ctx?.findFocusableElement(this.params.rootElement); -debugger; + debugger; if (firstItem != null) { this.focusNext(firstItem); } diff --git a/src/core/component/directives/index.ts b/src/core/component/directives/index.ts index e90a8b1f94..f788c4e911 100644 --- a/src/core/component/directives/index.ts +++ b/src/core/component/directives/index.ts @@ -22,8 +22,6 @@ import 'core/component/directives/image'; import 'core/component/directives/update-on'; //#endif -import 'core/component/directives/aria'; - import 'core/component/directives/hook'; import 'core/component/directives/id'; diff --git a/src/form/b-checkbox/b-checkbox.ts b/src/form/b-checkbox/b-checkbox.ts index 8e712e8ee4..bc4b9420d2 100644 --- a/src/form/b-checkbox/b-checkbox.ts +++ b/src/form/b-checkbox/b-checkbox.ts @@ -15,6 +15,8 @@ import 'models/demo/checkbox'; //#endif +import 'core/component/directives/aria'; + import symbolGenerator from 'core/symbol'; import SyncPromise from 'core/promise/sync'; diff --git a/src/form/b-select/b-select.ts b/src/form/b-select/b-select.ts index 0c4424a2b4..821a7d76a2 100644 --- a/src/form/b-select/b-select.ts +++ b/src/form/b-select/b-select.ts @@ -15,6 +15,8 @@ import 'models/demo/select'; //#endif +import 'core/component/directives/aria'; + import SyncPromise from 'core/promise/sync'; import { derive } from 'core/functools/trait'; @@ -887,8 +889,9 @@ class bSelect extends iInputText implements iOpenToggle, iItems { protected getAriaConfig(role: 'option', item: this['Item']): Dictionary; protected getAriaConfig(role: 'combobox' | 'option', item?: this['Item']): Dictionary { - const - isSelected = this.isSelected.bind(this, item?.value); + const isSelected = item?.value != null ? + this.isSelected.bind(this, item.value) : + () => undefined; const comboboxConfig = { isMultiple: this.multiple, From 689f5dc721fc7d1a5ee3424ad2bf69c3e61821ce Mon Sep 17 00:00:00 2001 From: Aleksandr Bunin Date: Thu, 18 Aug 2022 18:21:11 +0300 Subject: [PATCH 064/185] refactoring --- .../component/directives/aria/roles-engines/combobox/index.ts | 2 +- .../component/directives/aria/roles-engines/controls/index.ts | 2 +- .../component/directives/aria/roles-engines/dialog/index.ts | 2 +- .../component/directives/aria/roles-engines/listbox/index.ts | 2 +- .../component/directives/aria/roles-engines/option/index.ts | 2 +- src/core/component/directives/aria/roles-engines/tab/index.ts | 2 +- .../component/directives/aria/roles-engines/tablist/index.ts | 2 +- .../component/directives/aria/roles-engines/tabpanel/index.ts | 2 +- .../component/directives/aria/roles-engines/tree/index.ts | 2 +- .../component/directives/aria/roles-engines/treeitem/index.ts | 4 ++-- 10 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/core/component/directives/aria/roles-engines/combobox/index.ts b/src/core/component/directives/aria/roles-engines/combobox/index.ts index 4f9b89a44b..09e94b92b6 100644 --- a/src/core/component/directives/aria/roles-engines/combobox/index.ts +++ b/src/core/component/directives/aria/roles-engines/combobox/index.ts @@ -34,7 +34,7 @@ export class ComboboxEngine extends AriaRoleEngine { this.el = this.ctx?.findFocusableElement() ?? el; } - /* @inheritDoc */ + /** @inheritDoc */ init(): void { this.el.setAttribute('role', 'combobox'); this.el.setAttribute('aria-expanded', 'false'); diff --git a/src/core/component/directives/aria/roles-engines/controls/index.ts b/src/core/component/directives/aria/roles-engines/controls/index.ts index f0bb7da1f6..6bd4133542 100644 --- a/src/core/component/directives/aria/roles-engines/controls/index.ts +++ b/src/core/component/directives/aria/roles-engines/controls/index.ts @@ -16,7 +16,7 @@ export class ControlsEngine extends AriaRoleEngine { /** @see [[AriaRoleEngine.params]] */ static override params: string[]; - /* @inheritDoc */ + /** @inheritDoc */ init(): void { const {ctx, modifiers, el} = this, diff --git a/src/core/component/directives/aria/roles-engines/dialog/index.ts b/src/core/component/directives/aria/roles-engines/dialog/index.ts index c3f99369de..7dd58ab641 100644 --- a/src/core/component/directives/aria/roles-engines/dialog/index.ts +++ b/src/core/component/directives/aria/roles-engines/dialog/index.ts @@ -10,7 +10,7 @@ import { AriaRoleEngine } from 'core/component/directives/aria/roles-engines/int import iOpen from 'traits/i-open/i-open'; export class DialogEngine extends AriaRoleEngine { - /* @inheritDoc */ + /** @inheritDoc */ init(): void { this.el.setAttribute('role', 'dialog'); this.el.setAttribute('aria-modal', 'true'); diff --git a/src/core/component/directives/aria/roles-engines/listbox/index.ts b/src/core/component/directives/aria/roles-engines/listbox/index.ts index 42bc7dbc89..5ae60ad6b7 100644 --- a/src/core/component/directives/aria/roles-engines/listbox/index.ts +++ b/src/core/component/directives/aria/roles-engines/listbox/index.ts @@ -9,7 +9,7 @@ import { AriaRoleEngine } from 'core/component/directives/aria/roles-engines/interface'; export class ListboxEngine extends AriaRoleEngine { - /* @inheritDoc */ + /** @inheritDoc */ init(): void { this.el.setAttribute('role', 'listbox'); this.el.setAttribute('tabindex', '-1'); diff --git a/src/core/component/directives/aria/roles-engines/option/index.ts b/src/core/component/directives/aria/roles-engines/option/index.ts index 3a31e1802f..ddd256c0ba 100644 --- a/src/core/component/directives/aria/roles-engines/option/index.ts +++ b/src/core/component/directives/aria/roles-engines/option/index.ts @@ -16,7 +16,7 @@ export class OptionEngine extends AriaRoleEngine { /** @see [[AriaRoleEngine.params]] */ static override params: string[] = ['isSelected', '@change']; - /* @inheritDoc */ + /** @inheritDoc */ init(): void { this.el.setAttribute('role', 'option'); this.el.setAttribute('aria-selected', String(this.params.isSelected)); diff --git a/src/core/component/directives/aria/roles-engines/tab/index.ts b/src/core/component/directives/aria/roles-engines/tab/index.ts index 6516157e40..9e7e9e9e40 100644 --- a/src/core/component/directives/aria/roles-engines/tab/index.ts +++ b/src/core/component/directives/aria/roles-engines/tab/index.ts @@ -25,7 +25,7 @@ export class TabEngine extends AriaRoleEngine { /** @see [[AriaRoleEngine.params]] */ static override params: string[] = ['isFirst', 'isSelected', 'hasDefaultSelectedTabs', 'orientation', '@change']; - /* @inheritDoc */ + /** @inheritDoc */ init(): void { const {el} = this, diff --git a/src/core/component/directives/aria/roles-engines/tablist/index.ts b/src/core/component/directives/aria/roles-engines/tablist/index.ts index 8f43ad2125..c04775c5c4 100644 --- a/src/core/component/directives/aria/roles-engines/tablist/index.ts +++ b/src/core/component/directives/aria/roles-engines/tablist/index.ts @@ -16,7 +16,7 @@ export class TablistEngine extends AriaRoleEngine { /** @see [[AriaRoleEngine.params]] */ static override params: string[] = ['isMultiple', 'orientation']; - /* @inheritDoc */ + /** @inheritDoc */ init(): void { const {el, params} = this; diff --git a/src/core/component/directives/aria/roles-engines/tabpanel/index.ts b/src/core/component/directives/aria/roles-engines/tabpanel/index.ts index 0eea14ef23..d9ccd4921c 100644 --- a/src/core/component/directives/aria/roles-engines/tabpanel/index.ts +++ b/src/core/component/directives/aria/roles-engines/tabpanel/index.ts @@ -9,7 +9,7 @@ import { AriaRoleEngine } from 'core/component/directives/aria/roles-engines/interface'; export class TabpanelEngine extends AriaRoleEngine { - /* @inheritDoc */ + /** @inheritDoc */ init(): void { const {el} = this; diff --git a/src/core/component/directives/aria/roles-engines/tree/index.ts b/src/core/component/directives/aria/roles-engines/tree/index.ts index 1418d1ecdc..33c450429d 100644 --- a/src/core/component/directives/aria/roles-engines/tree/index.ts +++ b/src/core/component/directives/aria/roles-engines/tree/index.ts @@ -16,7 +16,7 @@ export class TreeEngine extends AriaRoleEngine { /** @see [[AriaRoleEngine.params]] */ static override params: string[] = ['isRoot', 'orientation', '@change']; - /* @inheritDoc */ + /** @inheritDoc */ init(): void { const {orientation, isRoot} = this.params; diff --git a/src/core/component/directives/aria/roles-engines/treeitem/index.ts b/src/core/component/directives/aria/roles-engines/treeitem/index.ts index 4b533cc2fb..8b10244a51 100644 --- a/src/core/component/directives/aria/roles-engines/treeitem/index.ts +++ b/src/core/component/directives/aria/roles-engines/treeitem/index.ts @@ -25,7 +25,7 @@ export class TreeitemEngine extends AriaRoleEngine { /** @see [[AriaRoleEngine.params]] */ static override params: string[] = ['isFirstRootItem', 'isExpandable', 'isExpanded', 'orientation', 'rootElement', 'toggleFold']; - /* @inheritDoc */ + /** @inheritDoc */ init(): void { if (!iAccess.is(this.ctx)) { Object.throw('Treeitem aria directive expects the component to realize iAccess interface'); @@ -41,7 +41,7 @@ export class TreeitemEngine extends AriaRoleEngine { this.ctx?.restoreAllToTabSequence(this.el); } else { - this.el.tabIndex = 0; + this.el.setAttribute('tabindex', '0'); } } From ef9e00cf22ded030b35d4fe779331e301e38a288 Mon Sep 17 00:00:00 2001 From: Aleksandr Bunin Date: Fri, 19 Aug 2022 16:24:32 +0300 Subject: [PATCH 065/185] refactoring --- src/core/component/directives/aria/adapter.ts | 45 +++++++++++-------- .../aria/roles-engines/combobox/index.ts | 28 +++++------- .../aria/roles-engines/combobox/interface.ts | 14 +++--- .../aria/roles-engines/controls/index.ts | 16 +++---- .../aria/roles-engines/controls/interface.ts | 4 +- .../aria/roles-engines/dialog/index.ts | 4 +- .../aria/roles-engines/interface.ts | 21 ++++----- .../aria/roles-engines/listbox/index.ts | 4 +- .../aria/roles-engines/option/index.ts | 12 ++--- .../aria/roles-engines/option/interface.ts | 6 +-- .../aria/roles-engines/tab/index.ts | 41 +++++++---------- .../aria/roles-engines/tab/interface.ts | 12 ++--- .../aria/roles-engines/tablist/index.ts | 16 +++---- .../aria/roles-engines/tablist/interface.ts | 6 +-- .../aria/roles-engines/tabpanel/index.ts | 2 +- .../aria/roles-engines/tree/index.ts | 14 +++--- .../aria/roles-engines/tree/interface.ts | 8 ++-- .../aria/roles-engines/treeitem/index.ts | 22 ++++----- .../aria/roles-engines/treeitem/interface.ts | 16 ++++--- 19 files changed, 133 insertions(+), 158 deletions(-) diff --git a/src/core/component/directives/aria/adapter.ts b/src/core/component/directives/aria/adapter.ts index 8439be4ed9..6249b2e139 100644 --- a/src/core/component/directives/aria/adapter.ts +++ b/src/core/component/directives/aria/adapter.ts @@ -30,11 +30,6 @@ export default class AriaAdapter { */ protected role: CanUndef; - /** - * Role engine params list - */ - protected roleParams: CanUndef; - constructor(options: DirectiveOptions) { this.options = options; this.setAriaRole(); @@ -87,7 +82,6 @@ export default class AriaAdapter { }; this.role = new roles[engine](options); - this.roleParams = roles[engine].params; } /** @@ -97,7 +91,11 @@ export default class AriaAdapter { const {binding, el} = this.options, {dom} = this.ctx, - params = Object.isCustomObject(binding.value) ? binding.value : {}; + {labelledby} = binding.value ?? {}, + attr = 'aria-labelledby'; + + let + isAttrSet = false; for (const mod in binding.modifiers) { if (!mod.startsWith('#')) { @@ -108,18 +106,17 @@ export default class AriaAdapter { title = mod.slice(1), id = dom.getId(title); - el.setAttribute('aria-labelledby', id); + el.setAttribute(attr, id); + isAttrSet = true; } - if (params.labelledby == null) { - return; + if (labelledby != null) { + el.setAttribute(attr, Object.isArray(labelledby) ? labelledby.join(' ') : labelledby); + isAttrSet = true; } - if (Object.isArray(params.labelledby)) { - el.setAttribute('aria-labelledby', params.labelledby.join(' ')); - - } else { - el.setAttribute('aria-labelledby', params.labelledby); + if (isAttrSet) { + this.async.worker(() => el.removeAttribute(attr)); } } @@ -129,11 +126,23 @@ export default class AriaAdapter { protected setAriaAttributes(): void { const {el, binding} = this.options, - params = binding.value; + params: Dictionary = binding.value; for (const key in params) { - if (!this.roleParams?.includes(key) && key !== 'labelledby') { - el.setAttribute(`aria-${key}`, params[key]); + if (!params.hasOwnProperty(key)) { + continue; + } + + const + roleParams = this.role?.Params, + param = params[key]; + + if (!roleParams?.hasOwnProperty(key) && key !== 'labelledby' && param != null) { + const + attr = `aria-${key}`; + + el.setAttribute(attr, param); + this.async.worker(() => el.removeAttribute(attr)); } } } diff --git a/src/core/component/directives/aria/roles-engines/combobox/index.ts b/src/core/component/directives/aria/roles-engines/combobox/index.ts index 09e94b92b6..7af277460e 100644 --- a/src/core/component/directives/aria/roles-engines/combobox/index.ts +++ b/src/core/component/directives/aria/roles-engines/combobox/index.ts @@ -9,21 +9,13 @@ import type iAccess from 'traits/i-access/i-access'; import type { ComponentInterface } from 'super/i-block/i-block'; -import type { ComboboxParams } from 'core/component/directives/aria/roles-engines/combobox/interface'; +import { ComboboxParams } from 'core/component/directives/aria/roles-engines/combobox/interface'; import { AriaRoleEngine, EngineOptions } from 'core/component/directives/aria/roles-engines/interface'; export class ComboboxEngine extends AriaRoleEngine { - /** @see [[AriaRoleEngine.Prams]] */ - override Params!: ComboboxParams; - - /** @see [[AriaRoleEngine.el]] */ - override el: HTMLElement; - - /** @see [[AriaRoleEngine.Ctx]] */ + override Params: ComboboxParams = new ComboboxParams(); override Ctx!: ComponentInterface & iAccess; - - /** @see [[AriaRoleEngine.params]] */ - static override params: string[] = ['isMultiple', '@change', '@open', '@close']; + override el: HTMLElement; constructor(options: EngineOptions) { super(options); @@ -36,15 +28,15 @@ export class ComboboxEngine extends AriaRoleEngine { /** @inheritDoc */ init(): void { - this.el.setAttribute('role', 'combobox'); - this.el.setAttribute('aria-expanded', 'false'); + this.setAttribute('role', 'combobox'); + this.setAttribute('aria-expanded', 'false'); if (this.params.isMultiple) { - this.el.setAttribute('aria-multiselectable', 'true'); + this.setAttribute('aria-multiselectable', 'true'); } if (this.el.tabIndex < 0) { - this.el.setAttribute('tabindex', '0'); + this.setAttribute('tabindex', '0'); } } @@ -52,7 +44,7 @@ export class ComboboxEngine extends AriaRoleEngine { * Sets or deletes the id of active descendant element */ protected setAriaActive(el?: HTMLElement): void { - this.el.setAttribute('aria-activedescendant', el?.id ?? ''); + this.setAttribute('aria-activedescendant', el?.id ?? ''); } /** @@ -60,7 +52,7 @@ export class ComboboxEngine extends AriaRoleEngine { * @param el */ protected onOpen(el: HTMLElement): void { - this.el.setAttribute('aria-expanded', 'true'); + this.setAttribute('aria-expanded', 'true'); this.setAriaActive(el); } @@ -68,7 +60,7 @@ export class ComboboxEngine extends AriaRoleEngine { * Handler: the option list is closed */ protected onClose(): void { - this.el.setAttribute('aria-expanded', 'false'); + this.setAttribute('aria-expanded', 'false'); this.setAriaActive(); } diff --git a/src/core/component/directives/aria/roles-engines/combobox/interface.ts b/src/core/component/directives/aria/roles-engines/combobox/interface.ts index a319311e58..03ee51ae17 100644 --- a/src/core/component/directives/aria/roles-engines/combobox/interface.ts +++ b/src/core/component/directives/aria/roles-engines/combobox/interface.ts @@ -6,11 +6,13 @@ * https://github.com/V4Fire/Client/blob/master/LICENSE */ -import type { AbstractParams, HandlerAttachment } from 'core/component/directives/aria/roles-engines/interface'; +import type { HandlerAttachment } from 'core/component/directives/aria/roles-engines/interface'; -export interface ComboboxParams extends AbstractParams { - isMultiple: boolean; - '@change': HandlerAttachment; - '@open': HandlerAttachment; - '@close': HandlerAttachment; +const defaultFn = (): void => undefined; + +export class ComboboxParams { + isMultiple: boolean = false; + '@change': HandlerAttachment = defaultFn; + '@open': HandlerAttachment = defaultFn; + '@close': HandlerAttachment = defaultFn; } diff --git a/src/core/component/directives/aria/roles-engines/controls/index.ts b/src/core/component/directives/aria/roles-engines/controls/index.ts index 6bd4133542..adb57ea056 100644 --- a/src/core/component/directives/aria/roles-engines/controls/index.ts +++ b/src/core/component/directives/aria/roles-engines/controls/index.ts @@ -6,15 +6,11 @@ * https://github.com/V4Fire/Client/blob/master/LICENSE */ -import type { ControlsParams } from 'core/component/directives/aria/roles-engines/controls/interface'; +import { ControlsParams } from 'core/component/directives/aria/roles-engines/controls/interface'; import { AriaRoleEngine } from 'core/component/directives/aria/roles-engines/interface'; export class ControlsEngine extends AriaRoleEngine { - /** @see [[AriaRoleEngine.Params]] */ - override Params!: ControlsParams; - - /** @see [[AriaRoleEngine.params]] */ - static override params: string[]; + override Params: ControlsParams = new ControlsParams(); /** @inheritDoc */ init(): void { @@ -45,7 +41,7 @@ export class ControlsEngine extends AriaRoleEngine { elems.forEach((el, i) => { if (Object.isString(forId)) { - el.setAttribute('aria-controls', forId); + this.setAttribute('aria-controls', forId, el); return; } @@ -53,7 +49,7 @@ export class ControlsEngine extends AriaRoleEngine { id = forId[i]; if (Object.isString(id)) { - el.setAttribute('aria-controls', id); + this.setAttribute('aria-controls', id, el); } }); }); @@ -64,7 +60,9 @@ export class ControlsEngine extends AriaRoleEngine { [elId, controlsId] = param, element = el.querySelector(`#${elId}`); - element?.setAttribute('aria-controls', controlsId); + if (element != null) { + this.setAttribute('aria-controls', controlsId, element); + } }); } } diff --git a/src/core/component/directives/aria/roles-engines/controls/interface.ts b/src/core/component/directives/aria/roles-engines/controls/interface.ts index 608bf5c139..c73270ecbb 100644 --- a/src/core/component/directives/aria/roles-engines/controls/interface.ts +++ b/src/core/component/directives/aria/roles-engines/controls/interface.ts @@ -6,6 +6,6 @@ * https://github.com/V4Fire/Client/blob/master/LICENSE */ -export interface ControlsParams { - for: CanArray | Array<[string, string]>; +export class ControlsParams { + for: CanArray | Array<[string, string]> = 'for'; } diff --git a/src/core/component/directives/aria/roles-engines/dialog/index.ts b/src/core/component/directives/aria/roles-engines/dialog/index.ts index 7dd58ab641..d004f5dc4b 100644 --- a/src/core/component/directives/aria/roles-engines/dialog/index.ts +++ b/src/core/component/directives/aria/roles-engines/dialog/index.ts @@ -12,8 +12,8 @@ import iOpen from 'traits/i-open/i-open'; export class DialogEngine extends AriaRoleEngine { /** @inheritDoc */ init(): void { - this.el.setAttribute('role', 'dialog'); - this.el.setAttribute('aria-modal', 'true'); + this.setAttribute('role', 'dialog'); + this.setAttribute('aria-modal', 'true'); if (!iOpen.is(this.ctx)) { Object.throw('Dialog aria directive expects the component to realize iOpen interface'); diff --git a/src/core/component/directives/aria/roles-engines/interface.ts b/src/core/component/directives/aria/roles-engines/interface.ts index ad7a391346..e6e79394b8 100644 --- a/src/core/component/directives/aria/roles-engines/interface.ts +++ b/src/core/component/directives/aria/roles-engines/interface.ts @@ -40,15 +40,8 @@ export abstract class AriaRoleEngine { */ readonly params: this['Params']; - /** - * Async instance - */ - async: CanUndef; - - /** - * Directive expected params list - */ - static params: string[]; + /** @see [[Async]] */ + async: Async; constructor({el, ctx, modifiers, params, async}: EngineOptions) { this.el = el; @@ -62,9 +55,17 @@ export abstract class AriaRoleEngine { * Sets base aria attributes for current role */ abstract init(): void; + + /** + * Sets aria attributes and the `Async` destructor + */ + setAttribute(attr: string, value: string, el: Element = this.el): void { + el.setAttribute(attr, value); + this.async.worker(() => el.removeAttribute(attr)); + } } -export interface AbstractParams {} +interface AbstractParams {} export interface EngineOptions

    { el: HTMLElement; diff --git a/src/core/component/directives/aria/roles-engines/listbox/index.ts b/src/core/component/directives/aria/roles-engines/listbox/index.ts index 5ae60ad6b7..4e28b0c596 100644 --- a/src/core/component/directives/aria/roles-engines/listbox/index.ts +++ b/src/core/component/directives/aria/roles-engines/listbox/index.ts @@ -11,7 +11,7 @@ import { AriaRoleEngine } from 'core/component/directives/aria/roles-engines/int export class ListboxEngine extends AriaRoleEngine { /** @inheritDoc */ init(): void { - this.el.setAttribute('role', 'listbox'); - this.el.setAttribute('tabindex', '-1'); + this.setAttribute('role', 'listbox'); + this.setAttribute('tabindex', '-1'); } } diff --git a/src/core/component/directives/aria/roles-engines/option/index.ts b/src/core/component/directives/aria/roles-engines/option/index.ts index ddd256c0ba..1791c48b4b 100644 --- a/src/core/component/directives/aria/roles-engines/option/index.ts +++ b/src/core/component/directives/aria/roles-engines/option/index.ts @@ -6,20 +6,16 @@ * https://github.com/V4Fire/Client/blob/master/LICENSE */ -import type { OptionParams } from 'core/component/directives/aria/roles-engines/option/interface'; +import { OptionParams } from 'core/component/directives/aria/roles-engines/option/interface'; import { AriaRoleEngine } from 'core/component/directives/aria/roles-engines/interface'; export class OptionEngine extends AriaRoleEngine { - /** @see [[AriaRoleEngine.Params]] */ - override Params!: OptionParams; - - /** @see [[AriaRoleEngine.params]] */ - static override params: string[] = ['isSelected', '@change']; + override Params: OptionParams = new OptionParams(); /** @inheritDoc */ init(): void { - this.el.setAttribute('role', 'option'); - this.el.setAttribute('aria-selected', String(this.params.isSelected)); + this.setAttribute('role', 'option'); + this.setAttribute('aria-selected', String(this.params.isSelected)); } /** diff --git a/src/core/component/directives/aria/roles-engines/option/interface.ts b/src/core/component/directives/aria/roles-engines/option/interface.ts index 4e90c740ae..58d4bffe1f 100644 --- a/src/core/component/directives/aria/roles-engines/option/interface.ts +++ b/src/core/component/directives/aria/roles-engines/option/interface.ts @@ -8,7 +8,7 @@ import type { HandlerAttachment } from 'core/component/directives/aria/roles-engines/interface'; -export interface OptionParams { - isSelected: boolean; - '@change': HandlerAttachment; +export class OptionParams { + isSelected: boolean = false; + '@change': HandlerAttachment = () => undefined; } diff --git a/src/core/component/directives/aria/roles-engines/tab/index.ts b/src/core/component/directives/aria/roles-engines/tab/index.ts index 9e7e9e9e40..ddf9fb43d9 100644 --- a/src/core/component/directives/aria/roles-engines/tab/index.ts +++ b/src/core/component/directives/aria/roles-engines/tab/index.ts @@ -12,18 +12,12 @@ import type iBlock from 'super/i-block/i-block'; import type iAccess from 'traits/i-access/i-access'; -import type { TabParams } from 'core/component/directives/aria/roles-engines/tab/interface'; +import { TabParams } from 'core/component/directives/aria/roles-engines/tab/interface'; import { AriaRoleEngine, KeyCodes } from 'core/component/directives/aria/roles-engines/interface'; export class TabEngine extends AriaRoleEngine { - /** @see [[AriaRoleEngine.Params]] */ - override Params!: TabParams; - - /** @see [[AriaRoleEngine.ctx]] */ - override ctx?: iBlock & iAccess; - - /** @see [[AriaRoleEngine.params]] */ - static override params: string[] = ['isFirst', 'isSelected', 'hasDefaultSelectedTabs', 'orientation', '@change']; + override Params: TabParams = new TabParams(); + override Ctx!: iBlock & iAccess; /** @inheritDoc */ init(): void { @@ -31,26 +25,24 @@ export class TabEngine extends AriaRoleEngine { {el} = this, {isFirst, isSelected, hasDefaultSelectedTabs} = this.params; - el.setAttribute('role', 'tab'); - el.setAttribute('aria-selected', String(isSelected)); + this.setAttribute('role', 'tab'); + this.setAttribute('aria-selected', String(isSelected)); if (isFirst && !hasDefaultSelectedTabs) { if (el.tabIndex < 0) { - el.setAttribute('tabindex', '0'); + this.setAttribute('tabindex', '0'); } } else if (hasDefaultSelectedTabs && isSelected) { if (el.tabIndex < 0) { - el.setAttribute('tabindex', '0'); + this.setAttribute('tabindex', '0'); } } else { - el.setAttribute('tabindex', '-1'); + this.setAttribute('tabindex', '-1'); } - if (this.async != null) { - this.async.on(el, 'keydown', this.onKeydown.bind(this)); - } + this.async.on(el, 'keydown', this.onKeydown.bind(this)); } /** @@ -100,23 +92,20 @@ export class TabEngine extends AriaRoleEngine { * @param active */ protected onChange(active: Element | NodeListOf): void { - const - {el} = this; - - function setAttributes(isSelected: boolean) { - el.setAttribute('aria-selected', String(isSelected)); - el.setAttribute('tabindex', isSelected ? '0' : '-1'); - } + const setAttributes = (isSelected: boolean) => { + this.setAttribute('aria-selected', String(isSelected)); + this.setAttribute('tabindex', isSelected ? '0' : '-1'); + }; if (Object.isArrayLike(active)) { for (let i = 0; i < active.length; i++) { - setAttributes(el === active[i]); + setAttributes(this.el === active[i]); } return; } - setAttributes(el === active); + setAttributes(this.el === active); } /** diff --git a/src/core/component/directives/aria/roles-engines/tab/interface.ts b/src/core/component/directives/aria/roles-engines/tab/interface.ts index e42c5bdc11..190e244b0a 100644 --- a/src/core/component/directives/aria/roles-engines/tab/interface.ts +++ b/src/core/component/directives/aria/roles-engines/tab/interface.ts @@ -8,10 +8,10 @@ import type { HandlerAttachment } from 'core/component/directives/aria/roles-engines/interface'; -export interface TabParams { - isFirst: boolean; - isSelected: boolean; - hasDefaultSelectedTabs: boolean; - orientation: string; - '@change': HandlerAttachment; +export class TabParams { + isFirst: boolean = false; + isSelected: boolean = false; + hasDefaultSelectedTabs: boolean = false; + orientation: string = 'false'; + '@change': HandlerAttachment = () => undefined; } diff --git a/src/core/component/directives/aria/roles-engines/tablist/index.ts b/src/core/component/directives/aria/roles-engines/tablist/index.ts index c04775c5c4..c464b54c37 100644 --- a/src/core/component/directives/aria/roles-engines/tablist/index.ts +++ b/src/core/component/directives/aria/roles-engines/tablist/index.ts @@ -6,29 +6,25 @@ * https://github.com/V4Fire/Client/blob/master/LICENSE */ -import type { TablistParams } from 'core/component/directives/aria/roles-engines/tablist/interface'; +import { TablistParams } from 'core/component/directives/aria/roles-engines/tablist/interface'; import { AriaRoleEngine } from 'core/component/directives/aria/roles-engines/interface'; export class TablistEngine extends AriaRoleEngine { - /** @see [[AriaRoleEngine.Params]] */ - override Params!: TablistParams; - - /** @see [[AriaRoleEngine.params]] */ - static override params: string[] = ['isMultiple', 'orientation']; + override Params: TablistParams = new TablistParams(); /** @inheritDoc */ init(): void { const - {el, params} = this; + {params} = this; - el.setAttribute('role', 'tablist'); + this.setAttribute('role', 'tablist'); if (params.isMultiple) { - el.setAttribute('aria-multiselectable', 'true'); + this.setAttribute('aria-multiselectable', 'true'); } if (params.orientation === 'vertical') { - el.setAttribute('aria-orientation', params.orientation); + this.setAttribute('aria-orientation', params.orientation); } } } diff --git a/src/core/component/directives/aria/roles-engines/tablist/interface.ts b/src/core/component/directives/aria/roles-engines/tablist/interface.ts index ec88e5a93a..6144a59fba 100644 --- a/src/core/component/directives/aria/roles-engines/tablist/interface.ts +++ b/src/core/component/directives/aria/roles-engines/tablist/interface.ts @@ -6,7 +6,7 @@ * https://github.com/V4Fire/Client/blob/master/LICENSE */ -export interface TablistParams { - isMultiple: boolean; - orientation: string; +export class TablistParams { + isMultiple: boolean = false; + orientation: string = 'false'; } diff --git a/src/core/component/directives/aria/roles-engines/tabpanel/index.ts b/src/core/component/directives/aria/roles-engines/tabpanel/index.ts index d9ccd4921c..cd76548539 100644 --- a/src/core/component/directives/aria/roles-engines/tabpanel/index.ts +++ b/src/core/component/directives/aria/roles-engines/tabpanel/index.ts @@ -18,6 +18,6 @@ export class TabpanelEngine extends AriaRoleEngine { Object.throw('Tabpanel aria directive expects "label" or "labelledby" value to be passed'); } - el.setAttribute('role', 'tabpanel'); + this.setAttribute('role', 'tabpanel'); } } diff --git a/src/core/component/directives/aria/roles-engines/tree/index.ts b/src/core/component/directives/aria/roles-engines/tree/index.ts index 33c450429d..92665ae3fa 100644 --- a/src/core/component/directives/aria/roles-engines/tree/index.ts +++ b/src/core/component/directives/aria/roles-engines/tree/index.ts @@ -6,15 +6,11 @@ * https://github.com/V4Fire/Client/blob/master/LICENSE */ -import type { TreeParams } from 'core/component/directives/aria/roles-engines/tree/interface'; +import { TreeParams } from 'core/component/directives/aria/roles-engines/tree/interface'; import { AriaRoleEngine } from 'core/component/directives/aria/roles-engines/interface'; export class TreeEngine extends AriaRoleEngine { - /** @see [[AriaRoleEngine.Params]] */ - override Params!: TreeParams; - - /** @see [[AriaRoleEngine.params]] */ - static override params: string[] = ['isRoot', 'orientation', '@change']; + override Params: TreeParams = new TreeParams(); /** @inheritDoc */ init(): void { @@ -24,7 +20,7 @@ export class TreeEngine extends AriaRoleEngine { this.setRootRole(); if (orientation === 'horizontal' && isRoot) { - this.el.setAttribute('aria-orientation', orientation); + this.setAttribute('aria-orientation', orientation); } } @@ -32,7 +28,7 @@ export class TreeEngine extends AriaRoleEngine { * Sets the role to the element depending on whether the tree is root or nested */ protected setRootRole(): void { - this.el.setAttribute('role', this.params.isRoot ? 'tree' : 'group'); + this.setAttribute('role', this.params.isRoot ? 'tree' : 'group'); } /** @@ -41,6 +37,6 @@ export class TreeEngine extends AriaRoleEngine { * @param isFolded */ protected onChange(el: Element, isFolded: boolean): void { - el.setAttribute('aria-expanded', String(!isFolded)); + this.setAttribute('aria-expanded', String(!isFolded), el); } } diff --git a/src/core/component/directives/aria/roles-engines/tree/interface.ts b/src/core/component/directives/aria/roles-engines/tree/interface.ts index 44b0e9cf3f..bad9ae817c 100644 --- a/src/core/component/directives/aria/roles-engines/tree/interface.ts +++ b/src/core/component/directives/aria/roles-engines/tree/interface.ts @@ -8,8 +8,8 @@ import type { HandlerAttachment } from 'core/component/directives/aria/roles-engines/interface'; -export interface TreeParams { - isRoot: boolean; - orientation: string; - '@change': HandlerAttachment; +export class TreeParams { + isRoot: boolean = false; + orientation: string = 'false'; + '@change': HandlerAttachment = () => undefined; } diff --git a/src/core/component/directives/aria/roles-engines/treeitem/index.ts b/src/core/component/directives/aria/roles-engines/treeitem/index.ts index 8b10244a51..a65884bc12 100644 --- a/src/core/component/directives/aria/roles-engines/treeitem/index.ts +++ b/src/core/component/directives/aria/roles-engines/treeitem/index.ts @@ -12,18 +12,12 @@ import iAccess from 'traits/i-access/i-access'; import type iBlock from 'super/i-block/i-block'; -import type { TreeitemParams } from 'core/component/directives/aria/roles-engines/treeitem/interface'; +import { TreeitemParams } from 'core/component/directives/aria/roles-engines/treeitem/interface'; import { AriaRoleEngine, KeyCodes } from 'core/component/directives/aria/roles-engines/interface'; export class TreeitemEngine extends AriaRoleEngine { - /** @see [[AriaRoleEngine.Params]] */ - override Params!: TreeitemParams; - - /** @see [[AriaRoleEngine.ctx]] */ - override ctx?: iBlock & iAccess; - - /** @see [[AriaRoleEngine.params]] */ - static override params: string[] = ['isFirstRootItem', 'isExpandable', 'isExpanded', 'orientation', 'rootElement', 'toggleFold']; + override Params: TreeitemParams = new TreeitemParams(); + override Ctx!: iBlock & iAccess; /** @inheritDoc */ init(): void { @@ -31,7 +25,7 @@ export class TreeitemEngine extends AriaRoleEngine { Object.throw('Treeitem aria directive expects the component to realize iAccess interface'); } - this.async?.on(this.el, 'keydown', this.onKeyDown.bind(this)); + this.async.on(this.el, 'keydown', this.onKeyDown.bind(this)); const isMuted = this.ctx?.removeAllFromTabSequence(this.el); @@ -41,15 +35,15 @@ export class TreeitemEngine extends AriaRoleEngine { this.ctx?.restoreAllToTabSequence(this.el); } else { - this.el.setAttribute('tabindex', '0'); + this.setAttribute('tabindex', '0'); } } - this.el.setAttribute('role', 'treeitem'); + this.setAttribute('role', 'treeitem'); this.ctx?.$nextTick(() => { if (this.params.isExpandable) { - this.el.setAttribute('aria-expanded', String(this.params.isExpanded)); + this.setAttribute('aria-expanded', String(this.params.isExpanded)); } }); } @@ -125,7 +119,7 @@ export class TreeitemEngine extends AriaRoleEngine { protected setFocusToFirstItem(): void { const firstItem = this.ctx?.findFocusableElement(this.params.rootElement); - debugger; + if (firstItem != null) { this.focusNext(firstItem); } diff --git a/src/core/component/directives/aria/roles-engines/treeitem/interface.ts b/src/core/component/directives/aria/roles-engines/treeitem/interface.ts index 38bfb24418..bcd0834b2d 100644 --- a/src/core/component/directives/aria/roles-engines/treeitem/interface.ts +++ b/src/core/component/directives/aria/roles-engines/treeitem/interface.ts @@ -6,11 +6,13 @@ * https://github.com/V4Fire/Client/blob/master/LICENSE */ -export interface TreeitemParams { - isFirstRootItem: boolean; - isExpandable: boolean; - isExpanded: boolean; - orientation: string; - rootElement: CanUndef; - toggleFold(el: Element, value?: boolean): void; +type FoldToggle = (el: Element, value?: boolean) => void; + +export class TreeitemParams { + isFirstRootItem: boolean = false; + isExpandable: boolean = false; + isExpanded: boolean = false; + orientation: string = 'false'; + rootElement?: HTMLElement = undefined; + toggleFold: FoldToggle = () => undefined; } From fac1b99546d17244b1be8ae5f67f2289701f9f0c Mon Sep 17 00:00:00 2001 From: Aleksandr Bunin Date: Fri, 19 Aug 2022 17:47:12 +0300 Subject: [PATCH 066/185] refactoring --- src/form/b-checkbox/b-checkbox.ss | 10 ---------- src/form/b-checkbox/b-checkbox.styl | 4 +++- src/form/b-checkbox/b-checkbox.ts | 5 ++++- 3 files changed, 7 insertions(+), 12 deletions(-) diff --git a/src/form/b-checkbox/b-checkbox.ss b/src/form/b-checkbox/b-checkbox.ss index 8a92080ac7..a46209e12b 100644 --- a/src/form/b-checkbox/b-checkbox.ss +++ b/src/form/b-checkbox/b-checkbox.ss @@ -14,16 +14,6 @@ - nativeInputType = "'checkbox'" - nativeInputModel = undefined - - block hiddenInput() - += self.nativeInput({ & - elName: 'hidden-input', - id: 'id || dom.getId("input")', - - attrs: { - autocomplete: 'off' - } - }) . - - block rootAttrs - super ? rootAttrs[':-parent-id'] = 'parentId' diff --git a/src/form/b-checkbox/b-checkbox.styl b/src/form/b-checkbox/b-checkbox.styl index 6d1605175d..320bbca008 100644 --- a/src/form/b-checkbox/b-checkbox.styl +++ b/src/form/b-checkbox/b-checkbox.styl @@ -17,7 +17,9 @@ b-checkbox extends i-input contain paint position relative - &__wrapper, &__checkbox, &__label + &__wrapper, + &__checkbox, + &__label cursor pointer &__wrapper diff --git a/src/form/b-checkbox/b-checkbox.ts b/src/form/b-checkbox/b-checkbox.ts index bc4b9420d2..ef9dd10945 100644 --- a/src/form/b-checkbox/b-checkbox.ts +++ b/src/form/b-checkbox/b-checkbox.ts @@ -158,6 +158,9 @@ export default class bCheckbox extends iInput implements iSize { return this.defaultProp; } + @system((ctx) => ctx.sync.link((v: Dictionary) => ({...v, id: ctx.id ?? 'hidden-input'}))) + override attrs?: Dictionary; + /** * True if the checkbox is checked */ @@ -198,7 +201,7 @@ export default class bCheckbox extends iInput implements iSize { @system() protected override valueStore!: this['Value']; - protected override readonly $refs!: {input: HTMLInputElement}; + protected override readonly $refs!: { input: HTMLInputElement }; /** * Checks the checkbox From cf37e1b611263f8ae5ab007b0fdb435d64adf9c9 Mon Sep 17 00:00:00 2001 From: Aleksandr Bunin Date: Sat, 20 Aug 2022 15:15:05 +0300 Subject: [PATCH 067/185] fixing tests & adding new tests --- src/form/b-checkbox/b-checkbox.ss | 2 +- src/form/b-checkbox/b-checkbox.ts | 2 +- src/form/b-checkbox/test/unit/simple.ts | 32 +++++++++++++++++++++++++ 3 files changed, 34 insertions(+), 2 deletions(-) diff --git a/src/form/b-checkbox/b-checkbox.ss b/src/form/b-checkbox/b-checkbox.ss index a46209e12b..2011b4fd2f 100644 --- a/src/form/b-checkbox/b-checkbox.ss +++ b/src/form/b-checkbox/b-checkbox.ss @@ -39,7 +39,7 @@ - block label < label.&__label & v-if = label || vdom.getSlot('label') | - :for = id || dom.getId('input') + :for = id || dom.getId('hidden-input') . += self.slot('label', {':label': 'label'}) {{ t(label) }} diff --git a/src/form/b-checkbox/b-checkbox.ts b/src/form/b-checkbox/b-checkbox.ts index ef9dd10945..e8f2109db0 100644 --- a/src/form/b-checkbox/b-checkbox.ts +++ b/src/form/b-checkbox/b-checkbox.ts @@ -158,7 +158,7 @@ export default class bCheckbox extends iInput implements iSize { return this.defaultProp; } - @system((ctx) => ctx.sync.link((v: Dictionary) => ({...v, id: ctx.id ?? 'hidden-input'}))) + @system((ctx) => ctx.sync.link((v: Dictionary) => ({...v, id: ctx.id ?? ctx.dom.getId('hidden-input')}))) override attrs?: Dictionary; /** diff --git a/src/form/b-checkbox/test/unit/simple.ts b/src/form/b-checkbox/test/unit/simple.ts index cd0feea7c0..f422aa1dda 100644 --- a/src/form/b-checkbox/test/unit/simple.ts +++ b/src/form/b-checkbox/test/unit/simple.ts @@ -179,6 +179,23 @@ test.describe('b-checkbox simple usage', () => { ).toBeUndefined(); }); + test('checking with id prop', async ({page}) => { + const target = await init(page, {value: 'bar', id: 'foo'}); + + test.expect( + await target.evaluate((ctx) => ctx.unsafe.block!.element('hidden-input')?.id) + ).toBe('foo'); + }); + + test('checking without id prop', async ({page}) => { + const target = await init(page, {value: 'bar'}); + const id = await target.evaluate((ctx) => ctx.unsafe.dom.getId('')); + + test.expect( + await target.evaluate((ctx) => ctx.unsafe.block!.element('hidden-input')?.id) + ).toBe(`${id}hidden-input`); + }); + test('checkbox with a `label` prop', async ({page}) => { const target = await init(page, { label: 'Foo' @@ -197,6 +214,21 @@ test.describe('b-checkbox simple usage', () => { test.expect( await target.evaluate((ctx) => ctx.value) ).toBe(true); + + const id = await target.evaluate((ctx) => ctx.unsafe.dom.getId('')); + + test.expect( + await target.evaluate((ctx) => ctx.unsafe.block!.element('label')?.getAttribute('for')) + ).toBe(`${id}hidden-input`); + + const target2 = await init(page, { + label: 'Foo', + id: 'bla' + }); + + test.expect( + await target2.evaluate((ctx) => ctx.unsafe.block!.element('label')?.getAttribute('for')) + ).toBe('bla'); }); /** From 4c14a60bcf7bca1e36e2891f65303ae19e9441a0 Mon Sep 17 00:00:00 2001 From: Aleksandr Bunin Date: Tue, 23 Aug 2022 11:43:03 +0300 Subject: [PATCH 068/185] =?UTF-8?q?=20=F0=9F=93=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/base/b-tree/b-tree.ts | 2 +- src/core/component/directives/aria/README.md | 25 +++++------ .../directives/aria/roles-engines/README.md | 33 ++++++++++++++ .../aria/roles-engines/combobox/README.md | 26 +++++++++-- .../aria/roles-engines/combobox/index.ts | 6 +-- .../aria/roles-engines/controls/README.md | 31 ++++++------- .../aria/roles-engines/dialog/README.md | 10 +++-- .../aria/roles-engines/interface.ts | 2 +- .../aria/roles-engines/listbox/README.md | 11 +++-- .../aria/roles-engines/option/README.md | 18 ++++++-- .../aria/roles-engines/tab/README.md | 40 +++++++++++++---- .../aria/roles-engines/tablist/README.md | 23 ++++++++-- .../aria/roles-engines/tabpanel/README.md | 21 ++++----- .../aria/roles-engines/tree/README.md | 25 +++++++++-- .../aria/roles-engines/tree/index.ts | 6 +-- .../aria/roles-engines/treeitem/README.md | 44 ++++++++++++------- .../aria/roles-engines/treeitem/interface.ts | 2 +- 17 files changed, 231 insertions(+), 94 deletions(-) create mode 100644 src/core/component/directives/aria/roles-engines/README.md diff --git a/src/base/b-tree/b-tree.ts b/src/base/b-tree/b-tree.ts index 8c5fac81a2..bec9ae954d 100644 --- a/src/base/b-tree/b-tree.ts +++ b/src/base/b-tree/b-tree.ts @@ -280,7 +280,7 @@ class bTree extends iData implements iItems, iAccess { isRoot: this.top == null, orientation: this.orientation, '@change': (cb: Function) => { - this.on('fold', (ctx, el, item, value) => cb(el, value)); + this.on('fold', (ctx, el: Element, item, value: boolean) => cb(el, !value)); } }; diff --git a/src/core/component/directives/aria/README.md b/src/core/component/directives/aria/README.md index 375596c8c0..e1a8b4ed43 100644 --- a/src/core/component/directives/aria/README.md +++ b/src/core/component/directives/aria/README.md @@ -47,6 +47,18 @@ Each role can accept its own set of options, which are described in its document ## Available options +All ARIA attributes could be added in options through short syntax. + +``` +< div v-aria = {label: 'foo', desribedby: 'id1', details: 'id2'} + +/// The same as + +< div :aria-label = 'foo' | :aria-desribedby = 'id1' | :aria-details = 'id2' +``` + +The most common are described below: + ### [label] Defines a string value that labels the current element. @@ -76,19 +88,6 @@ See [this](https://www.w3.org/TR/wai-aria/#aria-labelledby) for more information < input type = text | v-aria = {labelledby: 'billing address'} ``` -### [description] - -Defines a string value that describes or annotates the current element. -See [this](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes/aria-description) for more information. - -``` -< div role = application | v-aria = {label: 'calendar', decription: 'Game schedule for the Boston Red Sox 2021 Season'} - < h1 - Red Sox 2021 - - ... -``` - ### [describedby] Identifies the element (or elements) that describes the object. diff --git a/src/core/component/directives/aria/roles-engines/README.md b/src/core/component/directives/aria/roles-engines/README.md new file mode 100644 index 0000000000..d5e9d13fff --- /dev/null +++ b/src/core/component/directives/aria/roles-engines/README.md @@ -0,0 +1,33 @@ +# core/component/directives/aria/roles-engines/combobox + +This module provides engines for `v-aria` directive. + +## API + +Some roles need to handle components state changes or react to some events (add, delete or change certain attributes). +The fields in directive passed options which name starts with `@` respond for this (ex. `@change`, `@open`). +The certain contract should be followed: +the name of the callback, which should be 'connected' with such field should start with `on` and be named in camelCase style (ex. `onChange`, `onOpen`). + +Directive supports this field type to be function, promise or string (type [`HandlerAttachment`](`core/component/directives/aria/roles-engines/interface.ts`)). +- Function: +expects a callback to be passed. +In this function callback could be added as a listener to certain component events or provide to the callback some component's state. + +``` +< div v-aria:somerole = {'@change': (cb) => on('event', cb)} +``` + +- Promise: +If the field is a `Promise` or a `PromiseLike` object the callback would be passed to `then`. + +- String: +If the field is a `string`, the callback would be added as a listener to component's event similar to the string. + +``` +< div v-aria:somerole = {'@change': 'event'} + +// the same as + +< div v-aria:somerole = {'@change': (cb) => on('event', cb)} +``` diff --git a/src/core/component/directives/aria/roles-engines/combobox/README.md b/src/core/component/directives/aria/roles-engines/combobox/README.md index 57e6f9c96f..8a5792f9df 100644 --- a/src/core/component/directives/aria/roles-engines/combobox/README.md +++ b/src/core/component/directives/aria/roles-engines/combobox/README.md @@ -3,12 +3,30 @@ This module provides an engine for `v-aria` directive. The engine to set `combobox` role attribute. -For more information about attributes go to [`https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/combobox_role`]. -For recommendations how to make accessible widget go to [`https://www.w3.org/WAI/ARIA/apg/patterns/combobox/`]. +The `combobox` role identifies an element as an input that controls another element, such as a `listbox`, that can dynamically pop up to help the user set the value of that input. + +For more information about attributes go to [combobox](`https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/combobox_role`). +For recommendations how to make accessible widget go to [combobox](`https://www.w3.org/WAI/ARIA/apg/patterns/combobox/`). + +## API + +The engine expects specific parameters to be passed. +- `isMultiple`:`boolean`. +If true widget supports multiple selected options. +- `@change`:`HandlerAttachment`, see `core/component/directives/aria/roles-engines/README.md`. +Internal callback `onChange` expects an `Element` to be passed. +- `@open`:`HandlerAttachment`. +Internal callback `onChange` expects an `Element` to be passed. +- `@close`:`HandlerAttachment`. ## Usage ``` -< &__foo v-aria:combobox = {...} - +< div v-aria:combobox = { & + isMultiple: multiple, + '@change': (cb) => on('actionChange', cb), + '@open': 'open', + '@close': 'close' + } +. ``` diff --git a/src/core/component/directives/aria/roles-engines/combobox/index.ts b/src/core/component/directives/aria/roles-engines/combobox/index.ts index 7af277460e..9240a547d0 100644 --- a/src/core/component/directives/aria/roles-engines/combobox/index.ts +++ b/src/core/component/directives/aria/roles-engines/combobox/index.ts @@ -43,7 +43,7 @@ export class ComboboxEngine extends AriaRoleEngine { /** * Sets or deletes the id of active descendant element */ - protected setAriaActive(el?: HTMLElement): void { + protected setAriaActive(el?: Element): void { this.setAttribute('aria-activedescendant', el?.id ?? ''); } @@ -51,7 +51,7 @@ export class ComboboxEngine extends AriaRoleEngine { * Handler: the option list is expanded * @param el */ - protected onOpen(el: HTMLElement): void { + protected onOpen(el: Element): void { this.setAttribute('aria-expanded', 'true'); this.setAriaActive(el); } @@ -68,7 +68,7 @@ export class ComboboxEngine extends AriaRoleEngine { * Handler: active option element was changed * @param el */ - protected onChange(el: HTMLElement): void { + protected onChange(el: Element): void { this.setAriaActive(el); } } diff --git a/src/core/component/directives/aria/roles-engines/controls/README.md b/src/core/component/directives/aria/roles-engines/controls/README.md index 1b3744eddd..c23104c222 100644 --- a/src/core/component/directives/aria/roles-engines/controls/README.md +++ b/src/core/component/directives/aria/roles-engines/controls/README.md @@ -2,17 +2,12 @@ This module provides an engine for `v-aria` directive. -The engine to set aria-controls attribute. -For more information go to [`https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes/aria-controls`]. +The engine is used to set `aria-controls` attribute. +The global `aria-controls` property identifies the element (or elements) whose contents or presence are controlled by the element on which this attribute is set. -## Usage - -``` -< &__foo v-aria:controls = {...} - -``` +For more information go to [controls](`https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes/aria-controls`). -## How to use +## API Directive can be added to any tag that includes tag with needed role. Role should be passed as a modifier. ID or IDs are passed as value. @@ -25,10 +20,10 @@ If element controls several elements `for` should be passed as a string with IDs Example: ``` -< &__foo v-aria:controls.tab = {for: 'id1 id2 id3'} +< div v-aria:controls.tab = {for: 'id1 id2 id3'} -the same as -< &__foo +// the same as +< div < button aria-controls = "id1 id2 id3" role = "tab" ``` @@ -39,12 +34,18 @@ The second one is an id of an element to set as value in aria-controls attribute Example: ``` -< &__foo v-aria:controls = {for: [[id1, id3], [id2, id4]]} +< div v-aria:controls = {for: [[id1, id3], [id2, id4]]} < span :id = "id1" < span :id = "id2" -the same as -< &__foo +// the same as +< div < span :id = "id1" aria-controls = "id3" < span :id = "id2" aria-controls = "id4" ``` + +## Usage + +``` +< div v-aria:controls = {...} +``` diff --git a/src/core/component/directives/aria/roles-engines/dialog/README.md b/src/core/component/directives/aria/roles-engines/dialog/README.md index 342b5b3e6c..2540480c79 100644 --- a/src/core/component/directives/aria/roles-engines/dialog/README.md +++ b/src/core/component/directives/aria/roles-engines/dialog/README.md @@ -3,14 +3,16 @@ This module provides an engine for `v-aria` directive. The engine to set `dialog` role attribute. -For more information go to [`https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/dialog_role`]. +The `dialog` role is used to mark up an HTML based application dialog or window that separates content or UI from the rest of the web application or page. -Expects `iOpen` trait to be realized. +For more information go to [dialog](`https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/dialog_role`). +## API + +The engine expects the component to realize the`iOpen` trait. ## Usage ``` -< &__foo v-aria:dialog - +< div v-aria:dialog ``` diff --git a/src/core/component/directives/aria/roles-engines/interface.ts b/src/core/component/directives/aria/roles-engines/interface.ts index e6e79394b8..f82549b390 100644 --- a/src/core/component/directives/aria/roles-engines/interface.ts +++ b/src/core/component/directives/aria/roles-engines/interface.ts @@ -75,7 +75,7 @@ export interface EngineOptions

    void; +export type HandlerAttachment = ((cb: Function) => void) | Promise | string; export const enum KeyCodes { ENTER = 'Enter', diff --git a/src/core/component/directives/aria/roles-engines/listbox/README.md b/src/core/component/directives/aria/roles-engines/listbox/README.md index fdd53494f0..8083d3a1fe 100644 --- a/src/core/component/directives/aria/roles-engines/listbox/README.md +++ b/src/core/component/directives/aria/roles-engines/listbox/README.md @@ -3,12 +3,15 @@ This module provides an engine for `v-aria` directive. The engine to set `listbox` role attribute. -For more information go to [`https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/listbox_role`]. -For recommendations how to make accessible widget go to [`https://www.w3.org/WAI/ARIA/apg/patterns/listbox/`]. +The `listbox` role is used for lists from which a user may select one or more items which are static and may contain images. + +For more information go to [listbox](`https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/listbox_role`). +For recommendations how to make accessible widget go to [listbox](`https://www.w3.org/WAI/ARIA/apg/patterns/listbox/`). + +Widget `listbox` also contains elements with role `option` (see specified engine) ## Usage ``` -< &__foo v-aria:listbox = {...} - +< div v-aria:listbox = {...} ``` diff --git a/src/core/component/directives/aria/roles-engines/option/README.md b/src/core/component/directives/aria/roles-engines/option/README.md index cdf2fdcf28..c3141835a3 100644 --- a/src/core/component/directives/aria/roles-engines/option/README.md +++ b/src/core/component/directives/aria/roles-engines/option/README.md @@ -3,11 +3,23 @@ This module provides an engine for `v-aria` directive. The engine to set `option` role attribute. -For more information go to [`https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/option_role`]. +The option role is used for selectable items in a `listbox`. + +For more information go to [option](`https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/option_role`). + +## API + +The engine expects specific parameters to be passed. +- `isSelected`: `boolean`. +If true current option is selected by default. +- `@change`:`HandlerAttachment`, see `core/component/directives/aria/roles-engines/README.md`. +Internal callback `onChange` expects an `boolean` value if current option is selected. ## Usage ``` -< &__foo v-aria:option - +< div v-aria:option = { & + isSelected: el.active + '@change': (cb) => on('actionChange', () => cb(el.active)) + } ``` diff --git a/src/core/component/directives/aria/roles-engines/tab/README.md b/src/core/component/directives/aria/roles-engines/tab/README.md index 0395e3bad9..95e1c8e4c0 100644 --- a/src/core/component/directives/aria/roles-engines/tab/README.md +++ b/src/core/component/directives/aria/roles-engines/tab/README.md @@ -3,19 +3,26 @@ This module provides an engine for `v-aria` directive. The engine to set `tab` role attribute. -For more information go to [`https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/tab_role`]. -For recommendations how to make accessible widget go to [`https://www.w3.org/WAI/ARIA/apg/patterns/tabpanel/`]. +The ARIA tab role indicates an interactive element inside a `tablist` that, when activated, displays its associated `tabpanel`. -## Usage +For more information go to [tab](`https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/tab_role`). +For recommendations how to make accessible widget go to [tab](`https://www.w3.org/WAI/ARIA/apg/patterns/tabpanel/`). -``` -< &__foo v-aria:tab = {...} +## API -``` +The engine expects specific parameters to be passed. +- `isFirst`: `boolean`. +If true current tab is the first one in the list of tabs. +- `isSelected`: `boolean`. +If true current tab is active. +- `hasDefaultSelectedTabs`: `boolean`. +If true there are active tabs in the tablist widget by default. +- `orientation`: `string`. +The tablist widget view orientation. +- `@change`:`HandlerAttachment`, see `core/component/directives/aria/roles-engines/README.md`. +Internal callback `onChange` expects an `Element` or `NodeListOf` to be passed. -## How to use - -Tabs expect the `controls` role engine to be added in addition. ID passed to `controls` engine should be the id of the element with role `tabpanel`. +In addition, tabs expect the `controls` role engine to be added. An id passed to `controls` engine should be the id of the element with role `tabpanel`. Example: ``` @@ -25,3 +32,18 @@ Example: < span :id = 'id2' // content ``` + +The engine expects the component to realize`iAccess` trait. + +## Usage + +``` +< div v-aria:tab = { & + isFirst: i === 0, + isSelected: el.active, + hasDefaultSelectedTabs: items.some((el) => !!el.active), + orientation: orientation, + '@change': (cb) => cb(el.active) + } +. +``` diff --git a/src/core/component/directives/aria/roles-engines/tablist/README.md b/src/core/component/directives/aria/roles-engines/tablist/README.md index 078d4fd7e6..e3a123639c 100644 --- a/src/core/component/directives/aria/roles-engines/tablist/README.md +++ b/src/core/component/directives/aria/roles-engines/tablist/README.md @@ -3,12 +3,27 @@ This module provides an engine for `v-aria` directive. The engine to set `tablist` role attribute. -For more information go to [`https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/tablist_role`]. -For recommendations how to make accessible widget go to [`https://www.w3.org/WAI/ARIA/apg/patterns/tabpanel/`]. +The `tablist` role identifies the element that serves as the container for a set of `tabs`. The `tab` content are referred to as `tabpanel` elements. + +For more information go to [tablist](`https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/tablist_role`). +For recommendations how to make accessible widget go to [tablist](`https://www.w3.org/WAI/ARIA/apg/patterns/tabpanel/`). + +## API + +The engine expects specific parameters to be passed. +- `isMultiple`:`boolean`. +If true widget supports multiple selected options. +- `orientation`: `string`. +The tablist widget view orientation. + +The engine expects the component to realize`iAccess` trait. ## Usage ``` -< &__foo v-aria:tablist = {...} - +< div v-aria:tablist = { & + isMultiple: multiple; + orientation: orientation; + } +. ``` diff --git a/src/core/component/directives/aria/roles-engines/tabpanel/README.md b/src/core/component/directives/aria/roles-engines/tabpanel/README.md index a60f4f322d..f4a6bba828 100644 --- a/src/core/component/directives/aria/roles-engines/tabpanel/README.md +++ b/src/core/component/directives/aria/roles-engines/tabpanel/README.md @@ -3,19 +3,14 @@ This module provides an engine for `v-aria` directive. The engine to set `tabpanel` role attribute. -For more information go to [`https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/tabpanel_role`]. -For recommendations how to make accessible widget go to [`https://www.w3.org/WAI/ARIA/apg/patterns/tabpanel/`]. +The ARIA `tabpanel` is a container for the resources of layered content associated with a `tab`. -## Usage - -``` -< &__foo v-aria:tabpanel = {...} - -``` +For more information go to [tabpanel](`https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/tabpanel_role`). +For recommendations how to make accessible widget go to [tabpanel](`https://www.w3.org/WAI/ARIA/apg/patterns/tabpanel/`). -## How to use +## API -Expects `label` or `labelledby` params to be passed. +The engine expects `label` or `labelledby` params to be passed. Example: ``` @@ -23,3 +18,9 @@ Example: < span :id = 'id1' // content ``` + +## Usage + +``` +< div v-aria:tabpanel = {label: 'content'} +``` diff --git a/src/core/component/directives/aria/roles-engines/tree/README.md b/src/core/component/directives/aria/roles-engines/tree/README.md index f18c69c773..73f926850c 100644 --- a/src/core/component/directives/aria/roles-engines/tree/README.md +++ b/src/core/component/directives/aria/roles-engines/tree/README.md @@ -3,13 +3,30 @@ This module provides an engine for `v-aria` directive. The engine to set `tree` role attribute. -For more information go to [`https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/tree_role`]. +A `tree` is a widget that allows the user to select one or more items from a hierarchically organized collection. -For recommendations how to make accessible widget go to [`https://www.w3.org/WAI/ARIA/apg/patterns/treeview/`]. +For more information go to [tree](`https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/tree_role`). +For recommendations how to make accessible widget go to [tree](`https://www.w3.org/WAI/ARIA/apg/patterns/treeview/`). + +## API + +The engine expects specific parameters to be passed. +- `isRoot`: `boolean`. +If true current tree is the root tree in the component. +- `orientation`: `string`. +The tablist widget view orientation. +- `@change`:`HandlerAttachment`, see `core/component/directives/aria/roles-engines/README.md`. +Internal callback `onChange` expects an `Element` and `boolean` value if current tree is expanded. + +The engine expects the component to realize`iAccess` trait. ## Usage ``` -< &__foo v-aria:tree = {...} - +< div v-aria:tree = { & + isRoot: boolean = false; + orientation: string = 'false'; + '@change': HandlerAttachment = () => undefined; + } +. ``` diff --git a/src/core/component/directives/aria/roles-engines/tree/index.ts b/src/core/component/directives/aria/roles-engines/tree/index.ts index 92665ae3fa..3611b14510 100644 --- a/src/core/component/directives/aria/roles-engines/tree/index.ts +++ b/src/core/component/directives/aria/roles-engines/tree/index.ts @@ -34,9 +34,9 @@ export class TreeEngine extends AriaRoleEngine { /** * Handler: treeitem was expanded or closed * @param el - * @param isFolded + * @param isExpanded */ - protected onChange(el: Element, isFolded: boolean): void { - this.setAttribute('aria-expanded', String(!isFolded), el); + protected onChange(el: Element, isExpanded: boolean): void { + this.setAttribute('aria-expanded', String(isExpanded), el); } } diff --git a/src/core/component/directives/aria/roles-engines/treeitem/README.md b/src/core/component/directives/aria/roles-engines/treeitem/README.md index 3c262ae318..59f4cb951f 100644 --- a/src/core/component/directives/aria/roles-engines/treeitem/README.md +++ b/src/core/component/directives/aria/roles-engines/treeitem/README.md @@ -3,25 +3,39 @@ This module provides an engine for `v-aria` directive. The engine to set `treeitem` role attribute. -For more information go to [`https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/treeitem_role`]. +A `treeitem` is an item in a `tree`. -For recommendations how to make accessible widget go to [`https://www.w3.org/WAI/ARIA/apg/patterns/treeview/`]. +For more information go to [treeitem](`https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/treeitem_role`). +For recommendations how to make accessible widget go to [treeitem](`https://www.w3.org/WAI/ARIA/apg/patterns/treeview/`). -Expects `iAccess` trait to be realized. +## API + +The engine expects specific parameters to be passed. +- `isFirstRootItem`: `boolean`. +If true the item is first one in the root tree. +- `isExpandable`: `boolean`. +If true the item has children and can be expanded. +- `isExpanded`: `boolean`. +If true the item is expanded in the current moment. +- `orientation`: `string`. +The tablist widget view orientation. +- `rootElement`: `Element`. +The link to the root tree element. +- `toggleFold`: `function`. +The function to toggle the expandable item. + +The engine expects the component to realize`iAccess` trait. ## Usage ``` -< &__foo v-aria:treeitem = {...} - +< div v-aria:treeitem = { & + isFirstRootItem: el === top; + isExpandable: el.children != null; + isExpanded: !el.folded; + orientation: 'orientation'; + rootElement?: top; + toggleFold: () => ...; + } +. ``` - -## Adding new role engines -When creating a new role engine which handles some components events the contract of passed params types and naming should be respected. - -The name of handlers in engine should be like `onChange`, `onOpen`, etc. -The name of property in passed params should be like `@change`, `@open`, etc. -Types of the property on passed params could be: -- `Function` that accepts callback parameter; -- `Promise`, so the handler will be passed in `.then` method; -- `String` that is the name of component's event, so the handler will be added as a listener to this event. diff --git a/src/core/component/directives/aria/roles-engines/treeitem/interface.ts b/src/core/component/directives/aria/roles-engines/treeitem/interface.ts index bcd0834b2d..c9ec8aedd1 100644 --- a/src/core/component/directives/aria/roles-engines/treeitem/interface.ts +++ b/src/core/component/directives/aria/roles-engines/treeitem/interface.ts @@ -13,6 +13,6 @@ export class TreeitemParams { isExpandable: boolean = false; isExpanded: boolean = false; orientation: string = 'false'; - rootElement?: HTMLElement = undefined; + rootElement?: Element = undefined; toggleFold: FoldToggle = () => undefined; } From 38fb29d1e31aeb9cc21332d3da7a1bbd9a7b8894 Mon Sep 17 00:00:00 2001 From: Andrey Kobets Date: Fri, 9 Sep 2022 12:59:32 +0300 Subject: [PATCH 069/185] doc: added more examples --- src/base/b-list/README.md | 64 +++++++++++++++++++++++++++------------ src/base/b-list/b-list.ts | 7 +++-- 2 files changed, 48 insertions(+), 23 deletions(-) diff --git a/src/base/b-list/README.md b/src/base/b-list/README.md index 4160d137a4..07610605a4 100644 --- a/src/base/b-list/README.md +++ b/src/base/b-list/README.md @@ -30,6 +30,43 @@ If you need a more complex layout, provide it via a slot or by using `item/itemP * Dynamic data loading. +## Accessibility + +The component supports two standard logical roles. + +### List of links + +If the component is used as a list of links, then standard HTML link semantics will be used. +That is, links can be navigated using the Tab key, etc. + +### List of tabs + +If the component is used as a list of tabs it will implement the ARIA [tablist](https://www.w3.org/TR/wai-aria/#tablist) role. +All available features included in this [widget] (https://www.w3.org/WAI/ARIA/apg/patterns/tabpanel/) are supported. + +Please note that the component does not support the ability to set the content of the tabs. +You need to do it yourself using the ARIA [tabpanel](https://www.w3.org/TR/wai-aria/#tabpanel) role. + +``` +< b-list :items = [ & + {label: 'First tab', id: 'tab-1', controls: 'panel-1'}, + {label: 'Second tag', id: 'tab-2', controls: 'panel-2'}, + {label: 'Third tab', id: 'tab-3', controls: 'panel-3'} +] . + +< div id = panel-1" | v-aria:tabpanel = {labelledby: 'tab-1'} + < p + Content for the first panel + +< div id = panel-2" | v-aria:tabpanel = {labelledby: 'tab-2'} + < p + Content for the second panel + +< div id = panel-3" | v-aria:tabpanel = {labelledby: 'tab-3'} + < p + Content for the third panel +``` + ## Modifiers | Name | Description | Values | Default | @@ -213,13 +250,18 @@ Also, you can see the implemented traits or the parent component. ### Props +#### [orientation = `horizontal`] + +Indicates whether the component orientation is `horizontal`, `vertical`, or unknown/ambiguous. +This props affects the ARIA component role. + #### [listTag = `'ul'`] -A type of the list' root tag. +A type of the list root tag. #### [listElTag = `'li'`] -A type of list' element tags. +A type of list element tags. #### [activeProp] @@ -243,10 +285,6 @@ By default, if the component is switched to the `multiple` mode, this value is s Initial additional attributes are provided to an "internal" (native) list tag. -#### [orientation = `horizontal`] - -The component view orientation. - ### Fields #### items @@ -337,17 +375,3 @@ class Test extends iData { } } ``` - -## Accessibility - -If the component is used as a list of tabs it will implement an ARIA role [tablist](https://www.w3.org/TR/wai-aria/#tablist). -All the accessible functionality included in this [widget](https://www.w3.org/WAI/ARIA/apg/patterns/tabpanel/) is supported. - -When the component is used as a list of links it bases on internal HTML semantics of list tags. - -The component includes the following roles: -- [tablist](https://www.w3.org/TR/wai-aria/#tablist) -- [tab](https://www.w3.org/TR/wai-aria/#tab) - -The widget should also include [tabpanel](https://www.w3.org/TR/wai-aria/#tabpanel) role which is a block that contains the content of each tab. -But the component does not provide such block. So the 'connection' with other component should be set with the help of [`v-aria:controls`](core/component/directives/aria/aria-engines/controls/README.md) diff --git a/src/base/b-list/b-list.ts b/src/base/b-list/b-list.ts index 5aacacdb28..a85d58a5da 100644 --- a/src/base/b-list/b-list.ts +++ b/src/base/b-list/b-list.ts @@ -84,13 +84,13 @@ class bList extends iData implements iVisible, iWidth, iItems, iAccess { readonly itemProps?: iItems['itemProps']; /** - * Type of the list' root tag + * Type of the list root tag */ @prop(String) readonly listTag: string = 'ul'; /** - * Type of list' element tags + * Type of list element tags */ @prop(String) readonly listElTag: string = 'li'; @@ -116,7 +116,8 @@ class bList extends iData implements iVisible, iWidth, iItems, iAccess { readonly multiple: boolean = false; /** - * The component view orientation + * Indicates whether the component orientation is `horizontal`, `vertical`, or unknown/ambiguous. + * This props affects the ARIA component role. */ @prop(String) readonly orientation: Orientation = 'horizontal'; From c59df15a8159ebf9016a1fe2eea6033939496ffe Mon Sep 17 00:00:00 2001 From: Andrey Kobets Date: Fri, 9 Sep 2022 13:01:10 +0300 Subject: [PATCH 070/185] chore: moved down a18ly charter --- src/base/b-list/README.md | 38 +++++++++++++++++++------------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/src/base/b-list/README.md b/src/base/b-list/README.md index 07610605a4..aebef4b582 100644 --- a/src/base/b-list/README.md +++ b/src/base/b-list/README.md @@ -30,6 +30,25 @@ If you need a more complex layout, provide it via a slot or by using `item/itemP * Dynamic data loading. +## Modifiers + +| Name | Description | Values | Default | +|--------------|------------------------|-----------|---------| +| `hideLabels` | Item labels are hidden | `boolean` | `false` | + +Also, you can see the parent component and the component traits. + +## Events + +| EventName | Description | Payload description | Payload | +|-------------------|-----------------------------------------------------------------------------------------------------------------------------|---------------------------------------|----------| +| `change` | An active item of the component has been changed | Active value or a set of active items | `Active` | +| `immediateChange` | An active item of the component has been changed (the event can fire at component initializing if `activeProp` is provided) | Active value or a set of active items | `Active` | +| `actionChange` | An active item of the component has been changed due to some user action | Active value or a set of active items | `Active` | +| `itemsChange` | A list of items has been changed | List of items | `Items` | + +Also, you can see the parent component and the component traits. + ## Accessibility The component supports two standard logical roles. @@ -67,25 +86,6 @@ You need to do it yourself using the ARIA [tabpanel](https://www.w3.org/TR/wai-a Content for the third panel ``` -## Modifiers - -| Name | Description | Values | Default | -|--------------|------------------------|-----------|---------| -| `hideLabels` | Item labels are hidden | `boolean` | `false` | - -Also, you can see the parent component and the component traits. - -## Events - -| EventName | Description | Payload description | Payload | -|-------------------|-----------------------------------------------------------------------------------------------------------------------------|---------------------------------------|----------| -| `change` | An active item of the component has been changed | Active value or a set of active items | `Active` | -| `immediateChange` | An active item of the component has been changed (the event can fire at component initializing if `activeProp` is provided) | Active value or a set of active items | `Active` | -| `actionChange` | An active item of the component has been changed due to some user action | Active value or a set of active items | `Active` | -| `itemsChange` | A list of items has been changed | List of items | `Items` | - -Also, you can see the parent component and the component traits. - ## Associated types The component has associated type to specify active component item: **Active**. From a7afb17898b2ba1b1a1d392c71e471019f6db556 Mon Sep 17 00:00:00 2001 From: Andrey Kobets Date: Fri, 9 Sep 2022 13:05:39 +0300 Subject: [PATCH 071/185] chore: removed redundant chars from the example --- src/base/b-list/README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/base/b-list/README.md b/src/base/b-list/README.md index aebef4b582..f3f10c7e48 100644 --- a/src/base/b-list/README.md +++ b/src/base/b-list/README.md @@ -73,15 +73,15 @@ You need to do it yourself using the ARIA [tabpanel](https://www.w3.org/TR/wai-a {label: 'Third tab', id: 'tab-3', controls: 'panel-3'} ] . -< div id = panel-1" | v-aria:tabpanel = {labelledby: 'tab-1'} +< div id = panel-1 | v-aria:tabpanel = {labelledby: 'tab-1'} < p Content for the first panel -< div id = panel-2" | v-aria:tabpanel = {labelledby: 'tab-2'} +< div id = panel-2 | v-aria:tabpanel = {labelledby: 'tab-2'} < p Content for the second panel -< div id = panel-3" | v-aria:tabpanel = {labelledby: 'tab-3'} +< div id = panel-3 | v-aria:tabpanel = {labelledby: 'tab-3'} < p Content for the third panel ``` From da9c5b696b425bc705f199af212a0600fc6436c5 Mon Sep 17 00:00:00 2001 From: Aleksandr Bunin Date: Sat, 25 Jun 2022 12:10:50 +0300 Subject: [PATCH 072/185] Add aria directive classes and modal role engine --- src/base/b-window/b-window.ss | 10 ++- src/core/component/directives/aria/helpers.ts | 82 +++++++++++++++++++ src/core/component/directives/aria/index.ts | 40 +++++++++ .../component/directives/aria/interface.ts | 24 ++++++ .../directives/aria/roles-engines/dialog.ts | 41 ++++++++++ .../directives/aria/roles-engines/index.ts | 1 + .../aria/roles-engines/interface.ts | 22 +++++ src/core/component/directives/index.ts | 4 + src/super/i-input/i-input.ss | 2 +- src/traits/i-open/i-open.ts | 14 ++++ 10 files changed, 237 insertions(+), 3 deletions(-) create mode 100644 src/core/component/directives/aria/helpers.ts create mode 100644 src/core/component/directives/aria/index.ts create mode 100644 src/core/component/directives/aria/interface.ts create mode 100644 src/core/component/directives/aria/roles-engines/dialog.ts create mode 100644 src/core/component/directives/aria/roles-engines/index.ts create mode 100644 src/core/component/directives/aria/roles-engines/interface.ts diff --git a/src/base/b-window/b-window.ss b/src/base/b-window/b-window.ss index 480ef0e322..616d92ba40 100644 --- a/src/base/b-window/b-window.ss +++ b/src/base/b-window/b-window.ss @@ -24,7 +24,10 @@ opt.ifOnce('opened', m.opened === 'true') && delete watchModsStore.opened . - < :section.&__window ref = window + < :section.&__window & + ref = window | + v-aria:dialog.#title + . - if thirdPartySlots < template v-if = slotName : isSlot = /^windowSlot[A-Z]/ @@ -36,7 +39,10 @@ < template v-else += self.slot() - < h1.&__title v-if = title || vdom.getSlot('title') + < h1.&__title & + v-if = title || vdom.getSlot('title') | + :id = dom.getId('title') + . += self.slot('title', {':title': 'title'}) - block title {{ title }} diff --git a/src/core/component/directives/aria/helpers.ts b/src/core/component/directives/aria/helpers.ts new file mode 100644 index 0000000000..f2746c0620 --- /dev/null +++ b/src/core/component/directives/aria/helpers.ts @@ -0,0 +1,82 @@ +import type { DirectiveHookParams, AriaRoleEngine } from 'core/component/directives/aria/interface'; +import type iBlock from 'super/i-block/i-block'; +import * as ariaRoles from 'core/component/directives/aria/roles-engines/index'; + +export function setAriaLabel({el, opts, vnode}: DirectiveHookParams): void { + const + {dom, vdom, $createElement: createElem} = Object.cast(vnode.fakeContext), + value = opts.value ?? {}; + + for (const mod in opts.modifiers) { + if (!mod.startsWith('#')) { + continue; + } + + const + title = mod.slice(1), + id = dom.getId(title); + + if ('labelledby' in opts.modifiers) { + el.setAttribute('aria-labelledby', id); + + } else { + el.setAttribute('id', id); + + const + labelNode = createElem.call(vnode.fakeContext, + 'label', + { + attrs: {for: id} + }); + + const + labelElem = vdom.render(labelNode); + + el.prepend(labelElem); + } + } + + if (value.label != null) { + el.setAttribute('aria-label', value.label); + + } else if (value.labelledby != null) { + el.setAttribute('aria-labelledby', dom.getId(value.labelledby)); + } + + if (value.description != null) { + el.setAttribute('aria-description', value.description); + + } else if (value.describedby != null) { + el.setAttribute('aria-describedby', dom.getId(value.describedby)); + } +} + +export function setAriaTabIndex({opts, vnode}: DirectiveHookParams): void { + if (opts.value == null) { + return; + } + + const + names = opts.value.children, + {block} = Object.cast(vnode.fakeContext); + + for (const name of names) { + const + elems = block?.elements(name); + + elems?.forEach((el: Element) => { + el.setAttribute('tabindex', '0'); + }); + } +} + +export function setAriaRole(options: DirectiveHookParams): CanUndef { + const + {arg: role} = options.opts; + + if (role == null) { + return; + } + + return new ariaRoles[role](options); +} diff --git a/src/core/component/directives/aria/index.ts b/src/core/component/directives/aria/index.ts new file mode 100644 index 0000000000..7288f71c6f --- /dev/null +++ b/src/core/component/directives/aria/index.ts @@ -0,0 +1,40 @@ +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ + +/** + * [[include:core/component/directives/aria/README.md]] + * @packageDocumentation + */ + +import { ComponentEngine, VNode, VNodeDirective } from 'core/component/engines'; +import { setAriaLabel, setAriaRole, setAriaTabIndex } from 'core/component/directives/aria/helpers'; + +ComponentEngine.directive('aria', { + inserted(el: Element, opts: VNodeDirective, vnode: VNode): void { + const + {value, arg, modifiers} = opts; + + if (value == null && arg == null && modifiers == null) { + return; + } + + const + options = {el, opts, vnode}; + + setAriaLabel(options); + setAriaTabIndex(options); + setAriaRole(options)?.init(); + }, + + unbind(el: Element, opts: VNodeDirective, vnode: VNode) { + const + options = {el, opts, vnode}; + + setAriaRole(options)?.clear(); + } +}); diff --git a/src/core/component/directives/aria/interface.ts b/src/core/component/directives/aria/interface.ts new file mode 100644 index 0000000000..d48eb0748c --- /dev/null +++ b/src/core/component/directives/aria/interface.ts @@ -0,0 +1,24 @@ +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ + +import type { VNodeDirective, VNode } from 'core/component/engines'; + +export interface DirectiveHookParams { + el: Element; + opts: VNodeDirective; + vnode: VNode; +} + +export interface AriaRoleEngine { + el: Element; + value: any; + vnode: VNode; + + init(): void; + clear(): void; +} diff --git a/src/core/component/directives/aria/roles-engines/dialog.ts b/src/core/component/directives/aria/roles-engines/dialog.ts new file mode 100644 index 0000000000..fec7e6a538 --- /dev/null +++ b/src/core/component/directives/aria/roles-engines/dialog.ts @@ -0,0 +1,41 @@ +import iOpen from 'traits/i-open/i-open'; +import type iBlock from 'super/i-block/i-block'; +import type { DirectiveHookParams } from 'core/component/directives/aria/interface'; +import RoleEngine from 'core/component/directives/aria/roles-engines/interface'; + +export default class DialogEngine extends RoleEngine { + group: Dictionary = {}; + + constructor(options: DirectiveHookParams) { + super(options); + + if (!iOpen.is(options.vnode.fakeContext)) { + Object.throw('Dialog directive expects the component to realize iOpen interface'); + } + } + + override init(): void { + const + {localEmitter: $e} = Object.cast(this.vnode.fakeContext); + + this.el.setAttribute('role', 'dialog'); + this.el.setAttribute('aria-modal', 'false'); + + this.group = {group: 'ariaAttributes'}; + + $e.on('open', () => { + this.el.setAttribute('aria-modal', 'true'); + }, this.group); + + $e.on('close', () => { + this.el.setAttribute('aria-modal', 'false'); + }, this.group); + } + + override clear(): void { + const + {localEmitter: $e} = Object.cast(this.vnode.fakeContext); + + $e.off(this.group); + } +} diff --git a/src/core/component/directives/aria/roles-engines/index.ts b/src/core/component/directives/aria/roles-engines/index.ts new file mode 100644 index 0000000000..4ff45a36fc --- /dev/null +++ b/src/core/component/directives/aria/roles-engines/index.ts @@ -0,0 +1 @@ +export * from 'core/component/directives/aria/roles-engines/dialog'; diff --git a/src/core/component/directives/aria/roles-engines/interface.ts b/src/core/component/directives/aria/roles-engines/interface.ts new file mode 100644 index 0000000000..3ab1b6f073 --- /dev/null +++ b/src/core/component/directives/aria/roles-engines/interface.ts @@ -0,0 +1,22 @@ +import type { AriaRoleEngine, DirectiveHookParams } from 'core/component/directives/aria/interface'; +import type { VNode } from 'core/component'; + +export default abstract class RoleEngine implements AriaRoleEngine { + el: Element; + value: any; + vnode: VNode; + + constructor({el, opts, vnode}: DirectiveHookParams) { + this.el = el; + this.value = opts.value; + this.vnode = vnode; + } + + init(): void { + // + } + + clear(): void { + // + } +} diff --git a/src/core/component/directives/index.ts b/src/core/component/directives/index.ts index ca12cad9c1..710b6fdb56 100644 --- a/src/core/component/directives/index.ts +++ b/src/core/component/directives/index.ts @@ -22,4 +22,8 @@ import 'core/component/directives/image'; import 'core/component/directives/update-on'; //#endif +//#if runtime has directives/aria +import 'core/component/directives/aria'; +//#endif + import 'core/component/directives/hook'; diff --git a/src/super/i-input/i-input.ss b/src/super/i-input/i-input.ss index 5fb3be3649..8d8c85a2c8 100644 --- a/src/super/i-input/i-input.ss +++ b/src/super/i-input/i-input.ss @@ -36,7 +36,7 @@ * *) [type=nativeInputType] - value of the `:type` attribute * * *) [autofocus] - value of the `:autofocus` attribute - * *) [tabIndex] - value of the `:autofocus` attribute + * *) [tabIndex] - value of the `:tabindex` attribute * * *) [focusHandler] - value of the `@focus` attribute * *) [blurHandler] - value of the `@blur` attribute diff --git a/src/traits/i-open/i-open.ts b/src/traits/i-open/i-open.ts index 63a9e9df7d..96efa485ea 100644 --- a/src/traits/i-open/i-open.ts +++ b/src/traits/i-open/i-open.ts @@ -134,6 +134,20 @@ export default abstract class iOpen { }); } + /** + * Checks if the component realize current trait + * + * @param obj + */ + static is(obj: unknown): obj is iOpen { + if (Object.isPrimitive(obj)) { + return true; + } + + const dict = Object.cast(obj); + return Object.isFunction(dict.open) && Object.isFunction(dict.close); + } + /** * Opens the component * @param args From bdecfd7d3b94ab6d2e323abc7f4d6144a21e1e66 Mon Sep 17 00:00:00 2001 From: Aleksandr Bunin Date: Thu, 21 Jul 2022 12:05:35 +0300 Subject: [PATCH 073/185] add aria engines --- src/base/b-list/CHANGELOG.md | 10 + src/base/b-list/b-list.ss | 23 +- src/base/b-list/b-list.ts | 66 +++--- src/base/b-tree/CHANGELOG.md | 9 + src/base/b-tree/b-tree.ss | 98 +++++---- src/base/b-tree/b-tree.ts | 36 ++- .../component/directives/aria/CHANGELOG.md | 10 + src/core/component/directives/aria/README.md | 44 ++++ .../component/directives/aria/aria-setter.ts | 130 +++++++++++ src/core/component/directives/aria/helpers.ts | 82 ------- src/core/component/directives/aria/index.ts | 38 +++- .../component/directives/aria/interface.ts | 35 ++- .../directives/aria/roles-engines/combobox.ts | 55 +++++ .../directives/aria/roles-engines/controls.ts | 43 ++++ .../directives/aria/roles-engines/dialog.ts | 46 ++-- .../directives/aria/roles-engines/index.ts | 19 +- .../aria/roles-engines/interface.ts | 43 ++-- .../directives/aria/roles-engines/listbox.ts | 19 ++ .../directives/aria/roles-engines/option.ts | 27 +++ .../directives/aria/roles-engines/tab.ts | 154 +++++++++++++ .../directives/aria/roles-engines/tablist.ts | 28 +++ .../directives/aria/roles-engines/tabpanel.ts | 22 ++ .../directives/aria/roles-engines/tree.ts | 38 ++++ .../directives/aria/roles-engines/treeitem.ts | 208 ++++++++++++++++++ src/core/component/directives/index.ts | 2 - src/form/b-checkbox/CHANGELOG.md | 6 + src/form/b-checkbox/b-checkbox.ss | 14 +- src/form/b-select/CHANGELOG.md | 13 ++ src/form/b-select/b-select.ss | 31 ++- src/form/b-select/b-select.ts | 43 ++-- src/form/b-select/modules/handlers.ts | 26 ++- src/super/i-input/CHANGELOG.md | 6 + src/super/i-input/README.md | 2 +- src/super/i-input/i-input.ts | 8 +- src/traits/i-access/CHANGELOG.md | 9 + src/traits/i-access/const.ts | 2 + src/traits/i-access/i-access.ts | 139 +++++++++++- src/traits/i-open/CHANGELOG.md | 6 + src/traits/i-open/i-open.ts | 3 +- 39 files changed, 1328 insertions(+), 265 deletions(-) create mode 100644 src/core/component/directives/aria/CHANGELOG.md create mode 100644 src/core/component/directives/aria/README.md create mode 100644 src/core/component/directives/aria/aria-setter.ts delete mode 100644 src/core/component/directives/aria/helpers.ts create mode 100644 src/core/component/directives/aria/roles-engines/combobox.ts create mode 100644 src/core/component/directives/aria/roles-engines/controls.ts create mode 100644 src/core/component/directives/aria/roles-engines/listbox.ts create mode 100644 src/core/component/directives/aria/roles-engines/option.ts create mode 100644 src/core/component/directives/aria/roles-engines/tab.ts create mode 100644 src/core/component/directives/aria/roles-engines/tablist.ts create mode 100644 src/core/component/directives/aria/roles-engines/tabpanel.ts create mode 100644 src/core/component/directives/aria/roles-engines/tree.ts create mode 100644 src/core/component/directives/aria/roles-engines/treeitem.ts create mode 100644 src/traits/i-access/const.ts diff --git a/src/base/b-list/CHANGELOG.md b/src/base/b-list/CHANGELOG.md index 028effae00..18fbd9fe1c 100644 --- a/src/base/b-list/CHANGELOG.md +++ b/src/base/b-list/CHANGELOG.md @@ -9,6 +9,16 @@ Changelog > - :house: [Internal] > - :nail_care: [Polish] +## v3.0.0-rc.??? (2022-??-??) + +#### :rocket: New Feature + +* Added `v-aria` directive +* Added a new prop `vertical` +* Added `isTablist` +* Added `onActiveChange` +* Now the component derive `iAccess` + ## v3.0.0-rc.211 (2021-07-21) #### :boom: Breaking Change diff --git a/src/base/b-list/b-list.ss b/src/base/b-list/b-list.ss index b1b663dd6c..a41fc70787 100644 --- a/src/base/b-list/b-list.ss +++ b/src/base/b-list/b-list.ss @@ -32,7 +32,6 @@ :href = el.href | :value = el.value | - :aria-selected = el.href === undefined ? isActive(el.value) : undefined | :-id = values.get(el.value) | :-hint = el.hint | @@ -48,7 +47,17 @@ } })) | - :v-attrs = el.attrs + :v-attrs = isTablist + ? { + 'v-aria:tab': { + isFirst: i === 0, + isVertical: vertical, + onChange: onActiveChange, + activeElement + }, + ...el.attrs + } + : el.attrs . - block preIcon < span.&__cell.&__link-icon.&__link-pre-icon v-if = el.preIcon || vdom.getSlot('preIcon') @@ -101,6 +110,14 @@ < tag.&__wrapper & :is = listTag | - :v-attrs = attrs + :v-attrs = isTablist + ? { + 'v-aria:tablist': { + isMultiple: multiple, + isVertical: vertical + }, + ...attrs + } + : attrs . += self.list('items') diff --git a/src/base/b-list/b-list.ts b/src/base/b-list/b-list.ts index 4ecebb9ebd..cd5ef5460b 100644 --- a/src/base/b-list/b-list.ts +++ b/src/base/b-list/b-list.ts @@ -20,12 +20,14 @@ import SyncPromise from 'core/promise/sync'; import { isAbsURL } from 'core/url'; +import { derive } from 'core/functools/trait'; import iVisible from 'traits/i-visible/i-visible'; import iWidth from 'traits/i-width/i-width'; import iItems, { IterationKey } from 'traits/i-items/i-items'; import iData, { component, prop, field, system, computed, hook, watch, ModsDecl } from 'super/i-data/i-data'; import type { Active, Item, Items } from 'base/b-list/interface'; +import iAccess from 'traits/i-access/i-access'; export * from 'super/i-data/i-data'; export * from 'base/b-list/interface'; @@ -33,6 +35,8 @@ export * from 'base/b-list/interface'; export const $$ = symbolGenerator(); +interface bList extends Trait {} + /** * Component to create a list of tabs/links */ @@ -47,7 +51,8 @@ export const } }) -export default class bList extends iData implements iVisible, iWidth, iItems { +@derive(iAccess) +class bList extends iData implements iVisible, iWidth, iItems, iAccess { /** @see [[iVisible.prototype.hideIfOffline]] */ @prop(Boolean) readonly hideIfOffline: boolean = false; @@ -111,6 +116,12 @@ export default class bList extends iData implements iVisible, iWidth, iItems { @prop(Boolean) readonly multiple: boolean = false; + /** + * If true, the component view orientation is vertical. Horizontal is default + */ + @prop(Boolean) + readonly vertical: boolean = false; + /** * If true, the active item can be unset by using another click to it. * By default, if the component is switched to the `multiple` mode, this value is set to `true`, @@ -130,15 +141,7 @@ export default class bList extends iData implements iVisible, iWidth, iItems { * @see [[bList.attrsProp]] */ get attrs(): Dictionary { - const - attrs = {...this.attrsProp}; - - if (this.items.some((el) => el.href === undefined)) { - attrs.role = 'tablist'; - attrs['aria-multiselectable'] = this.multiple; - } - - return attrs; + return {...this.attrsProp}; } /** @@ -385,10 +388,6 @@ export default class bList extends iData implements iVisible, iWidth, iItems { if (previousLinkEl !== linkEl) { $b.setElMod(previousLinkEl, 'link', 'active', false); - - if (previousLinkEl.hasAttribute('aria-selected')) { - previousLinkEl.setAttribute('aria-selected', 'false'); - } } } } @@ -400,10 +399,6 @@ export default class bList extends iData implements iVisible, iWidth, iItems { for (let i = 0; i < els.length; i++) { const el = els[i]; $b.setElMod(el, 'link', 'active', true); - - if (el.hasAttribute('aria-selected')) { - el.setAttribute('aria-selected', 'true'); - } } }, stderr); } @@ -489,10 +484,6 @@ export default class bList extends iData implements iVisible, iWidth, iItems { if (needChangeMod) { $b.setElMod(el, 'link', 'active', false); - - if (el.hasAttribute('aria-selected')) { - el.setAttribute('aria-selected', 'false'); - } } } }, stderr); @@ -621,13 +612,6 @@ export default class bList extends iData implements iVisible, iWidth, iItems { item.classes = this.provide.hintClasses(item.hintPos) .concat(item.classes ?? []); - if (href === undefined) { - item.attrs = { - ...item.attrs, - role: 'tab' - }; - } - normalizedItems.push({...item, value, href}); } @@ -707,6 +691,13 @@ export default class bList extends iData implements iVisible, iWidth, iItems { } } + /** + * Returns true if the component is used as tab list + */ + protected get isTablist(): boolean { + return this.items.some((el) => el.href === undefined); + } + protected override onAddData(data: unknown): void { Object.assign(this.db, this.convertDataToDB(data)); } @@ -738,4 +729,21 @@ export default class bList extends iData implements iVisible, iWidth, iItems { this.toggleActive(this.indexes[id]); this.emit('actionChange', this.active); } + + /** + * Handler: on active element changes + * @param cb + */ + protected onActiveChange(cb: Function): void { + this.on('change', () => { + if (Object.isSet(this.active)) { + cb(this.block?.elements('link', {active: true})); + + } else { + cb(this.block?.element('link', {active: true})); + } + }); + } } + +export default bList; diff --git a/src/base/b-tree/CHANGELOG.md b/src/base/b-tree/CHANGELOG.md index 5920f061c2..12a877eb58 100644 --- a/src/base/b-tree/CHANGELOG.md +++ b/src/base/b-tree/CHANGELOG.md @@ -9,6 +9,15 @@ Changelog > - :house: [Internal] > - :nail_care: [Polish] +## v3.0.0-rc.??? (2022-??-??) + +#### :rocket: New Feature + +* Added `v-aria` directive +* Added a new prop `vertical` +* Added `changeFoldedMod` +* Now the component derive `iAccess` + ## v3.0.0-rc.164 (2021-03-22) #### :house: Internal diff --git a/src/base/b-tree/b-tree.ss b/src/base/b-tree/b-tree.ss index 4849e3a49f..4ba11ef0a9 100644 --- a/src/base/b-tree/b-tree.ss +++ b/src/base/b-tree/b-tree.ss @@ -12,51 +12,65 @@ - template index() extends ['i-data'].index - block body - < template & - v-for = (el, i) in asyncRender.iterate(items, renderChunks, renderTaskParams) | - :key = getItemKey(el, i) + < .&__root & + v-aria:tree = { + isVertical: vertical, + isRootTree: top == null, + onChange: (cb) => on('fold', (ctx, el, item, value) => cb(el, value)) + } . - < .&__node & - :-id = dom.getId(el.id) | - :-level = level | - :class = provide.elClasses({ - node: { - level, - folded: getFoldedPropValue(el) - } - }) + < template & + v-for = (el, i) in asyncRender.iterate(items, renderChunks, renderTaskParams) | + :key = getItemKey(el, i) . - < .&__item-wrapper - < .&__marker - - block fold - < template v-if = Object.size(field.get('children.length', el)) > 0 - += self.slot('fold', {':params': 'getFoldProps(el)'}) - < .&__fold :v-attrs = getFoldProps(el) + < .&__node & + :-id = dom.getId(el.id) | + :-level = level | + :class = provide.elClasses({ + node: { + level, + folded: el.children && getFoldedPropValue(el) + } + }) | + v-aria:treeitem = { + getRootElement: () => (top ? top.$el : $el), + toggleFold: changeFoldedMod.bind(this, el), + getFoldedMod: getFoldedModById.bind(this, el.id), + isVeryFirstItem: top == null && i === 0, + } + . + < .&__item-wrapper + < .&__marker + - block fold + < template v-if = Object.size(field.get('children.length', el)) > 0 + += self.slot('fold', {':params': 'getFoldProps(el)'}) + < .&__fold :v-attrs = getFoldProps(el) - - block item - += self.slot('default', {':item': 'getItemProps(el, i)'}) - < component.&__item & - v-if = item | - :is = Object.isFunction(item) ? item(el, i) : item | - :v-attrs = getItemProps(el, i) - . + - block item + += self.slot('default', {':item': 'getItemProps(el, i)'}) + < component.&__item & + v-if = item | + :is = Object.isFunction(item) ? item(el, i) : item | + :v-attrs = getItemProps(el, i) | + dispatching = true + . - - block children - < .&__children v-if = Object.size(field.get('children', el)) > 0 - < b-tree.&__child & - :items = el.children | - :folded = getFoldedPropValue(el) | - :item = item | - :v-attrs = nestedTreeProps - . - < template & - #default = o | - v-if = vdom.getSlot('default') + - block children + < .&__children v-if = Object.size(field.get('children', el)) > 0 + < b-tree.&__child & + :items = el.children | + :folded = getFoldedPropValue(el) | + :item = item | + :v-attrs = nestedTreeProps . - += self.slot('default', {':item': 'o.item'}) + < template & + #default = o | + v-if = vdom.getSlot('default') + . + += self.slot('default', {':item': 'o.item'}) - < template & - #fold = o | - v-if = vdom.getSlot('fold') - . - += self.slot('fold', {':params': 'o.params'}) + < template & + #fold = o | + v-if = vdom.getSlot('fold') + . + += self.slot('fold', {':params': 'o.params'}) diff --git a/src/base/b-tree/b-tree.ts b/src/base/b-tree/b-tree.ts index ee70c44885..fae29b1f57 100644 --- a/src/base/b-tree/b-tree.ts +++ b/src/base/b-tree/b-tree.ts @@ -17,10 +17,12 @@ import 'models/demo/nested-list'; import symbolGenerator from 'core/symbol'; +import { derive } from 'core/functools/trait'; import iItems, { IterationKey } from 'traits/i-items/i-items'; import iData, { component, prop, field, TaskParams, TaskI } from 'super/i-data/i-data'; import type { Item, RenderFilter } from 'base/b-tree/interface'; +import iAccess from 'traits/i-access/i-access'; export * from 'super/i-data/i-data'; export * from 'base/b-tree/interface'; @@ -28,11 +30,14 @@ export * from 'base/b-tree/interface'; export const $$ = symbolGenerator(); +interface bTree extends Trait {} + /** * Component to render tree of any elements */ @component() -export default class bTree extends iData implements iItems { +@derive(iAccess) +class bTree extends iData implements iItems, iAccess { /** @see [[iItems.Item]] */ readonly Item!: Item; @@ -98,6 +103,12 @@ export default class bTree extends iData implements iItems { @prop(Boolean) readonly folded: boolean = true; + /** + * If true, the component view orientation is vertical. Horizontal is default + */ + @prop(Boolean) + readonly vertical: boolean = false; + /** * Link to the top level component (internal parameter) */ @@ -255,4 +266,27 @@ export default class bTree extends iData implements iItems { this.emit('fold', target, item, newVal); } } + + /** + * Toggle folded state + * + * @params target, value + * @emits `fold(target: HTMLElement, item:` [[Item]]`, value: boolean)` + */ + protected changeFoldedMod(item: this['Item'], target: HTMLElement, value?: boolean): void { + const + mod = this.block?.getElMod(target, 'node', 'folded'); + + if (mod == null) { + return; + } + + const + newVal = value ? value : mod === 'false'; + + this.block?.setElMod(target, 'node', 'folded', newVal); + this.emit('fold', target, item, newVal); + } } + +export default bTree; diff --git a/src/core/component/directives/aria/CHANGELOG.md b/src/core/component/directives/aria/CHANGELOG.md new file mode 100644 index 0000000000..1670fa7e71 --- /dev/null +++ b/src/core/component/directives/aria/CHANGELOG.md @@ -0,0 +1,10 @@ +Changelog +========= + +> **Tags:** +> - :boom: [Breaking Change] +> - :rocket: [New Feature] +> - :bug: [Bug Fix] +> - :memo: [Documentation] +> - :house: [Internal] +> - :nail_care: [Polish] diff --git a/src/core/component/directives/aria/README.md b/src/core/component/directives/aria/README.md new file mode 100644 index 0000000000..e12cd09021 --- /dev/null +++ b/src/core/component/directives/aria/README.md @@ -0,0 +1,44 @@ +# core/component/directives/aria + +This module provides a directive to add aria attributes and logic to elements through single API. + +## Usage + +``` +< &__foo v-aria.#bla + +< &__foo v-aria = {labelledby: dom.getId('bla')} + +``` + +## Available modifiers: + +- .#[string] (ex. '.#title') the same as = {labelledby: [id-'title']} + + +-- Roles: +- controls: +Directive can be added to any tag that includes tag with needed role. Role should be passed as a modifier. +ID or IDs are passed as value. +ID could be single or multiple written in string with space between. + +Example: +``` +< &__foo v-aria:controls.select = {id: 'id1 id2 id3'} + +same as + +< select aria-controls = "id1 id2 id3" +``` + +- tabs: +Tabs always expect the 'controls' role engine to be added. + + +## Available standard values: +Value is expected to always be an object type. Possible keys: +- label +- labelledby +- description +- describedby +- id diff --git a/src/core/component/directives/aria/aria-setter.ts b/src/core/component/directives/aria/aria-setter.ts new file mode 100644 index 0000000000..dbd1d87292 --- /dev/null +++ b/src/core/component/directives/aria/aria-setter.ts @@ -0,0 +1,130 @@ +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ + +import * as ariaRoles from 'core/component/directives/aria/roles-engines'; +import Async from 'core/async'; +import AriaRoleEngine from 'core/component/directives/aria/interface'; +import type iBlock from 'super/i-block/i-block'; +import type { DirectiveOptions } from 'core/component/directives/aria/interface'; + +export default class AriaSetter extends AriaRoleEngine { + override $a: Async; + role: CanUndef; + + constructor(options: DirectiveOptions) { + super(options); + + this.$a = new Async(); + this.setAriaRole(); + + if (this.role != null) { + this.role.$a = this.$a; + } + } + + init(): void { + this.setAriaLabel(); + this.addEventHandlers(); + + this.role?.init(); + } + + override update(): void { + const + ctx = this.options.vnode.fakeContext; + + if (ctx.isFunctional) { + ctx.off(); + } + + if (this.role != null) { + this.role.options = this.options; + this.role.update?.(); + } + } + + override clear(): void { + this.$a.clearAll(); + + this.role?.clear?.(); + } + + addEventHandlers(): void { + if (this.role == null) { + return; + } + + const + $v = this.options.binding.value; + + for (const p in $v) { + if (p === 'onOpen' || p === 'onClose' || p === 'onChange') { + const + callback = this.role[p], + property = $v[p]; + + if (Object.isFunction(property)) { + property(callback); + + } else if (Object.isPromiseLike(property)) { + void property.then(callback); + + } else if (Object.isString(property)) { + const + ctx = this.options.vnode.fakeContext; + + ctx.on(property, callback); + } + } + } + } + + setAriaRole(): CanUndef { + const + {arg: role} = this.options.binding; + + if (role == null) { + return; + } + + this.role = new ariaRoles[role](this.options); + } + + setAriaLabel(): void { + const + {vnode, binding, el} = this.options, + {dom} = Object.cast(vnode.fakeContext), + value = Object.isCustomObject(binding.value) ? binding.value : {}; + + for (const mod in binding.modifiers) { + if (!mod.startsWith('#')) { + continue; + } + + const + title = mod.slice(1), + id = dom.getId(title); + + el.setAttribute('aria-labelledby', id); + } + + if (value.label != null) { + el.setAttribute('aria-label', value.label); + + } else if (value.labelledby != null) { + el.setAttribute('aria-labelledby', dom.getId(value.labelledby)); + } + + if (value.description != null) { + el.setAttribute('aria-description', value.description); + + } else if (value.describedby != null) { + el.setAttribute('aria-describedby', dom.getId(value.describedby)); + } + } +} diff --git a/src/core/component/directives/aria/helpers.ts b/src/core/component/directives/aria/helpers.ts deleted file mode 100644 index f2746c0620..0000000000 --- a/src/core/component/directives/aria/helpers.ts +++ /dev/null @@ -1,82 +0,0 @@ -import type { DirectiveHookParams, AriaRoleEngine } from 'core/component/directives/aria/interface'; -import type iBlock from 'super/i-block/i-block'; -import * as ariaRoles from 'core/component/directives/aria/roles-engines/index'; - -export function setAriaLabel({el, opts, vnode}: DirectiveHookParams): void { - const - {dom, vdom, $createElement: createElem} = Object.cast(vnode.fakeContext), - value = opts.value ?? {}; - - for (const mod in opts.modifiers) { - if (!mod.startsWith('#')) { - continue; - } - - const - title = mod.slice(1), - id = dom.getId(title); - - if ('labelledby' in opts.modifiers) { - el.setAttribute('aria-labelledby', id); - - } else { - el.setAttribute('id', id); - - const - labelNode = createElem.call(vnode.fakeContext, - 'label', - { - attrs: {for: id} - }); - - const - labelElem = vdom.render(labelNode); - - el.prepend(labelElem); - } - } - - if (value.label != null) { - el.setAttribute('aria-label', value.label); - - } else if (value.labelledby != null) { - el.setAttribute('aria-labelledby', dom.getId(value.labelledby)); - } - - if (value.description != null) { - el.setAttribute('aria-description', value.description); - - } else if (value.describedby != null) { - el.setAttribute('aria-describedby', dom.getId(value.describedby)); - } -} - -export function setAriaTabIndex({opts, vnode}: DirectiveHookParams): void { - if (opts.value == null) { - return; - } - - const - names = opts.value.children, - {block} = Object.cast(vnode.fakeContext); - - for (const name of names) { - const - elems = block?.elements(name); - - elems?.forEach((el: Element) => { - el.setAttribute('tabindex', '0'); - }); - } -} - -export function setAriaRole(options: DirectiveHookParams): CanUndef { - const - {arg: role} = options.opts; - - if (role == null) { - return; - } - - return new ariaRoles[role](options); -} diff --git a/src/core/component/directives/aria/index.ts b/src/core/component/directives/aria/index.ts index 7288f71c6f..255fa3b2f8 100644 --- a/src/core/component/directives/aria/index.ts +++ b/src/core/component/directives/aria/index.ts @@ -11,30 +11,48 @@ * @packageDocumentation */ +import symbolGenerator from 'core/symbol'; import { ComponentEngine, VNode, VNodeDirective } from 'core/component/engines'; -import { setAriaLabel, setAriaRole, setAriaTabIndex } from 'core/component/directives/aria/helpers'; +import AriaSetter from 'core/component/directives/aria/aria-setter'; + +const + ariaMap = new Map(); + +const + $$ = symbolGenerator(); ComponentEngine.directive('aria', { - inserted(el: Element, opts: VNodeDirective, vnode: VNode): void { + inserted(el: HTMLElement, binding: VNodeDirective, vnode: VNode): void { const - {value, arg, modifiers} = opts; + {value, arg, modifiers} = binding; if (value == null && arg == null && modifiers == null) { return; } const - options = {el, opts, vnode}; + aria = new AriaSetter({el, binding, vnode}); + + aria.init(); - setAriaLabel(options); - setAriaTabIndex(options); - setAriaRole(options)?.init(); + ariaMap.set($$.aria, aria); }, - unbind(el: Element, opts: VNodeDirective, vnode: VNode) { + update(el: HTMLElement, binding: VNodeDirective, vnode: VNode) { const - options = {el, opts, vnode}; + aria: AriaSetter = ariaMap.get($$.aria); + + aria.options = {el, binding, vnode}; + + aria.update(); + }, + + unbind(el: HTMLElement, binding: VNodeDirective, vnode: VNode) { + const + aria: AriaSetter = ariaMap.get($$.aria); + + aria.options = {el, binding, vnode}; - setAriaRole(options)?.clear(); + aria.clear(); } }); diff --git a/src/core/component/directives/aria/interface.ts b/src/core/component/directives/aria/interface.ts index d48eb0748c..499b73f1c7 100644 --- a/src/core/component/directives/aria/interface.ts +++ b/src/core/component/directives/aria/interface.ts @@ -6,19 +6,34 @@ * https://github.com/V4Fire/Client/blob/master/LICENSE */ -import type { VNodeDirective, VNode } from 'core/component/engines'; +import type { VNode, VNodeDirective } from 'core/component/engines'; +import type Async from 'core/async'; -export interface DirectiveHookParams { - el: Element; - opts: VNodeDirective; +export interface DirectiveOptions { + el: HTMLElement; + binding: VNodeDirective; vnode: VNode; } -export interface AriaRoleEngine { - el: Element; - value: any; - vnode: VNode; +export default abstract class AriaRoleEngine { + options: DirectiveOptions; + $a: CanUndef; + + protected constructor(options: DirectiveOptions) { + this.options = options; + } + + abstract init(): void; + update?(): void; + clear?(): void; +} - init(): void; - clear(): void; +export enum keyCodes { + ENTER = 'Enter', + END = 'End', + HOME = 'Home', + LEFT = 'ArrowLeft', + UP = 'ArrowUp', + RIGHT = 'ArrowRight', + DOWN = 'ArrowDown' } diff --git a/src/core/component/directives/aria/roles-engines/combobox.ts b/src/core/component/directives/aria/roles-engines/combobox.ts new file mode 100644 index 0000000000..e0ed0a71d2 --- /dev/null +++ b/src/core/component/directives/aria/roles-engines/combobox.ts @@ -0,0 +1,55 @@ +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ + +import AriaRoleEngine, { DirectiveOptions } from 'core/component/directives/aria/interface'; +import { FOCUSABLE_SELECTOR } from 'traits/i-access/const'; +import type { ComboboxBindingValue } from 'core/component/directives/aria/roles-engines/interface'; + +export default class ComboboxEngine extends AriaRoleEngine { + el: Element; + $v: ComboboxBindingValue; + + constructor(options: DirectiveOptions) { + super(options); + + const + {el} = this.options; + + this.el = el.querySelector(FOCUSABLE_SELECTOR) ?? el; + this.$v = this.options.binding.value; + } + + init(): void { + this.el.setAttribute('role', 'combobox'); + this.el.setAttribute('aria-expanded', 'false'); + + if (this.$v.isMultiple) { + this.el.setAttribute('aria-multiselectable', 'true'); + } + } + + onOpen = (element: HTMLElement): void => { + this.el.setAttribute('aria-expanded', 'true'); + + this.setAriaActive(element); + }; + + onClose = (): void => { + this.el.setAttribute('aria-expanded', 'false'); + + this.setAriaActive(); + }; + + onChange = (element: HTMLElement): void => { + this.setAriaActive(element); + }; + + setAriaActive = (element?: HTMLElement): void => { + this.el.setAttribute('aria-activedescendant', element?.id ?? ''); + }; +} diff --git a/src/core/component/directives/aria/roles-engines/controls.ts b/src/core/component/directives/aria/roles-engines/controls.ts new file mode 100644 index 0000000000..98fc5e68b6 --- /dev/null +++ b/src/core/component/directives/aria/roles-engines/controls.ts @@ -0,0 +1,43 @@ +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ + +import AriaRoleEngine from 'core/component/directives/aria/interface'; + +export default class ControlsEngine extends AriaRoleEngine { + init(): void { + const + {vnode, binding, el} = this.options, + {fakeContext: ctx} = vnode; + + if (binding.modifiers == null) { + Object.throw('Controls aria directive expects the role modifier to be passed'); + return; + } + + if (binding.value?.controls == null) { + Object.throw('Controls aria directive expects the controls value to be passed'); + return; + } + + const + roleName = Object.keys(binding.modifiers)[0]; + + ctx?.$nextTick().then(() => { + const + elems = el.querySelectorAll(`[role=${roleName}]`); + + for (let i = 0; i < elems.length; i++) { + const + elem = elems[i], + {id} = binding.value; + + elem.setAttribute('aria-controls', id); + } + }); + } +} diff --git a/src/core/component/directives/aria/roles-engines/dialog.ts b/src/core/component/directives/aria/roles-engines/dialog.ts index fec7e6a538..9a8d91cfc4 100644 --- a/src/core/component/directives/aria/roles-engines/dialog.ts +++ b/src/core/component/directives/aria/roles-engines/dialog.ts @@ -1,41 +1,29 @@ -import iOpen from 'traits/i-open/i-open'; -import type iBlock from 'super/i-block/i-block'; -import type { DirectiveHookParams } from 'core/component/directives/aria/interface'; -import RoleEngine from 'core/component/directives/aria/roles-engines/interface'; +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ -export default class DialogEngine extends RoleEngine { - group: Dictionary = {}; +import iOpen from 'traits/i-open/i-open'; +import AriaRoleEngine from 'core/component/directives/aria/interface'; +import type { DirectiveOptions } from 'core/component/directives/aria/interface'; - constructor(options: DirectiveHookParams) { +export default class DialogEngine extends AriaRoleEngine { + constructor(options: DirectiveOptions) { super(options); if (!iOpen.is(options.vnode.fakeContext)) { - Object.throw('Dialog directive expects the component to realize iOpen interface'); + Object.throw('Dialog aria directive expects the component to realize iOpen interface'); } } - override init(): void { - const - {localEmitter: $e} = Object.cast(this.vnode.fakeContext); - - this.el.setAttribute('role', 'dialog'); - this.el.setAttribute('aria-modal', 'false'); - - this.group = {group: 'ariaAttributes'}; - - $e.on('open', () => { - this.el.setAttribute('aria-modal', 'true'); - }, this.group); - - $e.on('close', () => { - this.el.setAttribute('aria-modal', 'false'); - }, this.group); - } - - override clear(): void { + init(): void { const - {localEmitter: $e} = Object.cast(this.vnode.fakeContext); + {el} = this.options; - $e.off(this.group); + el.setAttribute('role', 'dialog'); + el.setAttribute('aria-modal', 'true'); } } diff --git a/src/core/component/directives/aria/roles-engines/index.ts b/src/core/component/directives/aria/roles-engines/index.ts index 4ff45a36fc..7e8e48a6f5 100644 --- a/src/core/component/directives/aria/roles-engines/index.ts +++ b/src/core/component/directives/aria/roles-engines/index.ts @@ -1 +1,18 @@ -export * from 'core/component/directives/aria/roles-engines/dialog'; +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ + +export { default as dialog } from 'core/component/directives/aria/roles-engines/dialog'; +export { default as tablist } from 'core/component/directives/aria/roles-engines/tablist'; +export { default as tab } from 'core/component/directives/aria/roles-engines/tab'; +export { default as tabpanel } from 'core/component/directives/aria/roles-engines/tabpanel'; +export { default as controls } from 'core/component/directives/aria/roles-engines/controls'; +export { default as combobox } from 'core/component/directives/aria/roles-engines/combobox'; +export { default as listbox } from 'core/component/directives/aria/roles-engines/listbox'; +export { default as option } from 'core/component/directives/aria/roles-engines/option'; +export { default as tree } from 'core/component/directives/aria/roles-engines/tree'; +export { default as treeitem } from 'core/component/directives/aria/roles-engines/treeitem'; diff --git a/src/core/component/directives/aria/roles-engines/interface.ts b/src/core/component/directives/aria/roles-engines/interface.ts index 3ab1b6f073..e3666bd148 100644 --- a/src/core/component/directives/aria/roles-engines/interface.ts +++ b/src/core/component/directives/aria/roles-engines/interface.ts @@ -1,22 +1,31 @@ -import type { AriaRoleEngine, DirectiveHookParams } from 'core/component/directives/aria/interface'; -import type { VNode } from 'core/component'; +export interface TabBindingValue { + isFirst: boolean; + isVertical: boolean; + activeElement: CanUndef>>; + onChange(cb: Function): void; +} -export default abstract class RoleEngine implements AriaRoleEngine { - el: Element; - value: any; - vnode: VNode; +export interface TablistBindingValue { + isVertical: boolean; + isMultiple: boolean; +} - constructor({el, opts, vnode}: DirectiveHookParams) { - this.el = el; - this.value = opts.value; - this.vnode = vnode; - } +export interface TreeBindingValue { + isVertical: boolean; + isRootTree: boolean; + onChange(cb: Function): void; +} - init(): void { - // - } +export interface TreeitemBindingValue { + isVeryFirstItem: boolean; + getRootElement(): CanUndef; + toggleFold(el: Element, value?: boolean): void; + getFoldedMod(): CanUndef; +} - clear(): void { - // - } +export interface ComboboxBindingValue { + isMultiple: boolean; + onChange(cb: Function): void; + onOpen(cb: Function): void; + onClose(cb: Function): void; } diff --git a/src/core/component/directives/aria/roles-engines/listbox.ts b/src/core/component/directives/aria/roles-engines/listbox.ts new file mode 100644 index 0000000000..203b12cde6 --- /dev/null +++ b/src/core/component/directives/aria/roles-engines/listbox.ts @@ -0,0 +1,19 @@ +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ + +import AriaRoleEngine from 'core/component/directives/aria/interface'; + +export default class ListboxEngine extends AriaRoleEngine { + init(): void { + const + {el} = this.options; + + el.setAttribute('role', 'listbox'); + el.setAttribute('tabindex', '-1'); + } +} diff --git a/src/core/component/directives/aria/roles-engines/option.ts b/src/core/component/directives/aria/roles-engines/option.ts new file mode 100644 index 0000000000..be180c2e80 --- /dev/null +++ b/src/core/component/directives/aria/roles-engines/option.ts @@ -0,0 +1,27 @@ +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ + +import AriaRoleEngine from 'core/component/directives/aria/interface'; + +export default class ListboxEngine extends AriaRoleEngine { + init(): void { + const + {el} = this.options, + {value: {preSelected}} = this.options.binding; + + el.setAttribute('role', 'option'); + el.setAttribute('aria-selected', String(preSelected)); + } + + onChange = (isSelected: boolean): void => { + const + {el} = this.options; + + el.setAttribute('aria-selected', String(isSelected)); + }; +} diff --git a/src/core/component/directives/aria/roles-engines/tab.ts b/src/core/component/directives/aria/roles-engines/tab.ts new file mode 100644 index 0000000000..6e5e43834c --- /dev/null +++ b/src/core/component/directives/aria/roles-engines/tab.ts @@ -0,0 +1,154 @@ +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + * + * This software or document includes material copied from or derived from ["Example of Tabs with Manual Activation", https://www.w3.org/WAI/ARIA/apg/example-index/tabs/tabs-manual.html]. + * Copyright © [2022] W3C® (MIT, ERCIM, Keio, Beihang). + */ + +import AriaRoleEngine, { DirectiveOptions, keyCodes } from 'core/component/directives/aria/interface'; +import type { TabBindingValue } from 'core/component/directives/aria/roles-engines/interface'; +import type iAccess from 'traits/i-access/i-access'; +import type iBlock from 'super/i-block/i-block'; +import { FOCUSABLE_SELECTOR } from 'traits/i-access/const'; + +export default class TabEngine extends AriaRoleEngine { + $v: TabBindingValue; + ctx: iAccess & iBlock; + + constructor(options: DirectiveOptions) { + super(options); + + this.$v = this.options.binding.value; + this.ctx = Object.cast(this.options.vnode.fakeContext); + } + + init(): void { + const + {el} = this.options, + {isFirst} = this.$v; + + el.setAttribute('role', 'tab'); + el.setAttribute('aria-selected', 'false'); + + if (isFirst) { + if (el.tabIndex < 0) { + el.setAttribute('tabindex', '0'); + } + + } else { + el.setAttribute('tabindex', '-1'); + } + + this.$v.activeElement?.then((el) => { + if (Object.isArray(el)) { + for (let i = 0; i < el.length; i++) { + const + activeEl = el[i]; + + if (activeEl.getAttribute('aria-selected') !== 'true') { + activeEl.setAttribute('aria-selected', 'true'); + } + } + + return; + } + + if (el.getAttribute('aria-selected') !== 'true') { + el.setAttribute('aria-selected', 'true'); + } + }); + + if (this.$a != null) { + this.$a.on(el, 'keydown', this.onKeydown); + } + } + + onChange = (active: Element | NodeListOf): void => { + const + {el} = this.options; + + if (Object.isArrayLike(active)) { + for (let i = 0; i < active.length; i++) { + el.setAttribute('aria-selected', String(el === active[i])); + } + + return; + } + + el.setAttribute('aria-selected', String(el === active)); + }; + + moveFocusToFirstTab(): void { + const + firstEl = >this.ctx.$el?.querySelector(FOCUSABLE_SELECTOR); + + firstEl?.focus(); + } + + moveFocusToLastTab(): void { + const + focusable = >>this.ctx.$el?.querySelectorAll(FOCUSABLE_SELECTOR); + + if (focusable != null && focusable.length > 0) { + focusable[focusable.length - 1].focus(); + } + } + + focusNext(): void { + this.ctx.nextFocusableElement(1)?.focus(); + } + + focusPrev(): void { + this.ctx.nextFocusableElement(-1)?.focus(); + } + + onKeydown = (event: Event): void => { + const + evt = (event), + {isVertical} = this.$v; + + switch (evt.key) { + case keyCodes.LEFT: + this.focusPrev(); + break; + + case keyCodes.UP: + if (isVertical) { + this.focusPrev(); + break; + } + + return; + + case keyCodes.RIGHT: + this.focusNext(); + break; + + case keyCodes.DOWN: + if (isVertical) { + this.focusNext(); + break; + } + + return; + + case keyCodes.HOME: + this.moveFocusToFirstTab(); + break; + + case keyCodes.END: + this.moveFocusToLastTab(); + break; + + default: + return; + } + + event.stopPropagation(); + event.preventDefault(); + }; +} diff --git a/src/core/component/directives/aria/roles-engines/tablist.ts b/src/core/component/directives/aria/roles-engines/tablist.ts new file mode 100644 index 0000000000..2105747cb3 --- /dev/null +++ b/src/core/component/directives/aria/roles-engines/tablist.ts @@ -0,0 +1,28 @@ +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ + +import AriaRoleEngine from 'core/component/directives/aria/interface'; +import type { TablistBindingValue } from 'core/component/directives/aria/roles-engines/interface'; + +export default class TablistEngine extends AriaRoleEngine { + init(): void { + const + {el, binding} = this.options, + $v: TablistBindingValue = binding.value; + + el.setAttribute('role', 'tablist'); + + if ($v.isMultiple) { + el.setAttribute('aria-multiselectable', 'true'); + } + + if ($v.isVertical) { + el.setAttribute('aria-orientation', 'vertical'); + } + } +} diff --git a/src/core/component/directives/aria/roles-engines/tabpanel.ts b/src/core/component/directives/aria/roles-engines/tabpanel.ts new file mode 100644 index 0000000000..b128b2b079 --- /dev/null +++ b/src/core/component/directives/aria/roles-engines/tabpanel.ts @@ -0,0 +1,22 @@ +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ + +import AriaRoleEngine from 'core/component/directives/aria/interface'; + +export default class TabpanelEngine extends AriaRoleEngine { + init(): void { + const + {el, binding} = this.options; + + el.setAttribute('role', 'tabpanel'); + + if (binding.value?.labelledby == null) { + Object.throw('Tabpanel aria directive expects "label" or "labelledby" value to be passed'); + } + } +} diff --git a/src/core/component/directives/aria/roles-engines/tree.ts b/src/core/component/directives/aria/roles-engines/tree.ts new file mode 100644 index 0000000000..3ce6bf3a27 --- /dev/null +++ b/src/core/component/directives/aria/roles-engines/tree.ts @@ -0,0 +1,38 @@ +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ + +import AriaRoleEngine, { DirectiveOptions } from 'core/component/directives/aria/interface'; +import type { TreeBindingValue } from 'core/component/directives/aria/roles-engines/interface'; + +export default class TreeEngine extends AriaRoleEngine { + $v: TreeBindingValue; + el: HTMLElement; + + constructor(options: DirectiveOptions) { + super(options); + + this.$v = options.binding.value; + this.el = this.options.el; + } + + init(): void { + this.setRootRole(); + + if (this.$v.isVertical) { + this.el.setAttribute('aria-orientation', 'vertical'); + } + } + + setRootRole(): void { + this.el.setAttribute('role', this.$v.isRootTree ? 'tree' : 'group'); + } + + onChange = (el: HTMLElement, isFolded: boolean): void => { + el.setAttribute('aria-expanded', String(!isFolded)); + }; +} diff --git a/src/core/component/directives/aria/roles-engines/treeitem.ts b/src/core/component/directives/aria/roles-engines/treeitem.ts new file mode 100644 index 0000000000..c1f385d431 --- /dev/null +++ b/src/core/component/directives/aria/roles-engines/treeitem.ts @@ -0,0 +1,208 @@ +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + * + * This software or document includes material copied from or derived from ["Example of Tabs with Manual Activation", https://www.w3.org/WAI/ARIA/apg/example-index/tabs/tabs-manual.html]. + * Copyright © [2022] W3C® (MIT, ERCIM, Keio, Beihang). + */ + +import symbolGenerator from 'core/symbol'; +import AriaRoleEngine, { DirectiveOptions, keyCodes } from 'core/component/directives/aria/interface'; +import iAccess from 'traits/i-access/i-access'; +import { FOCUSABLE_SELECTOR } from 'traits/i-access/const'; +import type { TreeitemBindingValue } from 'core/component/directives/aria/roles-engines/interface'; +import type iBlock from 'super/i-block/i-block'; + +export const + $$ = symbolGenerator(); + +export default class TreeItemEngine extends AriaRoleEngine { + ctx: iAccess & iBlock['unsafe']; + el: HTMLElement; + $v: TreeitemBindingValue; + + constructor(options: DirectiveOptions) { + super(options); + + if (!iAccess.is(options.vnode.fakeContext)) { + Object.throw('Treeitem aria directive expects the component to realize iAccess interface'); + } + + this.ctx = Object.cast(options.vnode.fakeContext); + this.el = this.options.el; + this.$v = this.options.binding.value; + } + + init(): void { + this.$a?.on(this.el, 'keydown', this.onKeyDown); + + const + isMuted = this.ctx.muteTabIndexes(this.el); + + if (this.$v.isVeryFirstItem) { + if (isMuted) { + this.ctx.unmuteTabIndexes(this.el); + + } else { + this.el.tabIndex = 0; + } + } + + this.el.setAttribute('role', 'treeitem'); + + this.ctx.$nextTick(() => { + if (this.isExpandable) { + this.el.setAttribute('aria-expanded', String(this.isExpanded)); + } + }); + } + + onKeyDown = (e: KeyboardEvent): void => { + if (e.altKey || e.ctrlKey || e.metaKey || e.shiftKey) { + return; + } + + switch (e.code) { + case keyCodes.UP: + this.moveFocus(-1); + break; + + case keyCodes.DOWN: + this.moveFocus(1); + break; + + case keyCodes.ENTER: + this.$v.toggleFold(this.el); + break; + + case keyCodes.RIGHT: + if (this.isExpandable) { + if (this.isExpanded) { + this.moveFocus(1); + + } else { + this.openFold(); + } + } + + break; + + case keyCodes.LEFT: + if (this.isExpandable && this.isExpanded) { + this.closeFold(); + + } else { + this.focusParent(); + } + + break; + + case keyCodes.HOME: + void this.setFocusToFirstItem(); + break; + + case keyCodes.END: + void this.setFocusToLastItem(); + break; + + default: + return; + } + + e.stopPropagation(); + e.preventDefault(); + }; + + focusNext(nextEl: HTMLElement): void { + this.ctx.muteTabIndexes(this.el); + this.ctx.unmuteTabIndexes(nextEl); + nextEl.focus(); + } + + moveFocus(step: 1 | -1): void { + const + nextEl = this.ctx.nextFocusableElement(step); + + if (nextEl != null) { + this.focusNext(nextEl); + } + } + + get isExpandable(): boolean { + return this.$v.getFoldedMod() != null; + } + + get isExpanded(): boolean { + return this.$v.getFoldedMod() === 'false'; + } + + openFold(): void { + this.$v.toggleFold(this.el, false); + } + + closeFold(): void { + this.$v.toggleFold(this.el, true); + } + + focusParent(): void { + let + parent = this.el.parentElement; + + while (parent != null) { + if (parent.getAttribute('role') === 'treeitem') { + break; + } + + parent = parent.parentElement; + } + + const + focusableParent = (>parent?.querySelector(FOCUSABLE_SELECTOR)); + + if (focusableParent != null) { + this.focusNext(focusableParent); + } + } + + async setFocusToFirstItem(): Promise { + await this.ctx.async.wait( + this.$v.getRootElement.bind(this), + {label: $$.waitRoot} + ); + + const + firstEl = >this.$v.getRootElement()?.querySelector(FOCUSABLE_SELECTOR); + + if (firstEl != null) { + this.focusNext(firstEl); + } + } + + async setFocusToLastItem(): Promise { + await this.ctx.async.wait( + this.$v.getRootElement.bind(this), + {label: $$.waitRoot} + ); + + const + items = >>this.$v.getRootElement()?.querySelectorAll(FOCUSABLE_SELECTOR); + + const visibleItems: HTMLElement[] = [].filter.call( + items, + (el: HTMLElement) => ( + el.offsetWidth > 0 || + el.offsetHeight > 0 + ) + ); + + const + lastEl = visibleItems.at(-1); + + if (lastEl != null) { + this.focusNext(lastEl); + } + } +} diff --git a/src/core/component/directives/index.ts b/src/core/component/directives/index.ts index 710b6fdb56..6874942b5c 100644 --- a/src/core/component/directives/index.ts +++ b/src/core/component/directives/index.ts @@ -22,8 +22,6 @@ import 'core/component/directives/image'; import 'core/component/directives/update-on'; //#endif -//#if runtime has directives/aria import 'core/component/directives/aria'; -//#endif import 'core/component/directives/hook'; diff --git a/src/form/b-checkbox/CHANGELOG.md b/src/form/b-checkbox/CHANGELOG.md index f39903c9e3..647883be9e 100644 --- a/src/form/b-checkbox/CHANGELOG.md +++ b/src/form/b-checkbox/CHANGELOG.md @@ -9,6 +9,12 @@ Changelog > - :house: [Internal] > - :nail_care: [Polish] +## v3.0.0-rc.??? (2022-??-??) + +#### :bug: Bug Fix + +* Added `for` link for label and `id` for nativeInput in template + ## v3.0.0-rc.199 (2021-06-16) #### :boom: Breaking Change diff --git a/src/form/b-checkbox/b-checkbox.ss b/src/form/b-checkbox/b-checkbox.ss index a670526800..660bb9f08d 100644 --- a/src/form/b-checkbox/b-checkbox.ss +++ b/src/form/b-checkbox/b-checkbox.ss @@ -14,6 +14,16 @@ - nativeInputType = "'checkbox'" - nativeInputModel = undefined + - block hiddenInput() + += self.nativeInput({ & + elName: 'hidden-input', + id: 'id || dom.getId("input")', + + attrs: { + autocomplete: 'off' + } + }) . + - block rootAttrs - super ? rootAttrs[':-parent-id'] = 'parentId' @@ -37,6 +47,8 @@ < _.&__check - block label - < span.&__label v-if = label || vdom.getSlot('label') + < label.&__label & + v-if = label || vdom.getSlot('label') | + :for = id || dom.getId('input') . += self.slot('label', {':label': 'label'}) {{ t(label) }} diff --git a/src/form/b-select/CHANGELOG.md b/src/form/b-select/CHANGELOG.md index 0db12f6a78..4b0b85a820 100644 --- a/src/form/b-select/CHANGELOG.md +++ b/src/form/b-select/CHANGELOG.md @@ -9,6 +9,19 @@ Changelog > - :house: [Internal] > - :nail_care: [Polish] +## v3.0.0-rc.??? (2022-??-??) + +#### :rocket: New Feature + +* Added `v-aria` directive +* Added `onItemMarked` +* Added `onOpen` +* Now the component derive `iAccess` + +#### :bug: Bug Fix + +* Fixed the component to emit `iOpen` events + ## v3.5.3 (2021-10-06) #### :bug: Bug Fix diff --git a/src/form/b-select/b-select.ss b/src/form/b-select/b-select.ss index b89db45cd3..3d6b74dbe8 100644 --- a/src/form/b-select/b-select.ss +++ b/src/form/b-select/b-select.ss @@ -27,10 +27,6 @@ - if tag === 'option' ? itemAttrs[':selected'] = 'isSelected(el.value)' - - else - ? itemAttrs.role = 'option' - ? itemAttrs[':aria-selected'] = 'isSelected(el.value)' - < ${tag} & :-id = values.get(el.value) | @@ -43,7 +39,16 @@ } })) | - :v-attrs = el.attrs | + :v-attrs = native + ? el.attrs + : {'v-aria:option': { + preSelected: isSelected(el.value), + onChange: (cb) => on('actionChange', () => cb(isSelected(el.value))) + }, + ...el.attrs + } | + + :id = dom.getId(el.value) | ${itemAttrs} . += self.slot('default', {':item': 'el'}) @@ -87,12 +92,19 @@ . - block input - < _.&__cell.&__input-wrapper - < template v-if = native + < template v-if = native + < _.&__cell.&__input-wrapper += self.nativeInput({tag: 'select', model: 'undefined', attrs: {'@change': 'onNativeChange'}}) += self.items('option') - < template v-else + < template v-else + < _.&__cell.&__input-wrapper & + v-aria:combobox = { + onOpen, + onClose: (cb) => on('close', cb), + onChange: onItemMarked, + isMultiple: multiple + } . += self.nativeInput({model: 'textStore', attrs: {'@input': 'onSearchInput'}}) - block icon @@ -151,6 +163,7 @@ v-if = !native && items.length && ( isFunctional || opt.ifOnce('opened', m.opened !== 'false') && delete watchModsStore.opened - ) + ) | + v-aria:listbox . += self.items() diff --git a/src/form/b-select/b-select.ts b/src/form/b-select/b-select.ts index 631d29aab8..d20654d3a0 100644 --- a/src/form/b-select/b-select.ts +++ b/src/form/b-select/b-select.ts @@ -257,18 +257,9 @@ class bSelect extends iInputText implements iOpenToggle, iItems { } override get rootAttrs(): Dictionary { - const attrs = { + return { ...super['rootAttrsGetter']() }; - - if (!this.native) { - Object.assign(attrs, { - role: 'listbox', - 'aria-multiselectable': this.multiple - }); - } - - return attrs; } override get value(): this['Value'] { @@ -581,9 +572,6 @@ class bSelect extends iInputText implements iOpenToggle, iItems { if (this.native) { previousItemEl.selected = false; - - } else { - previousItemEl.setAttribute('aria-selected', 'false'); } } } @@ -599,9 +587,6 @@ class bSelect extends iInputText implements iOpenToggle, iItems { if (this.native) { el.selected = true; - - } else { - el.setAttribute('aria-selected', 'true'); } } }).catch(stderr); @@ -688,9 +673,6 @@ class bSelect extends iInputText implements iOpenToggle, iItems { if (this.native) { el.selected = false; - - } else { - el.setAttribute('aria-selected', 'false'); } } } @@ -883,6 +865,9 @@ class bSelect extends iInputText implements iOpenToggle, iItems { protected override initModEvents(): void { super.initModEvents(); + + iOpenToggle.initModEvents(this); + this.sync.mod('native', 'native', Boolean); this.sync.mod('multiple', 'multiple', Boolean); this.sync.mod('opened', 'multiple', Boolean); @@ -980,6 +965,26 @@ class bSelect extends iInputText implements iOpenToggle, iItems { protected onItemsNavigate(e: KeyboardEvent): void { void on.itemsNavigate(this, e); } + + /** + * Handler: executes callback on "open" event + * @param cb + */ + protected onOpen(cb: Function): void { + this.on('open', () => { + void this.$nextTick(() => { + cb.call(this, this.selectedElement); + }); + }); + } + + /** + * Handler: executes callback on item set "marked" mod + * @param cb + */ + protected onItemMarked(cb: Function): void { + this.localEmitter.on('el.mod.set.**', ({link}) => cb(link)); + } } export default bSelect; diff --git a/src/form/b-select/modules/handlers.ts b/src/form/b-select/modules/handlers.ts index f88496340a..3ca6943462 100644 --- a/src/form/b-select/modules/handlers.ts +++ b/src/form/b-select/modules/handlers.ts @@ -225,11 +225,21 @@ export async function itemsNavigate(component: C, e: Keyboard break; case 'ArrowUp': + if (unsafe.mods.opened !== 'true') { + await unsafe.open(); + break; + } + if (currentItemEl?.previousElementSibling != null) { markItem(currentItemEl.previousElementSibling); + } + + if (currentItemEl == null) { + const + items = $b.elements('item'), + lastItem = items[items.length - 1]; - } else { - await unsafe.close(); + markItem(lastItem); } break; @@ -245,7 +255,17 @@ export async function itemsNavigate(component: C, e: Keyboard currentItemEl ??= getMarkedOrSelectedItem(); } - markItem(currentItemEl?.nextElementSibling) || markItem($b.element('item')); + if (currentItemEl?.nextElementSibling != null) { + markItem(currentItemEl.nextElementSibling); + } + + if (currentItemEl == null) { + const + firstItem = $b.elements('item')[0]; + + markItem(firstItem); + } + break; } diff --git a/src/super/i-input/CHANGELOG.md b/src/super/i-input/CHANGELOG.md index 35945e1404..8aeb101da1 100644 --- a/src/super/i-input/CHANGELOG.md +++ b/src/super/i-input/CHANGELOG.md @@ -9,6 +9,12 @@ Changelog > - :house: [Internal] > - :nail_care: [Polish] +## v3.0.0-rc.??? (2021-??-??) + +#### :rocket: New Feature + +* Now the component derive `iAccess` + ## v3.0.0-rc.199 (2021-06-16) #### :boom: Breaking Change diff --git a/src/super/i-input/README.md b/src/super/i-input/README.md index 4719bb593f..12a5abf63b 100644 --- a/src/super/i-input/README.md +++ b/src/super/i-input/README.md @@ -459,7 +459,7 @@ You can also manage a type of the created tag and other options by using the pre * *) [type=nativeInputType] - value of the `:type` attribute * * *) [autofocus] - value of the `:autofocus` attribute - * *) [tabIndex] - value of the `:autofocus` attribute + * *) [tabIndex] - value of the `:tabindex` attribute * * *) [focusHandler] - value of the `@focus` attribute * *) [blurHandler] - value of the `@blur` attribute diff --git a/src/super/i-input/i-input.ts b/src/super/i-input/i-input.ts index a7828e61db..df9e60e4e6 100644 --- a/src/super/i-input/i-input.ts +++ b/src/super/i-input/i-input.ts @@ -16,6 +16,7 @@ import SyncPromise from 'core/promise/sync'; import { Option } from 'core/prelude/structures'; +import { derive } from 'core/functools/trait'; import iAccess from 'traits/i-access/i-access'; import iVisible from 'traits/i-visible/i-visible'; @@ -62,6 +63,8 @@ export * from 'super/i-input/interface'; export const $$ = symbolGenerator(); +interface iInput extends Trait {} + /** * Superclass for all form components */ @@ -76,7 +79,8 @@ export const } }) -export default abstract class iInput extends iData implements iVisible, iAccess { +@derive(iAccess) +abstract class iInput extends iData implements iVisible, iAccess { /** * Type: component value */ @@ -1040,3 +1044,5 @@ export default abstract class iInput extends iData implements iVisible, iAccess } } } + +export default iInput; diff --git a/src/traits/i-access/CHANGELOG.md b/src/traits/i-access/CHANGELOG.md index 3d5a9e651b..0a0c8f3527 100644 --- a/src/traits/i-access/CHANGELOG.md +++ b/src/traits/i-access/CHANGELOG.md @@ -9,6 +9,15 @@ Changelog > - :house: [Internal] > - :nail_care: [Polish] +## v3.0.0-rc.??? (2022-??-??) + +#### :rocket: New Feature + +* Added `muteTabIndexes` +* Added `unmuteTabIndexes` +* Added `nextFocusableElement` +* Added `is` + ## v3.0.0-rc.211 (2021-07-21) * Now the trait uses `aria` attributes diff --git a/src/traits/i-access/const.ts b/src/traits/i-access/const.ts new file mode 100644 index 0000000000..e4b3110847 --- /dev/null +++ b/src/traits/i-access/const.ts @@ -0,0 +1,2 @@ +export const + FOCUSABLE_SELECTOR = '[tabindex]:not([disabled]), a:not([disabled]), button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled])'; diff --git a/src/traits/i-access/i-access.ts b/src/traits/i-access/i-access.ts index 489836c10b..61b8f43327 100644 --- a/src/traits/i-access/i-access.ts +++ b/src/traits/i-access/i-access.ts @@ -17,6 +17,7 @@ import SyncPromise from 'core/promise/sync'; import type iBlock from 'super/i-block/i-block'; import type { ModsDecl, ModEvent } from 'super/i-block/i-block'; +import { FOCUSABLE_SELECTOR } from 'traits/i-access/const'; export default abstract class iAccess { /** @@ -147,10 +148,10 @@ export default abstract class iAccess { return; } - if ($el.hasAttribute('tab-index')) { - const - el = ($el); + const + el = ($el); + if (el.hasAttribute('tabindex') || el.tabIndex > -1) { if (focused) { el.focus(); @@ -162,6 +163,114 @@ export default abstract class iAccess { }); } + /** @see [[iAccess.muteTabIndexes]] */ + static muteTabIndexes: AddSelf = + (component, ctx?): boolean => { + const + el = ctx ?? component.$el; + + if (el == null) { + return false; + } + + const + elems = el.querySelectorAll(FOCUSABLE_SELECTOR); + + for (let i = 0; i < elems.length; i++) { + const + elem = (elems[i]); + + if (elem.dataset.tabindex == null) { + elem.dataset.tabindex = String(elem.tabIndex); + } + + elem.tabIndex = -1; + } + + if (ctx != null && ctx.tabIndex > -1) { + if (ctx.dataset.tabindex == null) { + ctx.dataset.tabindex = String(ctx.tabIndex); + } + + ctx.tabIndex = -1; + + return true; + } + + return elems.length > 0; + }; + + /** @see [[iAccess.unmuteTabIndexes]] */ + static unmuteTabIndexes: AddSelf = + (component, ctx?): boolean => { + const + el = ctx ?? component.$el; + + if (el == null) { + return false; + } + + const + elems = el.querySelectorAll('[data-tabindex]'); + + for (let i = 0; i < elems.length; i++) { + const + elem = (elems[i]); + + elem.tabIndex = Number(elem.dataset.tabindex); + delete elem.dataset.tabindex; + } + + if (ctx?.dataset.tabindex != null) { + ctx.tabIndex = Number(ctx.dataset.tabindex); + delete ctx.dataset.tabindex; + + return true; + } + + return elems.length > 0; + }; + + /** @see [[iAccess.unmuteTabIndexes]] */ + static nextFocusableElement: AddSelf = + (component, step, el?): CanUndef => { + if (document.activeElement == null) { + return; + } + + const + nodeListOfFocusable = (el ?? document).querySelectorAll(FOCUSABLE_SELECTOR); + + const focusable: HTMLElement[] = [].filter.call( + nodeListOfFocusable, + (el: HTMLElement) => ( + el.offsetWidth > 0 || + el.offsetHeight > 0 || + el === document.activeElement + ) + ); + + const + index = focusable.indexOf(document.activeElement); + + if (index > -1) { + return focusable[index + step]; + } + }; + + /** + * Checks if the component realize current trait + * @param obj + */ + static is(obj: unknown): obj is iAccess { + if (Object.isPrimitive(obj)) { + return false; + } + + const dict = Object.cast(obj); + return Object.isFunction(dict.muteTabIndexes) && Object.isFunction(dict.nextFocusableElement); + } + /** * A Boolean attribute which, if present, indicates that the component should automatically * have focus when the page has finished loading (or when the `

    ` containing the element has been displayed) @@ -218,4 +327,28 @@ export default abstract class iAccess { blur(...args: unknown[]): Promise { return Object.throw(); } + + /** + * Remove all descendants with tabindex attribute from tab sequence and saves previous value. + * @param el + */ + muteTabIndexes(el?: HTMLElement): boolean { + return Object.throw(); + } + + /** + * Recovers previous saved tabindex values to the elements that were changed. + * @param el + */ + unmuteTabIndexes(el?: HTMLElement): boolean { + return Object.throw(); + } + + /** + * Sets the focus to the next or previous focusable element via the step parameter + * @params step, el? + */ + nextFocusableElement(step: 1 | -1, el?: HTMLElement): CanUndef { + return Object.throw(); + } } diff --git a/src/traits/i-open/CHANGELOG.md b/src/traits/i-open/CHANGELOG.md index f435f65495..7f2f3680c6 100644 --- a/src/traits/i-open/CHANGELOG.md +++ b/src/traits/i-open/CHANGELOG.md @@ -9,6 +9,12 @@ Changelog > - :house: [Internal] > - :nail_care: [Polish] +## v3.0.0-rc.??? (2022-??-??) + +#### :rocket: New Feature + +* Added `is` + ## v3.0.0-rc.184 (2021-05-12) #### :rocket: New Feature diff --git a/src/traits/i-open/i-open.ts b/src/traits/i-open/i-open.ts index 96efa485ea..93ef25a1a6 100644 --- a/src/traits/i-open/i-open.ts +++ b/src/traits/i-open/i-open.ts @@ -136,12 +136,11 @@ export default abstract class iOpen { /** * Checks if the component realize current trait - * * @param obj */ static is(obj: unknown): obj is iOpen { if (Object.isPrimitive(obj)) { - return true; + return false; } const dict = Object.cast(obj); From 3916298d0f72d362f7de896271925852d0f37658 Mon Sep 17 00:00:00 2001 From: Aleksandr Bunin Date: Thu, 21 Jul 2022 19:34:28 +0300 Subject: [PATCH 074/185] fix tests --- src/base/b-list/b-list.ss | 16 ++++++-------- src/base/b-tree/b-tree.ss | 22 +++++++++---------- .../aria/roles-engines/interface.ts | 3 ++- .../directives/aria/roles-engines/treeitem.ts | 18 +++++---------- src/form/b-checkbox/CHANGELOG.md | 2 +- src/form/b-checkbox/b-checkbox.ss | 3 ++- src/form/b-checkbox/b-checkbox.ts | 7 ++++++ 7 files changed, 35 insertions(+), 36 deletions(-) diff --git a/src/base/b-list/b-list.ss b/src/base/b-list/b-list.ss index a41fc70787..4d42dd8b02 100644 --- a/src/base/b-list/b-list.ss +++ b/src/base/b-list/b-list.ss @@ -48,15 +48,14 @@ })) | :v-attrs = isTablist - ? { + ? Object.assign(el.attrs, { 'v-aria:tab': { isFirst: i === 0, isVertical: vertical, onChange: onActiveChange, activeElement - }, - ...el.attrs - } + } + }) : el.attrs . - block preIcon @@ -111,13 +110,12 @@ < tag.&__wrapper & :is = listTag | :v-attrs = isTablist - ? { + ? Object.assign(attrs, { 'v-aria:tablist': { isMultiple: multiple, - isVertical: vertical - }, - ...attrs - } + isVertical: vertical + } + }) : attrs . += self.list('items') diff --git a/src/base/b-tree/b-tree.ss b/src/base/b-tree/b-tree.ss index 4ba11ef0a9..287def532d 100644 --- a/src/base/b-tree/b-tree.ss +++ b/src/base/b-tree/b-tree.ss @@ -29,14 +29,15 @@ :class = provide.elClasses({ node: { level, - folded: el.children && getFoldedPropValue(el) + folded: getFoldedPropValue(el) } }) | v-aria:treeitem = { getRootElement: () => (top ? top.$el : $el), - toggleFold: changeFoldedMod.bind(this, el), - getFoldedMod: getFoldedModById.bind(this, el.id), - isVeryFirstItem: top == null && i === 0, + toggleFold: changeFoldedMod.bind(this, el) , + isExpanded: () => getFoldedModById(el.id) === 'false', + isExpandable: el.children != null, + isVeryFirstItem: top == null && i === 0 } . < .&__item-wrapper @@ -51,8 +52,7 @@ < component.&__item & v-if = item | :is = Object.isFunction(item) ? item(el, i) : item | - :v-attrs = getItemProps(el, i) | - dispatching = true + :v-attrs = getItemProps(el, i) . - block children @@ -69,8 +69,8 @@ . += self.slot('default', {':item': 'o.item'}) - < template & - #fold = o | - v-if = vdom.getSlot('fold') - . - += self.slot('fold', {':params': 'o.params'}) + < template & + #fold = o | + v-if = vdom.getSlot('fold') + . + += self.slot('fold', {':params': 'o.params'}) diff --git a/src/core/component/directives/aria/roles-engines/interface.ts b/src/core/component/directives/aria/roles-engines/interface.ts index e3666bd148..293701d0cb 100644 --- a/src/core/component/directives/aria/roles-engines/interface.ts +++ b/src/core/component/directives/aria/roles-engines/interface.ts @@ -18,9 +18,10 @@ export interface TreeBindingValue { export interface TreeitemBindingValue { isVeryFirstItem: boolean; + isExpandable: boolean; + isExpanded(): boolean; getRootElement(): CanUndef; toggleFold(el: Element, value?: boolean): void; - getFoldedMod(): CanUndef; } export interface ComboboxBindingValue { diff --git a/src/core/component/directives/aria/roles-engines/treeitem.ts b/src/core/component/directives/aria/roles-engines/treeitem.ts index c1f385d431..78b61b5145 100644 --- a/src/core/component/directives/aria/roles-engines/treeitem.ts +++ b/src/core/component/directives/aria/roles-engines/treeitem.ts @@ -54,8 +54,8 @@ export default class TreeItemEngine extends AriaRoleEngine { this.el.setAttribute('role', 'treeitem'); this.ctx.$nextTick(() => { - if (this.isExpandable) { - this.el.setAttribute('aria-expanded', String(this.isExpanded)); + if (this.$v.isExpandable) { + this.el.setAttribute('aria-expanded', String(this.$v.isExpanded())); } }); } @@ -79,8 +79,8 @@ export default class TreeItemEngine extends AriaRoleEngine { break; case keyCodes.RIGHT: - if (this.isExpandable) { - if (this.isExpanded) { + if (this.$v.isExpandable) { + if (this.$v.isExpanded()) { this.moveFocus(1); } else { @@ -91,7 +91,7 @@ export default class TreeItemEngine extends AriaRoleEngine { break; case keyCodes.LEFT: - if (this.isExpandable && this.isExpanded) { + if (this.$v.isExpandable && this.$v.isExpanded()) { this.closeFold(); } else { @@ -131,14 +131,6 @@ export default class TreeItemEngine extends AriaRoleEngine { } } - get isExpandable(): boolean { - return this.$v.getFoldedMod() != null; - } - - get isExpanded(): boolean { - return this.$v.getFoldedMod() === 'false'; - } - openFold(): void { this.$v.toggleFold(this.el, false); } diff --git a/src/form/b-checkbox/CHANGELOG.md b/src/form/b-checkbox/CHANGELOG.md index 647883be9e..5a05e47de2 100644 --- a/src/form/b-checkbox/CHANGELOG.md +++ b/src/form/b-checkbox/CHANGELOG.md @@ -13,7 +13,7 @@ Changelog #### :bug: Bug Fix -* Added `for` link for label and `id` for nativeInput in template +* Added `label` tag with `for` attribute to label and `id` to nativeInput in template ## v3.0.0-rc.199 (2021-06-16) diff --git a/src/form/b-checkbox/b-checkbox.ss b/src/form/b-checkbox/b-checkbox.ss index 660bb9f08d..8a92080ac7 100644 --- a/src/form/b-checkbox/b-checkbox.ss +++ b/src/form/b-checkbox/b-checkbox.ss @@ -49,6 +49,7 @@ - block label < label.&__label & v-if = label || vdom.getSlot('label') | - :for = id || dom.getId('input') . + :for = id || dom.getId('input') + . += self.slot('label', {':label': 'label'}) {{ t(label) }} diff --git a/src/form/b-checkbox/b-checkbox.ts b/src/form/b-checkbox/b-checkbox.ts index a34a2657b6..8e712e8ee4 100644 --- a/src/form/b-checkbox/b-checkbox.ts +++ b/src/form/b-checkbox/b-checkbox.ts @@ -316,6 +316,13 @@ export default class bCheckbox extends iInput implements iSize { */ // eslint-disable-next-line @typescript-eslint/no-unused-vars-experimental protected onClick(e: Event): void { + const + target = e.target; + + if (target.tagName === 'LABEL') { + e.preventDefault(); + } + void this.focus(); if (this.value === undefined || this.value === false || this.changeable) { From b2f67dec0dc3645ec941556c555068c37c909b24 Mon Sep 17 00:00:00 2001 From: Aleksandr Bunin Date: Fri, 22 Jul 2022 08:04:48 +0300 Subject: [PATCH 075/185] fix checkbox styles --- src/form/b-checkbox/b-checkbox.styl | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/form/b-checkbox/b-checkbox.styl b/src/form/b-checkbox/b-checkbox.styl index 70b1c55b59..6d1605175d 100644 --- a/src/form/b-checkbox/b-checkbox.styl +++ b/src/form/b-checkbox/b-checkbox.styl @@ -17,13 +17,14 @@ b-checkbox extends i-input contain paint position relative + &__wrapper, &__checkbox, &__label + cursor pointer + &__wrapper display flex - cursor pointer &__checkbox display block - cursor pointer &__label user-select none From d3ef58e9c3125d6abab13acc96b8229d3c695a3a Mon Sep 17 00:00:00 2001 From: Aleksandr Bunin Date: Fri, 22 Jul 2022 13:57:26 +0300 Subject: [PATCH 076/185] fix b-list --- src/base/b-list/b-list.ss | 20 ++++++++++-------- src/base/b-list/b-list.ts | 2 +- .../aria/roles-engines/interface.ts | 2 +- .../directives/aria/roles-engines/tab.ts | 21 +------------------ 4 files changed, 14 insertions(+), 31 deletions(-) diff --git a/src/base/b-list/b-list.ss b/src/base/b-list/b-list.ss index 4d42dd8b02..1b01addf63 100644 --- a/src/base/b-list/b-list.ss +++ b/src/base/b-list/b-list.ss @@ -47,15 +47,16 @@ } })) | - :v-attrs = isTablist - ? Object.assign(el.attrs, { + :v-attrs = isTablist() + ? { 'v-aria:tab': { isFirst: i === 0, isVertical: vertical, onChange: onActiveChange, - activeElement - } - }) + isActive: isActive(el.value) + }, + ...el.attrs + } : el.attrs . - block preIcon @@ -109,13 +110,14 @@ < tag.&__wrapper & :is = listTag | - :v-attrs = isTablist - ? Object.assign(attrs, { + :v-attrs = isTablist() + ? { 'v-aria:tablist': { isMultiple: multiple, isVertical: vertical - } - }) + }, + ...attrs + } : attrs . += self.list('items') diff --git a/src/base/b-list/b-list.ts b/src/base/b-list/b-list.ts index cd5ef5460b..aad3f29916 100644 --- a/src/base/b-list/b-list.ts +++ b/src/base/b-list/b-list.ts @@ -694,7 +694,7 @@ class bList extends iData implements iVisible, iWidth, iItems, iAccess { /** * Returns true if the component is used as tab list */ - protected get isTablist(): boolean { + protected isTablist(): boolean { return this.items.some((el) => el.href === undefined); } diff --git a/src/core/component/directives/aria/roles-engines/interface.ts b/src/core/component/directives/aria/roles-engines/interface.ts index 293701d0cb..4b7c81c3c1 100644 --- a/src/core/component/directives/aria/roles-engines/interface.ts +++ b/src/core/component/directives/aria/roles-engines/interface.ts @@ -1,7 +1,7 @@ export interface TabBindingValue { isFirst: boolean; isVertical: boolean; - activeElement: CanUndef>>; + isActive: boolean; onChange(cb: Function): void; } diff --git a/src/core/component/directives/aria/roles-engines/tab.ts b/src/core/component/directives/aria/roles-engines/tab.ts index 6e5e43834c..083952a10d 100644 --- a/src/core/component/directives/aria/roles-engines/tab.ts +++ b/src/core/component/directives/aria/roles-engines/tab.ts @@ -32,7 +32,7 @@ export default class TabEngine extends AriaRoleEngine { {isFirst} = this.$v; el.setAttribute('role', 'tab'); - el.setAttribute('aria-selected', 'false'); + el.setAttribute('aria-selected', String(this.$v.isActive)); if (isFirst) { if (el.tabIndex < 0) { @@ -43,25 +43,6 @@ export default class TabEngine extends AriaRoleEngine { el.setAttribute('tabindex', '-1'); } - this.$v.activeElement?.then((el) => { - if (Object.isArray(el)) { - for (let i = 0; i < el.length; i++) { - const - activeEl = el[i]; - - if (activeEl.getAttribute('aria-selected') !== 'true') { - activeEl.setAttribute('aria-selected', 'true'); - } - } - - return; - } - - if (el.getAttribute('aria-selected') !== 'true') { - el.setAttribute('aria-selected', 'true'); - } - }); - if (this.$a != null) { this.$a.on(el, 'keydown', this.onKeydown); } From 80979b8fe5945a7bf3d300642006cd93b9e45873 Mon Sep 17 00:00:00 2001 From: Aleksandr Bunin Date: Sun, 24 Jul 2022 10:12:02 +0300 Subject: [PATCH 077/185] refactoring --- src/base/b-list/b-list.ss | 16 +--- src/base/b-list/b-list.ts | 67 +++++++++++---- src/base/b-tree/b-tree.ss | 14 +--- src/base/b-tree/b-tree.ts | 84 +++++++++++++++---- .../component/directives/aria/aria-setter.ts | 10 +-- .../component/directives/aria/interface.ts | 6 ++ .../directives/aria/roles-engines/combobox.ts | 2 - .../aria/roles-engines/interface.ts | 18 ++-- .../directives/aria/roles-engines/option.ts | 4 +- .../directives/aria/roles-engines/tree.ts | 2 +- .../directives/aria/roles-engines/treeitem.ts | 30 +++---- src/form/b-select/b-select.ss | 18 ++-- src/form/b-select/b-select.ts | 60 ++++++++----- 13 files changed, 202 insertions(+), 129 deletions(-) diff --git a/src/base/b-list/b-list.ss b/src/base/b-list/b-list.ss index 1b01addf63..b702cfa292 100644 --- a/src/base/b-list/b-list.ss +++ b/src/base/b-list/b-list.ss @@ -47,14 +47,9 @@ } })) | - :v-attrs = isTablist() + :v-attrs = isTablist ? { - 'v-aria:tab': { - isFirst: i === 0, - isVertical: vertical, - onChange: onActiveChange, - isActive: isActive(el.value) - }, + 'v-aria:tab': getAriaOpt('tab', el, i), ...el.attrs } : el.attrs @@ -110,12 +105,9 @@ < tag.&__wrapper & :is = listTag | - :v-attrs = isTablist() + :v-attrs = isTablist ? { - 'v-aria:tablist': { - isMultiple: multiple, - isVertical: vertical - }, + 'v-aria:tablist': getAriaOpt('tablist'), ...attrs } : attrs diff --git a/src/base/b-list/b-list.ts b/src/base/b-list/b-list.ts index aad3f29916..dc370821bd 100644 --- a/src/base/b-list/b-list.ts +++ b/src/base/b-list/b-list.ts @@ -694,7 +694,7 @@ class bList extends iData implements iVisible, iWidth, iItems, iAccess { /** * Returns true if the component is used as tab list */ - protected isTablist(): boolean { + protected get isTablist(): boolean { return this.items.some((el) => el.href === undefined); } @@ -711,30 +711,47 @@ class bList extends iData implements iVisible, iWidth, iItems, iAccess { } /** - * Handler: click to some item element + * Returns a dictionary with options for aria directive for tab role + * @param role + */ + protected getAriaOpt(role: 'tab'): Dictionary; + + /** + * Returns a dictionary with options for aria directive for tablist role * - * @param e - * @emits `actionChange(active: this['Active'])` + * @param role + * @param item + * @param i - position index */ - @watch({ - field: '?$el:click', - wrapper: (o, cb) => o.dom.delegateElement('link', cb) - }) + protected getAriaOpt(role: 'tablist', item: this['Item'], i: number): Dictionary; - protected onItemClick(e: Event): void { + protected getAriaOpt(role: 'tab' | 'tablist', item?: this['Item'], i?: number): Dictionary { const - target = e.delegateTarget, - id = Number(target.getAttribute('data-id')); + isActive = this.isActive.bind(this, item?.value); + + const opts = { + tablist: { + isMultiple: this.multiple, + isVertical: this.vertical + }, + tab: { + isFirst: i === 0, + isVertical: this.vertical, + changeEvent: this.bindToChange.bind(this), + get isActive() { + return isActive(); + } + } + }; - this.toggleActive(this.indexes[id]); - this.emit('actionChange', this.active); + return opts[role]; } /** - * Handler: on active element changes + * Binds callback to change event * @param cb */ - protected onActiveChange(cb: Function): void { + protected bindToChange(cb: Function): void { this.on('change', () => { if (Object.isSet(this.active)) { cb(this.block?.elements('link', {active: true})); @@ -744,6 +761,26 @@ class bList extends iData implements iVisible, iWidth, iItems, iAccess { } }); } + + /** + * Handler: click to some item element + * + * @param e + * @emits `actionChange(active: this['Active'])` + */ + @watch({ + field: '?$el:click', + wrapper: (o, cb) => o.dom.delegateElement('link', cb) + }) + + protected onItemClick(e: Event): void { + const + target = e.delegateTarget, + id = Number(target.getAttribute('data-id')); + + this.toggleActive(this.indexes[id]); + this.emit('actionChange', this.active); + } } export default bList; diff --git a/src/base/b-tree/b-tree.ss b/src/base/b-tree/b-tree.ss index 287def532d..8b9146a4e9 100644 --- a/src/base/b-tree/b-tree.ss +++ b/src/base/b-tree/b-tree.ss @@ -13,11 +13,7 @@ - template index() extends ['i-data'].index - block body < .&__root & - v-aria:tree = { - isVertical: vertical, - isRootTree: top == null, - onChange: (cb) => on('fold', (ctx, el, item, value) => cb(el, value)) - } + v-aria:tree = getAriaOpt('tree') . < template & v-for = (el, i) in asyncRender.iterate(items, renderChunks, renderTaskParams) | @@ -32,13 +28,7 @@ folded: getFoldedPropValue(el) } }) | - v-aria:treeitem = { - getRootElement: () => (top ? top.$el : $el), - toggleFold: changeFoldedMod.bind(this, el) , - isExpanded: () => getFoldedModById(el.id) === 'false', - isExpandable: el.children != null, - isVeryFirstItem: top == null && i === 0 - } + v-aria:treeitem = getAriaOpt('treeitem', el, i) . < .&__item-wrapper < .&__marker diff --git a/src/base/b-tree/b-tree.ts b/src/base/b-tree/b-tree.ts index fae29b1f57..f4d993baaa 100644 --- a/src/base/b-tree/b-tree.ts +++ b/src/base/b-tree/b-tree.ts @@ -250,27 +250,12 @@ class bTree extends iData implements iItems, iAccess { return this.$el?.querySelector(`[data-id=${itemId}]`) ?? undefined; } - /** - * Handler: fold element has been clicked - * - * @param item - * @emits `fold(target: HTMLElement, item:` [[Item]]`, value: boolean)` - */ - protected onFoldClick(item: this['Item']): void { - const - target = this.findItemElement(item.id), - newVal = this.getFoldedModById(item.id) === 'false'; - - if (target) { - this.block?.setElMod(target, 'node', 'folded', newVal); - this.emit('fold', target, item, newVal); - } - } - /** * Toggle folded state * - * @params target, value + * @param item + * @param target + * @param value? * @emits `fold(target: HTMLElement, item:` [[Item]]`, value: boolean)` */ protected changeFoldedMod(item: this['Item'], target: HTMLElement, value?: boolean): void { @@ -287,6 +272,69 @@ class bTree extends iData implements iItems, iAccess { this.block?.setElMod(target, 'node', 'folded', newVal); this.emit('fold', target, item, newVal); } + + /** + * Returns a dictionary with options for aria directive for tree role + * @param role + */ + protected getAriaOpt(role: 'tree'): Dictionary + + /** + * Returns a dictionary with options for aria directive for treeitem role + * + * @param role + * @param item + * @param i - position index + */ + protected getAriaOpt(role: 'treeitem', item: this['Item'], i: number): Dictionary + + protected getAriaOpt(role: 'tree' | 'treeitem', item?: this['Item'], i?: number): Dictionary { + const + getFoldedMod = this.getFoldedModById.bind(this, item?.id), + root = () => this.top?.$el ?? this.$el; + + const opts = { + tree: { + isVertical: this.vertical, + isRoot: this.top == null, + changeEvent: (cb: Function) => { + this.on('fold', (ctx, el, item, value) => cb(el, value)); + } + }, + treeitem: { + isRootFirstItem: this.top == null && i === 0, + toggleFold: this.changeFoldedMod.bind(this, item), + get rootElement() { + return root(); + }, + get isExpanded() { + return getFoldedMod() === 'false'; + }, + get isExpandable() { + return item?.children != null; + } + } + }; + + return opts[role]; + } + + /** + * Handler: fold element has been clicked + * + * @param item + * @emits `fold(target: HTMLElement, item:` [[Item]]`, value: boolean)` + */ + protected onFoldClick(item: this['Item']): void { + const + target = this.findItemElement(item.id), + newVal = this.getFoldedModById(item.id) === 'false'; + + if (target) { + this.block?.setElMod(target, 'node', 'folded', newVal); + this.emit('fold', target, item, newVal); + } + } } export default bTree; diff --git a/src/core/component/directives/aria/aria-setter.ts b/src/core/component/directives/aria/aria-setter.ts index dbd1d87292..a3f73a1f89 100644 --- a/src/core/component/directives/aria/aria-setter.ts +++ b/src/core/component/directives/aria/aria-setter.ts @@ -8,7 +8,7 @@ import * as ariaRoles from 'core/component/directives/aria/roles-engines'; import Async from 'core/async'; -import AriaRoleEngine from 'core/component/directives/aria/interface'; +import AriaRoleEngine, { eventsNames } from 'core/component/directives/aria/interface'; import type iBlock from 'super/i-block/i-block'; import type { DirectiveOptions } from 'core/component/directives/aria/interface'; @@ -62,11 +62,11 @@ export default class AriaSetter extends AriaRoleEngine { const $v = this.options.binding.value; - for (const p in $v) { - if (p === 'onOpen' || p === 'onClose' || p === 'onChange') { + for (const key in $v) { + if (key in eventsNames) { const - callback = this.role[p], - property = $v[p]; + callback = this.role[eventsNames[key]], + property = $v[key]; if (Object.isFunction(property)) { property(callback); diff --git a/src/core/component/directives/aria/interface.ts b/src/core/component/directives/aria/interface.ts index 499b73f1c7..3ced68a655 100644 --- a/src/core/component/directives/aria/interface.ts +++ b/src/core/component/directives/aria/interface.ts @@ -37,3 +37,9 @@ export enum keyCodes { RIGHT = 'ArrowRight', DOWN = 'ArrowDown' } + +export enum eventsNames { + openEvent = 'onOpen', + closeEvent = 'onClose', + changeEvent = 'onChange' +} diff --git a/src/core/component/directives/aria/roles-engines/combobox.ts b/src/core/component/directives/aria/roles-engines/combobox.ts index e0ed0a71d2..c63785b16f 100644 --- a/src/core/component/directives/aria/roles-engines/combobox.ts +++ b/src/core/component/directives/aria/roles-engines/combobox.ts @@ -35,13 +35,11 @@ export default class ComboboxEngine extends AriaRoleEngine { onOpen = (element: HTMLElement): void => { this.el.setAttribute('aria-expanded', 'true'); - this.setAriaActive(element); }; onClose = (): void => { this.el.setAttribute('aria-expanded', 'false'); - this.setAriaActive(); }; diff --git a/src/core/component/directives/aria/roles-engines/interface.ts b/src/core/component/directives/aria/roles-engines/interface.ts index 4b7c81c3c1..f135f22028 100644 --- a/src/core/component/directives/aria/roles-engines/interface.ts +++ b/src/core/component/directives/aria/roles-engines/interface.ts @@ -2,7 +2,7 @@ export interface TabBindingValue { isFirst: boolean; isVertical: boolean; isActive: boolean; - onChange(cb: Function): void; + changeEvent(cb: Function): void; } export interface TablistBindingValue { @@ -11,22 +11,22 @@ export interface TablistBindingValue { } export interface TreeBindingValue { + isRoot: boolean; isVertical: boolean; - isRootTree: boolean; - onChange(cb: Function): void; + changeEvent(cb: Function): void; } export interface TreeitemBindingValue { - isVeryFirstItem: boolean; + isRootFirstItem: boolean; isExpandable: boolean; - isExpanded(): boolean; - getRootElement(): CanUndef; + isExpanded: boolean; + rootElement: CanUndef; toggleFold(el: Element, value?: boolean): void; } export interface ComboboxBindingValue { isMultiple: boolean; - onChange(cb: Function): void; - onOpen(cb: Function): void; - onClose(cb: Function): void; + changeEvent(cb: Function): void; + openEvent(cb: Function): void; + closeEvent(cb: Function): void; } diff --git a/src/core/component/directives/aria/roles-engines/option.ts b/src/core/component/directives/aria/roles-engines/option.ts index be180c2e80..522bbb1e1a 100644 --- a/src/core/component/directives/aria/roles-engines/option.ts +++ b/src/core/component/directives/aria/roles-engines/option.ts @@ -8,7 +8,7 @@ import AriaRoleEngine from 'core/component/directives/aria/interface'; -export default class ListboxEngine extends AriaRoleEngine { +export default class OptionEngine extends AriaRoleEngine { init(): void { const {el} = this.options, @@ -21,7 +21,7 @@ export default class ListboxEngine extends AriaRoleEngine { onChange = (isSelected: boolean): void => { const {el} = this.options; - + console.log(isSelected) el.setAttribute('aria-selected', String(isSelected)); }; } diff --git a/src/core/component/directives/aria/roles-engines/tree.ts b/src/core/component/directives/aria/roles-engines/tree.ts index 3ce6bf3a27..09affe7603 100644 --- a/src/core/component/directives/aria/roles-engines/tree.ts +++ b/src/core/component/directives/aria/roles-engines/tree.ts @@ -29,7 +29,7 @@ export default class TreeEngine extends AriaRoleEngine { } setRootRole(): void { - this.el.setAttribute('role', this.$v.isRootTree ? 'tree' : 'group'); + this.el.setAttribute('role', this.$v.isRoot ? 'tree' : 'group'); } onChange = (el: HTMLElement, isFolded: boolean): void => { diff --git a/src/core/component/directives/aria/roles-engines/treeitem.ts b/src/core/component/directives/aria/roles-engines/treeitem.ts index 78b61b5145..7d4247e5c2 100644 --- a/src/core/component/directives/aria/roles-engines/treeitem.ts +++ b/src/core/component/directives/aria/roles-engines/treeitem.ts @@ -42,7 +42,7 @@ export default class TreeItemEngine extends AriaRoleEngine { const isMuted = this.ctx.muteTabIndexes(this.el); - if (this.$v.isVeryFirstItem) { + if (this.$v.isRootFirstItem) { if (isMuted) { this.ctx.unmuteTabIndexes(this.el); @@ -55,7 +55,7 @@ export default class TreeItemEngine extends AriaRoleEngine { this.ctx.$nextTick(() => { if (this.$v.isExpandable) { - this.el.setAttribute('aria-expanded', String(this.$v.isExpanded())); + this.el.setAttribute('aria-expanded', String(this.$v.isExpanded)); } }); } @@ -80,7 +80,7 @@ export default class TreeItemEngine extends AriaRoleEngine { case keyCodes.RIGHT: if (this.$v.isExpandable) { - if (this.$v.isExpanded()) { + if (this.$v.isExpanded) { this.moveFocus(1); } else { @@ -91,7 +91,7 @@ export default class TreeItemEngine extends AriaRoleEngine { break; case keyCodes.LEFT: - if (this.$v.isExpandable && this.$v.isExpanded()) { + if (this.$v.isExpandable && this.$v.isExpanded) { this.closeFold(); } else { @@ -101,11 +101,11 @@ export default class TreeItemEngine extends AriaRoleEngine { break; case keyCodes.HOME: - void this.setFocusToFirstItem(); + this.setFocusToFirstItem(); break; case keyCodes.END: - void this.setFocusToLastItem(); + this.setFocusToLastItem(); break; default: @@ -159,28 +159,18 @@ export default class TreeItemEngine extends AriaRoleEngine { } } - async setFocusToFirstItem(): Promise { - await this.ctx.async.wait( - this.$v.getRootElement.bind(this), - {label: $$.waitRoot} - ); - + setFocusToFirstItem(): void { const - firstEl = >this.$v.getRootElement()?.querySelector(FOCUSABLE_SELECTOR); + firstEl = >this.$v.rootElement?.querySelector(FOCUSABLE_SELECTOR); if (firstEl != null) { this.focusNext(firstEl); } } - async setFocusToLastItem(): Promise { - await this.ctx.async.wait( - this.$v.getRootElement.bind(this), - {label: $$.waitRoot} - ); - + setFocusToLastItem(): void { const - items = >>this.$v.getRootElement()?.querySelectorAll(FOCUSABLE_SELECTOR); + items = >>this.$v.rootElement?.querySelectorAll(FOCUSABLE_SELECTOR); const visibleItems: HTMLElement[] = [].filter.call( items, diff --git a/src/form/b-select/b-select.ss b/src/form/b-select/b-select.ss index 3d6b74dbe8..b98776de2f 100644 --- a/src/form/b-select/b-select.ss +++ b/src/form/b-select/b-select.ss @@ -41,12 +41,10 @@ :v-attrs = native ? el.attrs - : {'v-aria:option': { - preSelected: isSelected(el.value), - onChange: (cb) => on('actionChange', () => cb(isSelected(el.value))) - }, - ...el.attrs - } | + : { + 'v-aria:option': getAriaOpt('option', el), + ...el.attrs + } | :id = dom.getId(el.value) | ${itemAttrs} @@ -98,13 +96,7 @@ += self.items('option') < template v-else - < _.&__cell.&__input-wrapper & - v-aria:combobox = { - onOpen, - onClose: (cb) => on('close', cb), - onChange: onItemMarked, - isMultiple: multiple - } . + < _.&__cell.&__input-wrapper v-aria:combobox = getAriaOpt('combobox') += self.nativeInput({model: 'textStore', attrs: {'@input': 'onSearchInput'}}) - block icon diff --git a/src/form/b-select/b-select.ts b/src/form/b-select/b-select.ts index d20654d3a0..7eab4472f3 100644 --- a/src/form/b-select/b-select.ts +++ b/src/form/b-select/b-select.ts @@ -932,6 +932,46 @@ class bSelect extends iInputText implements iOpenToggle, iItems { return false; } + /** + * Returns a dictionary with options for aria directive for combobox role + * @param role + */ + protected getAriaOpt(role: 'combobox'): Dictionary; + + /** + * Returns a dictionary with options for aria directive for option role + * + * @param role + * @param item + */ + protected getAriaOpt(role: 'option', item: this['Item']): Dictionary; + + protected getAriaOpt(role: 'combobox' | 'option', item?: this['Item']): Dictionary { + const + event = 'el.mod.set.*.marked.*', + isSelected = this.isSelected.bind(this, item?.value); + + const + opts = { + combobox: { + isMultiple: this.multiple, + changeEvent: (cb) => this.localEmitter.on(event, ({link}) => cb(link)), + closeEvent: (cb) => this.on('close', cb), + openEvent: (cb) => this.on('open', () => { + void this.$nextTick(() => cb(this.selectedElement)); + }) + }, + option: { + get preSelected() { + return isSelected(); + }, + changeEvent: (cb) => this.on('actionChange', () => cb(isSelected())) + } + }; + + return opts[role]; + } + /** * Handler: typing text into a helper text input to search select options * @@ -965,26 +1005,6 @@ class bSelect extends iInputText implements iOpenToggle, iItems { protected onItemsNavigate(e: KeyboardEvent): void { void on.itemsNavigate(this, e); } - - /** - * Handler: executes callback on "open" event - * @param cb - */ - protected onOpen(cb: Function): void { - this.on('open', () => { - void this.$nextTick(() => { - cb.call(this, this.selectedElement); - }); - }); - } - - /** - * Handler: executes callback on item set "marked" mod - * @param cb - */ - protected onItemMarked(cb: Function): void { - this.localEmitter.on('el.mod.set.**', ({link}) => cb(link)); - } } export default bSelect; From 39e2dd2f9e0afa2a89aa77aed1bc669ccc96a01f Mon Sep 17 00:00:00 2001 From: Aleksandr Bunin Date: Sun, 24 Jul 2022 10:28:48 +0300 Subject: [PATCH 078/185] refactoring --- src/core/component/directives/aria/roles-engines/option.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/core/component/directives/aria/roles-engines/option.ts b/src/core/component/directives/aria/roles-engines/option.ts index 522bbb1e1a..109e240835 100644 --- a/src/core/component/directives/aria/roles-engines/option.ts +++ b/src/core/component/directives/aria/roles-engines/option.ts @@ -16,12 +16,16 @@ export default class OptionEngine extends AriaRoleEngine { el.setAttribute('role', 'option'); el.setAttribute('aria-selected', String(preSelected)); + + if (!el.hasAttribute('id')) { + Object.throw('Option aria directive expects the Element id to be added'); + } } onChange = (isSelected: boolean): void => { const {el} = this.options; - console.log(isSelected) + el.setAttribute('aria-selected', String(isSelected)); }; } From 53f840e518b7033cd7817924308abe46e88fc689 Mon Sep 17 00:00:00 2001 From: Aleksandr Bunin Date: Tue, 26 Jul 2022 15:55:34 +0300 Subject: [PATCH 079/185] refactoring in progress --- src/base/b-list/CHANGELOG.md | 2 +- src/base/b-list/b-list.ss | 24 ++-- src/base/b-list/b-list.ts | 128 +++++++++--------- src/base/b-list/interface.ts | 2 + src/base/b-tree/CHANGELOG.md | 2 +- src/base/b-tree/b-tree.ss | 7 +- src/base/b-tree/b-tree.ts | 109 ++++++++------- src/base/b-tree/interface.ts | 2 + .../component/directives/aria/aria-setter.ts | 4 +- .../directives/aria/roles-engines/tab.ts | 10 +- .../directives/aria/roles-engines/treeitem.ts | 10 +- src/form/b-select/CHANGELOG.md | 2 +- .../p-v4-components-demo.ss | 2 + src/traits/i-access/CHANGELOG.md | 6 +- src/traits/i-access/const.ts | 18 ++- 15 files changed, 174 insertions(+), 154 deletions(-) diff --git a/src/base/b-list/CHANGELOG.md b/src/base/b-list/CHANGELOG.md index 18fbd9fe1c..712f6a9004 100644 --- a/src/base/b-list/CHANGELOG.md +++ b/src/base/b-list/CHANGELOG.md @@ -13,7 +13,7 @@ Changelog #### :rocket: New Feature -* Added `v-aria` directive +* Added a new directive `v-aria` * Added a new prop `vertical` * Added `isTablist` * Added `onActiveChange` diff --git a/src/base/b-list/b-list.ss b/src/base/b-list/b-list.ss index b702cfa292..323280bec9 100644 --- a/src/base/b-list/b-list.ss +++ b/src/base/b-list/b-list.ss @@ -47,12 +47,12 @@ } })) | - :v-attrs = isTablist - ? { - 'v-aria:tab': getAriaOpt('tab', el, i), - ...el.attrs - } - : el.attrs + :v-attrs = isTablist ? + { + 'v-aria:tab': getAriaConfig('tab', el, i), + ...el.attrs + } : + el.attrs . - block preIcon < span.&__cell.&__link-icon.&__link-pre-icon v-if = el.preIcon || vdom.getSlot('preIcon') @@ -105,11 +105,11 @@ < tag.&__wrapper & :is = listTag | - :v-attrs = isTablist - ? { - 'v-aria:tablist': getAriaOpt('tablist'), - ...attrs - } - : attrs + :v-attrs = isTablist ? + { + 'v-aria:tablist': getAriaConfig('tablist'), + ...attrs + } : + attrs . += self.list('items') diff --git a/src/base/b-list/b-list.ts b/src/base/b-list/b-list.ts index dc370821bd..205130c1ca 100644 --- a/src/base/b-list/b-list.ts +++ b/src/base/b-list/b-list.ts @@ -21,13 +21,14 @@ import SyncPromise from 'core/promise/sync'; import { isAbsURL } from 'core/url'; import { derive } from 'core/functools/trait'; + import iVisible from 'traits/i-visible/i-visible'; import iWidth from 'traits/i-width/i-width'; import iItems, { IterationKey } from 'traits/i-items/i-items'; - -import iData, { component, prop, field, system, computed, hook, watch, ModsDecl } from 'super/i-data/i-data'; -import type { Active, Item, Items } from 'base/b-list/interface'; import iAccess from 'traits/i-access/i-access'; +import iData, { component, prop, field, system, computed, hook, watch, ModsDecl } from 'super/i-data/i-data'; + +import type { Active, Item, Items, Orientation } from 'base/b-list/interface'; export * from 'super/i-data/i-data'; export * from 'base/b-list/interface'; @@ -117,10 +118,10 @@ class bList extends iData implements iVisible, iWidth, iItems, iAccess { readonly multiple: boolean = false; /** - * If true, the component view orientation is vertical. Horizontal is default + * The component view orientation */ - @prop(Boolean) - readonly vertical: boolean = false; + @prop(String) + readonly orientation: Orientation = 'horizontal'; /** * If true, the active item can be unset by using another click to it. @@ -266,15 +267,19 @@ class bList extends iData implements iVisible, iWidth, iItems, iAccess { protected activeStore!: this['Active']; + /** + * True if the component is used as a tablist + */ + @computed({dependencies: ['items']}) + protected get isTablist(): boolean { + return this.items.some((el) => el.href === undefined); + } + /** * A link to the active item element. * If the component is switched to the `multiple` mode, the getter will return an array of elements. */ - @computed({ - cache: true, - dependencies: ['active'] - }) - + @computed({dependencies: ['active']}) protected get activeElement(): CanPromise>> { const {active} = this; @@ -692,74 +697,67 @@ class bList extends iData implements iVisible, iWidth, iItems, iAccess { } /** - * Returns true if the component is used as tab list - */ - protected get isTablist(): boolean { - return this.items.some((el) => el.href === undefined); - } - - protected override onAddData(data: unknown): void { - Object.assign(this.db, this.convertDataToDB(data)); - } - - protected override onUpdData(data: unknown): void { - Object.assign(this.db, this.convertDataToDB(data)); - } - - protected override onDelData(data: unknown): void { - Object.assign(this.db, this.convertDataToDB(data)); - } - - /** - * Returns a dictionary with options for aria directive for tab role + * Returns a dictionary with configurations for the v-aria directive used as a tablist * @param role */ - protected getAriaOpt(role: 'tab'): Dictionary; + protected getAriaConfig(role: 'tablist'): Dictionary; /** - * Returns a dictionary with options for aria directive for tablist role + * Returns a dictionary with configurations for the v-aria directive used as a tab * * @param role - * @param item - * @param i - position index + * @param item - tab item data + * @param i - tab item position index */ - protected getAriaOpt(role: 'tablist', item: this['Item'], i: number): Dictionary; + protected getAriaConfig(role: 'tab', item: this['Item'], i: number): Dictionary; - protected getAriaOpt(role: 'tab' | 'tablist', item?: this['Item'], i?: number): Dictionary { + protected getAriaConfig(role: 'tab' | 'tablist', item?: this['Item'], i?: number): Dictionary { const - isActive = this.isActive.bind(this, item?.value); - - const opts = { - tablist: { - isMultiple: this.multiple, - isVertical: this.vertical - }, - tab: { - isFirst: i === 0, - isVertical: this.vertical, - changeEvent: this.bindToChange.bind(this), - get isActive() { - return isActive(); - } + isActive = this.isActive.bind(this, item?.value), + isVertical = this.orientation === 'vertical'; + + const changeEvent = (cb: Function) => { + this.on('change', () => { + if (Object.isSet(this.active)) { + cb(this.block?.elements('link', {active: true})); + + } else { + cb(this.block?.element('link', {active: true})); } - }; + }); + }; + + const tablistConfig = { + isVertical, + isMultiple: this.multiple + }; + + const tabConfig = { + isVertical, + isFirst: i === 0, + changeEvent, + get isActive() { + return isActive(); + } + }; - return opts[role]; + switch (role) { + case 'tablist': return tablistConfig; + case 'tab': return tabConfig; + default: return {}; + } } - /** - * Binds callback to change event - * @param cb - */ - protected bindToChange(cb: Function): void { - this.on('change', () => { - if (Object.isSet(this.active)) { - cb(this.block?.elements('link', {active: true})); + protected override onAddData(data: unknown): void { + Object.assign(this.db, this.convertDataToDB(data)); + } - } else { - cb(this.block?.element('link', {active: true})); - } - }); + protected override onUpdData(data: unknown): void { + Object.assign(this.db, this.convertDataToDB(data)); + } + + protected override onDelData(data: unknown): void { + Object.assign(this.db, this.convertDataToDB(data)); } /** diff --git a/src/base/b-list/interface.ts b/src/base/b-list/interface.ts index 17dced5469..4139904270 100644 --- a/src/base/b-list/interface.ts +++ b/src/base/b-list/interface.ts @@ -102,3 +102,5 @@ export interface Item extends Dictionary { export type Items = Item[]; export type Active = unknown | Set; + +export type Orientation = 'vertical' | 'horizontal'; diff --git a/src/base/b-tree/CHANGELOG.md b/src/base/b-tree/CHANGELOG.md index 12a877eb58..afe156f069 100644 --- a/src/base/b-tree/CHANGELOG.md +++ b/src/base/b-tree/CHANGELOG.md @@ -13,7 +13,7 @@ Changelog #### :rocket: New Feature -* Added `v-aria` directive +* Added a new directive `v-aria` * Added a new prop `vertical` * Added `changeFoldedMod` * Now the component derive `iAccess` diff --git a/src/base/b-tree/b-tree.ss b/src/base/b-tree/b-tree.ss index 8b9146a4e9..c9c6a114c6 100644 --- a/src/base/b-tree/b-tree.ss +++ b/src/base/b-tree/b-tree.ss @@ -12,9 +12,7 @@ - template index() extends ['i-data'].index - block body - < .&__root & - v-aria:tree = getAriaOpt('tree') - . + < .&__root v-aria:tree = getAriaConfig('tree') < template & v-for = (el, i) in asyncRender.iterate(items, renderChunks, renderTaskParams) | :key = getItemKey(el, i) @@ -28,7 +26,8 @@ folded: getFoldedPropValue(el) } }) | - v-aria:treeitem = getAriaOpt('treeitem', el, i) + + v-aria:treeitem = getAriaConfig('treeitem', el, i) . < .&__item-wrapper < .&__marker diff --git a/src/base/b-tree/b-tree.ts b/src/base/b-tree/b-tree.ts index f4d993baaa..e68db8673f 100644 --- a/src/base/b-tree/b-tree.ts +++ b/src/base/b-tree/b-tree.ts @@ -18,19 +18,21 @@ import 'models/demo/nested-list'; import symbolGenerator from 'core/symbol'; import { derive } from 'core/functools/trait'; -import iItems, { IterationKey } from 'traits/i-items/i-items'; +import iItems, { IterationKey } from 'traits/i-items/i-items'; import iData, { component, prop, field, TaskParams, TaskI } from 'super/i-data/i-data'; -import type { Item, RenderFilter } from 'base/b-tree/interface'; import iAccess from 'traits/i-access/i-access'; +import type { Item, Orientation, RenderFilter } from 'base/b-tree/interface'; + export * from 'super/i-data/i-data'; export * from 'base/b-tree/interface'; export const $$ = symbolGenerator(); -interface bTree extends Trait {} +interface bTree extends Trait { +} /** * Component to render tree of any elements @@ -104,10 +106,10 @@ class bTree extends iData implements iItems, iAccess { readonly folded: boolean = true; /** - * If true, the component view orientation is vertical. Horizontal is default + * The component view orientation */ - @prop(Boolean) - readonly vertical: boolean = false; + @prop(String) + readonly orientation: Orientation = 'horizontal'; /** * Link to the top level component (internal parameter) @@ -251,72 +253,67 @@ class bTree extends iData implements iItems, iAccess { } /** - * Toggle folded state - * - * @param item - * @param target - * @param value? - * @emits `fold(target: HTMLElement, item:` [[Item]]`, value: boolean)` - */ - protected changeFoldedMod(item: this['Item'], target: HTMLElement, value?: boolean): void { - const - mod = this.block?.getElMod(target, 'node', 'folded'); - - if (mod == null) { - return; - } - - const - newVal = value ? value : mod === 'false'; - - this.block?.setElMod(target, 'node', 'folded', newVal); - this.emit('fold', target, item, newVal); - } - - /** - * Returns a dictionary with options for aria directive for tree role + * Returns a dictionary with configurations for the v-aria directive used as a tree * @param role */ - protected getAriaOpt(role: 'tree'): Dictionary + protected getAriaConfig(role: 'tree'): Dictionary /** - * Returns a dictionary with options for aria directive for treeitem role + * Returns a dictionary with configurations for the v-aria directive used as a treeitem * * @param role - * @param item - * @param i - position index + * @param item - tab item data + * @param i - tab item position index */ - protected getAriaOpt(role: 'treeitem', item: this['Item'], i: number): Dictionary + protected getAriaConfig(role: 'treeitem', item: this['Item'], i: number): Dictionary - protected getAriaOpt(role: 'tree' | 'treeitem', item?: this['Item'], i?: number): Dictionary { + protected getAriaConfig(role: 'tree' | 'treeitem', item?: this['Item'], i?: number): Dictionary { const getFoldedMod = this.getFoldedModById.bind(this, item?.id), root = () => this.top?.$el ?? this.$el; - const opts = { - tree: { - isVertical: this.vertical, - isRoot: this.top == null, - changeEvent: (cb: Function) => { - this.on('fold', (ctx, el, item, value) => cb(el, value)); - } + const toggleFold = (target: HTMLElement, value?: boolean): void => { + const + mod = this.block?.getElMod(target, 'node', 'folded'); + + if (mod == null) { + return; + } + + const + newVal = value ? value : mod === 'false'; + + this.block?.setElMod(target, 'node', 'folded', newVal); + this.emit('fold', target, item, newVal); + }; + + const treeConfig = { + isRoot: this.top == null, + isVertical: this.orientation === 'vertical', + changeEvent: (cb: Function) => { + this.on('fold', (ctx, el, item, value) => cb(el, value)); + } + }; + + const treeitemConfig = { + isRootFirstItem: this.top == null && i === 0, + toggleFold, + get rootElement() { + return root(); }, - treeitem: { - isRootFirstItem: this.top == null && i === 0, - toggleFold: this.changeFoldedMod.bind(this, item), - get rootElement() { - return root(); - }, - get isExpanded() { - return getFoldedMod() === 'false'; - }, - get isExpandable() { - return item?.children != null; - } + get isExpanded() { + return getFoldedMod() === 'false'; + }, + get isExpandable() { + return item?.children != null; } }; - return opts[role]; + switch (role) { + case 'tree': return treeConfig; + case 'treeitem': return treeitemConfig; + default: return {}; + } } /** diff --git a/src/base/b-tree/interface.ts b/src/base/b-tree/interface.ts index 1f513b2974..33471ae590 100644 --- a/src/base/b-tree/interface.ts +++ b/src/base/b-tree/interface.ts @@ -38,3 +38,5 @@ export interface Item extends Dictionary { export interface RenderFilter { (ctx: bTree, el: Item, i: number, task: TaskI): CanPromise; } + +export type Orientation = 'vertical' | 'horizontal'; diff --git a/src/core/component/directives/aria/aria-setter.ts b/src/core/component/directives/aria/aria-setter.ts index a3f73a1f89..2465382642 100644 --- a/src/core/component/directives/aria/aria-setter.ts +++ b/src/core/component/directives/aria/aria-setter.ts @@ -117,14 +117,14 @@ export default class AriaSetter extends AriaRoleEngine { el.setAttribute('aria-label', value.label); } else if (value.labelledby != null) { - el.setAttribute('aria-labelledby', dom.getId(value.labelledby)); + el.setAttribute('aria-labelledby', value.labelledby); } if (value.description != null) { el.setAttribute('aria-description', value.description); } else if (value.describedby != null) { - el.setAttribute('aria-describedby', dom.getId(value.describedby)); + el.setAttribute('aria-describedby', value.describedby); } } } diff --git a/src/core/component/directives/aria/roles-engines/tab.ts b/src/core/component/directives/aria/roles-engines/tab.ts index 083952a10d..27cfb859dc 100644 --- a/src/core/component/directives/aria/roles-engines/tab.ts +++ b/src/core/component/directives/aria/roles-engines/tab.ts @@ -80,11 +80,17 @@ export default class TabEngine extends AriaRoleEngine { } focusNext(): void { - this.ctx.nextFocusableElement(1)?.focus(); + const + focusable = >this.ctx.getNextFocusableElement(1); + + focusable?.focus(); } focusPrev(): void { - this.ctx.nextFocusableElement(-1)?.focus(); + const + focusable = >this.ctx.getNextFocusableElement(-1); + + focusable?.focus(); } onKeydown = (event: Event): void => { diff --git a/src/core/component/directives/aria/roles-engines/treeitem.ts b/src/core/component/directives/aria/roles-engines/treeitem.ts index 7d4247e5c2..3c6b5ddd59 100644 --- a/src/core/component/directives/aria/roles-engines/treeitem.ts +++ b/src/core/component/directives/aria/roles-engines/treeitem.ts @@ -40,11 +40,11 @@ export default class TreeItemEngine extends AriaRoleEngine { this.$a?.on(this.el, 'keydown', this.onKeyDown); const - isMuted = this.ctx.muteTabIndexes(this.el); + isMuted = this.ctx.removeAllFromTabSequence(this.el); if (this.$v.isRootFirstItem) { if (isMuted) { - this.ctx.unmuteTabIndexes(this.el); + this.ctx.restoreAllToTabSequence(this.el); } else { this.el.tabIndex = 0; @@ -117,14 +117,14 @@ export default class TreeItemEngine extends AriaRoleEngine { }; focusNext(nextEl: HTMLElement): void { - this.ctx.muteTabIndexes(this.el); - this.ctx.unmuteTabIndexes(nextEl); + this.ctx.removeAllFromTabSequence(this.el); + this.ctx.restoreAllToTabSequence(nextEl); nextEl.focus(); } moveFocus(step: 1 | -1): void { const - nextEl = this.ctx.nextFocusableElement(step); + nextEl = >this.ctx.getNextFocusableElement(step); if (nextEl != null) { this.focusNext(nextEl); diff --git a/src/form/b-select/CHANGELOG.md b/src/form/b-select/CHANGELOG.md index 4b0b85a820..4a18185ae7 100644 --- a/src/form/b-select/CHANGELOG.md +++ b/src/form/b-select/CHANGELOG.md @@ -13,7 +13,7 @@ Changelog #### :rocket: New Feature -* Added `v-aria` directive +* Added a new directive `v-aria` * Added `onItemMarked` * Added `onOpen` * Now the component derive `iAccess` diff --git a/src/pages/p-v4-components-demo/p-v4-components-demo.ss b/src/pages/p-v4-components-demo/p-v4-components-demo.ss index 176043a8e0..40cb2eb066 100644 --- a/src/pages/p-v4-components-demo/p-v4-components-demo.ss +++ b/src/pages/p-v4-components-demo/p-v4-components-demo.ss @@ -14,6 +14,8 @@ - block body : config = require('@config/config').build + < b-checkbox label = 'bla' | :id = 55 + - forEach config.components => @component - if config.inspectComponents < b-v4-component-demo diff --git a/src/traits/i-access/CHANGELOG.md b/src/traits/i-access/CHANGELOG.md index 0a0c8f3527..f4cab58731 100644 --- a/src/traits/i-access/CHANGELOG.md +++ b/src/traits/i-access/CHANGELOG.md @@ -13,9 +13,9 @@ Changelog #### :rocket: New Feature -* Added `muteTabIndexes` -* Added `unmuteTabIndexes` -* Added `nextFocusableElement` +* Added `removeAllFromTabSequence` +* Added `restoreAllToTabSequence` +* Added `getNextFocusableElement` * Added `is` ## v3.0.0-rc.211 (2021-07-21) diff --git a/src/traits/i-access/const.ts b/src/traits/i-access/const.ts index e4b3110847..ca78dba7b1 100644 --- a/src/traits/i-access/const.ts +++ b/src/traits/i-access/const.ts @@ -1,2 +1,16 @@ -export const - FOCUSABLE_SELECTOR = '[tabindex]:not([disabled]), a:not([disabled]), button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled])'; +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ + +export const FOCUSABLE_SELECTOR = [ + 'a', + 'input', + 'select', + 'button', + 'textarea', + '[tabindex]' +].join(); From 984645c10519330b850214cb426f73e062e2bd24 Mon Sep 17 00:00:00 2001 From: Aleksandr Bunin Date: Tue, 26 Jul 2022 17:19:46 +0300 Subject: [PATCH 080/185] refactoring i-access --- src/traits/i-access/i-access.ts | 203 +++++++++++++++++++++----------- 1 file changed, 137 insertions(+), 66 deletions(-) diff --git a/src/traits/i-access/i-access.ts b/src/traits/i-access/i-access.ts index 61b8f43327..88c983254b 100644 --- a/src/traits/i-access/i-access.ts +++ b/src/traits/i-access/i-access.ts @@ -14,9 +14,12 @@ */ import SyncPromise from 'core/promise/sync'; +import { sequence } from 'core/iter/combinators'; +import { intoIter } from 'core/iter'; import type iBlock from 'super/i-block/i-block'; import type { ModsDecl, ModEvent } from 'super/i-block/i-block'; + import { FOCUSABLE_SELECTOR } from 'traits/i-access/const'; export default abstract class iAccess { @@ -163,99 +166,145 @@ export default abstract class iAccess { }); } - /** @see [[iAccess.muteTabIndexes]] */ - static muteTabIndexes: AddSelf = - (component, ctx?): boolean => { + /** @see [[iAccess.removeAllFromTabSequence]] */ + static removeAllFromTabSequence: AddSelf = + (component, el?): boolean => { const - el = ctx ?? component.$el; + ctx = el ?? component.$el; - if (el == null) { + if (ctx == null) { return false; } - const - elems = el.querySelectorAll(FOCUSABLE_SELECTOR); - - for (let i = 0; i < elems.length; i++) { - const - elem = (elems[i]); + let + areElementsRemoved = false; - if (elem.dataset.tabindex == null) { - elem.dataset.tabindex = String(elem.tabIndex); - } - - elem.tabIndex = -1; - } + const + focusableIter = this.findAllFocusableElements(component, ctx); - if (ctx != null && ctx.tabIndex > -1) { - if (ctx.dataset.tabindex == null) { - ctx.dataset.tabindex = String(ctx.tabIndex); + for (const focusableEl of >focusableIter) { + if (!focusableEl.hasAttribute('data-tabindex')) { + focusableEl.setAttribute('data-tabindex', String(focusableEl.tabIndex)); } - ctx.tabIndex = -1; - - return true; + focusableEl.tabIndex = -1; + areElementsRemoved = true; } - return elems.length > 0; + return areElementsRemoved; }; - /** @see [[iAccess.unmuteTabIndexes]] */ - static unmuteTabIndexes: AddSelf = - (component, ctx?): boolean => { + /** @see [[iAccess.restoreAllToTabSequence]] */ + static restoreAllToTabSequence: AddSelf = + (component, el?): boolean => { const - el = ctx ?? component.$el; + ctx = el ?? component.$el; - if (el == null) { + if (ctx == null) { return false; } - const - elems = el.querySelectorAll('[data-tabindex]'); + let + areElementsRestored = false; - for (let i = 0; i < elems.length; i++) { - const - elem = (elems[i]); + let + removedElemsIter = intoIter(ctx.querySelectorAll('[data-tabindex]')); - elem.tabIndex = Number(elem.dataset.tabindex); - delete elem.dataset.tabindex; + if (el?.hasAttribute('data-tabindex')) { + removedElemsIter = sequence(removedElemsIter, intoIter([el])); } - if (ctx?.dataset.tabindex != null) { - ctx.tabIndex = Number(ctx.dataset.tabindex); - delete ctx.dataset.tabindex; + for (const elem of >removedElemsIter) { + const + originalTabIndex = elem.getAttribute('data-tabindex'); + + if (originalTabIndex != null) { + elem.tabIndex = Number(originalTabIndex); + elem.removeAttribute('data-tabindex'); - return true; + areElementsRestored = true; + } } - return elems.length > 0; + return areElementsRestored; }; - /** @see [[iAccess.unmuteTabIndexes]] */ - static nextFocusableElement: AddSelf = - (component, step, el?): CanUndef => { + /** @see [[iAccess.getNextFocusableElement]] */ + static getNextFocusableElement: AddSelf = + (component, step, el?): CanUndef => { if (document.activeElement == null) { return; } const - nodeListOfFocusable = (el ?? document).querySelectorAll(FOCUSABLE_SELECTOR); - - const focusable: HTMLElement[] = [].filter.call( - nodeListOfFocusable, - (el: HTMLElement) => ( - el.offsetWidth > 0 || - el.offsetHeight > 0 || - el === document.activeElement - ) - ); + ctx = el ?? document.documentElement, + focusableIter = this.findAllFocusableElements(component, ctx), + visibleFocusable: HTMLElement[] = []; + + for (const element of >focusableIter) { + if ( + element.offsetWidth > 0 || + element.offsetHeight > 0 || + element === document.activeElement + ) { + visibleFocusable.push(element); + } + } const - index = focusable.indexOf(document.activeElement); + index = visibleFocusable.indexOf(document.activeElement); if (index > -1) { - return focusable[index + step]; + return visibleFocusable[index + step]; + } + }; + + /** @see [[iAccess.findFocusableElement]] */ + static findFocusableElement: AddSelf = + (component, el?): CanUndef => { + const + ctx = el ?? component.$el, + focusableIter = this.findAllFocusableElements(component, ctx); + + for (const element of focusableIter) { + if (!element.hasAttribute('disabled')) { + return element; + } + } + }; + + /** @see [[iAccess.findAllFocusableElements]] */ + static findAllFocusableElements: AddSelf = + (component, el?): IterableIterator => { + const + ctx = el ?? component.$el, + focusableElems = ctx?.querySelectorAll(FOCUSABLE_SELECTOR); + + let + focusableIter = intoIter(focusableElems ?? []); + + if (el?.hasAttribute('tabindex')) { + focusableIter = sequence(focusableIter, intoIter([el])); } + + function* createFocusableWithoutDisabled(iter: IterableIterator): IterableIterator { + for (const iterEl of iter) { + if (!iterEl.hasAttribute('disabled')) { + yield iterEl; + } + } + } + + const + focusableWithoutDisabled = createFocusableWithoutDisabled(focusableIter); + + return { + [Symbol.iterator]() { + return this; + }, + + next: focusableWithoutDisabled.next.bind(focusableWithoutDisabled) + }; }; /** @@ -268,7 +317,7 @@ export default abstract class iAccess { } const dict = Object.cast(obj); - return Object.isFunction(dict.muteTabIndexes) && Object.isFunction(dict.nextFocusableElement); + return Object.isFunction(dict.removeAllFromTabSequence) && Object.isFunction(dict.getNextFocusableElement); } /** @@ -329,26 +378,48 @@ export default abstract class iAccess { } /** - * Remove all descendants with tabindex attribute from tab sequence and saves previous value. - * @param el + * Removes all children of the specified element that can be focused from the Tab toggle sequence. + * In effect, these elements are set to -1 for the tabindex attribute + * @param el - a context to search, if not set, the root element of the component will be used + */ + removeAllFromTabSequence(el?: Element): boolean { + return Object.throw(); + } + + /** + * Reverts all children of the specified element that can be focused to the Tab toggle sequence. + * This method is used to restore the state of elements to the state + * they had before removeAllFromTabSequence was applied + * + * @param el - a context to search, if not set, the root element of the component will be used + */ + restoreAllToTabSequence(el?: Element): boolean { + return Object.throw(); + } + + /** + * Gets a next or previous focusable element via the step parameter from the current focused element + * + * @param step + * @param el - a context to search, if not set, document will be used */ - muteTabIndexes(el?: HTMLElement): boolean { + getNextFocusableElement(step: 1 | -1, el?: Element): CanUndef { return Object.throw(); } /** - * Recovers previous saved tabindex values to the elements that were changed. - * @param el + * Find focusable element except disabled ones + * @param el - a context to search, if not set, component will be used */ - unmuteTabIndexes(el?: HTMLElement): boolean { + findFocusableElement(el?: Element): CanUndef { return Object.throw(); } /** - * Sets the focus to the next or previous focusable element via the step parameter - * @params step, el? + * Find all focusable elements except disabled ones. Search includes the specified element + * @param el - a context to search, if not set, component will be used */ - nextFocusableElement(step: 1 | -1, el?: HTMLElement): CanUndef { + findAllFocusableElements(el?: Element): IterableIterator { return Object.throw(); } } From 8316349ef14e0943cfae21305da3cc1fbaa85095 Mon Sep 17 00:00:00 2001 From: Aleksandr Bunin Date: Tue, 26 Jul 2022 18:28:08 +0300 Subject: [PATCH 081/185] refactoring --- .../component/directives/aria/CHANGELOG.md | 6 ++ .../component/directives/aria/aria-setter.ts | 99 +++++++++---------- src/core/component/directives/aria/index.ts | 24 ++--- .../component/directives/aria/interface.ts | 8 +- .../directives/aria/roles-engines/combobox.ts | 15 +-- .../aria/roles-engines/interface.ts | 10 +- .../directives/aria/roles-engines/tab.ts | 58 +++++------ .../directives/aria/roles-engines/tablist.ts | 8 +- .../directives/aria/roles-engines/tree.ts | 10 +- .../directives/aria/roles-engines/treeitem.ts | 67 +++++++------ .../p-v4-components-demo.ss | 2 - src/traits/i-access/i-access.ts | 30 +++--- 12 files changed, 163 insertions(+), 174 deletions(-) diff --git a/src/core/component/directives/aria/CHANGELOG.md b/src/core/component/directives/aria/CHANGELOG.md index 1670fa7e71..ba2980fb55 100644 --- a/src/core/component/directives/aria/CHANGELOG.md +++ b/src/core/component/directives/aria/CHANGELOG.md @@ -8,3 +8,9 @@ Changelog > - :memo: [Documentation] > - :house: [Internal] > - :nail_care: [Polish] + +## v?.??.? (2022-0?-??) + +#### :rocket: New Feature + +* Initial release diff --git a/src/core/component/directives/aria/aria-setter.ts b/src/core/component/directives/aria/aria-setter.ts index 2465382642..f395d86804 100644 --- a/src/core/component/directives/aria/aria-setter.ts +++ b/src/core/component/directives/aria/aria-setter.ts @@ -13,18 +13,20 @@ import type iBlock from 'super/i-block/i-block'; import type { DirectiveOptions } from 'core/component/directives/aria/interface'; export default class AriaSetter extends AriaRoleEngine { - override $a: Async; + override async: Async; role: CanUndef; constructor(options: DirectiveOptions) { super(options); - this.$a = new Async(); + this.async = new Async(); this.setAriaRole(); if (this.role != null) { - this.role.$a = this.$a; + this.role.async = this.async; } + + this.init(); } init(): void { @@ -34,54 +36,17 @@ export default class AriaSetter extends AriaRoleEngine { this.role?.init(); } - override update(): void { + update(): void { const ctx = this.options.vnode.fakeContext; if (ctx.isFunctional) { ctx.off(); } - - if (this.role != null) { - this.role.options = this.options; - this.role.update?.(); - } } - override clear(): void { - this.$a.clearAll(); - - this.role?.clear?.(); - } - - addEventHandlers(): void { - if (this.role == null) { - return; - } - - const - $v = this.options.binding.value; - - for (const key in $v) { - if (key in eventsNames) { - const - callback = this.role[eventsNames[key]], - property = $v[key]; - - if (Object.isFunction(property)) { - property(callback); - - } else if (Object.isPromiseLike(property)) { - void property.then(callback); - - } else if (Object.isString(property)) { - const - ctx = this.options.vnode.fakeContext; - - ctx.on(property, callback); - } - } - } + destroy(): void { + this.async.clearAll(); } setAriaRole(): CanUndef { @@ -99,7 +64,7 @@ export default class AriaSetter extends AriaRoleEngine { const {vnode, binding, el} = this.options, {dom} = Object.cast(vnode.fakeContext), - value = Object.isCustomObject(binding.value) ? binding.value : {}; + params = Object.isCustomObject(binding.value) ? binding.value : {}; for (const mod in binding.modifiers) { if (!mod.startsWith('#')) { @@ -113,18 +78,48 @@ export default class AriaSetter extends AriaRoleEngine { el.setAttribute('aria-labelledby', id); } - if (value.label != null) { - el.setAttribute('aria-label', value.label); + if (params.label != null) { + el.setAttribute('aria-label', params.label); - } else if (value.labelledby != null) { - el.setAttribute('aria-labelledby', value.labelledby); + } else if (params.labelledby != null) { + el.setAttribute('aria-labelledby', params.labelledby); } - if (value.description != null) { - el.setAttribute('aria-description', value.description); + if (params.description != null) { + el.setAttribute('aria-description', params.description); + + } else if (params.describedby != null) { + el.setAttribute('aria-describedby', params.describedby); + } + } - } else if (value.describedby != null) { - el.setAttribute('aria-describedby', value.describedby); + addEventHandlers(): void { + if (this.role == null) { + return; + } + + const + params = this.options.binding.value; + + for (const key in params) { + if (key in eventsNames) { + const + callback = this.role[eventsNames[key]], + property = params[key]; + + if (Object.isFunction(property)) { + property(callback); + + } else if (Object.isPromiseLike(property)) { + void property.then(callback); + + } else if (Object.isString(property)) { + const + ctx = this.options.vnode.fakeContext; + + ctx.on(property, callback); + } + } } } } diff --git a/src/core/component/directives/aria/index.ts b/src/core/component/directives/aria/index.ts index 255fa3b2f8..9ba81852e6 100644 --- a/src/core/component/directives/aria/index.ts +++ b/src/core/component/directives/aria/index.ts @@ -11,15 +11,11 @@ * @packageDocumentation */ -import symbolGenerator from 'core/symbol'; import { ComponentEngine, VNode, VNodeDirective } from 'core/component/engines'; import AriaSetter from 'core/component/directives/aria/aria-setter'; const - ariaMap = new Map(); - -const - $$ = symbolGenerator(); + ariaMap = new WeakMap(); ComponentEngine.directive('aria', { inserted(el: HTMLElement, binding: VNodeDirective, vnode: VNode): void { @@ -33,26 +29,20 @@ ComponentEngine.directive('aria', { const aria = new AriaSetter({el, binding, vnode}); - aria.init(); - - ariaMap.set($$.aria, aria); + ariaMap.set(el, aria); }, - update(el: HTMLElement, binding: VNodeDirective, vnode: VNode) { + update(el: HTMLElement) { const - aria: AriaSetter = ariaMap.get($$.aria); - - aria.options = {el, binding, vnode}; + aria: AriaSetter = ariaMap.get(el); aria.update(); }, - unbind(el: HTMLElement, binding: VNodeDirective, vnode: VNode) { + unbind(el: HTMLElement) { const - aria: AriaSetter = ariaMap.get($$.aria); - - aria.options = {el, binding, vnode}; + aria: AriaSetter = ariaMap.get(el); - aria.clear(); + aria.destroy(); } }); diff --git a/src/core/component/directives/aria/interface.ts b/src/core/component/directives/aria/interface.ts index 3ced68a655..8acdae7f86 100644 --- a/src/core/component/directives/aria/interface.ts +++ b/src/core/component/directives/aria/interface.ts @@ -17,18 +17,16 @@ export interface DirectiveOptions { export default abstract class AriaRoleEngine { options: DirectiveOptions; - $a: CanUndef; + async: CanUndef; protected constructor(options: DirectiveOptions) { this.options = options; } abstract init(): void; - update?(): void; - clear?(): void; } -export enum keyCodes { +export const enum keyCodes { ENTER = 'Enter', END = 'End', HOME = 'Home', @@ -38,7 +36,7 @@ export enum keyCodes { DOWN = 'ArrowDown' } -export enum eventsNames { +export const enum eventsNames { openEvent = 'onOpen', closeEvent = 'onClose', changeEvent = 'onChange' diff --git a/src/core/component/directives/aria/roles-engines/combobox.ts b/src/core/component/directives/aria/roles-engines/combobox.ts index c63785b16f..8ecf46a460 100644 --- a/src/core/component/directives/aria/roles-engines/combobox.ts +++ b/src/core/component/directives/aria/roles-engines/combobox.ts @@ -7,28 +7,29 @@ */ import AriaRoleEngine, { DirectiveOptions } from 'core/component/directives/aria/interface'; -import { FOCUSABLE_SELECTOR } from 'traits/i-access/const'; -import type { ComboboxBindingValue } from 'core/component/directives/aria/roles-engines/interface'; +import type { ComboboxParams } from 'core/component/directives/aria/roles-engines/interface'; +import type iAccess from 'traits/i-access/i-access'; export default class ComboboxEngine extends AriaRoleEngine { el: Element; - $v: ComboboxBindingValue; + params: ComboboxParams; constructor(options: DirectiveOptions) { super(options); const - {el} = this.options; + {el} = this.options, + ctx = Object.cast(this.options.vnode.fakeContext); - this.el = el.querySelector(FOCUSABLE_SELECTOR) ?? el; - this.$v = this.options.binding.value; + this.el = ctx.findFocusableElement() ?? el; + this.params = this.options.binding.value; } init(): void { this.el.setAttribute('role', 'combobox'); this.el.setAttribute('aria-expanded', 'false'); - if (this.$v.isMultiple) { + if (this.params.isMultiple) { this.el.setAttribute('aria-multiselectable', 'true'); } } diff --git a/src/core/component/directives/aria/roles-engines/interface.ts b/src/core/component/directives/aria/roles-engines/interface.ts index f135f22028..61922d6fa1 100644 --- a/src/core/component/directives/aria/roles-engines/interface.ts +++ b/src/core/component/directives/aria/roles-engines/interface.ts @@ -1,22 +1,22 @@ -export interface TabBindingValue { +export interface TabParams { isFirst: boolean; isVertical: boolean; isActive: boolean; changeEvent(cb: Function): void; } -export interface TablistBindingValue { +export interface TablistParams { isVertical: boolean; isMultiple: boolean; } -export interface TreeBindingValue { +export interface TreeParams { isRoot: boolean; isVertical: boolean; changeEvent(cb: Function): void; } -export interface TreeitemBindingValue { +export interface TreeitemParams { isRootFirstItem: boolean; isExpandable: boolean; isExpanded: boolean; @@ -24,7 +24,7 @@ export interface TreeitemBindingValue { toggleFold(el: Element, value?: boolean): void; } -export interface ComboboxBindingValue { +export interface ComboboxParams { isMultiple: boolean; changeEvent(cb: Function): void; openEvent(cb: Function): void; diff --git a/src/core/component/directives/aria/roles-engines/tab.ts b/src/core/component/directives/aria/roles-engines/tab.ts index 27cfb859dc..1d7260f6c1 100644 --- a/src/core/component/directives/aria/roles-engines/tab.ts +++ b/src/core/component/directives/aria/roles-engines/tab.ts @@ -10,29 +10,28 @@ */ import AriaRoleEngine, { DirectiveOptions, keyCodes } from 'core/component/directives/aria/interface'; -import type { TabBindingValue } from 'core/component/directives/aria/roles-engines/interface'; +import type { TabParams } from 'core/component/directives/aria/roles-engines/interface'; import type iAccess from 'traits/i-access/i-access'; import type iBlock from 'super/i-block/i-block'; -import { FOCUSABLE_SELECTOR } from 'traits/i-access/const'; export default class TabEngine extends AriaRoleEngine { - $v: TabBindingValue; + params: TabParams; ctx: iAccess & iBlock; constructor(options: DirectiveOptions) { super(options); - this.$v = this.options.binding.value; + this.params = this.options.binding.value; this.ctx = Object.cast(this.options.vnode.fakeContext); } init(): void { const {el} = this.options, - {isFirst} = this.$v; + {isFirst} = this.params; el.setAttribute('role', 'tab'); - el.setAttribute('aria-selected', String(this.$v.isActive)); + el.setAttribute('aria-selected', String(this.params.isActive)); if (isFirst) { if (el.tabIndex < 0) { @@ -43,8 +42,8 @@ export default class TabEngine extends AriaRoleEngine { el.setAttribute('tabindex', '-1'); } - if (this.$a != null) { - this.$a.on(el, 'keydown', this.onKeydown); + if (this.async != null) { + this.async.on(el, 'keydown', this.onKeydown); } } @@ -52,43 +51,46 @@ export default class TabEngine extends AriaRoleEngine { const {el} = this.options; + function setAttributes(isSelected: boolean) { + el.setAttribute('aria-selected', String(isSelected)); + el.setAttribute('tabindex', isSelected ? '0' : '-1'); + } + if (Object.isArrayLike(active)) { for (let i = 0; i < active.length; i++) { - el.setAttribute('aria-selected', String(el === active[i])); + setAttributes(el === active[i]); } return; } - el.setAttribute('aria-selected', String(el === active)); + setAttributes(el === active); }; moveFocusToFirstTab(): void { const - firstEl = >this.ctx.$el?.querySelector(FOCUSABLE_SELECTOR); + firstTab = >this.ctx.findFocusableElement(); - firstEl?.focus(); + firstTab?.focus(); } moveFocusToLastTab(): void { const - focusable = >>this.ctx.$el?.querySelectorAll(FOCUSABLE_SELECTOR); + tabs = >this.ctx.findAllFocusableElements(); - if (focusable != null && focusable.length > 0) { - focusable[focusable.length - 1].focus(); - } - } + let + lastTab: CanUndef; - focusNext(): void { - const - focusable = >this.ctx.getNextFocusableElement(1); + for (const tab of tabs) { + lastTab = tab; + } - focusable?.focus(); + lastTab?.focus(); } - focusPrev(): void { + moveFocus(step: 1 | -1): void { const - focusable = >this.ctx.getNextFocusableElement(-1); + focusable = >this.ctx.getNextFocusableElement(step); focusable?.focus(); } @@ -96,28 +98,28 @@ export default class TabEngine extends AriaRoleEngine { onKeydown = (event: Event): void => { const evt = (event), - {isVertical} = this.$v; + {isVertical} = this.params; switch (evt.key) { case keyCodes.LEFT: - this.focusPrev(); + this.moveFocus(-1); break; case keyCodes.UP: if (isVertical) { - this.focusPrev(); + this.moveFocus(-1); break; } return; case keyCodes.RIGHT: - this.focusNext(); + this.moveFocus(1); break; case keyCodes.DOWN: if (isVertical) { - this.focusNext(); + this.moveFocus(1); break; } diff --git a/src/core/component/directives/aria/roles-engines/tablist.ts b/src/core/component/directives/aria/roles-engines/tablist.ts index 2105747cb3..00cc31bcaa 100644 --- a/src/core/component/directives/aria/roles-engines/tablist.ts +++ b/src/core/component/directives/aria/roles-engines/tablist.ts @@ -7,21 +7,21 @@ */ import AriaRoleEngine from 'core/component/directives/aria/interface'; -import type { TablistBindingValue } from 'core/component/directives/aria/roles-engines/interface'; +import type { TablistParams } from 'core/component/directives/aria/roles-engines/interface'; export default class TablistEngine extends AriaRoleEngine { init(): void { const {el, binding} = this.options, - $v: TablistBindingValue = binding.value; + params: TablistParams = binding.value; el.setAttribute('role', 'tablist'); - if ($v.isMultiple) { + if (params.isMultiple) { el.setAttribute('aria-multiselectable', 'true'); } - if ($v.isVertical) { + if (params.isVertical) { el.setAttribute('aria-orientation', 'vertical'); } } diff --git a/src/core/component/directives/aria/roles-engines/tree.ts b/src/core/component/directives/aria/roles-engines/tree.ts index 09affe7603..4f7630d2fc 100644 --- a/src/core/component/directives/aria/roles-engines/tree.ts +++ b/src/core/component/directives/aria/roles-engines/tree.ts @@ -7,29 +7,29 @@ */ import AriaRoleEngine, { DirectiveOptions } from 'core/component/directives/aria/interface'; -import type { TreeBindingValue } from 'core/component/directives/aria/roles-engines/interface'; +import type { TreeParams } from 'core/component/directives/aria/roles-engines/interface'; export default class TreeEngine extends AriaRoleEngine { - $v: TreeBindingValue; + params: TreeParams; el: HTMLElement; constructor(options: DirectiveOptions) { super(options); - this.$v = options.binding.value; + this.params = options.binding.value; this.el = this.options.el; } init(): void { this.setRootRole(); - if (this.$v.isVertical) { + if (this.params.isVertical) { this.el.setAttribute('aria-orientation', 'vertical'); } } setRootRole(): void { - this.el.setAttribute('role', this.$v.isRoot ? 'tree' : 'group'); + this.el.setAttribute('role', this.params.isRoot ? 'tree' : 'group'); } onChange = (el: HTMLElement, isFolded: boolean): void => { diff --git a/src/core/component/directives/aria/roles-engines/treeitem.ts b/src/core/component/directives/aria/roles-engines/treeitem.ts index 3c6b5ddd59..529da86244 100644 --- a/src/core/component/directives/aria/roles-engines/treeitem.ts +++ b/src/core/component/directives/aria/roles-engines/treeitem.ts @@ -9,20 +9,16 @@ * Copyright © [2022] W3C® (MIT, ERCIM, Keio, Beihang). */ -import symbolGenerator from 'core/symbol'; import AriaRoleEngine, { DirectiveOptions, keyCodes } from 'core/component/directives/aria/interface'; import iAccess from 'traits/i-access/i-access'; -import { FOCUSABLE_SELECTOR } from 'traits/i-access/const'; -import type { TreeitemBindingValue } from 'core/component/directives/aria/roles-engines/interface'; -import type iBlock from 'super/i-block/i-block'; -export const - $$ = symbolGenerator(); +import type { TreeitemParams } from 'core/component/directives/aria/roles-engines/interface'; +import type iBlock from 'super/i-block/i-block'; export default class TreeItemEngine extends AriaRoleEngine { ctx: iAccess & iBlock['unsafe']; el: HTMLElement; - $v: TreeitemBindingValue; + params: TreeitemParams; constructor(options: DirectiveOptions) { super(options); @@ -33,16 +29,16 @@ export default class TreeItemEngine extends AriaRoleEngine { this.ctx = Object.cast(options.vnode.fakeContext); this.el = this.options.el; - this.$v = this.options.binding.value; + this.params = this.options.binding.value; } init(): void { - this.$a?.on(this.el, 'keydown', this.onKeyDown); + this.async?.on(this.el, 'keydown', this.onKeyDown); const isMuted = this.ctx.removeAllFromTabSequence(this.el); - if (this.$v.isRootFirstItem) { + if (this.params.isRootFirstItem) { if (isMuted) { this.ctx.restoreAllToTabSequence(this.el); @@ -54,8 +50,8 @@ export default class TreeItemEngine extends AriaRoleEngine { this.el.setAttribute('role', 'treeitem'); this.ctx.$nextTick(() => { - if (this.$v.isExpandable) { - this.el.setAttribute('aria-expanded', String(this.$v.isExpanded)); + if (this.params.isExpandable) { + this.el.setAttribute('aria-expanded', String(this.params.isExpanded)); } }); } @@ -75,12 +71,12 @@ export default class TreeItemEngine extends AriaRoleEngine { break; case keyCodes.ENTER: - this.$v.toggleFold(this.el); + this.params.toggleFold(this.el); break; case keyCodes.RIGHT: - if (this.$v.isExpandable) { - if (this.$v.isExpanded) { + if (this.params.isExpandable) { + if (this.params.isExpanded) { this.moveFocus(1); } else { @@ -91,7 +87,7 @@ export default class TreeItemEngine extends AriaRoleEngine { break; case keyCodes.LEFT: - if (this.$v.isExpandable && this.$v.isExpanded) { + if (this.params.isExpandable && this.params.isExpanded) { this.closeFold(); } else { @@ -119,6 +115,7 @@ export default class TreeItemEngine extends AriaRoleEngine { focusNext(nextEl: HTMLElement): void { this.ctx.removeAllFromTabSequence(this.el); this.ctx.restoreAllToTabSequence(nextEl); + nextEl.focus(); } @@ -132,11 +129,11 @@ export default class TreeItemEngine extends AriaRoleEngine { } openFold(): void { - this.$v.toggleFold(this.el, false); + this.params.toggleFold(this.el, false); } closeFold(): void { - this.$v.toggleFold(this.el, true); + this.params.toggleFold(this.el, true); } focusParent(): void { @@ -151,8 +148,12 @@ export default class TreeItemEngine extends AriaRoleEngine { parent = parent.parentElement; } + if (parent == null) { + return; + } + const - focusableParent = (>parent?.querySelector(FOCUSABLE_SELECTOR)); + focusableParent = >this.ctx.findFocusableElement(parent); if (focusableParent != null) { this.focusNext(focusableParent); @@ -161,30 +162,28 @@ export default class TreeItemEngine extends AriaRoleEngine { setFocusToFirstItem(): void { const - firstEl = >this.$v.rootElement?.querySelector(FOCUSABLE_SELECTOR); + firstItem = >this.ctx.findFocusableElement(this.params.rootElement); - if (firstEl != null) { - this.focusNext(firstEl); + if (firstItem != null) { + this.focusNext(firstItem); } } setFocusToLastItem(): void { const - items = >>this.$v.rootElement?.querySelectorAll(FOCUSABLE_SELECTOR); + items = >this.ctx.findAllFocusableElements(this.params.rootElement); - const visibleItems: HTMLElement[] = [].filter.call( - items, - (el: HTMLElement) => ( - el.offsetWidth > 0 || - el.offsetHeight > 0 - ) - ); + let + lastItem: CanUndef; - const - lastEl = visibleItems.at(-1); + for (const item of items) { + if (item.offsetWidth > 0 || item.offsetHeight > 0) { + lastItem = item; + } + } - if (lastEl != null) { - this.focusNext(lastEl); + if (lastItem != null) { + this.focusNext(lastItem); } } } diff --git a/src/pages/p-v4-components-demo/p-v4-components-demo.ss b/src/pages/p-v4-components-demo/p-v4-components-demo.ss index 40cb2eb066..176043a8e0 100644 --- a/src/pages/p-v4-components-demo/p-v4-components-demo.ss +++ b/src/pages/p-v4-components-demo/p-v4-components-demo.ss @@ -14,8 +14,6 @@ - block body : config = require('@config/config').build - < b-checkbox label = 'bla' | :id = 55 - - forEach config.components => @component - if config.inspectComponents < b-v4-component-demo diff --git a/src/traits/i-access/i-access.ts b/src/traits/i-access/i-access.ts index 88c983254b..2baf4546b6 100644 --- a/src/traits/i-access/i-access.ts +++ b/src/traits/i-access/i-access.ts @@ -180,9 +180,9 @@ export default abstract class iAccess { areElementsRemoved = false; const - focusableIter = this.findAllFocusableElements(component, ctx); + focusableElems = >this.findAllFocusableElements(component, ctx); - for (const focusableEl of >focusableIter) { + for (const focusableEl of focusableElems) { if (!focusableEl.hasAttribute('data-tabindex')) { focusableEl.setAttribute('data-tabindex', String(focusableEl.tabIndex)); } @@ -208,13 +208,13 @@ export default abstract class iAccess { areElementsRestored = false; let - removedElemsIter = intoIter(ctx.querySelectorAll('[data-tabindex]')); + removedElems = intoIter(ctx.querySelectorAll('[data-tabindex]')); if (el?.hasAttribute('data-tabindex')) { - removedElemsIter = sequence(removedElemsIter, intoIter([el])); + removedElems = sequence(removedElems, intoIter([el])); } - for (const elem of >removedElemsIter) { + for (const elem of >removedElems) { const originalTabIndex = elem.getAttribute('data-tabindex'); @@ -238,16 +238,16 @@ export default abstract class iAccess { const ctx = el ?? document.documentElement, - focusableIter = this.findAllFocusableElements(component, ctx), + focusableElems = >this.findAllFocusableElements(component, ctx), visibleFocusable: HTMLElement[] = []; - for (const element of >focusableIter) { + for (const focusableEl of focusableElems) { if ( - element.offsetWidth > 0 || - element.offsetHeight > 0 || - element === document.activeElement + focusableEl.offsetWidth > 0 || + focusableEl.offsetHeight > 0 || + focusableEl === document.activeElement ) { - visibleFocusable.push(element); + visibleFocusable.push(focusableEl); } } @@ -264,11 +264,11 @@ export default abstract class iAccess { (component, el?): CanUndef => { const ctx = el ?? component.$el, - focusableIter = this.findAllFocusableElements(component, ctx); + focusableElems = this.findAllFocusableElements(component, ctx); - for (const element of focusableIter) { - if (!element.hasAttribute('disabled')) { - return element; + for (const focusableEl of focusableElems) { + if (!focusableEl.hasAttribute('disabled')) { + return focusableEl; } } }; From 3b36688d4f7f209ca5b180c5573977f4c999225d Mon Sep 17 00:00:00 2001 From: Aleksandr Bunin Date: Wed, 27 Jul 2022 13:53:11 +0300 Subject: [PATCH 082/185] add v-id directive and implement in code --- src/base/b-window/CHANGELOG.md | 7 +++++ src/base/b-window/b-window.ss | 2 +- src/core/component/directives/id/CHANGELOG.md | 16 +++++++++++ src/core/component/directives/id/README.md | 17 ++++++++++++ src/core/component/directives/id/index.ts | 27 +++++++++++++++++++ src/core/component/directives/index.ts | 2 ++ .../b-v4-component-demo.ss | 2 +- 7 files changed, 71 insertions(+), 2 deletions(-) create mode 100644 src/core/component/directives/id/CHANGELOG.md create mode 100644 src/core/component/directives/id/README.md create mode 100644 src/core/component/directives/id/index.ts diff --git a/src/base/b-window/CHANGELOG.md b/src/base/b-window/CHANGELOG.md index 6fa59df32b..4fe7a2cff7 100644 --- a/src/base/b-window/CHANGELOG.md +++ b/src/base/b-window/CHANGELOG.md @@ -9,6 +9,13 @@ Changelog > - :house: [Internal] > - :nail_care: [Polish] +## v3.?.? (2022-0?-??) + +#### :rocket: New Feature + +* Added a new directive `v-aria` +* Added a new directive `v-id` + ## v3.0.0-rc.211 (2021-07-21) #### :boom: Breaking Change diff --git a/src/base/b-window/b-window.ss b/src/base/b-window/b-window.ss index 616d92ba40..38ecae2b96 100644 --- a/src/base/b-window/b-window.ss +++ b/src/base/b-window/b-window.ss @@ -41,7 +41,7 @@ += self.slot() < h1.&__title & v-if = title || vdom.getSlot('title') | - :id = dom.getId('title') + v-id = 'title' . += self.slot('title', {':title': 'title'}) - block title diff --git a/src/core/component/directives/id/CHANGELOG.md b/src/core/component/directives/id/CHANGELOG.md new file mode 100644 index 0000000000..ba2980fb55 --- /dev/null +++ b/src/core/component/directives/id/CHANGELOG.md @@ -0,0 +1,16 @@ +Changelog +========= + +> **Tags:** +> - :boom: [Breaking Change] +> - :rocket: [New Feature] +> - :bug: [Bug Fix] +> - :memo: [Documentation] +> - :house: [Internal] +> - :nail_care: [Polish] + +## v?.??.? (2022-0?-??) + +#### :rocket: New Feature + +* Initial release diff --git a/src/core/component/directives/id/README.md b/src/core/component/directives/id/README.md new file mode 100644 index 0000000000..4a8008f9bd --- /dev/null +++ b/src/core/component/directives/id/README.md @@ -0,0 +1,17 @@ +# core/component/directives/aria + +This module provides a directive for easy adding of id attribute. + +## Usage + +``` +< &__foo v-id = 'title' + +``` + +The same as +``` +< &__foo :id = dom.getId('title') + +``` + diff --git a/src/core/component/directives/id/index.ts b/src/core/component/directives/id/index.ts new file mode 100644 index 0000000000..d8636c4965 --- /dev/null +++ b/src/core/component/directives/id/index.ts @@ -0,0 +1,27 @@ +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ + +/** + * [[include:core/component/directives/id/README.md]] + * @packageDocumentation + */ + +import { ComponentEngine, VNode, VNodeDirective } from 'core/component/engines'; +import type iBlock from 'super/i-block/i-block'; + +ComponentEngine.directive('id', { + inserted(el: HTMLElement, binding: VNodeDirective, vnode: VNode): void { + const + ctx = Object.cast(vnode.fakeContext); + + const + id = ctx.dom.getId(binding.value); + + el.setAttribute('id', id); + } +}); diff --git a/src/core/component/directives/index.ts b/src/core/component/directives/index.ts index 6874942b5c..e90a8b1f94 100644 --- a/src/core/component/directives/index.ts +++ b/src/core/component/directives/index.ts @@ -25,3 +25,5 @@ import 'core/component/directives/update-on'; import 'core/component/directives/aria'; import 'core/component/directives/hook'; + +import 'core/component/directives/id'; diff --git a/src/pages/p-v4-components-demo/b-v4-component-demo/b-v4-component-demo.ss b/src/pages/p-v4-components-demo/b-v4-component-demo/b-v4-component-demo.ss index 8808a6aa9f..456c3784e8 100644 --- a/src/pages/p-v4-components-demo/b-v4-component-demo/b-v4-component-demo.ss +++ b/src/pages/p-v4-components-demo/b-v4-component-demo/b-v4-component-demo.ss @@ -47,7 +47,7 @@ < template v-else < input & :type = 'checkbox' | - :id = dom.getId(key) | + v-id = key | :checked = debugComponent.mods[key] === getModValue(mod[0]) | :class = provide.elClasses({ highlighted: field.get(['highlighting', key, mod[0]].join('.')) || false From 9332ec3fd23f91498ad258b1f358189cf1905ca9 Mon Sep 17 00:00:00 2001 From: Aleksandr Bunin Date: Wed, 27 Jul 2022 13:55:30 +0300 Subject: [PATCH 083/185] fix changlelogs --- src/base/b-list/CHANGELOG.md | 6 +-- src/base/b-tree/CHANGELOG.md | 6 +-- .../component/directives/aria/aria-setter.ts | 6 +-- .../component/directives/aria/interface.ts | 2 +- src/form/b-button/CHANGELOG.md | 6 +++ src/form/b-button/b-button.ss | 2 +- src/form/b-select/CHANGELOG.md | 6 +-- src/form/b-select/b-select.ss | 6 +-- src/form/b-select/b-select.ts | 41 ++++++++++--------- src/super/i-input/CHANGELOG.md | 2 +- 10 files changed, 46 insertions(+), 37 deletions(-) diff --git a/src/base/b-list/CHANGELOG.md b/src/base/b-list/CHANGELOG.md index 712f6a9004..bd365587c4 100644 --- a/src/base/b-list/CHANGELOG.md +++ b/src/base/b-list/CHANGELOG.md @@ -14,10 +14,10 @@ Changelog #### :rocket: New Feature * Added a new directive `v-aria` -* Added a new prop `vertical` +* Added a new prop `orientation` * Added `isTablist` -* Added `onActiveChange` -* Now the component derive `iAccess` +* Added `getAriaConfig` +* Now the component derives `iAccess` ## v3.0.0-rc.211 (2021-07-21) diff --git a/src/base/b-tree/CHANGELOG.md b/src/base/b-tree/CHANGELOG.md index afe156f069..9aeec86d84 100644 --- a/src/base/b-tree/CHANGELOG.md +++ b/src/base/b-tree/CHANGELOG.md @@ -14,9 +14,9 @@ Changelog #### :rocket: New Feature * Added a new directive `v-aria` -* Added a new prop `vertical` -* Added `changeFoldedMod` -* Now the component derive `iAccess` +* Added a new prop `orientation` +* Added `getAriaConfig` +* Now the component derives `iAccess` ## v3.0.0-rc.164 (2021-03-22) diff --git a/src/core/component/directives/aria/aria-setter.ts b/src/core/component/directives/aria/aria-setter.ts index f395d86804..27a72f804f 100644 --- a/src/core/component/directives/aria/aria-setter.ts +++ b/src/core/component/directives/aria/aria-setter.ts @@ -8,7 +8,7 @@ import * as ariaRoles from 'core/component/directives/aria/roles-engines'; import Async from 'core/async'; -import AriaRoleEngine, { eventsNames } from 'core/component/directives/aria/interface'; +import AriaRoleEngine, { eventNames } from 'core/component/directives/aria/interface'; import type iBlock from 'super/i-block/i-block'; import type { DirectiveOptions } from 'core/component/directives/aria/interface'; @@ -102,9 +102,9 @@ export default class AriaSetter extends AriaRoleEngine { params = this.options.binding.value; for (const key in params) { - if (key in eventsNames) { + if (key in eventNames) { const - callback = this.role[eventsNames[key]], + callback = this.role[eventNames[key]], property = params[key]; if (Object.isFunction(property)) { diff --git a/src/core/component/directives/aria/interface.ts b/src/core/component/directives/aria/interface.ts index 8acdae7f86..9a788e5dc1 100644 --- a/src/core/component/directives/aria/interface.ts +++ b/src/core/component/directives/aria/interface.ts @@ -36,7 +36,7 @@ export const enum keyCodes { DOWN = 'ArrowDown' } -export const enum eventsNames { +export enum eventNames { openEvent = 'onOpen', closeEvent = 'onClose', changeEvent = 'onChange' diff --git a/src/form/b-button/CHANGELOG.md b/src/form/b-button/CHANGELOG.md index 567364f2d1..95f35cb6b7 100644 --- a/src/form/b-button/CHANGELOG.md +++ b/src/form/b-button/CHANGELOG.md @@ -9,6 +9,12 @@ Changelog > - :house: [Internal] > - :nail_care: [Polish] +## v3.?.? (2022-0?-??) + +#### :rocket: New Feature + +* Added a new directive `v-id` + ## v3.0.0-rc.211 (2021-07-21) #### :rocket: New Feature diff --git a/src/form/b-button/b-button.ss b/src/form/b-button/b-button.ss index 27480985c3..235ae8da1e 100644 --- a/src/form/b-button/b-button.ss +++ b/src/form/b-button/b-button.ss @@ -89,7 +89,7 @@ < . & ref dropdown | v-if = hasDropdown | - :id = dom.getId('dropdown') | + v-id = 'dropdown' | :class = provide.elClasses({dropdown: {pos: dropdown}}) . < .&__dropdown-content diff --git a/src/form/b-select/CHANGELOG.md b/src/form/b-select/CHANGELOG.md index 4a18185ae7..334f6805f1 100644 --- a/src/form/b-select/CHANGELOG.md +++ b/src/form/b-select/CHANGELOG.md @@ -14,9 +14,9 @@ Changelog #### :rocket: New Feature * Added a new directive `v-aria` -* Added `onItemMarked` -* Added `onOpen` -* Now the component derive `iAccess` +* Added a new directive `v-id` +* Added `getAriaConfig` +* Now the component derives `iAccess` #### :bug: Bug Fix diff --git a/src/form/b-select/b-select.ss b/src/form/b-select/b-select.ss index b98776de2f..db1a29e258 100644 --- a/src/form/b-select/b-select.ss +++ b/src/form/b-select/b-select.ss @@ -42,11 +42,11 @@ :v-attrs = native ? el.attrs : { - 'v-aria:option': getAriaOpt('option', el), + 'v-aria:option': getAriaConfig('option', el), ...el.attrs } | - :id = dom.getId(el.value) | + v-id = el.value | ${itemAttrs} . += self.slot('default', {':item': 'el'}) @@ -96,7 +96,7 @@ += self.items('option') < template v-else - < _.&__cell.&__input-wrapper v-aria:combobox = getAriaOpt('combobox') + < _.&__cell.&__input-wrapper v-aria:combobox = getAriaConfig('combobox') += self.nativeInput({model: 'textStore', attrs: {'@input': 'onSearchInput'}}) - block icon diff --git a/src/form/b-select/b-select.ts b/src/form/b-select/b-select.ts index 7eab4472f3..d48147cf25 100644 --- a/src/form/b-select/b-select.ts +++ b/src/form/b-select/b-select.ts @@ -936,7 +936,7 @@ class bSelect extends iInputText implements iOpenToggle, iItems { * Returns a dictionary with options for aria directive for combobox role * @param role */ - protected getAriaOpt(role: 'combobox'): Dictionary; + protected getAriaConfig(role: 'combobox'): Dictionary; /** * Returns a dictionary with options for aria directive for option role @@ -944,32 +944,35 @@ class bSelect extends iInputText implements iOpenToggle, iItems { * @param role * @param item */ - protected getAriaOpt(role: 'option', item: this['Item']): Dictionary; + protected getAriaConfig(role: 'option', item: this['Item']): Dictionary; - protected getAriaOpt(role: 'combobox' | 'option', item?: this['Item']): Dictionary { + protected getAriaConfig(role: 'combobox' | 'option', item?: this['Item']): Dictionary { const event = 'el.mod.set.*.marked.*', isSelected = this.isSelected.bind(this, item?.value); const - opts = { - combobox: { - isMultiple: this.multiple, - changeEvent: (cb) => this.localEmitter.on(event, ({link}) => cb(link)), - closeEvent: (cb) => this.on('close', cb), - openEvent: (cb) => this.on('open', () => { - void this.$nextTick(() => cb(this.selectedElement)); - }) - }, - option: { - get preSelected() { - return isSelected(); - }, - changeEvent: (cb) => this.on('actionChange', () => cb(isSelected())) - } + comboboxConfig = { + isMultiple: this.multiple, + changeEvent: (cb) => this.localEmitter.on(event, ({link}) => cb(link)), + closeEvent: (cb) => this.on('close', cb), + openEvent: (cb) => this.on('open', () => { + void this.$nextTick(() => cb(this.selectedElement)); + }) }; - return opts[role]; + const optionConfig = { + get preSelected() { + return isSelected(); + }, + changeEvent: (cb) => this.on('actionChange', () => cb(isSelected())) + }; + + switch (role) { + case 'combobox': return comboboxConfig; + case 'option': return optionConfig; + default: return {}; + } } /** diff --git a/src/super/i-input/CHANGELOG.md b/src/super/i-input/CHANGELOG.md index 8aeb101da1..672b1738a9 100644 --- a/src/super/i-input/CHANGELOG.md +++ b/src/super/i-input/CHANGELOG.md @@ -13,7 +13,7 @@ Changelog #### :rocket: New Feature -* Now the component derive `iAccess` +* Now the component derives `iAccess` ## v3.0.0-rc.199 (2021-06-16) From 21fdbb529ccf91cdec96bdd149fc109782f5c2c5 Mon Sep 17 00:00:00 2001 From: Aleksandr Bunin Date: Wed, 27 Jul 2022 18:45:18 +0300 Subject: [PATCH 084/185] add methods description --- .../component/directives/aria/aria-setter.ts | 40 +++- .../component/directives/aria/interface.ts | 2 +- .../directives/aria/roles-engines/combobox.ts | 56 ++++-- .../directives/aria/roles-engines/controls.ts | 7 +- .../directives/aria/roles-engines/dialog.ts | 18 +- .../directives/aria/roles-engines/listbox.ts | 3 + .../directives/aria/roles-engines/option.ts | 11 +- .../directives/aria/roles-engines/tab.ts | 85 ++++++--- .../directives/aria/roles-engines/tablist.ts | 3 + .../directives/aria/roles-engines/tabpanel.ts | 3 + .../directives/aria/roles-engines/tree.ts | 32 +++- .../directives/aria/roles-engines/treeitem.ts | 174 +++++++++++------- 12 files changed, 297 insertions(+), 137 deletions(-) diff --git a/src/core/component/directives/aria/aria-setter.ts b/src/core/component/directives/aria/aria-setter.ts index 27a72f804f..5b402b2870 100644 --- a/src/core/component/directives/aria/aria-setter.ts +++ b/src/core/component/directives/aria/aria-setter.ts @@ -12,8 +12,18 @@ import AriaRoleEngine, { eventNames } from 'core/component/directives/aria/inter import type iBlock from 'super/i-block/i-block'; import type { DirectiveOptions } from 'core/component/directives/aria/interface'; +/** + * Class-helper for making base operations for the directive + */ export default class AriaSetter extends AriaRoleEngine { - override async: Async; + /** + * Async instance for aria directive + */ + override readonly async: Async; + + /** + * Role engine instance + */ role: CanUndef; constructor(options: DirectiveOptions) { @@ -29,6 +39,9 @@ export default class AriaSetter extends AriaRoleEngine { this.init(); } + /** + * Initiates the base logic of the directive + */ init(): void { this.setAriaLabel(); this.addEventHandlers(); @@ -36,6 +49,9 @@ export default class AriaSetter extends AriaRoleEngine { this.role?.init(); } + /** + * Runs on update directive hook. Removes listeners from component if the component is Functional + */ update(): void { const ctx = this.options.vnode.fakeContext; @@ -45,11 +61,17 @@ export default class AriaSetter extends AriaRoleEngine { } } + /** + * Runs on unbind directive hook. Clears the Async instance + */ destroy(): void { this.async.clearAll(); } - setAriaRole(): CanUndef { + /** + * If the role was passed as a directive argument sets specified engine + */ + protected setAriaRole(): CanUndef { const {arg: role} = this.options.binding; @@ -60,7 +82,11 @@ export default class AriaSetter extends AriaRoleEngine { this.role = new ariaRoles[role](this.options); } - setAriaLabel(): void { + /** + * Sets aria-label, aria-labelledby, aria-description and aria-describedby attributes to the element + * from passed parameters + */ + protected setAriaLabel(): void { const {vnode, binding, el} = this.options, {dom} = Object.cast(vnode.fakeContext), @@ -93,7 +119,11 @@ export default class AriaSetter extends AriaRoleEngine { } } - addEventHandlers(): void { + /** + * Sets handlers for the base role events: open, close, change. + * Expects the passed into directive specified event properties to be Function, Promise or String + */ + protected addEventHandlers(): void { if (this.role == null) { return; } @@ -104,7 +134,7 @@ export default class AriaSetter extends AriaRoleEngine { for (const key in params) { if (key in eventNames) { const - callback = this.role[eventNames[key]], + callback = this.role[eventNames[key]].bind(this.role), property = params[key]; if (Object.isFunction(property)) { diff --git a/src/core/component/directives/aria/interface.ts b/src/core/component/directives/aria/interface.ts index 9a788e5dc1..3c18527bfc 100644 --- a/src/core/component/directives/aria/interface.ts +++ b/src/core/component/directives/aria/interface.ts @@ -16,7 +16,7 @@ export interface DirectiveOptions { } export default abstract class AriaRoleEngine { - options: DirectiveOptions; + readonly options: DirectiveOptions; async: CanUndef; protected constructor(options: DirectiveOptions) { diff --git a/src/core/component/directives/aria/roles-engines/combobox.ts b/src/core/component/directives/aria/roles-engines/combobox.ts index 8ecf46a460..70921a3858 100644 --- a/src/core/component/directives/aria/roles-engines/combobox.ts +++ b/src/core/component/directives/aria/roles-engines/combobox.ts @@ -11,9 +11,16 @@ import type { ComboboxParams } from 'core/component/directives/aria/roles-engine import type iAccess from 'traits/i-access/i-access'; export default class ComboboxEngine extends AriaRoleEngine { - el: Element; + /** + * Passed directive params + */ params: ComboboxParams; + /** + * First focusable element inside the element with directive or this element if there is no focusable inside + */ + el: HTMLElement; + constructor(options: DirectiveOptions) { super(options); @@ -21,10 +28,13 @@ export default class ComboboxEngine extends AriaRoleEngine { {el} = this.options, ctx = Object.cast(this.options.vnode.fakeContext); - this.el = ctx.findFocusableElement() ?? el; + this.el = (>ctx.findFocusableElement()) ?? el; this.params = this.options.binding.value; } + /** + * Sets base aria attributes for current role + */ init(): void { this.el.setAttribute('role', 'combobox'); this.el.setAttribute('aria-expanded', 'false'); @@ -32,23 +42,41 @@ export default class ComboboxEngine extends AriaRoleEngine { if (this.params.isMultiple) { this.el.setAttribute('aria-multiselectable', 'true'); } + + if (this.el.tabIndex < 0) { + this.el.setAttribute('tabindex', '0'); + } + } + + /** + * Sets or deletes the id of active descendant element + */ + protected setAriaActive(el?: HTMLElement): void { + this.el.setAttribute('aria-activedescendant', el?.id ?? ''); } - onOpen = (element: HTMLElement): void => { + /** + * Handler: the option list is expanded + * @param el + */ + protected onOpen(el: HTMLElement): void { this.el.setAttribute('aria-expanded', 'true'); - this.setAriaActive(element); - }; + this.setAriaActive(el); + } - onClose = (): void => { + /** + * Handler: the option list is closed + */ + protected onClose(): void { this.el.setAttribute('aria-expanded', 'false'); this.setAriaActive(); - }; - - onChange = (element: HTMLElement): void => { - this.setAriaActive(element); - }; + } - setAriaActive = (element?: HTMLElement): void => { - this.el.setAttribute('aria-activedescendant', element?.id ?? ''); - }; + /** + * Handler: active option element was changed + * @param el + */ + protected onChange(el: HTMLElement): void { + this.setAriaActive(el); + } } diff --git a/src/core/component/directives/aria/roles-engines/controls.ts b/src/core/component/directives/aria/roles-engines/controls.ts index 98fc5e68b6..1d7b3d9a25 100644 --- a/src/core/component/directives/aria/roles-engines/controls.ts +++ b/src/core/component/directives/aria/roles-engines/controls.ts @@ -9,6 +9,9 @@ import AriaRoleEngine from 'core/component/directives/aria/interface'; export default class ControlsEngine extends AriaRoleEngine { + /** + * Sets base aria attributes for current role + */ init(): void { const {vnode, binding, el} = this.options, @@ -19,8 +22,8 @@ export default class ControlsEngine extends AriaRoleEngine { return; } - if (binding.value?.controls == null) { - Object.throw('Controls aria directive expects the controls value to be passed'); + if (binding.value?.id == null) { + Object.throw('Controls aria directive expects the id of controlling elements to be passed'); return; } diff --git a/src/core/component/directives/aria/roles-engines/dialog.ts b/src/core/component/directives/aria/roles-engines/dialog.ts index 9a8d91cfc4..4888432ccc 100644 --- a/src/core/component/directives/aria/roles-engines/dialog.ts +++ b/src/core/component/directives/aria/roles-engines/dialog.ts @@ -8,22 +8,20 @@ import iOpen from 'traits/i-open/i-open'; import AriaRoleEngine from 'core/component/directives/aria/interface'; -import type { DirectiveOptions } from 'core/component/directives/aria/interface'; export default class DialogEngine extends AriaRoleEngine { - constructor(options: DirectiveOptions) { - super(options); - - if (!iOpen.is(options.vnode.fakeContext)) { - Object.throw('Dialog aria directive expects the component to realize iOpen interface'); - } - } - + /** + * Sets base aria attributes for current role + */ init(): void { const - {el} = this.options; + {el, vnode} = this.options; el.setAttribute('role', 'dialog'); el.setAttribute('aria-modal', 'true'); + + if (!iOpen.is(vnode.fakeContext)) { + Object.throw('Dialog aria directive expects the component to realize iOpen interface'); + } } } diff --git a/src/core/component/directives/aria/roles-engines/listbox.ts b/src/core/component/directives/aria/roles-engines/listbox.ts index 203b12cde6..9bfd9acec3 100644 --- a/src/core/component/directives/aria/roles-engines/listbox.ts +++ b/src/core/component/directives/aria/roles-engines/listbox.ts @@ -9,6 +9,9 @@ import AriaRoleEngine from 'core/component/directives/aria/interface'; export default class ListboxEngine extends AriaRoleEngine { + /** + * Sets base aria attributes for current role + */ init(): void { const {el} = this.options; diff --git a/src/core/component/directives/aria/roles-engines/option.ts b/src/core/component/directives/aria/roles-engines/option.ts index 109e240835..b0cb7b3bbc 100644 --- a/src/core/component/directives/aria/roles-engines/option.ts +++ b/src/core/component/directives/aria/roles-engines/option.ts @@ -9,6 +9,9 @@ import AriaRoleEngine from 'core/component/directives/aria/interface'; export default class OptionEngine extends AriaRoleEngine { + /** + * Sets base aria attributes for current role + */ init(): void { const {el} = this.options, @@ -22,10 +25,14 @@ export default class OptionEngine extends AriaRoleEngine { } } - onChange = (isSelected: boolean): void => { + /** + * Handler: selected option changes + * @param isSelected + */ + protected onChange(isSelected: boolean): void { const {el} = this.options; el.setAttribute('aria-selected', String(isSelected)); - }; + } } diff --git a/src/core/component/directives/aria/roles-engines/tab.ts b/src/core/component/directives/aria/roles-engines/tab.ts index 1d7260f6c1..f2b656ffc6 100644 --- a/src/core/component/directives/aria/roles-engines/tab.ts +++ b/src/core/component/directives/aria/roles-engines/tab.ts @@ -15,7 +15,14 @@ import type iAccess from 'traits/i-access/i-access'; import type iBlock from 'super/i-block/i-block'; export default class TabEngine extends AriaRoleEngine { + /** + * Passed directive params + */ params: TabParams; + + /** + * Component instance + */ ctx: iAccess & iBlock; constructor(options: DirectiveOptions) { @@ -25,6 +32,9 @@ export default class TabEngine extends AriaRoleEngine { this.ctx = Object.cast(this.options.vnode.fakeContext); } + /** + * Sets base aria attributes for current role + */ init(): void { const {el} = this.options, @@ -43,38 +53,24 @@ export default class TabEngine extends AriaRoleEngine { } if (this.async != null) { - this.async.on(el, 'keydown', this.onKeydown); + this.async.on(el, 'keydown', this.onKeydown.bind(this)); } } - onChange = (active: Element | NodeListOf): void => { - const - {el} = this.options; - - function setAttributes(isSelected: boolean) { - el.setAttribute('aria-selected', String(isSelected)); - el.setAttribute('tabindex', isSelected ? '0' : '-1'); - } - - if (Object.isArrayLike(active)) { - for (let i = 0; i < active.length; i++) { - setAttributes(el === active[i]); - } - - return; - } - - setAttributes(el === active); - }; - - moveFocusToFirstTab(): void { + /** + * Moves focus to the first tab in tablist + */ + protected moveFocusToFirstTab(): void { const firstTab = >this.ctx.findFocusableElement(); firstTab?.focus(); } - moveFocusToLastTab(): void { + /** + * Moves focus to the last tab in tablist + */ + protected moveFocusToLastTab(): void { const tabs = >this.ctx.findAllFocusableElements(); @@ -88,16 +84,47 @@ export default class TabEngine extends AriaRoleEngine { lastTab?.focus(); } - moveFocus(step: 1 | -1): void { + /** + * Moves focus to the next or previous focusable element via the step parameter + * @param step + */ + protected moveFocus(step: 1 | -1): void { const focusable = >this.ctx.getNextFocusableElement(step); focusable?.focus(); } - onKeydown = (event: Event): void => { + /** + * Handler: active tab changes + * @param active + */ + protected onChange(active: Element | NodeListOf): void { const - evt = (event), + {el} = this.options; + + function setAttributes(isSelected: boolean) { + el.setAttribute('aria-selected', String(isSelected)); + el.setAttribute('tabindex', isSelected ? '0' : '-1'); + } + + if (Object.isArrayLike(active)) { + for (let i = 0; i < active.length; i++) { + setAttributes(el === active[i]); + } + + return; + } + + setAttributes(el === active); + } + + /** + * Handler: keyboard event + */ + protected onKeydown(e: Event): void { + const + evt = (e), {isVertical} = this.params; switch (evt.key) { @@ -137,7 +164,7 @@ export default class TabEngine extends AriaRoleEngine { return; } - event.stopPropagation(); - event.preventDefault(); - }; + e.stopPropagation(); + e.preventDefault(); + } } diff --git a/src/core/component/directives/aria/roles-engines/tablist.ts b/src/core/component/directives/aria/roles-engines/tablist.ts index 00cc31bcaa..9140b6246b 100644 --- a/src/core/component/directives/aria/roles-engines/tablist.ts +++ b/src/core/component/directives/aria/roles-engines/tablist.ts @@ -10,6 +10,9 @@ import AriaRoleEngine from 'core/component/directives/aria/interface'; import type { TablistParams } from 'core/component/directives/aria/roles-engines/interface'; export default class TablistEngine extends AriaRoleEngine { + /** + * Sets base aria attributes for current role + */ init(): void { const {el, binding} = this.options, diff --git a/src/core/component/directives/aria/roles-engines/tabpanel.ts b/src/core/component/directives/aria/roles-engines/tabpanel.ts index b128b2b079..9d59cac654 100644 --- a/src/core/component/directives/aria/roles-engines/tabpanel.ts +++ b/src/core/component/directives/aria/roles-engines/tabpanel.ts @@ -9,6 +9,9 @@ import AriaRoleEngine from 'core/component/directives/aria/interface'; export default class TabpanelEngine extends AriaRoleEngine { + /** + * Sets base aria attributes for current role + */ init(): void { const {el, binding} = this.options; diff --git a/src/core/component/directives/aria/roles-engines/tree.ts b/src/core/component/directives/aria/roles-engines/tree.ts index 4f7630d2fc..cc4216d758 100644 --- a/src/core/component/directives/aria/roles-engines/tree.ts +++ b/src/core/component/directives/aria/roles-engines/tree.ts @@ -10,29 +10,47 @@ import AriaRoleEngine, { DirectiveOptions } from 'core/component/directives/aria import type { TreeParams } from 'core/component/directives/aria/roles-engines/interface'; export default class TreeEngine extends AriaRoleEngine { + /** + * Passed directive params + */ params: TreeParams; - el: HTMLElement; constructor(options: DirectiveOptions) { super(options); this.params = options.binding.value; - this.el = this.options.el; } + /** + * Sets base aria attributes for current role + */ init(): void { + const + {el} = this.options; + this.setRootRole(); if (this.params.isVertical) { - this.el.setAttribute('aria-orientation', 'vertical'); + el.setAttribute('aria-orientation', 'vertical'); } } - setRootRole(): void { - this.el.setAttribute('role', this.params.isRoot ? 'tree' : 'group'); + /** + * Sets the role to the element depending on whether the tree is root or nested + */ + protected setRootRole(): void { + const + {el} = this.options; + + el.setAttribute('role', this.params.isRoot ? 'tree' : 'group'); } - onChange = (el: HTMLElement, isFolded: boolean): void => { + /** + * Handler: treeitem was expanded or closed + * @param el + * @param isFolded + */ + protected onChange(el: HTMLElement, isFolded: boolean): void { el.setAttribute('aria-expanded', String(!isFolded)); - }; + } } diff --git a/src/core/component/directives/aria/roles-engines/treeitem.ts b/src/core/component/directives/aria/roles-engines/treeitem.ts index 529da86244..5a2722424d 100644 --- a/src/core/component/directives/aria/roles-engines/treeitem.ts +++ b/src/core/component/directives/aria/roles-engines/treeitem.ts @@ -16,9 +16,20 @@ import type { TreeitemParams } from 'core/component/directives/aria/roles-engine import type iBlock from 'super/i-block/i-block'; export default class TreeItemEngine extends AriaRoleEngine { + /** + * Passed directive params + */ + params: TreeitemParams; + + /** + * Component instance + */ ctx: iAccess & iBlock['unsafe']; + + /** + * Element with current directive + */ el: HTMLElement; - params: TreeitemParams; constructor(options: DirectiveOptions) { super(options); @@ -32,8 +43,11 @@ export default class TreeItemEngine extends AriaRoleEngine { this.params = this.options.binding.value; } + /** + * Sets base aria attributes for current role + */ init(): void { - this.async?.on(this.el, 'keydown', this.onKeyDown); + this.async?.on(this.el, 'keydown', this.onKeyDown.bind(this)); const isMuted = this.ctx.removeAllFromTabSequence(this.el); @@ -56,70 +70,22 @@ export default class TreeItemEngine extends AriaRoleEngine { }); } - onKeyDown = (e: KeyboardEvent): void => { - if (e.altKey || e.ctrlKey || e.metaKey || e.shiftKey) { - return; - } - - switch (e.code) { - case keyCodes.UP: - this.moveFocus(-1); - break; - - case keyCodes.DOWN: - this.moveFocus(1); - break; - - case keyCodes.ENTER: - this.params.toggleFold(this.el); - break; - - case keyCodes.RIGHT: - if (this.params.isExpandable) { - if (this.params.isExpanded) { - this.moveFocus(1); - - } else { - this.openFold(); - } - } - - break; - - case keyCodes.LEFT: - if (this.params.isExpandable && this.params.isExpanded) { - this.closeFold(); - - } else { - this.focusParent(); - } - - break; - - case keyCodes.HOME: - this.setFocusToFirstItem(); - break; - - case keyCodes.END: - this.setFocusToLastItem(); - break; - - default: - return; - } - - e.stopPropagation(); - e.preventDefault(); - }; - - focusNext(nextEl: HTMLElement): void { + /** + * Changes focus from the current focused element to the passed one + * @param el + */ + protected focusNext(el: HTMLElement): void { this.ctx.removeAllFromTabSequence(this.el); - this.ctx.restoreAllToTabSequence(nextEl); + this.ctx.restoreAllToTabSequence(el); - nextEl.focus(); + el.focus(); } - moveFocus(step: 1 | -1): void { + /** + * Moves focus to the next or previous focusable element via the step parameter + * @param step + */ + protected moveFocus(step: 1 | -1): void { const nextEl = >this.ctx.getNextFocusableElement(step); @@ -128,15 +94,24 @@ export default class TreeItemEngine extends AriaRoleEngine { } } - openFold(): void { + /** + * Expands the treeitem + */ + protected openFold(): void { this.params.toggleFold(this.el, false); } - closeFold(): void { + /** + * Closes the treeitem + */ + protected closeFold(): void { this.params.toggleFold(this.el, true); } - focusParent(): void { + /** + * Moves focus to the parent treeitem + */ + protected focusParent(): void { let parent = this.el.parentElement; @@ -160,7 +135,10 @@ export default class TreeItemEngine extends AriaRoleEngine { } } - setFocusToFirstItem(): void { + /** + * Moves focus to the first visible treeitem + */ + protected setFocusToFirstItem(): void { const firstItem = >this.ctx.findFocusableElement(this.params.rootElement); @@ -169,7 +147,10 @@ export default class TreeItemEngine extends AriaRoleEngine { } } - setFocusToLastItem(): void { + /** + * Moves focus to the last visible treeitem + */ + protected setFocusToLastItem(): void { const items = >this.ctx.findAllFocusableElements(this.params.rootElement); @@ -186,4 +167,63 @@ export default class TreeItemEngine extends AriaRoleEngine { this.focusNext(lastItem); } } + + /** + * Handler: keyboard event + */ + protected onKeyDown(e: KeyboardEvent): void { + if (e.altKey || e.ctrlKey || e.metaKey || e.shiftKey) { + return; + } + + switch (e.key) { + case keyCodes.UP: + this.moveFocus(-1); + break; + + case keyCodes.DOWN: + this.moveFocus(1); + break; + + case keyCodes.ENTER: + this.params.toggleFold(this.el); + break; + + case keyCodes.RIGHT: + if (this.params.isExpandable) { + if (this.params.isExpanded) { + this.moveFocus(1); + + } else { + this.openFold(); + } + } + + break; + + case keyCodes.LEFT: + if (this.params.isExpandable && this.params.isExpanded) { + this.closeFold(); + + } else { + this.focusParent(); + } + + break; + + case keyCodes.HOME: + this.setFocusToFirstItem(); + break; + + case keyCodes.END: + this.setFocusToLastItem(); + break; + + default: + return; + } + + e.stopPropagation(); + e.preventDefault(); + } } From 5836d467a1a0050ffb0f6a28ea9061931f08867f Mon Sep 17 00:00:00 2001 From: Aleksandr Bunin Date: Fri, 29 Jul 2022 15:40:38 +0300 Subject: [PATCH 085/185] fix v-attrs parse regexp --- src/core/component/render-function/const.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/component/render-function/const.ts b/src/core/component/render-function/const.ts index 833defaf0c..18dc3de3d9 100644 --- a/src/core/component/render-function/const.ts +++ b/src/core/component/render-function/const.ts @@ -7,4 +7,4 @@ */ export const - vAttrsRgxp = /(v-(.*?))(?::(.*?))?(\..*)?$/; + vAttrsRgxp = /(v-(.*?))(?::(.*?))?(?:\.(.*))?$/; From 23f9a57a133a4aaa66d6ca8d469f1de76cf6ace2 Mon Sep 17 00:00:00 2001 From: Aleksandr Bunin Date: Tue, 2 Aug 2022 16:42:48 +0300 Subject: [PATCH 086/185] fixes --- src/base/b-list/b-list.ts | 10 +-- src/base/b-tree/b-tree.ts | 5 +- src/core/component/directives/aria/README.md | 28 ++++++++- .../directives/aria/roles-engines/controls.ts | 44 +++++++------ .../directives/aria/roles-engines/dialog.ts | 7 +-- .../aria/roles-engines/interface.ts | 8 ++- .../directives/aria/roles-engines/option.ts | 4 -- .../directives/aria/roles-engines/tab.ts | 19 +++++- .../directives/aria/roles-engines/tablist.ts | 4 +- .../directives/aria/roles-engines/tabpanel.ts | 9 +-- .../directives/aria/roles-engines/tree.ts | 7 ++- .../directives/aria/roles-engines/treeitem.ts | 61 ++++++++++++++----- src/core/component/directives/id/README.md | 9 +++ src/core/component/directives/id/index.ts | 7 ++- src/form/b-select/b-select.ss | 17 +++--- src/traits/i-access/i-access.ts | 6 +- 16 files changed, 164 insertions(+), 81 deletions(-) diff --git a/src/base/b-list/b-list.ts b/src/base/b-list/b-list.ts index 205130c1ca..6db33ab9f0 100644 --- a/src/base/b-list/b-list.ts +++ b/src/base/b-list/b-list.ts @@ -713,8 +713,7 @@ class bList extends iData implements iVisible, iWidth, iItems, iAccess { protected getAriaConfig(role: 'tab' | 'tablist', item?: this['Item'], i?: number): Dictionary { const - isActive = this.isActive.bind(this, item?.value), - isVertical = this.orientation === 'vertical'; + isActive = this.isActive.bind(this, item?.value); const changeEvent = (cb: Function) => { this.on('change', () => { @@ -728,13 +727,14 @@ class bList extends iData implements iVisible, iWidth, iItems, iAccess { }; const tablistConfig = { - isVertical, - isMultiple: this.multiple + isMultiple: this.multiple, + orientation: this.orientation }; const tabConfig = { - isVertical, + preSelected: this.active != null, isFirst: i === 0, + orientation: this.orientation, changeEvent, get isActive() { return isActive(); diff --git a/src/base/b-tree/b-tree.ts b/src/base/b-tree/b-tree.ts index e68db8673f..1d763346bb 100644 --- a/src/base/b-tree/b-tree.ts +++ b/src/base/b-tree/b-tree.ts @@ -109,7 +109,7 @@ class bTree extends iData implements iItems, iAccess { * The component view orientation */ @prop(String) - readonly orientation: Orientation = 'horizontal'; + readonly orientation: Orientation = 'vertical'; /** * Link to the top level component (internal parameter) @@ -289,7 +289,7 @@ class bTree extends iData implements iItems, iAccess { const treeConfig = { isRoot: this.top == null, - isVertical: this.orientation === 'vertical', + orientation: this.orientation, changeEvent: (cb: Function) => { this.on('fold', (ctx, el, item, value) => cb(el, value)); } @@ -297,6 +297,7 @@ class bTree extends iData implements iItems, iAccess { const treeitemConfig = { isRootFirstItem: this.top == null && i === 0, + orientation: this.orientation, toggleFold, get rootElement() { return root(); diff --git a/src/core/component/directives/aria/README.md b/src/core/component/directives/aria/README.md index e12cd09021..15c7ff29f8 100644 --- a/src/core/component/directives/aria/README.md +++ b/src/core/component/directives/aria/README.md @@ -22,13 +22,35 @@ Directive can be added to any tag that includes tag with needed role. Role shoul ID or IDs are passed as value. ID could be single or multiple written in string with space between. +There are two ways to use this engine: +1. To add role as a modifier to which passed IDs in `for` value should be added. `for` could be `string` or `string[]`. +If element controls several elements `for` should be passed as a string with IDs separated with space. +(!) Notice that this role attribute should already be added to the element. The engine does not set passed role to the current element. + Example: ``` -< &__foo v-aria:controls.select = {id: 'id1 id2 id3'} +< &__foo v-aria:controls.tab = {for: 'id1 id2 id3'} + +converts to + +< tab aria-controls = "id1 id2 id3" role = "tab" +``` -same as +2. To pass value `for` as an array of tuples. +First id in a tuple is an id of an element to which one the aria attributes should be added. +The second one is an id of an element to set as value in aria-controls attribute. +(!) Notice that id attribute should already be added to the element. The engine does not set passed ids to the elements. + +Example: +``` +< &__foo v-aria:controls = {for: [[id1, id3], [id2, id4]]} + < span :id = "id1" + < span :id = "id2" -< select aria-controls = "id1 id2 id3" +converts to +< &__foo + < span :id = "id1" aria-controls = "id3" + < span :id = "id2" aria-controls = "id4" ``` - tabs: diff --git a/src/core/component/directives/aria/roles-engines/controls.ts b/src/core/component/directives/aria/roles-engines/controls.ts index 1d7b3d9a25..76b2d597f9 100644 --- a/src/core/component/directives/aria/roles-engines/controls.ts +++ b/src/core/component/directives/aria/roles-engines/controls.ts @@ -15,32 +15,40 @@ export default class ControlsEngine extends AriaRoleEngine { init(): void { const {vnode, binding, el} = this.options, + {modifiers, value} = binding, {fakeContext: ctx} = vnode; - if (binding.modifiers == null) { - Object.throw('Controls aria directive expects the role modifier to be passed'); - return; - } - - if (binding.value?.id == null) { - Object.throw('Controls aria directive expects the id of controlling elements to be passed'); + if (value?.for == null) { + Object.throw('Controls aria directive expects the id of controlling elements to be passed as "for" prop'); return; } const - roleName = Object.keys(binding.modifiers)[0]; - - ctx?.$nextTick().then(() => { - const - elems = el.querySelectorAll(`[role=${roleName}]`); + isForPropArray = Object.isArray(value.for), + isForPropArrayOfTuples = Object.isArray(value.for) && Object.isArray(value.for[0]); - for (let i = 0; i < elems.length; i++) { + if (modifiers != null && Object.size(modifiers) > 0) { + ctx?.$nextTick().then(() => { const - elem = elems[i], - {id} = binding.value; + roleName = Object.keys(modifiers)[0], + elems = el.querySelectorAll(`[role=${roleName}]`); - elem.setAttribute('aria-controls', id); - } - }); + if (isForPropArray && value.for.length !== elems.length) { + Object.throw('Controls aria directive expects prop "for" length to be equal to amount of elements with specified role or string type'); + return; + } + + elems.forEach((el, i) => { + el.setAttribute('aria-controls', isForPropArray ? value.for[i] : value.for); + }); + }); + + } else if (isForPropArrayOfTuples) { + value.for.forEach(([elId, controlsId]) => { + const element = el.querySelector(`#${elId}`); + + element?.setAttribute('aria-controls', controlsId); + }); + } } } diff --git a/src/core/component/directives/aria/roles-engines/dialog.ts b/src/core/component/directives/aria/roles-engines/dialog.ts index 4888432ccc..de7e868539 100644 --- a/src/core/component/directives/aria/roles-engines/dialog.ts +++ b/src/core/component/directives/aria/roles-engines/dialog.ts @@ -6,7 +6,6 @@ * https://github.com/V4Fire/Client/blob/master/LICENSE */ -import iOpen from 'traits/i-open/i-open'; import AriaRoleEngine from 'core/component/directives/aria/interface'; export default class DialogEngine extends AriaRoleEngine { @@ -15,13 +14,9 @@ export default class DialogEngine extends AriaRoleEngine { */ init(): void { const - {el, vnode} = this.options; + {el} = this.options; el.setAttribute('role', 'dialog'); el.setAttribute('aria-modal', 'true'); - - if (!iOpen.is(vnode.fakeContext)) { - Object.throw('Dialog aria directive expects the component to realize iOpen interface'); - } } } diff --git a/src/core/component/directives/aria/roles-engines/interface.ts b/src/core/component/directives/aria/roles-engines/interface.ts index 61922d6fa1..3ab7869aa3 100644 --- a/src/core/component/directives/aria/roles-engines/interface.ts +++ b/src/core/component/directives/aria/roles-engines/interface.ts @@ -1,18 +1,19 @@ export interface TabParams { + preSelected: boolean; isFirst: boolean; - isVertical: boolean; isActive: boolean; + orientation: string; changeEvent(cb: Function): void; } export interface TablistParams { - isVertical: boolean; isMultiple: boolean; + orientation: string; } export interface TreeParams { isRoot: boolean; - isVertical: boolean; + orientation: string; changeEvent(cb: Function): void; } @@ -20,6 +21,7 @@ export interface TreeitemParams { isRootFirstItem: boolean; isExpandable: boolean; isExpanded: boolean; + orientation: string; rootElement: CanUndef; toggleFold(el: Element, value?: boolean): void; } diff --git a/src/core/component/directives/aria/roles-engines/option.ts b/src/core/component/directives/aria/roles-engines/option.ts index b0cb7b3bbc..3b30bfc48b 100644 --- a/src/core/component/directives/aria/roles-engines/option.ts +++ b/src/core/component/directives/aria/roles-engines/option.ts @@ -19,10 +19,6 @@ export default class OptionEngine extends AriaRoleEngine { el.setAttribute('role', 'option'); el.setAttribute('aria-selected', String(preSelected)); - - if (!el.hasAttribute('id')) { - Object.throw('Option aria directive expects the Element id to be added'); - } } /** diff --git a/src/core/component/directives/aria/roles-engines/tab.ts b/src/core/component/directives/aria/roles-engines/tab.ts index f2b656ffc6..ce7b7d845b 100644 --- a/src/core/component/directives/aria/roles-engines/tab.ts +++ b/src/core/component/directives/aria/roles-engines/tab.ts @@ -38,12 +38,17 @@ export default class TabEngine extends AriaRoleEngine { init(): void { const {el} = this.options, - {isFirst} = this.params; + {isFirst, preSelected} = this.params; el.setAttribute('role', 'tab'); el.setAttribute('aria-selected', String(this.params.isActive)); - if (isFirst) { + if (isFirst && !preSelected) { + if (el.tabIndex < 0) { + el.setAttribute('tabindex', '0'); + } + + } else if (preSelected && this.params.isActive) { if (el.tabIndex < 0) { el.setAttribute('tabindex', '0'); } @@ -125,10 +130,14 @@ export default class TabEngine extends AriaRoleEngine { protected onKeydown(e: Event): void { const evt = (e), - {isVertical} = this.params; + isVertical = this.params.orientation === 'vertical'; switch (evt.key) { case keyCodes.LEFT: + if (isVertical) { + return; + } + this.moveFocus(-1); break; @@ -141,6 +150,10 @@ export default class TabEngine extends AriaRoleEngine { return; case keyCodes.RIGHT: + if (isVertical) { + return; + } + this.moveFocus(1); break; diff --git a/src/core/component/directives/aria/roles-engines/tablist.ts b/src/core/component/directives/aria/roles-engines/tablist.ts index 9140b6246b..b5c4653cd4 100644 --- a/src/core/component/directives/aria/roles-engines/tablist.ts +++ b/src/core/component/directives/aria/roles-engines/tablist.ts @@ -24,8 +24,8 @@ export default class TablistEngine extends AriaRoleEngine { el.setAttribute('aria-multiselectable', 'true'); } - if (params.isVertical) { - el.setAttribute('aria-orientation', 'vertical'); + if (params.orientation === 'vertical') { + el.setAttribute('aria-orientation', params.orientation); } } } diff --git a/src/core/component/directives/aria/roles-engines/tabpanel.ts b/src/core/component/directives/aria/roles-engines/tabpanel.ts index 9d59cac654..ca3537088c 100644 --- a/src/core/component/directives/aria/roles-engines/tabpanel.ts +++ b/src/core/component/directives/aria/roles-engines/tabpanel.ts @@ -14,12 +14,13 @@ export default class TabpanelEngine extends AriaRoleEngine { */ init(): void { const - {el, binding} = this.options; + {el} = this.options; - el.setAttribute('role', 'tabpanel'); - - if (binding.value?.labelledby == null) { + if (!el.hasAttribute('aria-label') && !el.hasAttribute('aria-labelledby')) { Object.throw('Tabpanel aria directive expects "label" or "labelledby" value to be passed'); + return; } + + el.setAttribute('role', 'tabpanel'); } } diff --git a/src/core/component/directives/aria/roles-engines/tree.ts b/src/core/component/directives/aria/roles-engines/tree.ts index cc4216d758..3b8986e196 100644 --- a/src/core/component/directives/aria/roles-engines/tree.ts +++ b/src/core/component/directives/aria/roles-engines/tree.ts @@ -26,12 +26,13 @@ export default class TreeEngine extends AriaRoleEngine { */ init(): void { const - {el} = this.options; + {el} = this.options, + {orientation} = this.params; this.setRootRole(); - if (this.params.isVertical) { - el.setAttribute('aria-orientation', 'vertical'); + if (orientation === 'horizontal' && this.params.isRoot) { + el.setAttribute('aria-orientation', orientation); } } diff --git a/src/core/component/directives/aria/roles-engines/treeitem.ts b/src/core/component/directives/aria/roles-engines/treeitem.ts index 5a2722424d..9215388542 100644 --- a/src/core/component/directives/aria/roles-engines/treeitem.ts +++ b/src/core/component/directives/aria/roles-engines/treeitem.ts @@ -176,39 +176,68 @@ export default class TreeItemEngine extends AriaRoleEngine { return; } + const + isHorizontal = this.params.orientation === 'horizontal'; + + const open = () => { + if (this.params.isExpandable) { + if (this.params.isExpanded) { + this.moveFocus(1); + + } else { + this.openFold(); + } + } + }; + + const close = () => { + if (this.params.isExpandable && this.params.isExpanded) { + this.closeFold(); + + } else { + this.focusParent(); + } + }; + switch (e.key) { case keyCodes.UP: + if (isHorizontal) { + close(); + break; + } + this.moveFocus(-1); break; case keyCodes.DOWN: - this.moveFocus(1); - break; + if (isHorizontal) { + open(); + break; + } - case keyCodes.ENTER: - this.params.toggleFold(this.el); + this.moveFocus(1); break; case keyCodes.RIGHT: - if (this.params.isExpandable) { - if (this.params.isExpanded) { - this.moveFocus(1); - - } else { - this.openFold(); - } + if (isHorizontal) { + this.moveFocus(1); + break; } + open(); break; case keyCodes.LEFT: - if (this.params.isExpandable && this.params.isExpanded) { - this.closeFold(); - - } else { - this.focusParent(); + if (isHorizontal) { + this.moveFocus(-1); + break; } + close(); + break; + + case keyCodes.ENTER: + this.params.toggleFold(this.el); break; case keyCodes.HOME: diff --git a/src/core/component/directives/id/README.md b/src/core/component/directives/id/README.md index 4a8008f9bd..58c7b90465 100644 --- a/src/core/component/directives/id/README.md +++ b/src/core/component/directives/id/README.md @@ -15,3 +15,12 @@ The same as ``` +## Modifiers + +1. `preserve` means that if there is already an id attribute on the element, +the directive will left it and will not set another one + +``` +< &__foo v-id.preserve = 'title' + +``` diff --git a/src/core/component/directives/id/index.ts b/src/core/component/directives/id/index.ts index d8636c4965..7e1c4a51ab 100644 --- a/src/core/component/directives/id/index.ts +++ b/src/core/component/directives/id/index.ts @@ -17,7 +17,12 @@ import type iBlock from 'super/i-block/i-block'; ComponentEngine.directive('id', { inserted(el: HTMLElement, binding: VNodeDirective, vnode: VNode): void { const - ctx = Object.cast(vnode.fakeContext); + ctx = Object.cast(vnode.fakeContext), + {modifiers: mod} = binding; + + if (mod?.preserve != null && el.hasAttribute('id')) { + return; + } const id = ctx.dom.getId(binding.value); diff --git a/src/form/b-select/b-select.ss b/src/form/b-select/b-select.ss index db1a29e258..cd567ca839 100644 --- a/src/form/b-select/b-select.ss +++ b/src/form/b-select/b-select.ss @@ -39,14 +39,15 @@ } })) | - :v-attrs = native - ? el.attrs - : { - 'v-aria:option': getAriaConfig('option', el), - ...el.attrs - } | - - v-id = el.value | + :v-attrs = native ? + el.attrs : + { + 'v-aria:option': getAriaConfig('option', el), + ...el.attrs + } | + + v-id.preserve = el.value | + ${itemAttrs} . += self.slot('default', {':item': 'el'}) diff --git a/src/traits/i-access/i-access.ts b/src/traits/i-access/i-access.ts index 2baf4546b6..c5c6aced57 100644 --- a/src/traits/i-access/i-access.ts +++ b/src/traits/i-access/i-access.ts @@ -267,7 +267,7 @@ export default abstract class iAccess { focusableElems = this.findAllFocusableElements(component, ctx); for (const focusableEl of focusableElems) { - if (!focusableEl.hasAttribute('disabled')) { + if (!focusableEl?.hasAttribute('disabled')) { return focusableEl; } } @@ -275,7 +275,7 @@ export default abstract class iAccess { /** @see [[iAccess.findAllFocusableElements]] */ static findAllFocusableElements: AddSelf = - (component, el?): IterableIterator => { + (component, el?): IterableIterator> => { const ctx = el ?? component.$el, focusableElems = ctx?.querySelectorAll(FOCUSABLE_SELECTOR); @@ -419,7 +419,7 @@ export default abstract class iAccess { * Find all focusable elements except disabled ones. Search includes the specified element * @param el - a context to search, if not set, component will be used */ - findAllFocusableElements(el?: Element): IterableIterator { + findAllFocusableElements(el?: Element): IterableIterator> { return Object.throw(); } } From 50c53542e4df55354607c0445e66f5e0bd440dbf Mon Sep 17 00:00:00 2001 From: Aleksandr Bunin Date: Tue, 2 Aug 2022 16:43:24 +0300 Subject: [PATCH 087/185] add tests --- .../directives/aria/test/unit/combobox.ts | 131 ++++++++++ .../directives/aria/test/unit/controls.ts | 159 ++++++++++++ .../directives/aria/test/unit/dialog.ts | 51 ++++ .../directives/aria/test/unit/listbox.ts | 66 +++++ .../directives/aria/test/unit/option.ts | 117 +++++++++ .../directives/aria/test/unit/simple.ts | 72 ++++++ .../directives/aria/test/unit/tab.ts | 226 ++++++++++++++++++ .../directives/aria/test/unit/tablist.ts | 72 ++++++ .../directives/aria/test/unit/tabpanel.ts | 49 ++++ .../directives/aria/test/unit/tree.ts | 103 ++++++++ .../directives/aria/test/unit/treeitem.ts | 182 ++++++++++++++ .../directives/id/test/unit/functional.ts | 47 ++++ 12 files changed, 1275 insertions(+) create mode 100644 src/core/component/directives/aria/test/unit/combobox.ts create mode 100644 src/core/component/directives/aria/test/unit/controls.ts create mode 100644 src/core/component/directives/aria/test/unit/dialog.ts create mode 100644 src/core/component/directives/aria/test/unit/listbox.ts create mode 100644 src/core/component/directives/aria/test/unit/option.ts create mode 100644 src/core/component/directives/aria/test/unit/simple.ts create mode 100644 src/core/component/directives/aria/test/unit/tab.ts create mode 100644 src/core/component/directives/aria/test/unit/tablist.ts create mode 100644 src/core/component/directives/aria/test/unit/tabpanel.ts create mode 100644 src/core/component/directives/aria/test/unit/tree.ts create mode 100644 src/core/component/directives/aria/test/unit/treeitem.ts create mode 100644 src/core/component/directives/id/test/unit/functional.ts diff --git a/src/core/component/directives/aria/test/unit/combobox.ts b/src/core/component/directives/aria/test/unit/combobox.ts new file mode 100644 index 0000000000..8b99af6f76 --- /dev/null +++ b/src/core/component/directives/aria/test/unit/combobox.ts @@ -0,0 +1,131 @@ +// @ts-check + +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ +import type { JSHandle, Page } from 'playwright'; +import type iBlock from 'super/i-block/i-block'; + +import test from 'tests/config/unit/test'; +import Component from 'tests/helpers/component'; + +test.describe('v-aria:combobox', () => { + test.beforeEach(async ({demoPage}) => { + await demoPage.goto(); + }); + + const + selector = '[data-id="target"]'; + + /** + * Initial attributes + */ + test('role is set', async ({page}) => { + const target = await init(page); + + test.expect( + await target.evaluate((ctx) => ctx.unsafe.block?.element('input')?.getAttribute('role')) + ).toBe('combobox'); + }); + + test('aria-expanded is set to false', async ({page}) => { + const target = await init(page); + + test.expect( + await target.evaluate((ctx) => ctx.unsafe.block?.element('input')?.getAttribute('aria-expanded')) + ).toBe('false'); + }); + + test('aria-multiselectable is set', async ({page}) => { + const target = await init(page, {multiple: true}); + + test.expect( + await target.evaluate((ctx) => ctx.unsafe.block?.element('input')?.getAttribute('aria-multiselectable')) + ).toBe('true'); + }); + + test('element\'s tabindex is 0', async ({page}) => { + const target = await init(page); + + test.expect( + await target.evaluate((ctx) => { + const input = ctx.unsafe.block?.element('input'); + return input.tabIndex; + }) + ).toBe(0); + }); + + /** + * Handling events + */ + test('select is opened with no preselected option', async ({page}) => { + const target = await init(page); + + await page.click(selector); + + test.expect( + await target.evaluate((ctx) => ctx.unsafe.block?.element('input')?.getAttribute('aria-expanded')) + ).toBe('true'); + + test.expect( + await target.evaluate((ctx) => ctx.unsafe.block?.element('input')?.getAttribute('aria-activedescendant')) + ).toBe(''); + }); + + test('select is opened with preselected option', async ({page}) => { + const target = await init(page, {value: 1}); + + await page.focus('input'); + + const id = await target.evaluate((ctx) => ctx.unsafe.dom.getId('1')); + + test.expect( + await target.evaluate((ctx) => ctx.unsafe.block?.element('input')?.getAttribute('aria-expanded')) + ).toBe('true'); + + test.expect( + await target.evaluate((ctx) => ctx.unsafe.block?.element('input')?.getAttribute('aria-activedescendant')) + ).toBe(id); + }); + + test('select is opened and closed', async ({page}) => { + const target = await init(page, {value: 1}); + + await page.focus('input'); + + test.expect( + await target.evaluate((ctx) => ctx.unsafe.block?.element('input')?.getAttribute('aria-expanded')) + ).toBe('true'); + + await page.click('body'); + + test.expect( + await target.evaluate((ctx) => ctx.unsafe.block?.element('input')?.getAttribute('aria-expanded')) + ).toBe('false'); + + test.expect( + await target.evaluate((ctx) => ctx.unsafe.block?.element('input')?.getAttribute('aria-activedescendant')) + ).toBe(''); + }); + + /** + * @param page + * @param attrs + */ + async function init(page: Page, attrs: Dictionary = {}): Promise> { + return Component.createComponent(page, 'b-select', { + attrs: { + 'data-id': 'target', + items: [ + {label: 'foo', value: 0}, + {label: 'bar', value: 1} + ], + ...attrs + } + }); + } +}); diff --git a/src/core/component/directives/aria/test/unit/controls.ts b/src/core/component/directives/aria/test/unit/controls.ts new file mode 100644 index 0000000000..5d014fe155 --- /dev/null +++ b/src/core/component/directives/aria/test/unit/controls.ts @@ -0,0 +1,159 @@ +// @ts-check + +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ +import type { JSHandle, Page } from 'playwright'; +import type iBlock from 'super/i-block/i-block'; + +import test from 'tests/config/unit/test'; +import Component from 'tests/helpers/component'; + +test.describe('v-aria:controls', () => { + test.beforeEach(async ({demoPage}) => { + await demoPage.goto(); + }); + + /** + * With modifiers + */ + test('modifiers. "for" is a string', async ({page}) => { + const target = await init(page, {for: 'id3'}); + + test.expect( + await target.evaluate((ctx) => { + if (ctx.unsafe.block == null) { + return; + } + + const [el1, el2] = Array.from(ctx.unsafe.block.elements('link')); + + return el1.getAttribute('aria-controls') === 'id3' && el2.getAttribute('aria-controls') === 'id3'; + }) + ).toBe(true); + }); + + test('modifiers. "for" is an array', async ({page}) => { + const target = await init(page, {for: ['id3', 'id4']}); + + test.expect( + await target.evaluate((ctx) => { + if (ctx.unsafe.block == null) { + return; + } + + const [el1, el2]: HTMLElement[] = Array.from(ctx.unsafe.block.elements('link')); + + return [el1.getAttribute('aria-controls'), el2.getAttribute('aria-controls')]; + }) + ).toEqual(['id3', 'id4']); + }); + + test('modifiers. "for" is an array with wrong length', async ({page}) => { + const target = await init(page, {for: ['id3', 'id4', 'id5']}); + + test.expect( + await target.evaluate((ctx) => { + if (ctx.unsafe.block == null) { + return; + } + + const [el1, el2]: HTMLElement[] = Array.from(ctx.unsafe.block.elements('link')); + + return [el1.hasAttribute('aria-controls'), el2.hasAttribute('aria-controls')]; + }) + ).toEqual([false, false]); + }); + + test('modifiers. no "for" value passed', async ({page}) => { + const target = await init(page); + + test.expect( + await target.evaluate((ctx) => { + if (ctx.unsafe.block == null) { + return; + } + + const [el1, el2]: HTMLElement[] = Array.from(ctx.unsafe.block.elements('link')); + + return [el1.hasAttribute('aria-controls'), el2.hasAttribute('aria-controls')]; + }) + ).toEqual([false, false]); + }); + + /** + * With 'for' param as an array of tuples + */ + test('tuples', async ({page}) => { + const target = await init(page, {for: [['id1', 'id3'], ['id2', 'id4']]}, 'v-aria:controls'); + + test.expect( + await target.evaluate((ctx) => { + if (ctx.unsafe.block == null) { + return; + } + + const [el1, el2]: HTMLElement[] = Array.from(ctx.unsafe.block.elements('link')); + + return [el1.getAttribute('aria-controls'), el2.getAttribute('aria-controls')]; + }) + ).toEqual(['id3', 'id4']); + }); + + test('tuples. wrong ids', async ({page}) => { + const target = await init(page, {for: [['id5', 'id6'], ['id3', 'id8']]}, 'v-aria:controls'); + + test.expect( + await target.evaluate((ctx) => { + if (ctx.unsafe.block == null) { + return; + } + + const [el1, el2]: HTMLElement[] = Array.from(ctx.unsafe.block.elements('link')); + + return [el1.hasAttribute('aria-controls'), el2.hasAttribute('aria-controls')]; + }) + ).toEqual([false, false]); + }); + + test('no params passed', async ({page}) => { + const target = await init(page); + + test.expect( + await target.evaluate((ctx) => { + if (ctx.unsafe.block == null) { + return; + } + + const [el1, el2]: HTMLElement[] = Array.from(ctx.unsafe.block.elements('link')); + + return [el1.hasAttribute('aria-controls'), el2.hasAttribute('aria-controls')]; + }) + ).toEqual([false, false]); + }); + + /** + * @param page + * @param ariaConfig + * @param directive + */ + async function init( + page: Page, + ariaConfig: Dictionary = {}, + directive: string = 'v-aria:controls.tab' + ): Promise> { + return Component.createComponent(page, 'b-list', { + attrs: { + [directive]: ariaConfig, + items: [ + {label: 'foo', value: 0, attrs: {id: 'id1'}}, + {label: 'bla', value: 1, attrs: {id: 'id2'}} + ] + } + }); + } +}); diff --git a/src/core/component/directives/aria/test/unit/dialog.ts b/src/core/component/directives/aria/test/unit/dialog.ts new file mode 100644 index 0000000000..f1f2c031f1 --- /dev/null +++ b/src/core/component/directives/aria/test/unit/dialog.ts @@ -0,0 +1,51 @@ +// @ts-check + +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ +import type { JSHandle, Page } from 'playwright'; +import type iBlock from 'super/i-block/i-block'; + +import test from 'tests/config/unit/test'; +import Component from 'tests/helpers/component'; + +test.describe('v-aria:dialog', () => { + test.beforeEach(async ({demoPage}) => { + await demoPage.goto(); + }); + + test('role is set', async ({page}) => { + const target = await init(page); + + test.expect( + await target.evaluate((ctx) => { + const el = ctx.unsafe.block?.element('window'); + + return el?.getAttribute('role'); + }) + ).toBe('dialog'); + }); + + test('aria-modal is set', async ({page}) => { + const target = await init(page); + + test.expect( + await target.evaluate((ctx) => { + const el = ctx.unsafe.block?.element('window'); + + return el?.getAttribute('aria-modal'); + }) + ).toBe('true'); + }); + + /** + * @param page + */ + async function init(page: Page): Promise> { + return Component.createComponent(page, 'b-window'); + } +}); diff --git a/src/core/component/directives/aria/test/unit/listbox.ts b/src/core/component/directives/aria/test/unit/listbox.ts new file mode 100644 index 0000000000..ff654c727b --- /dev/null +++ b/src/core/component/directives/aria/test/unit/listbox.ts @@ -0,0 +1,66 @@ +// @ts-check + +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ +import type { JSHandle, Page } from 'playwright'; +import type iBlock from 'super/i-block/i-block'; + +import test from 'tests/config/unit/test'; +import Component from 'tests/helpers/component'; + +test.describe('v-aria:listbox', () => { + test.beforeEach(async ({demoPage}) => { + await demoPage.goto(); + }); + + const + selector = '[data-id="target"]'; + + test('role is set', async ({page}) => { + const target = await init(page); + + await page.click(selector); + + test.expect( + await target.evaluate((ctx) => { + const el = ctx.unsafe.block?.element('dropdown'); + + return el?.getAttribute('role'); + }) + ).toBe('listbox'); + }); + + test('tabindex is -1', async ({page}) => { + const target = await init(page); + + await page.click(selector); + + test.expect( + await target.evaluate((ctx) => { + const el = ctx.unsafe.block?.element('dropdown'); + + return el?.getAttribute('tabindex'); + }) + ).toBe('-1'); + }); + + /** + * @param page + */ + async function init(page: Page): Promise> { + return Component.createComponent(page, 'b-select', { + attrs: { + 'data-id': 'target', + items: [ + {label: 'foo', value: 0}, + {label: 'bar', value: 1} + ] + } + }); + } +}); diff --git a/src/core/component/directives/aria/test/unit/option.ts b/src/core/component/directives/aria/test/unit/option.ts new file mode 100644 index 0000000000..0b24a690fb --- /dev/null +++ b/src/core/component/directives/aria/test/unit/option.ts @@ -0,0 +1,117 @@ +// @ts-check + +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ +import type { JSHandle, Page } from 'playwright'; +import type iBlock from 'super/i-block/i-block'; + +import test from 'tests/config/unit/test'; +import Component from 'tests/helpers/component'; + +test.describe('v-aria:option', () => { + test.beforeEach(async ({demoPage}) => { + await demoPage.goto(); + }); + + const + selector = '[data-id="target"]'; + + test('role is set', async ({page}) => { + const target = await init(page); + + await page.click(selector); + + test.expect( + await target.evaluate((ctx) => { + if (ctx.unsafe.block == null) { + return; + } + + const [el1, el2]: HTMLElement[] = Array.from(ctx.unsafe.block.elements('item')); + + return [el1.getAttribute('role'), el2.getAttribute('role')]; + }) + ).toEqual(['option', 'option']); + }); + + test('has no preselected value', async ({page}) => { + const target = await init(page); + + await page.click(selector); + + test.expect( + await target.evaluate((ctx) => { + if (ctx.unsafe.block == null) { + return; + } + + const [el1, el2]: HTMLElement[] = Array.from(ctx.unsafe.block.elements('item')); + + return [el1.getAttribute('aria-selected'), el2.getAttribute('aria-selected')]; + }) + ).toEqual(['false', 'false']); + }); + + test('options with preselected value', async ({page}) => { + const target = await init(page, {value: 0}); + + await page.click(selector); + + test.expect( + await target.evaluate((ctx) => { + if (ctx.unsafe.block == null) { + return; + } + + const [el1, el2]: HTMLElement[] = Array.from(ctx.unsafe.block.elements('item')); + + return [el1.getAttribute('aria-selected'), el2.getAttribute('aria-selected')]; + }) + ).toEqual(['true', 'false']); + }); + + test('selected option changed', async ({page}) => { + const target = await init(page, {value: 0}); + + await page.click(selector); + + test.expect( + await target.evaluate((ctx) => { + if (ctx.unsafe.block == null) { + return; + } + + const input = ctx.unsafe.block.element('input'); + const [el1, el2]: HTMLElement[] = Array.from(ctx.unsafe.block.elements('item')); + + input?.dispatchEvent(new KeyboardEvent('keydown', {key: 'ArrowDown', bubbles: true})); + + input?.dispatchEvent(new KeyboardEvent('keydown', {key: 'Enter', bubbles: true})); + + return [el1.getAttribute('aria-selected'), el2.getAttribute('aria-selected')]; + }) + ).toEqual(['false', 'true']); + }); + + /** + * @param page + * @param attrs + */ + async function init(page: Page, attrs: Dictionary = {}): Promise> { + return Component.createComponent(page, 'b-select', { + attrs: { + 'data-id': 'target', + items: [ + {label: 'foo', value: 0, attrs: {id: 'item1'}}, + {label: 'bar', value: 1, attrs: {id: 'item2'}} + ], + ...attrs + } + }); + } +}); diff --git a/src/core/component/directives/aria/test/unit/simple.ts b/src/core/component/directives/aria/test/unit/simple.ts new file mode 100644 index 0000000000..649d116a97 --- /dev/null +++ b/src/core/component/directives/aria/test/unit/simple.ts @@ -0,0 +1,72 @@ +// @ts-check + +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ +import type { JSHandle, Page } from 'playwright'; +import type iBlock from 'super/i-block/i-block'; + +import test from 'tests/config/unit/test'; +import Component from 'tests/helpers/component'; + +test.describe('v-aria', () => { + test.beforeEach(async ({demoPage}) => { + await demoPage.goto(); + }); + + test('aria-label is added', async ({page}) => { + const target = await init(page, {'v-aria': {label: 'bla'}}); + + test.expect( + await target.evaluate((ctx) => ctx.$el?.getAttribute('aria-label')) + ).toBe('bla'); + }); + + test('aria-labelledby is added', async ({page}) => { + const target = await init(page, {'v-aria': {labelledby: 'bla'}}); + + test.expect( + await target.evaluate((ctx) => ctx.$el?.getAttribute('aria-labelledby')) + ).toBe('bla'); + }); + + test('aria-description is added', async ({page}) => { + const target = await init(page, {'v-aria': {description: 'bla'}}); + + test.expect( + await target.evaluate((ctx) => ctx.$el?.getAttribute('aria-description')) + ).toBe('bla'); + }); + + test('aria-describedby is added', async ({page}) => { + const target = await init(page, {'v-aria': {describedby: 'bla'}}); + + test.expect( + await target.evaluate((ctx) => ctx.$el?.getAttribute('aria-describedby')) + ).toBe('bla'); + }); + + test('aria-labelledby sugar syntax', async ({page}) => { + const target = await init(page, {'v-aria.#bla': {}}); + + const id = await target.evaluate((ctx) => ctx.$root.unsafe.dom.getId('bla')); + + test.expect( + await target.evaluate((ctx) => ctx.$el?.getAttribute('aria-labelledby')) + ).toBe(id); + }); + + /** + * @param page + * @param attrs + */ + async function init(page: Page, attrs: Dictionary = {}): Promise> { + return Component.createComponent(page, 'b-dummy', { + attrs + }); + } +}); diff --git a/src/core/component/directives/aria/test/unit/tab.ts b/src/core/component/directives/aria/test/unit/tab.ts new file mode 100644 index 0000000000..f2f1d8a31a --- /dev/null +++ b/src/core/component/directives/aria/test/unit/tab.ts @@ -0,0 +1,226 @@ +// @ts-check + +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ +import type { JSHandle, Page } from 'playwright'; +import type iBlock from 'super/i-block/i-block'; + +import test from 'tests/config/unit/test'; +import Component from 'tests/helpers/component'; + +test.describe('v-aria:tab', () => { + test.beforeEach(async ({demoPage}) => { + await demoPage.goto(); + }); + + const + selector = '[data-id="target"]'; + + test('role is set', async ({page}) => { + const target = await init(page); + + test.expect( + await target.evaluate((ctx) => { + if (ctx.unsafe.block == null) { + return; + } + + const tabs = ctx.unsafe.block.elements('link'); + + const res: Array> = []; + + tabs.forEach((el) => res.push(el.getAttribute('role'))); + + return res; + }) + ).toEqual(['tab', 'tab', 'tab']); + }); + + test('has active value', async ({page}) => { + const target = await init(page, {active: 1}); + + await page.focus(selector); + + test.expect( + await target.evaluate((ctx) => { + if (ctx.unsafe.block == null) { + return; + } + + const tabs = ctx.unsafe.block.elements('link'); + + const res: Array> = []; + + tabs.forEach((el) => res.push(el.getAttribute('aria-selected'))); + + return res; + }) + ).toEqual(['false', 'true', 'false']); + }); + + test('tabindexes are set without active item', async ({page}) => { + const target = await init(page); + + test.expect( + await target.evaluate((ctx) => { + if (ctx.unsafe.block == null) { + return; + } + + const tabs: NodeListOf = ctx.unsafe.block.elements('link'); + + const res: number[] = []; + + tabs.forEach((el) => res.push(el.tabIndex)); + + return res; + }) + ).toEqual([0, -1, -1]); + }); + + test('tabindexes are set with active item', async ({page}) => { + const target = await init(page, {active: 1}); + + test.expect( + await target.evaluate((ctx) => { + if (ctx.unsafe.block == null) { + return; + } + + const tabs: NodeListOf = ctx.unsafe.block.elements('link'); + + const res: number[] = []; + + tabs.forEach((el) => res.push(el.tabIndex)); + + return res; + }) + ).toEqual([-1, 0, -1]); + }); + + test('active item changed', async ({page}) => { + const target = await init(page); + + test.expect( + await target.evaluate((ctx) => { + if (ctx.unsafe.block == null) { + return; + } + + let tabs: NodeListOf = ctx.unsafe.block.elements('link'); + + tabs[1].click(); + + const res: Array<[number, Nullable]> = []; + + tabs = ctx.unsafe.block.elements('link'); + + tabs.forEach((el, i) => res[i] = [el.tabIndex, el.ariaSelected]); + + return res; + }) + ).toEqual([ + [-1, 'false'], + [0, 'true'], + [-1, 'false'] + ]); + }); + + test('keyboard keys handle on horizontal orientation', async ({page}) => { + const target = await init(page); + + test.expect( + await target.evaluate((ctx) => { + if (ctx.unsafe.block == null) { + return; + } + + const + res: Array> = [], + tab: CanUndef = ctx.unsafe.block.element('link'); + + tab?.focus(); + tab?.dispatchEvent(new KeyboardEvent('keydown', {key: 'ArrowRight'})); + res.push(document.activeElement?.id); + + document.activeElement?.dispatchEvent(new KeyboardEvent('keydown', {key: 'ArrowLeft'})); + res.push(document.activeElement?.id); + + document.activeElement?.dispatchEvent(new KeyboardEvent('keydown', {key: 'ArrowUp'})); + res.push(document.activeElement?.id); + + document.activeElement?.dispatchEvent(new KeyboardEvent('keydown', {key: 'ArrowDown'})); + res.push(document.activeElement?.id); + + document.activeElement?.dispatchEvent(new KeyboardEvent('keydown', {key: 'End'})); + res.push(document.activeElement?.id); + + document.activeElement?.dispatchEvent(new KeyboardEvent('keydown', {key: 'Home'})); + res.push(document.activeElement?.id); + + return res; + }) + ).toEqual(['id2', 'id1', 'id1', 'id1', 'id3', 'id1']); + }); + + test('keyboard keys handle on vertical orientation', async ({page}) => { + const target = await init(page, {orientation: 'vertical'}); + + test.expect( + await target.evaluate((ctx) => { + if (ctx.unsafe.block == null) { + return; + } + + const + res: Array> = [], + tab: CanUndef = ctx.unsafe.block.element('link'); + + tab?.focus(); + tab?.dispatchEvent(new KeyboardEvent('keydown', {key: 'ArrowRight'})); + res.push(document.activeElement?.id); + + document.activeElement?.dispatchEvent(new KeyboardEvent('keydown', {key: 'ArrowLeft'})); + res.push(document.activeElement?.id); + + document.activeElement?.dispatchEvent(new KeyboardEvent('keydown', {key: 'ArrowDown'})); + res.push(document.activeElement?.id); + + document.activeElement?.dispatchEvent(new KeyboardEvent('keydown', {key: 'ArrowUp'})); + res.push(document.activeElement?.id); + + document.activeElement?.dispatchEvent(new KeyboardEvent('keydown', {key: 'End'})); + res.push(document.activeElement?.id); + + document.activeElement?.dispatchEvent(new KeyboardEvent('keydown', {key: 'Home'})); + res.push(document.activeElement?.id); + + return res; + }) + ).toEqual(['id1', 'id1', 'id2', 'id1', 'id3', 'id1']); + }); + + /** + * @param page + * @param attrs + */ + async function init(page: Page, attrs: Dictionary = {}): Promise> { + return Component.createComponent(page, 'b-list', { + attrs: { + 'data-id': 'target', + items: [ + {label: 'Male', value: 0, attrs: {id: 'id1'}}, + {label: 'Female', value: 1, attrs: {id: 'id2'}}, + {label: 'Other', value: 2, attrs: {id: 'id3'}} + ], + ...attrs + } + }); + } +}); + diff --git a/src/core/component/directives/aria/test/unit/tablist.ts b/src/core/component/directives/aria/test/unit/tablist.ts new file mode 100644 index 0000000000..e688c633bc --- /dev/null +++ b/src/core/component/directives/aria/test/unit/tablist.ts @@ -0,0 +1,72 @@ +// @ts-check + +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ +import type { JSHandle, Page } from 'playwright'; +import type iBlock from 'super/i-block/i-block'; + +import test from 'tests/config/unit/test'; +import Component from 'tests/helpers/component'; + +test.describe('v-aria:tablist', () => { + test.beforeEach(async ({demoPage}) => { + await demoPage.goto(); + }); + + test('role is set', async ({page}) => { + const target = await init(page); + + test.expect( + await target.evaluate((ctx) => { + const el = ctx.unsafe.block?.element('wrapper'); + + return el?.getAttribute('role'); + }) + ).toBe('tablist'); + }); + + test('multiselectable is set', async ({page}) => { + const target = await init(page, {multiple: true}); + + test.expect( + await target.evaluate((ctx) => { + const el = ctx.unsafe.block?.element('wrapper'); + + return el?.getAttribute('aria-multiselectable'); + }) + ).toBe('true'); + }); + + test('orientation is set', async ({page}) => { + const target = await init(page, {orientation: 'vertical'}); + + test.expect( + await target.evaluate((ctx) => { + const el = ctx.unsafe.block?.element('wrapper'); + + return el?.getAttribute('aria-orientation'); + }) + ).toBe('vertical'); + }); + + /** + * @param page + * @param attrs + */ + async function init(page: Page, attrs: Dictionary = {}): Promise> { + return Component.createComponent(page, 'b-list', { + attrs: { + items: [ + {label: 'foo', value: 0}, + {label: 'bar', value: 1} + ], + ...attrs + } + }); + } +}); diff --git a/src/core/component/directives/aria/test/unit/tabpanel.ts b/src/core/component/directives/aria/test/unit/tabpanel.ts new file mode 100644 index 0000000000..f99e3050d1 --- /dev/null +++ b/src/core/component/directives/aria/test/unit/tabpanel.ts @@ -0,0 +1,49 @@ +// @ts-check + +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ +import type { JSHandle, Page } from 'playwright'; +import type iBlock from 'super/i-block/i-block'; + +import test from 'tests/config/unit/test'; +import Component from 'tests/helpers/component'; + +test.describe('v-aria:tabpanel', () => { + test.beforeEach(async ({demoPage}) => { + await demoPage.goto(); + }); + + test('role is set', async ({page}) => { + const target = await init(page, {}); + + test.expect( + await target.evaluate((ctx) => ctx.$el?.getAttribute('role')) + ).toBe('tabpanel'); + }); + + test('no label passed', async ({page}) => { + const target = await init(page, {'v-aria:tabpanel': {}}); + + test.expect( + await target.evaluate((ctx) => ctx.$el?.hasAttribute('role')) + ).toBe(false); + }); + + /** + * @param page + * @param attrs + */ + async function init(page: Page, attrs: Dictionary = {}): Promise> { + return Component.createComponent(page, 'b-dummy', { + attrs: { + 'v-aria:tabpanel': {label: 'foo'}, + ...attrs + } + }); + } +}); diff --git a/src/core/component/directives/aria/test/unit/tree.ts b/src/core/component/directives/aria/test/unit/tree.ts new file mode 100644 index 0000000000..5f76193047 --- /dev/null +++ b/src/core/component/directives/aria/test/unit/tree.ts @@ -0,0 +1,103 @@ +// @ts-check + +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ +import type { JSHandle, Page } from 'playwright'; +import type iBlock from 'super/i-block/i-block'; + +import test from 'tests/config/unit/test'; +import Component from 'tests/helpers/component'; + +test.describe('v-aria:option', () => { + test.beforeEach(async ({demoPage}) => { + await demoPage.goto(); + }); + + test('role is set', async ({page}) => { + const target = await init(page); + + await page.waitForSelector('[role="group"]'); + + test.expect( + await target.evaluate(() => { + const + roots = document.querySelectorAll('[role="tree"]'), + groups = document.querySelectorAll('[role="group"]'); + + return [roots.length, groups.length]; + }) + ).toEqual([1, 2]); + }); + + test('orientation is set', async ({page}) => { + const target = await init(page, {orientation: 'horizontal'}); + + test.expect( + await target.evaluate((ctx) => { + const el = ctx.unsafe.block?.element('root'); + + return el?.getAttribute('aria-orientation'); + }) + ).toBe('horizontal'); + }); + + test('treeitem is expanded', async ({page}) => { + const target = await init(page); + + test.expect( + await target.evaluate((ctx) => { + const + fold: CanUndef = ctx.unsafe.block?.element('fold'), + items = ctx.unsafe.block?.elements('node'), + expandableItem = items?.[1]; + + const + res: Array>> = []; + + fold?.click(); + res.push(expandableItem?.getAttribute('aria-expanded')); + + fold?.click(); + res.push(expandableItem?.getAttribute('aria-expanded')); + + return res; + }) + ).toEqual(['true', 'false']); + }); + + /** + * @param page + * @param attrs + */ + async function init(page: Page, attrs: Dictionary = {}): Promise> { + return Component.createComponent(page, 'b-tree', { + attrs: { + item: 'b-checkbox', + items: [ + {id: 'bar', label: 'bar', attrs: {id: 'bar'}}, + { + id: 'foo', + label: 'foo', + children: [ + {id: 'fooone', label: 'foo1'}, + {id: 'footwo', label: 'foo2'}, + { + id: 'foothree', + label: 'foo3', + children: [{id: 'foothreeone', label: 'foo4'}] + }, + {id: 'foosix', label: 'foo5'} + ] + } + ], + ...attrs + } + }); + } +}); + diff --git a/src/core/component/directives/aria/test/unit/treeitem.ts b/src/core/component/directives/aria/test/unit/treeitem.ts new file mode 100644 index 0000000000..109de5887b --- /dev/null +++ b/src/core/component/directives/aria/test/unit/treeitem.ts @@ -0,0 +1,182 @@ +// @ts-check + +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ +import type { JSHandle, Page } from 'playwright'; +import type iBlock from 'super/i-block/i-block'; + +import test from 'tests/config/unit/test'; +import Component from 'tests/helpers/component'; + +test.describe('v-aria:treeitem', () => { + test.beforeEach(async ({demoPage}) => { + await demoPage.goto(); + }); + + test('role is set', async ({page}) => { + const target = await init(page); + + test.expect( + await target.evaluate((ctx) => { + const el = ctx.unsafe.block?.element('node'); + + return el?.getAttribute('role'); + }) + ).toBe('treeitem'); + }); + + test('aria-expanded is set', async ({page}) => { + const target = await init(page); + + await page.waitForSelector('[role="group"]'); + + test.expect( + await target.evaluate((ctx) => { + const + items = ctx.unsafe.block?.elements('node'), + expandableItem = items?.[1]; + + return expandableItem?.getAttribute('aria-expanded'); + }) + ).toBe('false'); + }); + + test('keyboard keys handle on vertical orientation', async ({page}) => { + const target = await init(page); + + await page.waitForSelector('[role="group"]'); + + test.expect( + await target.evaluate((ctx) => { + if (ctx.unsafe.block == null) { + return; + } + + const + input = document.querySelector('input'), + items = ctx.unsafe.block.elements('node'), + labels = document.querySelectorAll('label'); + + const res: any[] = []; + + input?.focus(); + + input?.dispatchEvent(new KeyboardEvent('keydown', {key: 'ArrowDown', bubbles: true})); + res.push(document.activeElement?.id === labels[1].getAttribute('for')); + + document.activeElement?.dispatchEvent(new KeyboardEvent('keydown', {key: 'ArrowUp', bubbles: true})); + res.push(document.activeElement?.id === labels[0].getAttribute('for')); + + document.activeElement?.dispatchEvent(new KeyboardEvent('keydown', {key: 'ArrowDown', bubbles: true})); + document.activeElement?.dispatchEvent(new KeyboardEvent('keydown', {key: 'Enter', bubbles: true})); + res.push(items[1].getAttribute('aria-expanded')); + + document.activeElement?.dispatchEvent(new KeyboardEvent('keydown', {key: 'Enter', bubbles: true})); + res.push(items[1].getAttribute('aria-expanded')); + + document.activeElement?.dispatchEvent(new KeyboardEvent('keydown', {key: 'ArrowRight', bubbles: true})); + res.push(items[1].getAttribute('aria-expanded')); + + document.activeElement?.dispatchEvent(new KeyboardEvent('keydown', {key: 'ArrowRight', bubbles: true})); + res.push(document.activeElement?.id === labels[2].getAttribute('for')); + + document.activeElement?.dispatchEvent(new KeyboardEvent('keydown', {key: 'ArrowLeft', bubbles: true})); + res.push(document.activeElement?.id === labels[1].getAttribute('for')); + + document.activeElement?.dispatchEvent(new KeyboardEvent('keydown', {key: 'ArrowLeft', bubbles: true})); + res.push(items[1].getAttribute('aria-expanded')); + + document.activeElement?.dispatchEvent(new KeyboardEvent('keydown', {key: 'Home', bubbles: true})); + res.push(document.activeElement?.id === labels[0].getAttribute('for')); + + document.activeElement?.dispatchEvent(new KeyboardEvent('keydown', {key: 'End', bubbles: true})); + res.push(document.activeElement?.id === labels[3].getAttribute('for')); + + return res; + }) + ).toEqual([true, true, 'true', 'false', 'true', true, true, 'false', true, true]); + }); + + test('keyboard keys handle on horizontal orientation', async ({page}) => { + const target = await init(page, {orientation: 'horizontal'}); + + await page.waitForSelector('[role="group"]'); + + test.expect( + await target.evaluate((ctx) => { + if (ctx.unsafe.block == null) { + return; + } + + const + input = document.querySelector('input'), + items = ctx.unsafe.block.elements('node'), + labels = document.querySelectorAll('label'); + + const res: any[] = []; + + input?.focus(); + + input?.dispatchEvent(new KeyboardEvent('keydown', {key: 'ArrowRight', bubbles: true})); + res.push(document.activeElement?.id === labels[1].getAttribute('for')); + + document.activeElement?.dispatchEvent(new KeyboardEvent('keydown', {key: 'ArrowLeft', bubbles: true})); + res.push(document.activeElement?.id === labels[0].getAttribute('for')); + + document.activeElement?.dispatchEvent(new KeyboardEvent('keydown', {key: 'ArrowRight', bubbles: true})); + document.activeElement?.dispatchEvent(new KeyboardEvent('keydown', {key: 'Enter', bubbles: true})); + res.push(items[1].getAttribute('aria-expanded')); + + document.activeElement?.dispatchEvent(new KeyboardEvent('keydown', {key: 'Enter', bubbles: true})); + res.push(items[1].getAttribute('aria-expanded')); + + document.activeElement?.dispatchEvent(new KeyboardEvent('keydown', {key: 'ArrowDown', bubbles: true})); + res.push(items[1].getAttribute('aria-expanded')); + + document.activeElement?.dispatchEvent(new KeyboardEvent('keydown', {key: 'ArrowDown', bubbles: true})); + res.push(document.activeElement?.id === labels[2].getAttribute('for')); + + document.activeElement?.dispatchEvent(new KeyboardEvent('keydown', {key: 'ArrowUp', bubbles: true})); + res.push(document.activeElement?.id === labels[1].getAttribute('for')); + + document.activeElement?.dispatchEvent(new KeyboardEvent('keydown', {key: 'ArrowUp', bubbles: true})); + res.push(items[1].getAttribute('aria-expanded')); + + document.activeElement?.dispatchEvent(new KeyboardEvent('keydown', {key: 'Home', bubbles: true})); + res.push(document.activeElement?.id === labels[0].getAttribute('for')); + + document.activeElement?.dispatchEvent(new KeyboardEvent('keydown', {key: 'End', bubbles: true})); + res.push(document.activeElement?.id === labels[3].getAttribute('for')); + + return res; + }) + ).toEqual([true, true, 'true', 'false', 'true', true, true, 'false', true, true]); + }); + + /** + * @param page + * @param attrs + */ + async function init(page: Page, attrs: Dictionary = {}): Promise> { + return Component.createComponent(page, 'b-tree', { + attrs: { + item: 'b-checkbox', + items: [ + {id: 'bar', label: 'bar'}, + { + id: 'foo', + label: 'foo', + children: [{id: 'fooone', label: 'foo1'}] + }, + {id: 'bla', label: 'bla'} + ], + ...attrs + } + }); + } +}); diff --git a/src/core/component/directives/id/test/unit/functional.ts b/src/core/component/directives/id/test/unit/functional.ts new file mode 100644 index 0000000000..7c992d0aee --- /dev/null +++ b/src/core/component/directives/id/test/unit/functional.ts @@ -0,0 +1,47 @@ +// @ts-check + +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ +import type { JSHandle, Page } from 'playwright'; +import type bDummy from 'dummies/b-dummy/b-dummy'; + +import test from 'tests/config/unit/test'; +import Component from 'tests/helpers/component'; + +test.describe('v-id', () => { + test.beforeEach(async ({demoPage}) => { + await demoPage.goto(); + }); + + test('id is added', async ({page}) => { + const target = await init(page); + const id = await target.evaluate((ctx) => ctx.$root.unsafe.dom.getId('dummy')); + + test.expect( + await target.evaluate((ctx) => ctx.$el?.id) + ).toBe(id); + }); + + test('preserve mod', async ({page}) => { + const target = await init(page, {'v-id.preserve': 'dummy', id: 'foo'}); + + test.expect( + await target.evaluate((ctx) => ctx.$el?.id) + ).toBe('foo'); + }); + + /** + * @param page + * @param attrs + */ + async function init(page: Page, attrs: Dictionary = {}): Promise> { + return Component.createComponent(page, 'b-dummy', { + attrs: {'v-id': 'dummy', ...attrs} + }); + } +}); From 2b5ad51d9059e40a6fadf24bf0f4d05f63f68d22 Mon Sep 17 00:00:00 2001 From: Aleksandr Bunin Date: Tue, 2 Aug 2022 18:25:43 +0300 Subject: [PATCH 088/185] fixes --- src/base/b-tree/b-tree.ts | 3 +-- .../component/directives/aria/roles-engines/combobox.ts | 1 + src/core/component/directives/aria/roles-engines/tab.ts | 1 + src/traits/i-access/i-access.ts | 7 ++++--- 4 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/base/b-tree/b-tree.ts b/src/base/b-tree/b-tree.ts index 1d763346bb..7c08ed7ea5 100644 --- a/src/base/b-tree/b-tree.ts +++ b/src/base/b-tree/b-tree.ts @@ -31,8 +31,7 @@ export * from 'base/b-tree/interface'; export const $$ = symbolGenerator(); -interface bTree extends Trait { -} +interface bTree extends Trait {} /** * Component to render tree of any elements diff --git a/src/core/component/directives/aria/roles-engines/combobox.ts b/src/core/component/directives/aria/roles-engines/combobox.ts index 70921a3858..78ece722c3 100644 --- a/src/core/component/directives/aria/roles-engines/combobox.ts +++ b/src/core/component/directives/aria/roles-engines/combobox.ts @@ -7,6 +7,7 @@ */ import AriaRoleEngine, { DirectiveOptions } from 'core/component/directives/aria/interface'; + import type { ComboboxParams } from 'core/component/directives/aria/roles-engines/interface'; import type iAccess from 'traits/i-access/i-access'; diff --git a/src/core/component/directives/aria/roles-engines/tab.ts b/src/core/component/directives/aria/roles-engines/tab.ts index ce7b7d845b..ab2d0b3309 100644 --- a/src/core/component/directives/aria/roles-engines/tab.ts +++ b/src/core/component/directives/aria/roles-engines/tab.ts @@ -10,6 +10,7 @@ */ import AriaRoleEngine, { DirectiveOptions, keyCodes } from 'core/component/directives/aria/interface'; + import type { TabParams } from 'core/component/directives/aria/roles-engines/interface'; import type iAccess from 'traits/i-access/i-access'; import type iBlock from 'super/i-block/i-block'; diff --git a/src/traits/i-access/i-access.ts b/src/traits/i-access/i-access.ts index c5c6aced57..d49cadad51 100644 --- a/src/traits/i-access/i-access.ts +++ b/src/traits/i-access/i-access.ts @@ -283,7 +283,7 @@ export default abstract class iAccess { let focusableIter = intoIter(focusableElems ?? []); - if (el?.hasAttribute('tabindex')) { + if (ctx?.matches(FOCUSABLE_SELECTOR)) { focusableIter = sequence(focusableIter, intoIter([el])); } @@ -379,7 +379,8 @@ export default abstract class iAccess { /** * Removes all children of the specified element that can be focused from the Tab toggle sequence. - * In effect, these elements are set to -1 for the tabindex attribute + * In effect, these elements are set to -1 for the tabindex attribute. + * * @param el - a context to search, if not set, the root element of the component will be used */ removeAllFromTabSequence(el?: Element): boolean { @@ -389,7 +390,7 @@ export default abstract class iAccess { /** * Reverts all children of the specified element that can be focused to the Tab toggle sequence. * This method is used to restore the state of elements to the state - * they had before removeAllFromTabSequence was applied + * they had before 'removeAllFromTabSequence' was applied. * * @param el - a context to search, if not set, the root element of the component will be used */ From fd561f7f7b4a2920aef3867e1e4dc51c13db1c0c Mon Sep 17 00:00:00 2001 From: Aleksandr Bunin Date: Wed, 3 Aug 2022 13:06:58 +0300 Subject: [PATCH 089/185] add controls interface and fix roles --- .../directives/aria/roles-engines/controls.ts | 45 ++++++++++++++----- .../directives/aria/roles-engines/dialog.ts | 7 ++- .../aria/roles-engines/interface.ts | 4 ++ 3 files changed, 45 insertions(+), 11 deletions(-) diff --git a/src/core/component/directives/aria/roles-engines/controls.ts b/src/core/component/directives/aria/roles-engines/controls.ts index 76b2d597f9..ea1ab731ea 100644 --- a/src/core/component/directives/aria/roles-engines/controls.ts +++ b/src/core/component/directives/aria/roles-engines/controls.ts @@ -6,26 +6,39 @@ * https://github.com/V4Fire/Client/blob/master/LICENSE */ -import AriaRoleEngine from 'core/component/directives/aria/interface'; +import AriaRoleEngine, { DirectiveOptions } from 'core/component/directives/aria/interface'; +import type { ControlsParams } from 'core/component/directives/aria/roles-engines/interface'; export default class ControlsEngine extends AriaRoleEngine { + /** + * Passed directive params + */ + params: ControlsParams; + + constructor(options: DirectiveOptions) { + super(options); + + this.params = this.options.binding.value; + } + /** * Sets base aria attributes for current role */ init(): void { const {vnode, binding, el} = this.options, - {modifiers, value} = binding, - {fakeContext: ctx} = vnode; + {modifiers} = binding, + {fakeContext: ctx} = vnode, + {for: forId} = this.params; - if (value?.for == null) { + if (forId == null) { Object.throw('Controls aria directive expects the id of controlling elements to be passed as "for" prop'); return; } const - isForPropArray = Object.isArray(value.for), - isForPropArrayOfTuples = Object.isArray(value.for) && Object.isArray(value.for[0]); + isForPropArray = Object.isArray(forId), + isForPropArrayOfTuples = isForPropArray && Object.isArray(forId[0]); if (modifiers != null && Object.size(modifiers) > 0) { ctx?.$nextTick().then(() => { @@ -33,19 +46,31 @@ export default class ControlsEngine extends AriaRoleEngine { roleName = Object.keys(modifiers)[0], elems = el.querySelectorAll(`[role=${roleName}]`); - if (isForPropArray && value.for.length !== elems.length) { + if (isForPropArray && forId.length !== elems.length) { Object.throw('Controls aria directive expects prop "for" length to be equal to amount of elements with specified role or string type'); return; } elems.forEach((el, i) => { - el.setAttribute('aria-controls', isForPropArray ? value.for[i] : value.for); + if (Object.isString(forId)) { + el.setAttribute('aria-controls', forId); + return; + } + + const + id = forId[i]; + + if (Object.isString(id)) { + el.setAttribute('aria-controls', id); + } }); }); } else if (isForPropArrayOfTuples) { - value.for.forEach(([elId, controlsId]) => { - const element = el.querySelector(`#${elId}`); + forId.forEach((param) => { + const + [elId, controlsId] = param, + element = el.querySelector(`#${elId}`); element?.setAttribute('aria-controls', controlsId); }); diff --git a/src/core/component/directives/aria/roles-engines/dialog.ts b/src/core/component/directives/aria/roles-engines/dialog.ts index de7e868539..beb96370a6 100644 --- a/src/core/component/directives/aria/roles-engines/dialog.ts +++ b/src/core/component/directives/aria/roles-engines/dialog.ts @@ -7,6 +7,7 @@ */ import AriaRoleEngine from 'core/component/directives/aria/interface'; +import iOpen from 'traits/i-open/i-open'; export default class DialogEngine extends AriaRoleEngine { /** @@ -14,9 +15,13 @@ export default class DialogEngine extends AriaRoleEngine { */ init(): void { const - {el} = this.options; + {el, vnode} = this.options; el.setAttribute('role', 'dialog'); el.setAttribute('aria-modal', 'true'); + + if (!iOpen.is(vnode.fakeContext)) { + Object.throw('Dialog aria directive expects the component to realize iOpen interface'); + } } } diff --git a/src/core/component/directives/aria/roles-engines/interface.ts b/src/core/component/directives/aria/roles-engines/interface.ts index 3ab7869aa3..b30a06b543 100644 --- a/src/core/component/directives/aria/roles-engines/interface.ts +++ b/src/core/component/directives/aria/roles-engines/interface.ts @@ -32,3 +32,7 @@ export interface ComboboxParams { openEvent(cb: Function): void; closeEvent(cb: Function): void; } + +export interface ControlsParams { + for: CanArray | Array<[string, string]> | undefined; +} From f2b1bbd97dc8e0904a05e63d3e3982f78b6f7649 Mon Sep 17 00:00:00 2001 From: Aleksandr Bunin Date: Wed, 3 Aug 2022 13:07:18 +0300 Subject: [PATCH 090/185] add readme --- src/core/component/directives/aria/README.md | 101 ++++++++++++++++--- 1 file changed, 86 insertions(+), 15 deletions(-) diff --git a/src/core/component/directives/aria/README.md b/src/core/component/directives/aria/README.md index 15c7ff29f8..334d9c20ad 100644 --- a/src/core/component/directives/aria/README.md +++ b/src/core/component/directives/aria/README.md @@ -8,16 +8,25 @@ This module provides a directive to add aria attributes and logic to elements th < &__foo v-aria.#bla < &__foo v-aria = {labelledby: dom.getId('bla')} - ``` ## Available modifiers: -- .#[string] (ex. '.#title') the same as = {labelledby: [id-'title']} +- .#[string] (ex. '.#title') + +Example +``` +< v-aria.#title +the same as +< v-aria = {labelledby: dom.getId('title')} +``` -- Roles: -- controls: +- `controls`: +The engine to set aria-controls attribute. +For more information go to [`https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes/aria-controls`]. + Directive can be added to any tag that includes tag with needed role. Role should be passed as a modifier. ID or IDs are passed as value. ID could be single or multiple written in string with space between. @@ -25,21 +34,21 @@ ID could be single or multiple written in string with space between. There are two ways to use this engine: 1. To add role as a modifier to which passed IDs in `for` value should be added. `for` could be `string` or `string[]`. If element controls several elements `for` should be passed as a string with IDs separated with space. -(!) Notice that this role attribute should already be added to the element. The engine does not set passed role to the current element. +(!) Notice that this role attribute should already be added to the element. The engine does not set passed role to any element. Example: ``` < &__foo v-aria:controls.tab = {for: 'id1 id2 id3'} -converts to - -< tab aria-controls = "id1 id2 id3" role = "tab" +the same as +< &__foo + < button aria-controls = "id1 id2 id3" role = "tab" ``` 2. To pass value `for` as an array of tuples. -First id in a tuple is an id of an element to which one the aria attributes should be added. +First id in a tuple is an id of an element to add the aria attributes. The second one is an id of an element to set as value in aria-controls attribute. -(!) Notice that id attribute should already be added to the element. The engine does not set passed ids to the elements. +(!) Notice that id attribute should already be added to the element. The engine does not set passed ids to any element. Example: ``` @@ -47,20 +56,82 @@ Example: < span :id = "id1" < span :id = "id2" -converts to +the same as < &__foo < span :id = "id1" aria-controls = "id3" < span :id = "id2" aria-controls = "id4" ``` -- tabs: -Tabs always expect the 'controls' role engine to be added. +- `dialog`: +The engine to set `dialog` role attribute. +For more information go to [`https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/dialog_role`]. + +Always expects `iOpen` trait to be realized. + +- `tab`: +The engine to set `tab` role attribute. +For more information go to [`https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/tab_role`]. + +Tabs always expect the `controls` role engine to be added. It should 'point' to the element with role `tabpanel`. + +Example: +``` +< button v-aria:tab | v-aria:controls = {for: 'id1'} + +< v-aria:tabpanel = {labelledby: 'id2'} | :id = 'id1' + < span :id = 'id2' + // content +``` + +- `tablist`: +The engine to set `tablist` role attribute. +For more information go to [`https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/tablist_role`]. + +- `tabpanel`: +The engine to set `tablist` role attribute. +For more information go to [`https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/tabpanel_role`]. + +Always expects `label` or `labelledby` params to be passed. + +Example: +``` +< v-aria:tabpanel = {labelledby: 'id1'} + < span :id = 'id1' + // content +``` + +- `tree`: +The engine to set `tree` role attribute. +For more information go to [`https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/tree_role`]. +- `treeitem`: +The engine to set `treeitem` role attribute. +For more information go to [`https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/treeitem_role`]. -## Available standard values: -Value is expected to always be an object type. Possible keys: +- `combobox`: +The engine to set `combobox` role attribute. +For more information go to [`https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/combobox_role`]. + +- `listbox`: +The engine to set `listbox` role attribute. +For more information go to [`https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/listbox_role`]. + +- `option`: +The engine to set `option` role attribute. +For more information go to [`https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/option_role`]. + +## Available values: +Parameters passed to the directive are expected to always be object type. Any directive handle common keys: - label +Expects string as 'title' to the specified element + - labelledby +Expects string as an id of the element. This element is a 'title' of to the specified element + - description +Expects string as expanded 'description' to the specified element + - describedby -- id +Expects string as an id of the element. This element is an expanded 'description' to the specified element + +Also, there are specific role keys. For info go to [`core/component/directives/role-engines/interface.ts`](core/component/directives/role-engines/interface.ts). From 726db81efada2424d9c8c51977d95618ba8006b8 Mon Sep 17 00:00:00 2001 From: Aleksandr Bunin Date: Wed, 3 Aug 2022 13:20:20 +0300 Subject: [PATCH 091/185] refactoring --- .../component/directives/aria/roles-engines/controls.ts | 1 + .../component/directives/aria/roles-engines/interface.ts | 2 +- src/core/component/directives/aria/test/unit/treeitem.ts | 2 +- .../p-v4-components-demo/b-v4-component-demo/CHANGELOG.md | 6 ++++++ 4 files changed, 9 insertions(+), 2 deletions(-) diff --git a/src/core/component/directives/aria/roles-engines/controls.ts b/src/core/component/directives/aria/roles-engines/controls.ts index ea1ab731ea..efa51321a8 100644 --- a/src/core/component/directives/aria/roles-engines/controls.ts +++ b/src/core/component/directives/aria/roles-engines/controls.ts @@ -31,6 +31,7 @@ export default class ControlsEngine extends AriaRoleEngine { {fakeContext: ctx} = vnode, {for: forId} = this.params; + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition if (forId == null) { Object.throw('Controls aria directive expects the id of controlling elements to be passed as "for" prop'); return; diff --git a/src/core/component/directives/aria/roles-engines/interface.ts b/src/core/component/directives/aria/roles-engines/interface.ts index b30a06b543..8d2db1ab10 100644 --- a/src/core/component/directives/aria/roles-engines/interface.ts +++ b/src/core/component/directives/aria/roles-engines/interface.ts @@ -34,5 +34,5 @@ export interface ComboboxParams { } export interface ControlsParams { - for: CanArray | Array<[string, string]> | undefined; + for: CanArray | Array<[string, string]>; } diff --git a/src/core/component/directives/aria/test/unit/treeitem.ts b/src/core/component/directives/aria/test/unit/treeitem.ts index 109de5887b..58b3b56b50 100644 --- a/src/core/component/directives/aria/test/unit/treeitem.ts +++ b/src/core/component/directives/aria/test/unit/treeitem.ts @@ -118,7 +118,7 @@ test.describe('v-aria:treeitem', () => { items = ctx.unsafe.block.elements('node'), labels = document.querySelectorAll('label'); - const res: any[] = []; + const res: Array> = []; input?.focus(); diff --git a/src/pages/p-v4-components-demo/b-v4-component-demo/CHANGELOG.md b/src/pages/p-v4-components-demo/b-v4-component-demo/CHANGELOG.md index 76418ffae2..edbedc52a3 100644 --- a/src/pages/p-v4-components-demo/b-v4-component-demo/CHANGELOG.md +++ b/src/pages/p-v4-components-demo/b-v4-component-demo/CHANGELOG.md @@ -9,6 +9,12 @@ Changelog > - :house: [Internal] > - :nail_care: [Polish] +## v3.?.? (2022-0?-??) + +#### :rocket: New Feature + +* Added a new directive `v-id` + ## v3.0.0-rc.37 (2020-07-20) #### :house: Internal From f43e31336e7c4c9edbbd1e3160987982641153c1 Mon Sep 17 00:00:00 2001 From: Aleksandr Bunin Date: Wed, 3 Aug 2022 13:27:09 +0300 Subject: [PATCH 092/185] refactoring --- src/core/component/directives/aria/README.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/core/component/directives/aria/README.md b/src/core/component/directives/aria/README.md index 334d9c20ad..90463467fc 100644 --- a/src/core/component/directives/aria/README.md +++ b/src/core/component/directives/aria/README.md @@ -66,13 +66,13 @@ the same as The engine to set `dialog` role attribute. For more information go to [`https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/dialog_role`]. -Always expects `iOpen` trait to be realized. +Expects `iOpen` trait to be realized. - `tab`: The engine to set `tab` role attribute. For more information go to [`https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/tab_role`]. -Tabs always expect the `controls` role engine to be added. It should 'point' to the element with role `tabpanel`. +Tabs expect the `controls` role engine to be added in addition. ID passed to `controls` engine should be the id of the element with role `tabpanel`. Example: ``` @@ -91,7 +91,7 @@ For more information go to [`https://developer.mozilla.org/en-US/docs/Web/Access The engine to set `tablist` role attribute. For more information go to [`https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/tabpanel_role`]. -Always expects `label` or `labelledby` params to be passed. +Expects `label` or `labelledby` params to be passed. Example: ``` @@ -108,6 +108,8 @@ For more information go to [`https://developer.mozilla.org/en-US/docs/Web/Access The engine to set `treeitem` role attribute. For more information go to [`https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/treeitem_role`]. +Expects `iAccess` trait to be realized. + - `combobox`: The engine to set `combobox` role attribute. For more information go to [`https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/combobox_role`]. From 68e627518faced6aedeafdd3e2ad985deb276612 Mon Sep 17 00:00:00 2001 From: Aleksandr Bunin Date: Tue, 9 Aug 2022 16:43:04 +0300 Subject: [PATCH 093/185] refactoring --- package.json | 4 +- src/base/b-list/b-list.ts | 25 +++-- src/base/b-tree/b-tree.ts | 11 +- src/core/component/directives/aria/README.md | 104 +----------------- .../component/directives/aria/aria-setter.ts | 54 +++++++-- src/core/component/directives/aria/index.ts | 19 +--- .../component/directives/aria/interface.ts | 28 ----- .../aria/roles-engines/combobox/CHANGELOG.md | 16 +++ .../aria/roles-engines/combobox/README.md | 14 +++ .../{combobox.ts => combobox/index.ts} | 21 ++-- .../aria/roles-engines/combobox/interface.ts | 16 +++ .../aria/roles-engines/controls/CHANGELOG.md | 16 +++ .../aria/roles-engines/controls/README.md | 50 +++++++++ .../{controls.ts => controls/index.ts} | 16 ++- .../aria/roles-engines/controls/interface.ts | 11 ++ .../aria/roles-engines/dialog/CHANGELOG.md | 16 +++ .../aria/roles-engines/dialog/README.md | 16 +++ .../{dialog.ts => dialog/index.ts} | 13 +-- .../directives/aria/roles-engines/index.ts | 22 ++-- .../aria/roles-engines/interface.ts | 86 ++++++++++----- .../aria/roles-engines/listbox/CHANGELOG.md | 16 +++ .../aria/roles-engines/listbox/README.md | 14 +++ .../{listbox.ts => listbox/index.ts} | 11 +- .../directives/aria/roles-engines/option.ts | 34 ------ .../aria/roles-engines/option/CHANGELOG.md | 16 +++ .../aria/roles-engines/option/README.md | 13 +++ .../aria/roles-engines/option/index.ts | 39 +++++++ .../aria/roles-engines/option/interface.ts | 14 +++ .../aria/roles-engines/tab/CHANGELOG.md | 16 +++ .../aria/roles-engines/tab/README.md | 27 +++++ .../roles-engines/{tab.ts => tab/index.ts} | 55 +++++---- .../aria/roles-engines/tab/interface.ts | 17 +++ .../aria/roles-engines/tablist/CHANGELOG.md | 16 +++ .../aria/roles-engines/tablist/README.md | 14 +++ .../{tablist.ts => tablist/index.ts} | 20 +++- .../aria/roles-engines/tablist/interface.ts | 12 ++ .../aria/roles-engines/tabpanel/CHANGELOG.md | 16 +++ .../aria/roles-engines/tabpanel/README.md | 25 +++++ .../{tabpanel.ts => tabpanel/index.ts} | 7 +- .../aria/roles-engines/tree/CHANGELOG.md | 16 +++ .../aria/roles-engines/tree/README.md | 15 +++ .../roles-engines/{tree.ts => tree/index.ts} | 26 ++--- .../aria/roles-engines/tree/interface.ts | 15 +++ .../aria/roles-engines/treeitem/CHANGELOG.md | 16 +++ .../aria/roles-engines/treeitem/README.md | 17 +++ .../{treeitem.ts => treeitem/index.ts} | 61 ++++------ .../aria/roles-engines/treeitem/interface.ts | 16 +++ .../component/render-function/CHANGELOG.md | 6 + src/form/b-select/b-select.ts | 10 +- src/traits/i-access/i-access.ts | 14 +-- 50 files changed, 772 insertions(+), 380 deletions(-) create mode 100644 src/core/component/directives/aria/roles-engines/combobox/CHANGELOG.md create mode 100644 src/core/component/directives/aria/roles-engines/combobox/README.md rename src/core/component/directives/aria/roles-engines/{combobox.ts => combobox/index.ts} (73%) create mode 100644 src/core/component/directives/aria/roles-engines/combobox/interface.ts create mode 100644 src/core/component/directives/aria/roles-engines/controls/CHANGELOG.md create mode 100644 src/core/component/directives/aria/roles-engines/controls/README.md rename src/core/component/directives/aria/roles-engines/{controls.ts => controls/index.ts} (80%) create mode 100644 src/core/component/directives/aria/roles-engines/controls/interface.ts create mode 100644 src/core/component/directives/aria/roles-engines/dialog/CHANGELOG.md create mode 100644 src/core/component/directives/aria/roles-engines/dialog/README.md rename src/core/component/directives/aria/roles-engines/{dialog.ts => dialog/index.ts} (56%) create mode 100644 src/core/component/directives/aria/roles-engines/listbox/CHANGELOG.md create mode 100644 src/core/component/directives/aria/roles-engines/listbox/README.md rename src/core/component/directives/aria/roles-engines/{listbox.ts => listbox/index.ts} (50%) delete mode 100644 src/core/component/directives/aria/roles-engines/option.ts create mode 100644 src/core/component/directives/aria/roles-engines/option/CHANGELOG.md create mode 100644 src/core/component/directives/aria/roles-engines/option/README.md create mode 100644 src/core/component/directives/aria/roles-engines/option/index.ts create mode 100644 src/core/component/directives/aria/roles-engines/option/interface.ts create mode 100644 src/core/component/directives/aria/roles-engines/tab/CHANGELOG.md create mode 100644 src/core/component/directives/aria/roles-engines/tab/README.md rename src/core/component/directives/aria/roles-engines/{tab.ts => tab/index.ts} (69%) create mode 100644 src/core/component/directives/aria/roles-engines/tab/interface.ts create mode 100644 src/core/component/directives/aria/roles-engines/tablist/CHANGELOG.md create mode 100644 src/core/component/directives/aria/roles-engines/tablist/README.md rename src/core/component/directives/aria/roles-engines/{tablist.ts => tablist/index.ts} (59%) create mode 100644 src/core/component/directives/aria/roles-engines/tablist/interface.ts create mode 100644 src/core/component/directives/aria/roles-engines/tabpanel/CHANGELOG.md create mode 100644 src/core/component/directives/aria/roles-engines/tabpanel/README.md rename src/core/component/directives/aria/roles-engines/{tabpanel.ts => tabpanel/index.ts} (73%) create mode 100644 src/core/component/directives/aria/roles-engines/tree/CHANGELOG.md create mode 100644 src/core/component/directives/aria/roles-engines/tree/README.md rename src/core/component/directives/aria/roles-engines/{tree.ts => tree/index.ts} (52%) create mode 100644 src/core/component/directives/aria/roles-engines/tree/interface.ts create mode 100644 src/core/component/directives/aria/roles-engines/treeitem/CHANGELOG.md create mode 100644 src/core/component/directives/aria/roles-engines/treeitem/README.md rename src/core/component/directives/aria/roles-engines/{treeitem.ts => treeitem/index.ts} (72%) create mode 100644 src/core/component/directives/aria/roles-engines/treeitem/interface.ts diff --git a/package.json b/package.json index 2390ee4d30..56cc0ad330 100644 --- a/package.json +++ b/package.json @@ -146,7 +146,7 @@ "@types/glob": "7.2.0", "@types/semver": "7.3.10", "@types/webpack": "5.28.0", - "@v4fire/core": "3.86.2", + "@v4fire/core": "3.87.0", "@v4fire/linters": "1.9.0", "husky": "7.0.4", "nyc": "15.1.0", @@ -154,7 +154,7 @@ "webpack": "5.70.0" }, "peerDependencies": { - "@v4fire/core": "^3.86.2", + "@v4fire/core": "^3.87.0", "webpack": "^5.70.0" } } diff --git a/src/base/b-list/b-list.ts b/src/base/b-list/b-list.ts index 6db33ab9f0..04b3a322f3 100644 --- a/src/base/b-list/b-list.ts +++ b/src/base/b-list/b-list.ts @@ -36,7 +36,8 @@ export * from 'base/b-list/interface'; export const $$ = symbolGenerator(); -interface bList extends Trait {} +interface bList extends Trait { +} /** * Component to create a list of tabs/links @@ -697,13 +698,13 @@ class bList extends iData implements iVisible, iWidth, iItems, iAccess { } /** - * Returns a dictionary with configurations for the v-aria directive used as a tablist + * Returns a dictionary with configurations for the `v-aria` directive used as a tablist * @param role */ protected getAriaConfig(role: 'tablist'): Dictionary; /** - * Returns a dictionary with configurations for the v-aria directive used as a tab + * Returns a dictionary with configurations for the `v-aria` directive used as a tab * * @param role * @param item - tab item data @@ -715,7 +716,7 @@ class bList extends iData implements iVisible, iWidth, iItems, iAccess { const isActive = this.isActive.bind(this, item?.value); - const changeEvent = (cb: Function) => { + const bindChangeEvent = (cb: Function) => { this.on('change', () => { if (Object.isSet(this.active)) { cb(this.block?.elements('link', {active: true})); @@ -732,19 +733,23 @@ class bList extends iData implements iVisible, iWidth, iItems, iAccess { }; const tabConfig = { - preSelected: this.active != null, + hasDefaultSelectedTabs: this.active != null, isFirst: i === 0, orientation: this.orientation, - changeEvent, - get isActive() { + '@change': bindChangeEvent, + + get isSelected() { return isActive(); } }; switch (role) { - case 'tablist': return tablistConfig; - case 'tab': return tabConfig; - default: return {}; + case 'tablist': + return tablistConfig; + case 'tab': + return tabConfig; + default: + return {}; } } diff --git a/src/base/b-tree/b-tree.ts b/src/base/b-tree/b-tree.ts index 7c08ed7ea5..013cda0154 100644 --- a/src/base/b-tree/b-tree.ts +++ b/src/base/b-tree/b-tree.ts @@ -252,13 +252,13 @@ class bTree extends iData implements iItems, iAccess { } /** - * Returns a dictionary with configurations for the v-aria directive used as a tree + * Returns a dictionary with configurations for the `v-aria` directive used as a tree * @param role */ protected getAriaConfig(role: 'tree'): Dictionary /** - * Returns a dictionary with configurations for the v-aria directive used as a treeitem + * Returns a dictionary with configurations for the `v-aria` directive used as a treeitem * * @param role * @param item - tab item data @@ -289,21 +289,24 @@ class bTree extends iData implements iItems, iAccess { const treeConfig = { isRoot: this.top == null, orientation: this.orientation, - changeEvent: (cb: Function) => { + '@change': (cb: Function) => { this.on('fold', (ctx, el, item, value) => cb(el, value)); } }; const treeitemConfig = { - isRootFirstItem: this.top == null && i === 0, + isFirstRootItem: this.top == null && i === 0, orientation: this.orientation, toggleFold, + get rootElement() { return root(); }, + get isExpanded() { return getFoldedMod() === 'false'; }, + get isExpandable() { return item?.children != null; } diff --git a/src/core/component/directives/aria/README.md b/src/core/component/directives/aria/README.md index 90463467fc..aabae564b4 100644 --- a/src/core/component/directives/aria/README.md +++ b/src/core/component/directives/aria/README.md @@ -7,7 +7,7 @@ This module provides a directive to add aria attributes and logic to elements th ``` < &__foo v-aria.#bla -< &__foo v-aria = {labelledby: dom.getId('bla')} +< &__foo v-aria = {label: 'title'} ``` ## Available modifiers: @@ -22,106 +22,6 @@ the same as < v-aria = {labelledby: dom.getId('title')} ``` --- Roles: -- `controls`: -The engine to set aria-controls attribute. -For more information go to [`https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes/aria-controls`]. - -Directive can be added to any tag that includes tag with needed role. Role should be passed as a modifier. -ID or IDs are passed as value. -ID could be single or multiple written in string with space between. - -There are two ways to use this engine: -1. To add role as a modifier to which passed IDs in `for` value should be added. `for` could be `string` or `string[]`. -If element controls several elements `for` should be passed as a string with IDs separated with space. -(!) Notice that this role attribute should already be added to the element. The engine does not set passed role to any element. - -Example: -``` -< &__foo v-aria:controls.tab = {for: 'id1 id2 id3'} - -the same as -< &__foo - < button aria-controls = "id1 id2 id3" role = "tab" -``` - -2. To pass value `for` as an array of tuples. -First id in a tuple is an id of an element to add the aria attributes. -The second one is an id of an element to set as value in aria-controls attribute. -(!) Notice that id attribute should already be added to the element. The engine does not set passed ids to any element. - -Example: -``` -< &__foo v-aria:controls = {for: [[id1, id3], [id2, id4]]} - < span :id = "id1" - < span :id = "id2" - -the same as -< &__foo - < span :id = "id1" aria-controls = "id3" - < span :id = "id2" aria-controls = "id4" -``` - -- `dialog`: -The engine to set `dialog` role attribute. -For more information go to [`https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/dialog_role`]. - -Expects `iOpen` trait to be realized. - -- `tab`: -The engine to set `tab` role attribute. -For more information go to [`https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/tab_role`]. - -Tabs expect the `controls` role engine to be added in addition. ID passed to `controls` engine should be the id of the element with role `tabpanel`. - -Example: -``` -< button v-aria:tab | v-aria:controls = {for: 'id1'} - -< v-aria:tabpanel = {labelledby: 'id2'} | :id = 'id1' - < span :id = 'id2' - // content -``` - -- `tablist`: -The engine to set `tablist` role attribute. -For more information go to [`https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/tablist_role`]. - -- `tabpanel`: -The engine to set `tablist` role attribute. -For more information go to [`https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/tabpanel_role`]. - -Expects `label` or `labelledby` params to be passed. - -Example: -``` -< v-aria:tabpanel = {labelledby: 'id1'} - < span :id = 'id1' - // content -``` - -- `tree`: -The engine to set `tree` role attribute. -For more information go to [`https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/tree_role`]. - -- `treeitem`: -The engine to set `treeitem` role attribute. -For more information go to [`https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/treeitem_role`]. - -Expects `iAccess` trait to be realized. - -- `combobox`: -The engine to set `combobox` role attribute. -For more information go to [`https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/combobox_role`]. - -- `listbox`: -The engine to set `listbox` role attribute. -For more information go to [`https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/listbox_role`]. - -- `option`: -The engine to set `option` role attribute. -For more information go to [`https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/option_role`]. - ## Available values: Parameters passed to the directive are expected to always be object type. Any directive handle common keys: - label @@ -136,4 +36,4 @@ Expects string as expanded 'description' to the specified element - describedby Expects string as an id of the element. This element is an expanded 'description' to the specified element -Also, there are specific role keys. For info go to [`core/component/directives/role-engines/interface.ts`](core/component/directives/role-engines/interface.ts). +Also, there are specific role keys. For info go to [`core/component/directives/role-engines/`](core/component/directives/role-engines/). diff --git a/src/core/component/directives/aria/aria-setter.ts b/src/core/component/directives/aria/aria-setter.ts index 5b402b2870..05d62f9549 100644 --- a/src/core/component/directives/aria/aria-setter.ts +++ b/src/core/component/directives/aria/aria-setter.ts @@ -8,18 +8,26 @@ import * as ariaRoles from 'core/component/directives/aria/roles-engines'; import Async from 'core/async'; -import AriaRoleEngine, { eventNames } from 'core/component/directives/aria/interface'; + import type iBlock from 'super/i-block/i-block'; +import type iAccess from 'traits/i-access/i-access'; + import type { DirectiveOptions } from 'core/component/directives/aria/interface'; +import { AriaRoleEngine, EngineOptions, EventNames } from 'core/component/directives/aria/roles-engines'; /** * Class-helper for making base operations for the directive */ -export default class AriaSetter extends AriaRoleEngine { +export default class AriaSetter { + /** + * Aria directive options + */ + readonly options: DirectiveOptions; + /** * Async instance for aria directive */ - override readonly async: Async; + readonly async: Async; /** * Role engine instance @@ -27,8 +35,7 @@ export default class AriaSetter extends AriaRoleEngine { role: CanUndef; constructor(options: DirectiveOptions) { - super(options); - + this.options = options; this.async = new Async(); this.setAriaRole(); @@ -79,12 +86,40 @@ export default class AriaSetter extends AriaRoleEngine { return; } - this.role = new ariaRoles[role](this.options); + const + engine = this.createEngineName(role), + options = this.createRoleOptions(); + + this.role = new ariaRoles[engine](options); + } + + /** + * Creates an engine name from a passed parameter + * @param role + */ + protected createEngineName(role: string): string { + return `${role.capitalize()}Engine`; + } + + /** + * Creates a dictionary with engine options + */ + protected createRoleOptions(): EngineOptions { + const + {el, binding, vnode} = this.options, + {value, modifiers} = binding; + + return { + el, + modifiers, + params: value, + ctx: Object.cast(vnode.fakeContext) + }; } /** * Sets aria-label, aria-labelledby, aria-description and aria-describedby attributes to the element - * from passed parameters + * from directive parameters */ protected setAriaLabel(): void { const @@ -132,9 +167,10 @@ export default class AriaSetter extends AriaRoleEngine { params = this.options.binding.value; for (const key in params) { - if (key in eventNames) { + if (key in EventNames) { + const - callback = this.role[eventNames[key]].bind(this.role), + callback = this.role[EventNames[key]].bind(this.role), property = params[key]; if (Object.isFunction(property)) { diff --git a/src/core/component/directives/aria/index.ts b/src/core/component/directives/aria/index.ts index 9ba81852e6..0155ea4e8c 100644 --- a/src/core/component/directives/aria/index.ts +++ b/src/core/component/directives/aria/index.ts @@ -14,8 +14,10 @@ import { ComponentEngine, VNode, VNodeDirective } from 'core/component/engines'; import AriaSetter from 'core/component/directives/aria/aria-setter'; +export * from 'core/component/directives/aria/interface'; + const - ariaMap = new WeakMap(); + ariaInstances = new WeakMap(); ComponentEngine.directive('aria', { inserted(el: HTMLElement, binding: VNodeDirective, vnode: VNode): void { @@ -26,23 +28,14 @@ ComponentEngine.directive('aria', { return; } - const - aria = new AriaSetter({el, binding, vnode}); - - ariaMap.set(el, aria); + ariaInstances.set(el, new AriaSetter({el, binding, vnode})); }, update(el: HTMLElement) { - const - aria: AriaSetter = ariaMap.get(el); - - aria.update(); + ariaInstances.get(el)?.update(); }, unbind(el: HTMLElement) { - const - aria: AriaSetter = ariaMap.get(el); - - aria.destroy(); + ariaInstances.get(el)?.destroy(); } }); diff --git a/src/core/component/directives/aria/interface.ts b/src/core/component/directives/aria/interface.ts index 3c18527bfc..8fd1a114f2 100644 --- a/src/core/component/directives/aria/interface.ts +++ b/src/core/component/directives/aria/interface.ts @@ -7,37 +7,9 @@ */ import type { VNode, VNodeDirective } from 'core/component/engines'; -import type Async from 'core/async'; export interface DirectiveOptions { el: HTMLElement; binding: VNodeDirective; vnode: VNode; } - -export default abstract class AriaRoleEngine { - readonly options: DirectiveOptions; - async: CanUndef; - - protected constructor(options: DirectiveOptions) { - this.options = options; - } - - abstract init(): void; -} - -export const enum keyCodes { - ENTER = 'Enter', - END = 'End', - HOME = 'Home', - LEFT = 'ArrowLeft', - UP = 'ArrowUp', - RIGHT = 'ArrowRight', - DOWN = 'ArrowDown' -} - -export enum eventNames { - openEvent = 'onOpen', - closeEvent = 'onClose', - changeEvent = 'onChange' -} diff --git a/src/core/component/directives/aria/roles-engines/combobox/CHANGELOG.md b/src/core/component/directives/aria/roles-engines/combobox/CHANGELOG.md new file mode 100644 index 0000000000..ba2980fb55 --- /dev/null +++ b/src/core/component/directives/aria/roles-engines/combobox/CHANGELOG.md @@ -0,0 +1,16 @@ +Changelog +========= + +> **Tags:** +> - :boom: [Breaking Change] +> - :rocket: [New Feature] +> - :bug: [Bug Fix] +> - :memo: [Documentation] +> - :house: [Internal] +> - :nail_care: [Polish] + +## v?.??.? (2022-0?-??) + +#### :rocket: New Feature + +* Initial release diff --git a/src/core/component/directives/aria/roles-engines/combobox/README.md b/src/core/component/directives/aria/roles-engines/combobox/README.md new file mode 100644 index 0000000000..57e6f9c96f --- /dev/null +++ b/src/core/component/directives/aria/roles-engines/combobox/README.md @@ -0,0 +1,14 @@ +# core/component/directives/aria/roles-engines/combobox + +This module provides an engine for `v-aria` directive. + +The engine to set `combobox` role attribute. +For more information about attributes go to [`https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/combobox_role`]. +For recommendations how to make accessible widget go to [`https://www.w3.org/WAI/ARIA/apg/patterns/combobox/`]. + +## Usage + +``` +< &__foo v-aria:combobox = {...} + +``` diff --git a/src/core/component/directives/aria/roles-engines/combobox.ts b/src/core/component/directives/aria/roles-engines/combobox/index.ts similarity index 73% rename from src/core/component/directives/aria/roles-engines/combobox.ts rename to src/core/component/directives/aria/roles-engines/combobox/index.ts index 78ece722c3..ad703aa2f3 100644 --- a/src/core/component/directives/aria/roles-engines/combobox.ts +++ b/src/core/component/directives/aria/roles-engines/combobox/index.ts @@ -6,31 +6,28 @@ * https://github.com/V4Fire/Client/blob/master/LICENSE */ -import AriaRoleEngine, { DirectiveOptions } from 'core/component/directives/aria/interface'; +import type { ComboboxParams } from 'core/component/directives/aria/roles-engines/combobox/interface'; +import { AriaRoleEngine, EngineOptions } from 'core/component/directives/aria/roles-engines/interface'; -import type { ComboboxParams } from 'core/component/directives/aria/roles-engines/interface'; -import type iAccess from 'traits/i-access/i-access'; - -export default class ComboboxEngine extends AriaRoleEngine { +export class ComboboxEngine extends AriaRoleEngine { /** - * Passed directive params + * Engine params */ params: ComboboxParams; /** * First focusable element inside the element with directive or this element if there is no focusable inside */ - el: HTMLElement; + override el: HTMLElement; - constructor(options: DirectiveOptions) { + constructor(options: EngineOptions) { super(options); const - {el} = this.options, - ctx = Object.cast(this.options.vnode.fakeContext); + {el} = this; - this.el = (>ctx.findFocusableElement()) ?? el; - this.params = this.options.binding.value; + this.el = this.ctx?.findFocusableElement() ?? el; + this.params = options.params; } /** diff --git a/src/core/component/directives/aria/roles-engines/combobox/interface.ts b/src/core/component/directives/aria/roles-engines/combobox/interface.ts new file mode 100644 index 0000000000..7675f2fbc4 --- /dev/null +++ b/src/core/component/directives/aria/roles-engines/combobox/interface.ts @@ -0,0 +1,16 @@ +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ + +import type { EventBinder } from 'core/component/directives/aria/roles-engines/interface'; + +export interface ComboboxParams { + isMultiple: boolean; + '@change': EventBinder; + '@open': EventBinder; + '@close': EventBinder; +} diff --git a/src/core/component/directives/aria/roles-engines/controls/CHANGELOG.md b/src/core/component/directives/aria/roles-engines/controls/CHANGELOG.md new file mode 100644 index 0000000000..ba2980fb55 --- /dev/null +++ b/src/core/component/directives/aria/roles-engines/controls/CHANGELOG.md @@ -0,0 +1,16 @@ +Changelog +========= + +> **Tags:** +> - :boom: [Breaking Change] +> - :rocket: [New Feature] +> - :bug: [Bug Fix] +> - :memo: [Documentation] +> - :house: [Internal] +> - :nail_care: [Polish] + +## v?.??.? (2022-0?-??) + +#### :rocket: New Feature + +* Initial release diff --git a/src/core/component/directives/aria/roles-engines/controls/README.md b/src/core/component/directives/aria/roles-engines/controls/README.md new file mode 100644 index 0000000000..1b3744eddd --- /dev/null +++ b/src/core/component/directives/aria/roles-engines/controls/README.md @@ -0,0 +1,50 @@ +# core/component/directives/aria/roles-engines/controls + +This module provides an engine for `v-aria` directive. + +The engine to set aria-controls attribute. +For more information go to [`https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes/aria-controls`]. + +## Usage + +``` +< &__foo v-aria:controls = {...} + +``` + +## How to use + +Directive can be added to any tag that includes tag with needed role. Role should be passed as a modifier. +ID or IDs are passed as value. +ID could be single or multiple written in string with space between. + +There are two ways to use this engine: +1. To add role as a modifier to which passed IDs in `for` value should be added. `for` could be `string` or `string[]`. +If element controls several elements `for` should be passed as a string with IDs separated with space. +(!) Notice that this role attribute should already be added to the element. The engine does not set passed role to any element. + +Example: +``` +< &__foo v-aria:controls.tab = {for: 'id1 id2 id3'} + +the same as +< &__foo + < button aria-controls = "id1 id2 id3" role = "tab" +``` + +2. To pass value `for` as an array of tuples. +First id in a tuple is an id of an element to add the aria attributes. +The second one is an id of an element to set as value in aria-controls attribute. +(!) Notice that id attribute should already be added to the element. The engine does not set passed ids to any element. + +Example: +``` +< &__foo v-aria:controls = {for: [[id1, id3], [id2, id4]]} + < span :id = "id1" + < span :id = "id2" + +the same as +< &__foo + < span :id = "id1" aria-controls = "id3" + < span :id = "id2" aria-controls = "id4" +``` diff --git a/src/core/component/directives/aria/roles-engines/controls.ts b/src/core/component/directives/aria/roles-engines/controls/index.ts similarity index 80% rename from src/core/component/directives/aria/roles-engines/controls.ts rename to src/core/component/directives/aria/roles-engines/controls/index.ts index efa51321a8..f106754ddd 100644 --- a/src/core/component/directives/aria/roles-engines/controls.ts +++ b/src/core/component/directives/aria/roles-engines/controls/index.ts @@ -6,19 +6,19 @@ * https://github.com/V4Fire/Client/blob/master/LICENSE */ -import AriaRoleEngine, { DirectiveOptions } from 'core/component/directives/aria/interface'; -import type { ControlsParams } from 'core/component/directives/aria/roles-engines/interface'; +import type { ControlsParams } from 'core/component/directives/aria/roles-engines/controls/interface'; +import { AriaRoleEngine, EngineOptions } from 'core/component/directives/aria/roles-engines/interface'; -export default class ControlsEngine extends AriaRoleEngine { +export class ControlsEngine extends AriaRoleEngine { /** - * Passed directive params + * Engine params */ params: ControlsParams; - constructor(options: DirectiveOptions) { + constructor(options: EngineOptions) { super(options); - this.params = this.options.binding.value; + this.params = options.params; } /** @@ -26,9 +26,7 @@ export default class ControlsEngine extends AriaRoleEngine { */ init(): void { const - {vnode, binding, el} = this.options, - {modifiers} = binding, - {fakeContext: ctx} = vnode, + {ctx, modifiers, el} = this, {for: forId} = this.params; // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition diff --git a/src/core/component/directives/aria/roles-engines/controls/interface.ts b/src/core/component/directives/aria/roles-engines/controls/interface.ts new file mode 100644 index 0000000000..608bf5c139 --- /dev/null +++ b/src/core/component/directives/aria/roles-engines/controls/interface.ts @@ -0,0 +1,11 @@ +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ + +export interface ControlsParams { + for: CanArray | Array<[string, string]>; +} diff --git a/src/core/component/directives/aria/roles-engines/dialog/CHANGELOG.md b/src/core/component/directives/aria/roles-engines/dialog/CHANGELOG.md new file mode 100644 index 0000000000..ba2980fb55 --- /dev/null +++ b/src/core/component/directives/aria/roles-engines/dialog/CHANGELOG.md @@ -0,0 +1,16 @@ +Changelog +========= + +> **Tags:** +> - :boom: [Breaking Change] +> - :rocket: [New Feature] +> - :bug: [Bug Fix] +> - :memo: [Documentation] +> - :house: [Internal] +> - :nail_care: [Polish] + +## v?.??.? (2022-0?-??) + +#### :rocket: New Feature + +* Initial release diff --git a/src/core/component/directives/aria/roles-engines/dialog/README.md b/src/core/component/directives/aria/roles-engines/dialog/README.md new file mode 100644 index 0000000000..342b5b3e6c --- /dev/null +++ b/src/core/component/directives/aria/roles-engines/dialog/README.md @@ -0,0 +1,16 @@ +# core/component/directives/aria/roles-engines/dialog + +This module provides an engine for `v-aria` directive. + +The engine to set `dialog` role attribute. +For more information go to [`https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/dialog_role`]. + +Expects `iOpen` trait to be realized. + + +## Usage + +``` +< &__foo v-aria:dialog + +``` diff --git a/src/core/component/directives/aria/roles-engines/dialog.ts b/src/core/component/directives/aria/roles-engines/dialog/index.ts similarity index 56% rename from src/core/component/directives/aria/roles-engines/dialog.ts rename to src/core/component/directives/aria/roles-engines/dialog/index.ts index beb96370a6..e1652ea13c 100644 --- a/src/core/component/directives/aria/roles-engines/dialog.ts +++ b/src/core/component/directives/aria/roles-engines/dialog/index.ts @@ -6,21 +6,18 @@ * https://github.com/V4Fire/Client/blob/master/LICENSE */ -import AriaRoleEngine from 'core/component/directives/aria/interface'; +import { AriaRoleEngine } from 'core/component/directives/aria/roles-engines/interface'; import iOpen from 'traits/i-open/i-open'; -export default class DialogEngine extends AriaRoleEngine { +export class DialogEngine extends AriaRoleEngine { /** * Sets base aria attributes for current role */ init(): void { - const - {el, vnode} = this.options; + this.el.setAttribute('role', 'dialog'); + this.el.setAttribute('aria-modal', 'true'); - el.setAttribute('role', 'dialog'); - el.setAttribute('aria-modal', 'true'); - - if (!iOpen.is(vnode.fakeContext)) { + if (!iOpen.is(this.ctx)) { Object.throw('Dialog aria directive expects the component to realize iOpen interface'); } } diff --git a/src/core/component/directives/aria/roles-engines/index.ts b/src/core/component/directives/aria/roles-engines/index.ts index 7e8e48a6f5..9fdac42030 100644 --- a/src/core/component/directives/aria/roles-engines/index.ts +++ b/src/core/component/directives/aria/roles-engines/index.ts @@ -6,13 +6,15 @@ * https://github.com/V4Fire/Client/blob/master/LICENSE */ -export { default as dialog } from 'core/component/directives/aria/roles-engines/dialog'; -export { default as tablist } from 'core/component/directives/aria/roles-engines/tablist'; -export { default as tab } from 'core/component/directives/aria/roles-engines/tab'; -export { default as tabpanel } from 'core/component/directives/aria/roles-engines/tabpanel'; -export { default as controls } from 'core/component/directives/aria/roles-engines/controls'; -export { default as combobox } from 'core/component/directives/aria/roles-engines/combobox'; -export { default as listbox } from 'core/component/directives/aria/roles-engines/listbox'; -export { default as option } from 'core/component/directives/aria/roles-engines/option'; -export { default as tree } from 'core/component/directives/aria/roles-engines/tree'; -export { default as treeitem } from 'core/component/directives/aria/roles-engines/treeitem'; +export * from 'core/component/directives/aria/roles-engines/dialog'; +export * from 'core/component/directives/aria/roles-engines/tablist'; +export * from 'core/component/directives/aria/roles-engines/tab'; +export * from 'core/component/directives/aria/roles-engines/tabpanel'; +export * from 'core/component/directives/aria/roles-engines/controls'; +export * from 'core/component/directives/aria/roles-engines/combobox'; +export * from 'core/component/directives/aria/roles-engines/listbox'; +export * from 'core/component/directives/aria/roles-engines/option'; +export * from 'core/component/directives/aria/roles-engines/tree'; +export * from 'core/component/directives/aria/roles-engines/treeitem'; + +export { AriaRoleEngine, EngineOptions, EventNames } from 'core/component/directives/aria/roles-engines/interface'; diff --git a/src/core/component/directives/aria/roles-engines/interface.ts b/src/core/component/directives/aria/roles-engines/interface.ts index 8d2db1ab10..06a5b3ac87 100644 --- a/src/core/component/directives/aria/roles-engines/interface.ts +++ b/src/core/component/directives/aria/roles-engines/interface.ts @@ -1,38 +1,66 @@ -export interface TabParams { - preSelected: boolean; - isFirst: boolean; - isActive: boolean; - orientation: string; - changeEvent(cb: Function): void; -} +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ -export interface TablistParams { - isMultiple: boolean; - orientation: string; -} +import type Async from 'core/async'; +import type iAccess from 'traits/i-access/i-access'; +import type iBlock from 'super/i-block/i-block'; + +export abstract class AriaRoleEngine { + /** + * Element on which the directive is set + */ + readonly el: HTMLElement; + + /** + * Component on which the directive is set + */ + readonly ctx: CanUndef; -export interface TreeParams { - isRoot: boolean; - orientation: string; - changeEvent(cb: Function): void; + /** + * Directive passed modifiers + */ + readonly modifiers: CanUndef>; + + /** + * Async instance + */ + async: CanUndef; + + constructor({el, ctx, modifiers}: EngineOptions) { + this.el = el; + this.ctx = ctx; + this.modifiers = modifiers; + } + + abstract init(): void; } -export interface TreeitemParams { - isRootFirstItem: boolean; - isExpandable: boolean; - isExpanded: boolean; - orientation: string; - rootElement: CanUndef; - toggleFold(el: Element, value?: boolean): void; +export interface EngineOptions { + el: HTMLElement; + modifiers: CanUndef>; + params: DictionaryType; + ctx: iBlock & iAccess; } -export interface ComboboxParams { - isMultiple: boolean; - changeEvent(cb: Function): void; - openEvent(cb: Function): void; - closeEvent(cb: Function): void; +export type EventBinder = (cb: Function) => void; + +export const enum KeyCodes { + ENTER = 'Enter', + END = 'End', + HOME = 'Home', + LEFT = 'ArrowLeft', + UP = 'ArrowUp', + RIGHT = 'ArrowRight', + DOWN = 'ArrowDown' } -export interface ControlsParams { - for: CanArray | Array<[string, string]>; +export enum EventNames { + '@open' = 'onOpen', + '@close' = 'onClose', + '@change' = 'onChange' } diff --git a/src/core/component/directives/aria/roles-engines/listbox/CHANGELOG.md b/src/core/component/directives/aria/roles-engines/listbox/CHANGELOG.md new file mode 100644 index 0000000000..ba2980fb55 --- /dev/null +++ b/src/core/component/directives/aria/roles-engines/listbox/CHANGELOG.md @@ -0,0 +1,16 @@ +Changelog +========= + +> **Tags:** +> - :boom: [Breaking Change] +> - :rocket: [New Feature] +> - :bug: [Bug Fix] +> - :memo: [Documentation] +> - :house: [Internal] +> - :nail_care: [Polish] + +## v?.??.? (2022-0?-??) + +#### :rocket: New Feature + +* Initial release diff --git a/src/core/component/directives/aria/roles-engines/listbox/README.md b/src/core/component/directives/aria/roles-engines/listbox/README.md new file mode 100644 index 0000000000..fdd53494f0 --- /dev/null +++ b/src/core/component/directives/aria/roles-engines/listbox/README.md @@ -0,0 +1,14 @@ +# core/component/directives/aria/roles-engines/listbox + +This module provides an engine for `v-aria` directive. + +The engine to set `listbox` role attribute. +For more information go to [`https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/listbox_role`]. +For recommendations how to make accessible widget go to [`https://www.w3.org/WAI/ARIA/apg/patterns/listbox/`]. + +## Usage + +``` +< &__foo v-aria:listbox = {...} + +``` diff --git a/src/core/component/directives/aria/roles-engines/listbox.ts b/src/core/component/directives/aria/roles-engines/listbox/index.ts similarity index 50% rename from src/core/component/directives/aria/roles-engines/listbox.ts rename to src/core/component/directives/aria/roles-engines/listbox/index.ts index 9bfd9acec3..7d451389d5 100644 --- a/src/core/component/directives/aria/roles-engines/listbox.ts +++ b/src/core/component/directives/aria/roles-engines/listbox/index.ts @@ -6,17 +6,14 @@ * https://github.com/V4Fire/Client/blob/master/LICENSE */ -import AriaRoleEngine from 'core/component/directives/aria/interface'; +import { AriaRoleEngine } from 'core/component/directives/aria/roles-engines/interface'; -export default class ListboxEngine extends AriaRoleEngine { +export class ListboxEngine extends AriaRoleEngine { /** * Sets base aria attributes for current role */ init(): void { - const - {el} = this.options; - - el.setAttribute('role', 'listbox'); - el.setAttribute('tabindex', '-1'); + this.el.setAttribute('role', 'listbox'); + this.el.setAttribute('tabindex', '-1'); } } diff --git a/src/core/component/directives/aria/roles-engines/option.ts b/src/core/component/directives/aria/roles-engines/option.ts deleted file mode 100644 index 3b30bfc48b..0000000000 --- a/src/core/component/directives/aria/roles-engines/option.ts +++ /dev/null @@ -1,34 +0,0 @@ -/*! - * V4Fire Client Core - * https://github.com/V4Fire/Client - * - * Released under the MIT license - * https://github.com/V4Fire/Client/blob/master/LICENSE - */ - -import AriaRoleEngine from 'core/component/directives/aria/interface'; - -export default class OptionEngine extends AriaRoleEngine { - /** - * Sets base aria attributes for current role - */ - init(): void { - const - {el} = this.options, - {value: {preSelected}} = this.options.binding; - - el.setAttribute('role', 'option'); - el.setAttribute('aria-selected', String(preSelected)); - } - - /** - * Handler: selected option changes - * @param isSelected - */ - protected onChange(isSelected: boolean): void { - const - {el} = this.options; - - el.setAttribute('aria-selected', String(isSelected)); - } -} diff --git a/src/core/component/directives/aria/roles-engines/option/CHANGELOG.md b/src/core/component/directives/aria/roles-engines/option/CHANGELOG.md new file mode 100644 index 0000000000..ba2980fb55 --- /dev/null +++ b/src/core/component/directives/aria/roles-engines/option/CHANGELOG.md @@ -0,0 +1,16 @@ +Changelog +========= + +> **Tags:** +> - :boom: [Breaking Change] +> - :rocket: [New Feature] +> - :bug: [Bug Fix] +> - :memo: [Documentation] +> - :house: [Internal] +> - :nail_care: [Polish] + +## v?.??.? (2022-0?-??) + +#### :rocket: New Feature + +* Initial release diff --git a/src/core/component/directives/aria/roles-engines/option/README.md b/src/core/component/directives/aria/roles-engines/option/README.md new file mode 100644 index 0000000000..cdf2fdcf28 --- /dev/null +++ b/src/core/component/directives/aria/roles-engines/option/README.md @@ -0,0 +1,13 @@ +# core/component/directives/aria/roles-engines/option + +This module provides an engine for `v-aria` directive. + +The engine to set `option` role attribute. +For more information go to [`https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/option_role`]. + +## Usage + +``` +< &__foo v-aria:option + +``` diff --git a/src/core/component/directives/aria/roles-engines/option/index.ts b/src/core/component/directives/aria/roles-engines/option/index.ts new file mode 100644 index 0000000000..566d2850ab --- /dev/null +++ b/src/core/component/directives/aria/roles-engines/option/index.ts @@ -0,0 +1,39 @@ +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ + +import type { OptionParams } from 'core/component/directives/aria/roles-engines/option/interface'; +import { AriaRoleEngine, EngineOptions } from 'core/component/directives/aria/roles-engines/interface'; + +export class OptionEngine extends AriaRoleEngine { + /** + * Engine params + */ + params: OptionParams; + + constructor(options: EngineOptions) { + super(options); + + this.params = options.params; + } + + /** + * Sets base aria attributes for current role + */ + init(): void { + this.el.setAttribute('role', 'option'); + this.el.setAttribute('aria-selected', String(this.params.isSelected)); + } + + /** + * Handler: selected option changes + * @param isSelected + */ + protected onChange(isSelected: boolean): void { + this.el.setAttribute('aria-selected', String(isSelected)); + } +} diff --git a/src/core/component/directives/aria/roles-engines/option/interface.ts b/src/core/component/directives/aria/roles-engines/option/interface.ts new file mode 100644 index 0000000000..6c3774f6e4 --- /dev/null +++ b/src/core/component/directives/aria/roles-engines/option/interface.ts @@ -0,0 +1,14 @@ +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ + +import type { EventBinder } from 'core/component/directives/aria/roles-engines/interface'; + +export interface OptionParams { + isSelected: boolean; + '@change': EventBinder; +} diff --git a/src/core/component/directives/aria/roles-engines/tab/CHANGELOG.md b/src/core/component/directives/aria/roles-engines/tab/CHANGELOG.md new file mode 100644 index 0000000000..ba2980fb55 --- /dev/null +++ b/src/core/component/directives/aria/roles-engines/tab/CHANGELOG.md @@ -0,0 +1,16 @@ +Changelog +========= + +> **Tags:** +> - :boom: [Breaking Change] +> - :rocket: [New Feature] +> - :bug: [Bug Fix] +> - :memo: [Documentation] +> - :house: [Internal] +> - :nail_care: [Polish] + +## v?.??.? (2022-0?-??) + +#### :rocket: New Feature + +* Initial release diff --git a/src/core/component/directives/aria/roles-engines/tab/README.md b/src/core/component/directives/aria/roles-engines/tab/README.md new file mode 100644 index 0000000000..0395e3bad9 --- /dev/null +++ b/src/core/component/directives/aria/roles-engines/tab/README.md @@ -0,0 +1,27 @@ +# core/component/directives/aria/roles-engines/tab + +This module provides an engine for `v-aria` directive. + +The engine to set `tab` role attribute. +For more information go to [`https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/tab_role`]. +For recommendations how to make accessible widget go to [`https://www.w3.org/WAI/ARIA/apg/patterns/tabpanel/`]. + +## Usage + +``` +< &__foo v-aria:tab = {...} + +``` + +## How to use + +Tabs expect the `controls` role engine to be added in addition. ID passed to `controls` engine should be the id of the element with role `tabpanel`. + +Example: +``` +< button v-aria:tab | v-aria:controls = {for: 'id1'} + +< v-aria:tabpanel = {labelledby: 'id2'} | :id = 'id1' + < span :id = 'id2' + // content +``` diff --git a/src/core/component/directives/aria/roles-engines/tab.ts b/src/core/component/directives/aria/roles-engines/tab/index.ts similarity index 69% rename from src/core/component/directives/aria/roles-engines/tab.ts rename to src/core/component/directives/aria/roles-engines/tab/index.ts index ab2d0b3309..71c58da143 100644 --- a/src/core/component/directives/aria/roles-engines/tab.ts +++ b/src/core/component/directives/aria/roles-engines/tab/index.ts @@ -9,28 +9,19 @@ * Copyright © [2022] W3C® (MIT, ERCIM, Keio, Beihang). */ -import AriaRoleEngine, { DirectiveOptions, keyCodes } from 'core/component/directives/aria/interface'; +import type { TabParams } from 'core/component/directives/aria/roles-engines/tab/interface'; +import { AriaRoleEngine, EngineOptions, KeyCodes } from 'core/component/directives/aria/roles-engines/interface'; -import type { TabParams } from 'core/component/directives/aria/roles-engines/interface'; -import type iAccess from 'traits/i-access/i-access'; -import type iBlock from 'super/i-block/i-block'; - -export default class TabEngine extends AriaRoleEngine { +export class TabEngine extends AriaRoleEngine { /** - * Passed directive params + * Engine params */ params: TabParams; - /** - * Component instance - */ - ctx: iAccess & iBlock; - - constructor(options: DirectiveOptions) { + constructor(options: EngineOptions) { super(options); - this.params = this.options.binding.value; - this.ctx = Object.cast(this.options.vnode.fakeContext); + this.params = options.params; } /** @@ -38,18 +29,18 @@ export default class TabEngine extends AriaRoleEngine { */ init(): void { const - {el} = this.options, - {isFirst, preSelected} = this.params; + {el} = this, + {isFirst, isSelected, hasDefaultSelectedTabs} = this.params; el.setAttribute('role', 'tab'); - el.setAttribute('aria-selected', String(this.params.isActive)); + el.setAttribute('aria-selected', String(isSelected)); - if (isFirst && !preSelected) { + if (isFirst && !hasDefaultSelectedTabs) { if (el.tabIndex < 0) { el.setAttribute('tabindex', '0'); } - } else if (preSelected && this.params.isActive) { + } else if (hasDefaultSelectedTabs && isSelected) { if (el.tabIndex < 0) { el.setAttribute('tabindex', '0'); } @@ -68,7 +59,7 @@ export default class TabEngine extends AriaRoleEngine { */ protected moveFocusToFirstTab(): void { const - firstTab = >this.ctx.findFocusableElement(); + firstTab = this.ctx?.findFocusableElement(); firstTab?.focus(); } @@ -78,7 +69,11 @@ export default class TabEngine extends AriaRoleEngine { */ protected moveFocusToLastTab(): void { const - tabs = >this.ctx.findAllFocusableElements(); + tabs = this.ctx?.findAllFocusableElements(); + + if (tabs == null) { + return; + } let lastTab: CanUndef; @@ -96,7 +91,7 @@ export default class TabEngine extends AriaRoleEngine { */ protected moveFocus(step: 1 | -1): void { const - focusable = >this.ctx.getNextFocusableElement(step); + focusable = this.ctx?.getNextFocusableElement(step); focusable?.focus(); } @@ -107,7 +102,7 @@ export default class TabEngine extends AriaRoleEngine { */ protected onChange(active: Element | NodeListOf): void { const - {el} = this.options; + {el} = this; function setAttributes(isSelected: boolean) { el.setAttribute('aria-selected', String(isSelected)); @@ -134,7 +129,7 @@ export default class TabEngine extends AriaRoleEngine { isVertical = this.params.orientation === 'vertical'; switch (evt.key) { - case keyCodes.LEFT: + case KeyCodes.LEFT: if (isVertical) { return; } @@ -142,7 +137,7 @@ export default class TabEngine extends AriaRoleEngine { this.moveFocus(-1); break; - case keyCodes.UP: + case KeyCodes.UP: if (isVertical) { this.moveFocus(-1); break; @@ -150,7 +145,7 @@ export default class TabEngine extends AriaRoleEngine { return; - case keyCodes.RIGHT: + case KeyCodes.RIGHT: if (isVertical) { return; } @@ -158,7 +153,7 @@ export default class TabEngine extends AriaRoleEngine { this.moveFocus(1); break; - case keyCodes.DOWN: + case KeyCodes.DOWN: if (isVertical) { this.moveFocus(1); break; @@ -166,11 +161,11 @@ export default class TabEngine extends AriaRoleEngine { return; - case keyCodes.HOME: + case KeyCodes.HOME: this.moveFocusToFirstTab(); break; - case keyCodes.END: + case KeyCodes.END: this.moveFocusToLastTab(); break; diff --git a/src/core/component/directives/aria/roles-engines/tab/interface.ts b/src/core/component/directives/aria/roles-engines/tab/interface.ts new file mode 100644 index 0000000000..c75904fcd1 --- /dev/null +++ b/src/core/component/directives/aria/roles-engines/tab/interface.ts @@ -0,0 +1,17 @@ +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ + +import type { EventBinder } from 'core/component/directives/aria/roles-engines/interface'; + +export interface TabParams { + isFirst: boolean; + isSelected: boolean; + hasDefaultSelectedTabs: boolean; + orientation: string; + '@change': EventBinder; +} diff --git a/src/core/component/directives/aria/roles-engines/tablist/CHANGELOG.md b/src/core/component/directives/aria/roles-engines/tablist/CHANGELOG.md new file mode 100644 index 0000000000..ba2980fb55 --- /dev/null +++ b/src/core/component/directives/aria/roles-engines/tablist/CHANGELOG.md @@ -0,0 +1,16 @@ +Changelog +========= + +> **Tags:** +> - :boom: [Breaking Change] +> - :rocket: [New Feature] +> - :bug: [Bug Fix] +> - :memo: [Documentation] +> - :house: [Internal] +> - :nail_care: [Polish] + +## v?.??.? (2022-0?-??) + +#### :rocket: New Feature + +* Initial release diff --git a/src/core/component/directives/aria/roles-engines/tablist/README.md b/src/core/component/directives/aria/roles-engines/tablist/README.md new file mode 100644 index 0000000000..078d4fd7e6 --- /dev/null +++ b/src/core/component/directives/aria/roles-engines/tablist/README.md @@ -0,0 +1,14 @@ +# core/component/directives/aria/roles-engines/tablist + +This module provides an engine for `v-aria` directive. + +The engine to set `tablist` role attribute. +For more information go to [`https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/tablist_role`]. +For recommendations how to make accessible widget go to [`https://www.w3.org/WAI/ARIA/apg/patterns/tabpanel/`]. + +## Usage + +``` +< &__foo v-aria:tablist = {...} + +``` diff --git a/src/core/component/directives/aria/roles-engines/tablist.ts b/src/core/component/directives/aria/roles-engines/tablist/index.ts similarity index 59% rename from src/core/component/directives/aria/roles-engines/tablist.ts rename to src/core/component/directives/aria/roles-engines/tablist/index.ts index b5c4653cd4..74ba0e4970 100644 --- a/src/core/component/directives/aria/roles-engines/tablist.ts +++ b/src/core/component/directives/aria/roles-engines/tablist/index.ts @@ -6,17 +6,27 @@ * https://github.com/V4Fire/Client/blob/master/LICENSE */ -import AriaRoleEngine from 'core/component/directives/aria/interface'; -import type { TablistParams } from 'core/component/directives/aria/roles-engines/interface'; +import type { TablistParams } from 'core/component/directives/aria/roles-engines/tablist/interface'; +import { AriaRoleEngine, EngineOptions } from 'core/component/directives/aria/roles-engines/interface'; + +export class TablistEngine extends AriaRoleEngine { + /** + * Engine params + */ + params: TablistParams; + + constructor(options: EngineOptions) { + super(options); + + this.params = options.params; + } -export default class TablistEngine extends AriaRoleEngine { /** * Sets base aria attributes for current role */ init(): void { const - {el, binding} = this.options, - params: TablistParams = binding.value; + {el, params} = this; el.setAttribute('role', 'tablist'); diff --git a/src/core/component/directives/aria/roles-engines/tablist/interface.ts b/src/core/component/directives/aria/roles-engines/tablist/interface.ts new file mode 100644 index 0000000000..ec88e5a93a --- /dev/null +++ b/src/core/component/directives/aria/roles-engines/tablist/interface.ts @@ -0,0 +1,12 @@ +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ + +export interface TablistParams { + isMultiple: boolean; + orientation: string; +} diff --git a/src/core/component/directives/aria/roles-engines/tabpanel/CHANGELOG.md b/src/core/component/directives/aria/roles-engines/tabpanel/CHANGELOG.md new file mode 100644 index 0000000000..ba2980fb55 --- /dev/null +++ b/src/core/component/directives/aria/roles-engines/tabpanel/CHANGELOG.md @@ -0,0 +1,16 @@ +Changelog +========= + +> **Tags:** +> - :boom: [Breaking Change] +> - :rocket: [New Feature] +> - :bug: [Bug Fix] +> - :memo: [Documentation] +> - :house: [Internal] +> - :nail_care: [Polish] + +## v?.??.? (2022-0?-??) + +#### :rocket: New Feature + +* Initial release diff --git a/src/core/component/directives/aria/roles-engines/tabpanel/README.md b/src/core/component/directives/aria/roles-engines/tabpanel/README.md new file mode 100644 index 0000000000..a60f4f322d --- /dev/null +++ b/src/core/component/directives/aria/roles-engines/tabpanel/README.md @@ -0,0 +1,25 @@ +# core/component/directives/aria/roles-engines/tabpanel + +This module provides an engine for `v-aria` directive. + +The engine to set `tabpanel` role attribute. +For more information go to [`https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/tabpanel_role`]. +For recommendations how to make accessible widget go to [`https://www.w3.org/WAI/ARIA/apg/patterns/tabpanel/`]. + +## Usage + +``` +< &__foo v-aria:tabpanel = {...} + +``` + +## How to use + +Expects `label` or `labelledby` params to be passed. + +Example: +``` +< v-aria:tabpanel = {labelledby: 'id1'} + < span :id = 'id1' + // content +``` diff --git a/src/core/component/directives/aria/roles-engines/tabpanel.ts b/src/core/component/directives/aria/roles-engines/tabpanel/index.ts similarity index 73% rename from src/core/component/directives/aria/roles-engines/tabpanel.ts rename to src/core/component/directives/aria/roles-engines/tabpanel/index.ts index ca3537088c..f49f2661ba 100644 --- a/src/core/component/directives/aria/roles-engines/tabpanel.ts +++ b/src/core/component/directives/aria/roles-engines/tabpanel/index.ts @@ -6,19 +6,18 @@ * https://github.com/V4Fire/Client/blob/master/LICENSE */ -import AriaRoleEngine from 'core/component/directives/aria/interface'; +import { AriaRoleEngine } from 'core/component/directives/aria/roles-engines/interface'; -export default class TabpanelEngine extends AriaRoleEngine { +export class TabpanelEngine extends AriaRoleEngine { /** * Sets base aria attributes for current role */ init(): void { const - {el} = this.options; + {el} = this; if (!el.hasAttribute('aria-label') && !el.hasAttribute('aria-labelledby')) { Object.throw('Tabpanel aria directive expects "label" or "labelledby" value to be passed'); - return; } el.setAttribute('role', 'tabpanel'); diff --git a/src/core/component/directives/aria/roles-engines/tree/CHANGELOG.md b/src/core/component/directives/aria/roles-engines/tree/CHANGELOG.md new file mode 100644 index 0000000000..ba2980fb55 --- /dev/null +++ b/src/core/component/directives/aria/roles-engines/tree/CHANGELOG.md @@ -0,0 +1,16 @@ +Changelog +========= + +> **Tags:** +> - :boom: [Breaking Change] +> - :rocket: [New Feature] +> - :bug: [Bug Fix] +> - :memo: [Documentation] +> - :house: [Internal] +> - :nail_care: [Polish] + +## v?.??.? (2022-0?-??) + +#### :rocket: New Feature + +* Initial release diff --git a/src/core/component/directives/aria/roles-engines/tree/README.md b/src/core/component/directives/aria/roles-engines/tree/README.md new file mode 100644 index 0000000000..f18c69c773 --- /dev/null +++ b/src/core/component/directives/aria/roles-engines/tree/README.md @@ -0,0 +1,15 @@ +# core/component/directives/aria/roles-engines/tree + +This module provides an engine for `v-aria` directive. + +The engine to set `tree` role attribute. +For more information go to [`https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/tree_role`]. + +For recommendations how to make accessible widget go to [`https://www.w3.org/WAI/ARIA/apg/patterns/treeview/`]. + +## Usage + +``` +< &__foo v-aria:tree = {...} + +``` diff --git a/src/core/component/directives/aria/roles-engines/tree.ts b/src/core/component/directives/aria/roles-engines/tree/index.ts similarity index 52% rename from src/core/component/directives/aria/roles-engines/tree.ts rename to src/core/component/directives/aria/roles-engines/tree/index.ts index 3b8986e196..4d94d71be4 100644 --- a/src/core/component/directives/aria/roles-engines/tree.ts +++ b/src/core/component/directives/aria/roles-engines/tree/index.ts @@ -6,19 +6,19 @@ * https://github.com/V4Fire/Client/blob/master/LICENSE */ -import AriaRoleEngine, { DirectiveOptions } from 'core/component/directives/aria/interface'; -import type { TreeParams } from 'core/component/directives/aria/roles-engines/interface'; +import type { TreeParams } from 'core/component/directives/aria/roles-engines/tree/interface'; +import { AriaRoleEngine, EngineOptions } from 'core/component/directives/aria/roles-engines/interface'; -export default class TreeEngine extends AriaRoleEngine { +export class TreeEngine extends AriaRoleEngine { /** - * Passed directive params + * Engine params */ params: TreeParams; - constructor(options: DirectiveOptions) { + constructor(options: EngineOptions) { super(options); - this.params = options.binding.value; + this.params = options.params; } /** @@ -26,13 +26,12 @@ export default class TreeEngine extends AriaRoleEngine { */ init(): void { const - {el} = this.options, - {orientation} = this.params; + {orientation, isRoot} = this.params; this.setRootRole(); - if (orientation === 'horizontal' && this.params.isRoot) { - el.setAttribute('aria-orientation', orientation); + if (orientation === 'horizontal' && isRoot) { + this.el.setAttribute('aria-orientation', orientation); } } @@ -40,10 +39,7 @@ export default class TreeEngine extends AriaRoleEngine { * Sets the role to the element depending on whether the tree is root or nested */ protected setRootRole(): void { - const - {el} = this.options; - - el.setAttribute('role', this.params.isRoot ? 'tree' : 'group'); + this.el.setAttribute('role', this.params.isRoot ? 'tree' : 'group'); } /** @@ -51,7 +47,7 @@ export default class TreeEngine extends AriaRoleEngine { * @param el * @param isFolded */ - protected onChange(el: HTMLElement, isFolded: boolean): void { + protected onChange(el: Element, isFolded: boolean): void { el.setAttribute('aria-expanded', String(!isFolded)); } } diff --git a/src/core/component/directives/aria/roles-engines/tree/interface.ts b/src/core/component/directives/aria/roles-engines/tree/interface.ts new file mode 100644 index 0000000000..150df682b1 --- /dev/null +++ b/src/core/component/directives/aria/roles-engines/tree/interface.ts @@ -0,0 +1,15 @@ +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ + +import type { EventBinder } from 'core/component/directives/aria/roles-engines/interface'; + +export interface TreeParams { + isRoot: boolean; + orientation: string; + '@change': EventBinder; +} diff --git a/src/core/component/directives/aria/roles-engines/treeitem/CHANGELOG.md b/src/core/component/directives/aria/roles-engines/treeitem/CHANGELOG.md new file mode 100644 index 0000000000..ba2980fb55 --- /dev/null +++ b/src/core/component/directives/aria/roles-engines/treeitem/CHANGELOG.md @@ -0,0 +1,16 @@ +Changelog +========= + +> **Tags:** +> - :boom: [Breaking Change] +> - :rocket: [New Feature] +> - :bug: [Bug Fix] +> - :memo: [Documentation] +> - :house: [Internal] +> - :nail_care: [Polish] + +## v?.??.? (2022-0?-??) + +#### :rocket: New Feature + +* Initial release diff --git a/src/core/component/directives/aria/roles-engines/treeitem/README.md b/src/core/component/directives/aria/roles-engines/treeitem/README.md new file mode 100644 index 0000000000..7e59a5b0bd --- /dev/null +++ b/src/core/component/directives/aria/roles-engines/treeitem/README.md @@ -0,0 +1,17 @@ +# core/component/directives/aria/roles-engines/treeitem + +This module provides an engine for `v-aria` directive. + +The engine to set `treeitem` role attribute. +For more information go to [`https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/treeitem_role`]. + +For recommendations how to make accessible widget go to [`https://www.w3.org/WAI/ARIA/apg/patterns/treeview/`]. + +Expects `iAccess` trait to be realized. + +## Usage + +``` +< &__foo v-aria:treeitem = {...} + +``` diff --git a/src/core/component/directives/aria/roles-engines/treeitem.ts b/src/core/component/directives/aria/roles-engines/treeitem/index.ts similarity index 72% rename from src/core/component/directives/aria/roles-engines/treeitem.ts rename to src/core/component/directives/aria/roles-engines/treeitem/index.ts index 9215388542..f2de0c1c60 100644 --- a/src/core/component/directives/aria/roles-engines/treeitem.ts +++ b/src/core/component/directives/aria/roles-engines/treeitem/index.ts @@ -9,38 +9,25 @@ * Copyright © [2022] W3C® (MIT, ERCIM, Keio, Beihang). */ -import AriaRoleEngine, { DirectiveOptions, keyCodes } from 'core/component/directives/aria/interface'; import iAccess from 'traits/i-access/i-access'; -import type { TreeitemParams } from 'core/component/directives/aria/roles-engines/interface'; -import type iBlock from 'super/i-block/i-block'; +import type { TreeitemParams } from 'core/component/directives/aria/roles-engines/treeitem/interface'; +import { AriaRoleEngine, KeyCodes, EngineOptions } from 'core/component/directives/aria/roles-engines/interface'; -export default class TreeItemEngine extends AriaRoleEngine { +export class TreeitemEngine extends AriaRoleEngine { /** - * Passed directive params + * Engine params */ params: TreeitemParams; - /** - * Component instance - */ - ctx: iAccess & iBlock['unsafe']; - - /** - * Element with current directive - */ - el: HTMLElement; - - constructor(options: DirectiveOptions) { + constructor(options: EngineOptions) { super(options); - if (!iAccess.is(options.vnode.fakeContext)) { + if (!iAccess.is(this.ctx)) { Object.throw('Treeitem aria directive expects the component to realize iAccess interface'); } - this.ctx = Object.cast(options.vnode.fakeContext); - this.el = this.options.el; - this.params = this.options.binding.value; + this.params = options.params; } /** @@ -50,11 +37,11 @@ export default class TreeItemEngine extends AriaRoleEngine { this.async?.on(this.el, 'keydown', this.onKeyDown.bind(this)); const - isMuted = this.ctx.removeAllFromTabSequence(this.el); + isMuted = this.ctx?.removeAllFromTabSequence(this.el); - if (this.params.isRootFirstItem) { + if (this.params.isFirstRootItem) { if (isMuted) { - this.ctx.restoreAllToTabSequence(this.el); + this.ctx?.restoreAllToTabSequence(this.el); } else { this.el.tabIndex = 0; @@ -63,7 +50,7 @@ export default class TreeItemEngine extends AriaRoleEngine { this.el.setAttribute('role', 'treeitem'); - this.ctx.$nextTick(() => { + this.ctx?.$nextTick(() => { if (this.params.isExpandable) { this.el.setAttribute('aria-expanded', String(this.params.isExpanded)); } @@ -75,8 +62,8 @@ export default class TreeItemEngine extends AriaRoleEngine { * @param el */ protected focusNext(el: HTMLElement): void { - this.ctx.removeAllFromTabSequence(this.el); - this.ctx.restoreAllToTabSequence(el); + this.ctx?.removeAllFromTabSequence(this.el); + this.ctx?.restoreAllToTabSequence(el); el.focus(); } @@ -87,7 +74,7 @@ export default class TreeItemEngine extends AriaRoleEngine { */ protected moveFocus(step: 1 | -1): void { const - nextEl = >this.ctx.getNextFocusableElement(step); + nextEl = this.ctx?.getNextFocusableElement(step); if (nextEl != null) { this.focusNext(nextEl); @@ -128,7 +115,7 @@ export default class TreeItemEngine extends AriaRoleEngine { } const - focusableParent = >this.ctx.findFocusableElement(parent); + focusableParent = this.ctx?.findFocusableElement(parent); if (focusableParent != null) { this.focusNext(focusableParent); @@ -140,7 +127,7 @@ export default class TreeItemEngine extends AriaRoleEngine { */ protected setFocusToFirstItem(): void { const - firstItem = >this.ctx.findFocusableElement(this.params.rootElement); + firstItem = this.ctx?.findFocusableElement(this.params.rootElement); if (firstItem != null) { this.focusNext(firstItem); @@ -152,7 +139,7 @@ export default class TreeItemEngine extends AriaRoleEngine { */ protected setFocusToLastItem(): void { const - items = >this.ctx.findAllFocusableElements(this.params.rootElement); + items = >this.ctx?.findAllFocusableElements(this.params.rootElement); let lastItem: CanUndef; @@ -200,7 +187,7 @@ export default class TreeItemEngine extends AriaRoleEngine { }; switch (e.key) { - case keyCodes.UP: + case KeyCodes.UP: if (isHorizontal) { close(); break; @@ -209,7 +196,7 @@ export default class TreeItemEngine extends AriaRoleEngine { this.moveFocus(-1); break; - case keyCodes.DOWN: + case KeyCodes.DOWN: if (isHorizontal) { open(); break; @@ -218,7 +205,7 @@ export default class TreeItemEngine extends AriaRoleEngine { this.moveFocus(1); break; - case keyCodes.RIGHT: + case KeyCodes.RIGHT: if (isHorizontal) { this.moveFocus(1); break; @@ -227,7 +214,7 @@ export default class TreeItemEngine extends AriaRoleEngine { open(); break; - case keyCodes.LEFT: + case KeyCodes.LEFT: if (isHorizontal) { this.moveFocus(-1); break; @@ -236,15 +223,15 @@ export default class TreeItemEngine extends AriaRoleEngine { close(); break; - case keyCodes.ENTER: + case KeyCodes.ENTER: this.params.toggleFold(this.el); break; - case keyCodes.HOME: + case KeyCodes.HOME: this.setFocusToFirstItem(); break; - case keyCodes.END: + case KeyCodes.END: this.setFocusToLastItem(); break; diff --git a/src/core/component/directives/aria/roles-engines/treeitem/interface.ts b/src/core/component/directives/aria/roles-engines/treeitem/interface.ts new file mode 100644 index 0000000000..38bfb24418 --- /dev/null +++ b/src/core/component/directives/aria/roles-engines/treeitem/interface.ts @@ -0,0 +1,16 @@ +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ + +export interface TreeitemParams { + isFirstRootItem: boolean; + isExpandable: boolean; + isExpanded: boolean; + orientation: string; + rootElement: CanUndef; + toggleFold(el: Element, value?: boolean): void; +} diff --git a/src/core/component/render-function/CHANGELOG.md b/src/core/component/render-function/CHANGELOG.md index 1c8c43aa5d..b80bbe6ed6 100644 --- a/src/core/component/render-function/CHANGELOG.md +++ b/src/core/component/render-function/CHANGELOG.md @@ -9,6 +9,12 @@ Changelog > - :house: [Internal] > - :nail_care: [Polish] +## v3.??.? (2022-??-??) + +#### :bug: Bug Fix + +* Fixed `v-attrs` regexp for parsing incoming modifiers + ## v3.12.1 (2021-11-26) #### :bug: Bug Fix diff --git a/src/form/b-select/b-select.ts b/src/form/b-select/b-select.ts index d48147cf25..0b39a564fb 100644 --- a/src/form/b-select/b-select.ts +++ b/src/form/b-select/b-select.ts @@ -954,18 +954,18 @@ class bSelect extends iInputText implements iOpenToggle, iItems { const comboboxConfig = { isMultiple: this.multiple, - changeEvent: (cb) => this.localEmitter.on(event, ({link}) => cb(link)), - closeEvent: (cb) => this.on('close', cb), - openEvent: (cb) => this.on('open', () => { + '@change': (cb) => this.localEmitter.on(event, ({link}) => cb(link)), + '@close': (cb) => this.on('close', cb), + '@open': (cb) => this.on('open', () => { void this.$nextTick(() => cb(this.selectedElement)); }) }; const optionConfig = { - get preSelected() { + get isSelected() { return isSelected(); }, - changeEvent: (cb) => this.on('actionChange', () => cb(isSelected())) + '@change': (cb) => this.on('actionChange', () => cb(isSelected())) }; switch (role) { diff --git a/src/traits/i-access/i-access.ts b/src/traits/i-access/i-access.ts index d49cadad51..37d03c86f9 100644 --- a/src/traits/i-access/i-access.ts +++ b/src/traits/i-access/i-access.ts @@ -211,10 +211,10 @@ export default abstract class iAccess { removedElems = intoIter(ctx.querySelectorAll('[data-tabindex]')); if (el?.hasAttribute('data-tabindex')) { - removedElems = sequence(removedElems, intoIter([el])); + removedElems = sequence(removedElems, intoIter([el])); } - for (const elem of >removedElems) { + for (const elem of removedElems) { const originalTabIndex = elem.getAttribute('data-tabindex'); @@ -261,7 +261,7 @@ export default abstract class iAccess { /** @see [[iAccess.findFocusableElement]] */ static findFocusableElement: AddSelf = - (component, el?): CanUndef => { + (component, el?) => { const ctx = el ?? component.$el, focusableElems = this.findAllFocusableElements(component, ctx); @@ -284,7 +284,7 @@ export default abstract class iAccess { focusableIter = intoIter(focusableElems ?? []); if (ctx?.matches(FOCUSABLE_SELECTOR)) { - focusableIter = sequence(focusableIter, intoIter([el])); + focusableIter = sequence(focusableIter, intoIter([el])); } function* createFocusableWithoutDisabled(iter: IterableIterator): IterableIterator { @@ -404,7 +404,7 @@ export default abstract class iAccess { * @param step * @param el - a context to search, if not set, document will be used */ - getNextFocusableElement(step: 1 | -1, el?: Element): CanUndef { + getNextFocusableElement(step: 1 | -1, el?: T): CanUndef { return Object.throw(); } @@ -412,7 +412,7 @@ export default abstract class iAccess { * Find focusable element except disabled ones * @param el - a context to search, if not set, component will be used */ - findFocusableElement(el?: Element): CanUndef { + findFocusableElement(el?: T): CanUndef { return Object.throw(); } @@ -420,7 +420,7 @@ export default abstract class iAccess { * Find all focusable elements except disabled ones. Search includes the specified element * @param el - a context to search, if not set, component will be used */ - findAllFocusableElements(el?: Element): IterableIterator> { + findAllFocusableElements(el?: T): IterableIterator> { return Object.throw(); } } From 93ff1db89b066cda18dd04c4fa007ec91e0ceff2 Mon Sep 17 00:00:00 2001 From: Aleksandr Bunin Date: Wed, 10 Aug 2022 11:25:02 +0300 Subject: [PATCH 094/185] refactoring tests folders --- .../aria/{ => roles-engines/combobox}/test/unit/combobox.ts | 0 .../aria/{ => roles-engines/controls}/test/unit/controls.ts | 0 .../aria/{ => roles-engines/dialog}/test/unit/dialog.ts | 0 .../aria/{ => roles-engines/listbox}/test/unit/listbox.ts | 0 .../aria/{ => roles-engines/option}/test/unit/option.ts | 0 .../directives/aria/{ => roles-engines/tab}/test/unit/tab.ts | 0 .../aria/{ => roles-engines/tablist}/test/unit/tablist.ts | 0 .../aria/{ => roles-engines/tabpanel}/test/unit/tabpanel.ts | 0 .../directives/aria/{ => roles-engines/tree}/test/unit/tree.ts | 0 .../aria/{ => roles-engines/treeitem}/test/unit/treeitem.ts | 0 10 files changed, 0 insertions(+), 0 deletions(-) rename src/core/component/directives/aria/{ => roles-engines/combobox}/test/unit/combobox.ts (100%) rename src/core/component/directives/aria/{ => roles-engines/controls}/test/unit/controls.ts (100%) rename src/core/component/directives/aria/{ => roles-engines/dialog}/test/unit/dialog.ts (100%) rename src/core/component/directives/aria/{ => roles-engines/listbox}/test/unit/listbox.ts (100%) rename src/core/component/directives/aria/{ => roles-engines/option}/test/unit/option.ts (100%) rename src/core/component/directives/aria/{ => roles-engines/tab}/test/unit/tab.ts (100%) rename src/core/component/directives/aria/{ => roles-engines/tablist}/test/unit/tablist.ts (100%) rename src/core/component/directives/aria/{ => roles-engines/tabpanel}/test/unit/tabpanel.ts (100%) rename src/core/component/directives/aria/{ => roles-engines/tree}/test/unit/tree.ts (100%) rename src/core/component/directives/aria/{ => roles-engines/treeitem}/test/unit/treeitem.ts (100%) diff --git a/src/core/component/directives/aria/test/unit/combobox.ts b/src/core/component/directives/aria/roles-engines/combobox/test/unit/combobox.ts similarity index 100% rename from src/core/component/directives/aria/test/unit/combobox.ts rename to src/core/component/directives/aria/roles-engines/combobox/test/unit/combobox.ts diff --git a/src/core/component/directives/aria/test/unit/controls.ts b/src/core/component/directives/aria/roles-engines/controls/test/unit/controls.ts similarity index 100% rename from src/core/component/directives/aria/test/unit/controls.ts rename to src/core/component/directives/aria/roles-engines/controls/test/unit/controls.ts diff --git a/src/core/component/directives/aria/test/unit/dialog.ts b/src/core/component/directives/aria/roles-engines/dialog/test/unit/dialog.ts similarity index 100% rename from src/core/component/directives/aria/test/unit/dialog.ts rename to src/core/component/directives/aria/roles-engines/dialog/test/unit/dialog.ts diff --git a/src/core/component/directives/aria/test/unit/listbox.ts b/src/core/component/directives/aria/roles-engines/listbox/test/unit/listbox.ts similarity index 100% rename from src/core/component/directives/aria/test/unit/listbox.ts rename to src/core/component/directives/aria/roles-engines/listbox/test/unit/listbox.ts diff --git a/src/core/component/directives/aria/test/unit/option.ts b/src/core/component/directives/aria/roles-engines/option/test/unit/option.ts similarity index 100% rename from src/core/component/directives/aria/test/unit/option.ts rename to src/core/component/directives/aria/roles-engines/option/test/unit/option.ts diff --git a/src/core/component/directives/aria/test/unit/tab.ts b/src/core/component/directives/aria/roles-engines/tab/test/unit/tab.ts similarity index 100% rename from src/core/component/directives/aria/test/unit/tab.ts rename to src/core/component/directives/aria/roles-engines/tab/test/unit/tab.ts diff --git a/src/core/component/directives/aria/test/unit/tablist.ts b/src/core/component/directives/aria/roles-engines/tablist/test/unit/tablist.ts similarity index 100% rename from src/core/component/directives/aria/test/unit/tablist.ts rename to src/core/component/directives/aria/roles-engines/tablist/test/unit/tablist.ts diff --git a/src/core/component/directives/aria/test/unit/tabpanel.ts b/src/core/component/directives/aria/roles-engines/tabpanel/test/unit/tabpanel.ts similarity index 100% rename from src/core/component/directives/aria/test/unit/tabpanel.ts rename to src/core/component/directives/aria/roles-engines/tabpanel/test/unit/tabpanel.ts diff --git a/src/core/component/directives/aria/test/unit/tree.ts b/src/core/component/directives/aria/roles-engines/tree/test/unit/tree.ts similarity index 100% rename from src/core/component/directives/aria/test/unit/tree.ts rename to src/core/component/directives/aria/roles-engines/tree/test/unit/tree.ts diff --git a/src/core/component/directives/aria/test/unit/treeitem.ts b/src/core/component/directives/aria/roles-engines/treeitem/test/unit/treeitem.ts similarity index 100% rename from src/core/component/directives/aria/test/unit/treeitem.ts rename to src/core/component/directives/aria/roles-engines/treeitem/test/unit/treeitem.ts From 4807eefdca047534547edf4520171c580981a329 Mon Sep 17 00:00:00 2001 From: Aleksandr Bunin Date: Wed, 10 Aug 2022 13:08:30 +0300 Subject: [PATCH 095/185] refactor --- .../directives/aria/roles-engines/combobox/interface.ts | 8 ++++---- .../component/directives/aria/roles-engines/interface.ts | 2 +- .../directives/aria/roles-engines/option/interface.ts | 4 ++-- .../directives/aria/roles-engines/tab/interface.ts | 4 ++-- .../directives/aria/roles-engines/tree/interface.ts | 4 ++-- 5 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/core/component/directives/aria/roles-engines/combobox/interface.ts b/src/core/component/directives/aria/roles-engines/combobox/interface.ts index 7675f2fbc4..b34ccb2270 100644 --- a/src/core/component/directives/aria/roles-engines/combobox/interface.ts +++ b/src/core/component/directives/aria/roles-engines/combobox/interface.ts @@ -6,11 +6,11 @@ * https://github.com/V4Fire/Client/blob/master/LICENSE */ -import type { EventBinder } from 'core/component/directives/aria/roles-engines/interface'; +import type { HandlerAttachment } from 'core/component/directives/aria/roles-engines/interface'; export interface ComboboxParams { isMultiple: boolean; - '@change': EventBinder; - '@open': EventBinder; - '@close': EventBinder; + '@change': HandlerAttachment; + '@open': HandlerAttachment; + '@close': HandlerAttachment; } diff --git a/src/core/component/directives/aria/roles-engines/interface.ts b/src/core/component/directives/aria/roles-engines/interface.ts index 06a5b3ac87..9a987a3689 100644 --- a/src/core/component/directives/aria/roles-engines/interface.ts +++ b/src/core/component/directives/aria/roles-engines/interface.ts @@ -47,7 +47,7 @@ export interface EngineOptions { ctx: iBlock & iAccess; } -export type EventBinder = (cb: Function) => void; +export type HandlerAttachment = (cb: Function) => void; export const enum KeyCodes { ENTER = 'Enter', diff --git a/src/core/component/directives/aria/roles-engines/option/interface.ts b/src/core/component/directives/aria/roles-engines/option/interface.ts index 6c3774f6e4..4e90c740ae 100644 --- a/src/core/component/directives/aria/roles-engines/option/interface.ts +++ b/src/core/component/directives/aria/roles-engines/option/interface.ts @@ -6,9 +6,9 @@ * https://github.com/V4Fire/Client/blob/master/LICENSE */ -import type { EventBinder } from 'core/component/directives/aria/roles-engines/interface'; +import type { HandlerAttachment } from 'core/component/directives/aria/roles-engines/interface'; export interface OptionParams { isSelected: boolean; - '@change': EventBinder; + '@change': HandlerAttachment; } diff --git a/src/core/component/directives/aria/roles-engines/tab/interface.ts b/src/core/component/directives/aria/roles-engines/tab/interface.ts index c75904fcd1..e42c5bdc11 100644 --- a/src/core/component/directives/aria/roles-engines/tab/interface.ts +++ b/src/core/component/directives/aria/roles-engines/tab/interface.ts @@ -6,12 +6,12 @@ * https://github.com/V4Fire/Client/blob/master/LICENSE */ -import type { EventBinder } from 'core/component/directives/aria/roles-engines/interface'; +import type { HandlerAttachment } from 'core/component/directives/aria/roles-engines/interface'; export interface TabParams { isFirst: boolean; isSelected: boolean; hasDefaultSelectedTabs: boolean; orientation: string; - '@change': EventBinder; + '@change': HandlerAttachment; } diff --git a/src/core/component/directives/aria/roles-engines/tree/interface.ts b/src/core/component/directives/aria/roles-engines/tree/interface.ts index 150df682b1..44b0e9cf3f 100644 --- a/src/core/component/directives/aria/roles-engines/tree/interface.ts +++ b/src/core/component/directives/aria/roles-engines/tree/interface.ts @@ -6,10 +6,10 @@ * https://github.com/V4Fire/Client/blob/master/LICENSE */ -import type { EventBinder } from 'core/component/directives/aria/roles-engines/interface'; +import type { HandlerAttachment } from 'core/component/directives/aria/roles-engines/interface'; export interface TreeParams { isRoot: boolean; orientation: string; - '@change': EventBinder; + '@change': HandlerAttachment; } From 8de104ff1e3b5dfdbf335fd28f55cc0122c3dfab Mon Sep 17 00:00:00 2001 From: Aleksandr Bunin Date: Fri, 12 Aug 2022 12:49:43 +0300 Subject: [PATCH 096/185] refactoring --- .../component/directives/aria/aria-setter.ts | 34 +++--- .../aria/roles-engines/combobox/index.ts | 10 +- .../aria/roles-engines/combobox/interface.ts | 4 +- .../combobox/test/unit/combobox.ts | 3 +- .../aria/roles-engines/controls/index.ts | 4 +- .../controls/test/unit/controls.ts | 3 +- .../roles-engines/dialog/test/unit/dialog.ts | 3 +- .../directives/aria/roles-engines/index.ts | 2 +- .../aria/roles-engines/interface.ts | 43 +++++--- .../listbox/test/unit/listbox.ts | 3 +- .../aria/roles-engines/option/index.ts | 4 +- .../roles-engines/option/test/unit/option.ts | 3 +- .../aria/roles-engines/tab/index.ts | 12 +- .../aria/roles-engines/tab/test/unit/tab.ts | 3 +- .../aria/roles-engines/tablist/index.ts | 4 +- .../tablist/test/unit/tablist.ts | 3 +- .../tabpanel/test/unit/tabpanel.ts | 3 +- .../aria/roles-engines/tree/index.ts | 4 +- .../aria/roles-engines/tree/test/unit/tree.ts | 3 +- .../aria/roles-engines/treeitem/README.md | 10 ++ .../aria/roles-engines/treeitem/index.ts | 10 +- .../treeitem/test/unit/treeitem.ts | 103 ++++++++++-------- .../directives/aria/test/unit/simple.ts | 3 +- src/traits/i-access/i-access.ts | 6 +- 24 files changed, 164 insertions(+), 116 deletions(-) diff --git a/src/core/component/directives/aria/aria-setter.ts b/src/core/component/directives/aria/aria-setter.ts index 05d62f9549..a88f7a9bfe 100644 --- a/src/core/component/directives/aria/aria-setter.ts +++ b/src/core/component/directives/aria/aria-setter.ts @@ -6,14 +6,12 @@ * https://github.com/V4Fire/Client/blob/master/LICENSE */ -import * as ariaRoles from 'core/component/directives/aria/roles-engines'; import Async from 'core/async'; - import type iBlock from 'super/i-block/i-block'; -import type iAccess from 'traits/i-access/i-access'; +import * as ariaRoles from 'core/component/directives/aria/roles-engines'; import type { DirectiveOptions } from 'core/component/directives/aria/interface'; -import { AriaRoleEngine, EngineOptions, EventNames } from 'core/component/directives/aria/roles-engines'; +import type { AriaRoleEngine, EngineOptions } from 'core/component/directives/aria/roles-engines'; /** * Class-helper for making base operations for the directive @@ -39,10 +37,6 @@ export default class AriaSetter { this.async = new Async(); this.setAriaRole(); - if (this.role != null) { - this.role.async = this.async; - } - this.init(); } @@ -57,7 +51,7 @@ export default class AriaSetter { } /** - * Runs on update directive hook. Removes listeners from component if the component is Functional + * Runs on update directive hook. Removes listeners from component if the component is Functional. */ update(): void { const @@ -69,7 +63,7 @@ export default class AriaSetter { } /** - * Runs on unbind directive hook. Clears the Async instance + * Runs on unbind directive hook. Clears the Async instance. */ destroy(): void { this.async.clearAll(); @@ -104,7 +98,7 @@ export default class AriaSetter { /** * Creates a dictionary with engine options */ - protected createRoleOptions(): EngineOptions { + protected createRoleOptions(): EngineOptions { const {el, binding, vnode} = this.options, {value, modifiers} = binding; @@ -113,7 +107,8 @@ export default class AriaSetter { el, modifiers, params: value, - ctx: Object.cast(vnode.fakeContext) + ctx: Object.cast(vnode.fakeContext), + async: this.async }; } @@ -156,7 +151,7 @@ export default class AriaSetter { /** * Sets handlers for the base role events: open, close, change. - * Expects the passed into directive specified event properties to be Function, Promise or String + * Expects the passed into directive specified event properties to be Function, Promise or String. */ protected addEventHandlers(): void { if (this.role == null) { @@ -166,11 +161,20 @@ export default class AriaSetter { const params = this.options.binding.value; + const + getCallbackName = (key: string) => `on-${key.slice(1)}`.camelize(false); + for (const key in params) { - if (key in EventNames) { + if (key.startsWith('@')) { + const + callbackName = getCallbackName(key); + + if (!Object.isFunction(this.role[callbackName])) { + Object.throw('Aria role engine does not contains event handler for passed event name or the type of engine\'s property is not a function'); + } const - callback = this.role[EventNames[key]].bind(this.role), + callback = this.role[callbackName].bind(this.role), property = params[key]; if (Object.isFunction(property)) { diff --git a/src/core/component/directives/aria/roles-engines/combobox/index.ts b/src/core/component/directives/aria/roles-engines/combobox/index.ts index ad703aa2f3..456714dfca 100644 --- a/src/core/component/directives/aria/roles-engines/combobox/index.ts +++ b/src/core/component/directives/aria/roles-engines/combobox/index.ts @@ -6,6 +6,9 @@ * https://github.com/V4Fire/Client/blob/master/LICENSE */ +import type iAccess from 'traits/i-access/i-access'; +import type { ComponentInterface } from 'super/i-block/i-block'; + import type { ComboboxParams } from 'core/component/directives/aria/roles-engines/combobox/interface'; import { AriaRoleEngine, EngineOptions } from 'core/component/directives/aria/roles-engines/interface'; @@ -13,14 +16,17 @@ export class ComboboxEngine extends AriaRoleEngine { /** * Engine params */ - params: ComboboxParams; + override params: ComboboxParams; /** * First focusable element inside the element with directive or this element if there is no focusable inside */ override el: HTMLElement; - constructor(options: EngineOptions) { + /** @see [[AriaRoleEngine.Ctx]] */ + override Ctx!: ComponentInterface & iAccess; + + constructor(options: EngineOptions) { super(options); const diff --git a/src/core/component/directives/aria/roles-engines/combobox/interface.ts b/src/core/component/directives/aria/roles-engines/combobox/interface.ts index b34ccb2270..a319311e58 100644 --- a/src/core/component/directives/aria/roles-engines/combobox/interface.ts +++ b/src/core/component/directives/aria/roles-engines/combobox/interface.ts @@ -6,9 +6,9 @@ * https://github.com/V4Fire/Client/blob/master/LICENSE */ -import type { HandlerAttachment } from 'core/component/directives/aria/roles-engines/interface'; +import type { AbstractParams, HandlerAttachment } from 'core/component/directives/aria/roles-engines/interface'; -export interface ComboboxParams { +export interface ComboboxParams extends AbstractParams { isMultiple: boolean; '@change': HandlerAttachment; '@open': HandlerAttachment; diff --git a/src/core/component/directives/aria/roles-engines/combobox/test/unit/combobox.ts b/src/core/component/directives/aria/roles-engines/combobox/test/unit/combobox.ts index 8b99af6f76..1e92ffca7c 100644 --- a/src/core/component/directives/aria/roles-engines/combobox/test/unit/combobox.ts +++ b/src/core/component/directives/aria/roles-engines/combobox/test/unit/combobox.ts @@ -1,5 +1,3 @@ -// @ts-check - /*! * V4Fire Client Core * https://github.com/V4Fire/Client @@ -7,6 +5,7 @@ * Released under the MIT license * https://github.com/V4Fire/Client/blob/master/LICENSE */ + import type { JSHandle, Page } from 'playwright'; import type iBlock from 'super/i-block/i-block'; diff --git a/src/core/component/directives/aria/roles-engines/controls/index.ts b/src/core/component/directives/aria/roles-engines/controls/index.ts index f106754ddd..7fed253895 100644 --- a/src/core/component/directives/aria/roles-engines/controls/index.ts +++ b/src/core/component/directives/aria/roles-engines/controls/index.ts @@ -13,9 +13,9 @@ export class ControlsEngine extends AriaRoleEngine { /** * Engine params */ - params: ControlsParams; + override params: ControlsParams; - constructor(options: EngineOptions) { + constructor(options: EngineOptions) { super(options); this.params = options.params; diff --git a/src/core/component/directives/aria/roles-engines/controls/test/unit/controls.ts b/src/core/component/directives/aria/roles-engines/controls/test/unit/controls.ts index 5d014fe155..b43c742542 100644 --- a/src/core/component/directives/aria/roles-engines/controls/test/unit/controls.ts +++ b/src/core/component/directives/aria/roles-engines/controls/test/unit/controls.ts @@ -1,5 +1,3 @@ -// @ts-check - /*! * V4Fire Client Core * https://github.com/V4Fire/Client @@ -7,6 +5,7 @@ * Released under the MIT license * https://github.com/V4Fire/Client/blob/master/LICENSE */ + import type { JSHandle, Page } from 'playwright'; import type iBlock from 'super/i-block/i-block'; diff --git a/src/core/component/directives/aria/roles-engines/dialog/test/unit/dialog.ts b/src/core/component/directives/aria/roles-engines/dialog/test/unit/dialog.ts index f1f2c031f1..5031e0debe 100644 --- a/src/core/component/directives/aria/roles-engines/dialog/test/unit/dialog.ts +++ b/src/core/component/directives/aria/roles-engines/dialog/test/unit/dialog.ts @@ -1,5 +1,3 @@ -// @ts-check - /*! * V4Fire Client Core * https://github.com/V4Fire/Client @@ -7,6 +5,7 @@ * Released under the MIT license * https://github.com/V4Fire/Client/blob/master/LICENSE */ + import type { JSHandle, Page } from 'playwright'; import type iBlock from 'super/i-block/i-block'; diff --git a/src/core/component/directives/aria/roles-engines/index.ts b/src/core/component/directives/aria/roles-engines/index.ts index 9fdac42030..cbb9f1bbce 100644 --- a/src/core/component/directives/aria/roles-engines/index.ts +++ b/src/core/component/directives/aria/roles-engines/index.ts @@ -17,4 +17,4 @@ export * from 'core/component/directives/aria/roles-engines/option'; export * from 'core/component/directives/aria/roles-engines/tree'; export * from 'core/component/directives/aria/roles-engines/treeitem'; -export { AriaRoleEngine, EngineOptions, EventNames } from 'core/component/directives/aria/roles-engines/interface'; +export { AriaRoleEngine, EngineOptions } from 'core/component/directives/aria/roles-engines/interface'; diff --git a/src/core/component/directives/aria/roles-engines/interface.ts b/src/core/component/directives/aria/roles-engines/interface.ts index 9a987a3689..26f1e50576 100644 --- a/src/core/component/directives/aria/roles-engines/interface.ts +++ b/src/core/component/directives/aria/roles-engines/interface.ts @@ -7,10 +7,19 @@ */ import type Async from 'core/async'; -import type iAccess from 'traits/i-access/i-access'; -import type iBlock from 'super/i-block/i-block'; +import type { ComponentInterface } from 'super/i-block/i-block'; export abstract class AriaRoleEngine { + /** + * Type: directive passed params + */ + readonly Params!: AbstractParams; + + /** + * Type: component on which the directive is set + */ + readonly Ctx!: ComponentInterface; + /** * Element on which the directive is set */ @@ -19,32 +28,42 @@ export abstract class AriaRoleEngine { /** * Component on which the directive is set */ - readonly ctx: CanUndef; + readonly ctx?: this['Ctx']; /** * Directive passed modifiers */ - readonly modifiers: CanUndef>; + readonly modifiers?: Dictionary; + + /** + * Directive passed params + */ + readonly params: this['Params']; /** * Async instance */ async: CanUndef; - constructor({el, ctx, modifiers}: EngineOptions) { + constructor({el, ctx, modifiers, params, async}: EngineOptions) { this.el = el; this.ctx = ctx; this.modifiers = modifiers; + this.params = params; + this.async = async; } abstract init(): void; } -export interface EngineOptions { +export interface AbstractParams {} + +export interface EngineOptions

    { el: HTMLElement; - modifiers: CanUndef>; - params: DictionaryType; - ctx: iBlock & iAccess; + ctx?: C; + modifiers?: Dictionary; + params: P; + async: Async; } export type HandlerAttachment = (cb: Function) => void; @@ -58,9 +77,3 @@ export const enum KeyCodes { RIGHT = 'ArrowRight', DOWN = 'ArrowDown' } - -export enum EventNames { - '@open' = 'onOpen', - '@close' = 'onClose', - '@change' = 'onChange' -} diff --git a/src/core/component/directives/aria/roles-engines/listbox/test/unit/listbox.ts b/src/core/component/directives/aria/roles-engines/listbox/test/unit/listbox.ts index ff654c727b..d4fd92d3cb 100644 --- a/src/core/component/directives/aria/roles-engines/listbox/test/unit/listbox.ts +++ b/src/core/component/directives/aria/roles-engines/listbox/test/unit/listbox.ts @@ -1,5 +1,3 @@ -// @ts-check - /*! * V4Fire Client Core * https://github.com/V4Fire/Client @@ -7,6 +5,7 @@ * Released under the MIT license * https://github.com/V4Fire/Client/blob/master/LICENSE */ + import type { JSHandle, Page } from 'playwright'; import type iBlock from 'super/i-block/i-block'; diff --git a/src/core/component/directives/aria/roles-engines/option/index.ts b/src/core/component/directives/aria/roles-engines/option/index.ts index 566d2850ab..49322476a5 100644 --- a/src/core/component/directives/aria/roles-engines/option/index.ts +++ b/src/core/component/directives/aria/roles-engines/option/index.ts @@ -13,9 +13,9 @@ export class OptionEngine extends AriaRoleEngine { /** * Engine params */ - params: OptionParams; + override params: OptionParams; - constructor(options: EngineOptions) { + constructor(options: EngineOptions) { super(options); this.params = options.params; diff --git a/src/core/component/directives/aria/roles-engines/option/test/unit/option.ts b/src/core/component/directives/aria/roles-engines/option/test/unit/option.ts index 0b24a690fb..f4b912d6d0 100644 --- a/src/core/component/directives/aria/roles-engines/option/test/unit/option.ts +++ b/src/core/component/directives/aria/roles-engines/option/test/unit/option.ts @@ -1,5 +1,3 @@ -// @ts-check - /*! * V4Fire Client Core * https://github.com/V4Fire/Client @@ -7,6 +5,7 @@ * Released under the MIT license * https://github.com/V4Fire/Client/blob/master/LICENSE */ + import type { JSHandle, Page } from 'playwright'; import type iBlock from 'super/i-block/i-block'; diff --git a/src/core/component/directives/aria/roles-engines/tab/index.ts b/src/core/component/directives/aria/roles-engines/tab/index.ts index 71c58da143..43429b08cb 100644 --- a/src/core/component/directives/aria/roles-engines/tab/index.ts +++ b/src/core/component/directives/aria/roles-engines/tab/index.ts @@ -9,6 +9,9 @@ * Copyright © [2022] W3C® (MIT, ERCIM, Keio, Beihang). */ +import type iBlock from 'super/i-block/i-block'; +import type iAccess from 'traits/i-access/i-access'; + import type { TabParams } from 'core/component/directives/aria/roles-engines/tab/interface'; import { AriaRoleEngine, EngineOptions, KeyCodes } from 'core/component/directives/aria/roles-engines/interface'; @@ -16,9 +19,12 @@ export class TabEngine extends AriaRoleEngine { /** * Engine params */ - params: TabParams; + override params: TabParams; + + /** @see [[AriaRoleEngine.ctx]] */ + override ctx?: iBlock & iAccess; - constructor(options: EngineOptions) { + constructor(options: EngineOptions) { super(options); this.params = options.params; @@ -91,7 +97,7 @@ export class TabEngine extends AriaRoleEngine { */ protected moveFocus(step: 1 | -1): void { const - focusable = this.ctx?.getNextFocusableElement(step); + focusable = this.ctx?.getNextFocusableElement(step); focusable?.focus(); } diff --git a/src/core/component/directives/aria/roles-engines/tab/test/unit/tab.ts b/src/core/component/directives/aria/roles-engines/tab/test/unit/tab.ts index f2f1d8a31a..ee1725eb04 100644 --- a/src/core/component/directives/aria/roles-engines/tab/test/unit/tab.ts +++ b/src/core/component/directives/aria/roles-engines/tab/test/unit/tab.ts @@ -1,5 +1,3 @@ -// @ts-check - /*! * V4Fire Client Core * https://github.com/V4Fire/Client @@ -7,6 +5,7 @@ * Released under the MIT license * https://github.com/V4Fire/Client/blob/master/LICENSE */ + import type { JSHandle, Page } from 'playwright'; import type iBlock from 'super/i-block/i-block'; diff --git a/src/core/component/directives/aria/roles-engines/tablist/index.ts b/src/core/component/directives/aria/roles-engines/tablist/index.ts index 74ba0e4970..83451227bd 100644 --- a/src/core/component/directives/aria/roles-engines/tablist/index.ts +++ b/src/core/component/directives/aria/roles-engines/tablist/index.ts @@ -13,9 +13,9 @@ export class TablistEngine extends AriaRoleEngine { /** * Engine params */ - params: TablistParams; + override params: TablistParams; - constructor(options: EngineOptions) { + constructor(options: EngineOptions) { super(options); this.params = options.params; diff --git a/src/core/component/directives/aria/roles-engines/tablist/test/unit/tablist.ts b/src/core/component/directives/aria/roles-engines/tablist/test/unit/tablist.ts index e688c633bc..fd4872facb 100644 --- a/src/core/component/directives/aria/roles-engines/tablist/test/unit/tablist.ts +++ b/src/core/component/directives/aria/roles-engines/tablist/test/unit/tablist.ts @@ -1,5 +1,3 @@ -// @ts-check - /*! * V4Fire Client Core * https://github.com/V4Fire/Client @@ -7,6 +5,7 @@ * Released under the MIT license * https://github.com/V4Fire/Client/blob/master/LICENSE */ + import type { JSHandle, Page } from 'playwright'; import type iBlock from 'super/i-block/i-block'; diff --git a/src/core/component/directives/aria/roles-engines/tabpanel/test/unit/tabpanel.ts b/src/core/component/directives/aria/roles-engines/tabpanel/test/unit/tabpanel.ts index f99e3050d1..72e2422067 100644 --- a/src/core/component/directives/aria/roles-engines/tabpanel/test/unit/tabpanel.ts +++ b/src/core/component/directives/aria/roles-engines/tabpanel/test/unit/tabpanel.ts @@ -1,5 +1,3 @@ -// @ts-check - /*! * V4Fire Client Core * https://github.com/V4Fire/Client @@ -7,6 +5,7 @@ * Released under the MIT license * https://github.com/V4Fire/Client/blob/master/LICENSE */ + import type { JSHandle, Page } from 'playwright'; import type iBlock from 'super/i-block/i-block'; diff --git a/src/core/component/directives/aria/roles-engines/tree/index.ts b/src/core/component/directives/aria/roles-engines/tree/index.ts index 4d94d71be4..483901f2f7 100644 --- a/src/core/component/directives/aria/roles-engines/tree/index.ts +++ b/src/core/component/directives/aria/roles-engines/tree/index.ts @@ -13,9 +13,9 @@ export class TreeEngine extends AriaRoleEngine { /** * Engine params */ - params: TreeParams; + override params: TreeParams; - constructor(options: EngineOptions) { + constructor(options: EngineOptions) { super(options); this.params = options.params; diff --git a/src/core/component/directives/aria/roles-engines/tree/test/unit/tree.ts b/src/core/component/directives/aria/roles-engines/tree/test/unit/tree.ts index 5f76193047..e040da54ed 100644 --- a/src/core/component/directives/aria/roles-engines/tree/test/unit/tree.ts +++ b/src/core/component/directives/aria/roles-engines/tree/test/unit/tree.ts @@ -1,5 +1,3 @@ -// @ts-check - /*! * V4Fire Client Core * https://github.com/V4Fire/Client @@ -7,6 +5,7 @@ * Released under the MIT license * https://github.com/V4Fire/Client/blob/master/LICENSE */ + import type { JSHandle, Page } from 'playwright'; import type iBlock from 'super/i-block/i-block'; diff --git a/src/core/component/directives/aria/roles-engines/treeitem/README.md b/src/core/component/directives/aria/roles-engines/treeitem/README.md index 7e59a5b0bd..3c262ae318 100644 --- a/src/core/component/directives/aria/roles-engines/treeitem/README.md +++ b/src/core/component/directives/aria/roles-engines/treeitem/README.md @@ -15,3 +15,13 @@ Expects `iAccess` trait to be realized. < &__foo v-aria:treeitem = {...} ``` + +## Adding new role engines +When creating a new role engine which handles some components events the contract of passed params types and naming should be respected. + +The name of handlers in engine should be like `onChange`, `onOpen`, etc. +The name of property in passed params should be like `@change`, `@open`, etc. +Types of the property on passed params could be: +- `Function` that accepts callback parameter; +- `Promise`, so the handler will be passed in `.then` method; +- `String` that is the name of component's event, so the handler will be added as a listener to this event. diff --git a/src/core/component/directives/aria/roles-engines/treeitem/index.ts b/src/core/component/directives/aria/roles-engines/treeitem/index.ts index f2de0c1c60..578b2a897a 100644 --- a/src/core/component/directives/aria/roles-engines/treeitem/index.ts +++ b/src/core/component/directives/aria/roles-engines/treeitem/index.ts @@ -10,6 +10,7 @@ */ import iAccess from 'traits/i-access/i-access'; +import type iBlock from 'super/i-block/i-block'; import type { TreeitemParams } from 'core/component/directives/aria/roles-engines/treeitem/interface'; import { AriaRoleEngine, KeyCodes, EngineOptions } from 'core/component/directives/aria/roles-engines/interface'; @@ -18,9 +19,12 @@ export class TreeitemEngine extends AriaRoleEngine { /** * Engine params */ - params: TreeitemParams; + override params: TreeitemParams; - constructor(options: EngineOptions) { + /** @see [[AriaRoleEngine.ctx]] */ + override ctx?: iBlock & iAccess; + + constructor(options: EngineOptions) { super(options); if (!iAccess.is(this.ctx)) { @@ -74,7 +78,7 @@ export class TreeitemEngine extends AriaRoleEngine { */ protected moveFocus(step: 1 | -1): void { const - nextEl = this.ctx?.getNextFocusableElement(step); + nextEl = this.ctx?.getNextFocusableElement(step); if (nextEl != null) { this.focusNext(nextEl); diff --git a/src/core/component/directives/aria/roles-engines/treeitem/test/unit/treeitem.ts b/src/core/component/directives/aria/roles-engines/treeitem/test/unit/treeitem.ts index 58b3b56b50..00bde5251e 100644 --- a/src/core/component/directives/aria/roles-engines/treeitem/test/unit/treeitem.ts +++ b/src/core/component/directives/aria/roles-engines/treeitem/test/unit/treeitem.ts @@ -1,5 +1,3 @@ -// @ts-check - /*! * V4Fire Client Core * https://github.com/V4Fire/Client @@ -7,6 +5,7 @@ * Released under the MIT license * https://github.com/V4Fire/Client/blob/master/LICENSE */ + import type { JSHandle, Page } from 'playwright'; import type iBlock from 'super/i-block/i-block'; @@ -62,40 +61,48 @@ test.describe('v-aria:treeitem', () => { items = ctx.unsafe.block.elements('node'), labels = document.querySelectorAll('label'); - const res: any[] = []; + const + res: Array> = []; + + const + eq = (index: number) => document.activeElement?.id === labels[index].getAttribute('for'), + att = (): Nullable => items[1].getAttribute('aria-expanded'), + dis = (key: string) => document.activeElement?.dispatchEvent( + new KeyboardEvent('keydown', {key, bubbles: true}) + ); input?.focus(); input?.dispatchEvent(new KeyboardEvent('keydown', {key: 'ArrowDown', bubbles: true})); - res.push(document.activeElement?.id === labels[1].getAttribute('for')); + res.push(eq(1)); - document.activeElement?.dispatchEvent(new KeyboardEvent('keydown', {key: 'ArrowUp', bubbles: true})); - res.push(document.activeElement?.id === labels[0].getAttribute('for')); + dis('ArrowUp'); + res.push(eq(0)); - document.activeElement?.dispatchEvent(new KeyboardEvent('keydown', {key: 'ArrowDown', bubbles: true})); - document.activeElement?.dispatchEvent(new KeyboardEvent('keydown', {key: 'Enter', bubbles: true})); - res.push(items[1].getAttribute('aria-expanded')); + dis('ArrowDown'); + dis('Enter'); + res.push(att()); - document.activeElement?.dispatchEvent(new KeyboardEvent('keydown', {key: 'Enter', bubbles: true})); - res.push(items[1].getAttribute('aria-expanded')); + dis('Enter'); + res.push(att()); - document.activeElement?.dispatchEvent(new KeyboardEvent('keydown', {key: 'ArrowRight', bubbles: true})); - res.push(items[1].getAttribute('aria-expanded')); + dis('ArrowRight'); + res.push(att()); - document.activeElement?.dispatchEvent(new KeyboardEvent('keydown', {key: 'ArrowRight', bubbles: true})); - res.push(document.activeElement?.id === labels[2].getAttribute('for')); + dis('ArrowRight'); + res.push(eq(2)); - document.activeElement?.dispatchEvent(new KeyboardEvent('keydown', {key: 'ArrowLeft', bubbles: true})); - res.push(document.activeElement?.id === labels[1].getAttribute('for')); + dis('ArrowLeft'); + res.push(eq(1)); - document.activeElement?.dispatchEvent(new KeyboardEvent('keydown', {key: 'ArrowLeft', bubbles: true})); - res.push(items[1].getAttribute('aria-expanded')); + dis('ArrowLeft'); + res.push(att()); - document.activeElement?.dispatchEvent(new KeyboardEvent('keydown', {key: 'Home', bubbles: true})); - res.push(document.activeElement?.id === labels[0].getAttribute('for')); + dis('Home'); + res.push(eq(0)); - document.activeElement?.dispatchEvent(new KeyboardEvent('keydown', {key: 'End', bubbles: true})); - res.push(document.activeElement?.id === labels[3].getAttribute('for')); + dis('End'); + res.push(eq(3)); return res; }) @@ -118,40 +125,48 @@ test.describe('v-aria:treeitem', () => { items = ctx.unsafe.block.elements('node'), labels = document.querySelectorAll('label'); - const res: Array> = []; + const + res: Array> = []; + + const + eq = (index: number) => document.activeElement?.id === labels[index].getAttribute('for'), + att = (): Nullable => items[1].getAttribute('aria-expanded'), + dis = (key: string) => document.activeElement?.dispatchEvent( + new KeyboardEvent('keydown', {key, bubbles: true}) + ); input?.focus(); input?.dispatchEvent(new KeyboardEvent('keydown', {key: 'ArrowRight', bubbles: true})); - res.push(document.activeElement?.id === labels[1].getAttribute('for')); + res.push(eq(1)); - document.activeElement?.dispatchEvent(new KeyboardEvent('keydown', {key: 'ArrowLeft', bubbles: true})); - res.push(document.activeElement?.id === labels[0].getAttribute('for')); + dis('ArrowLeft'); + res.push(eq(0)); - document.activeElement?.dispatchEvent(new KeyboardEvent('keydown', {key: 'ArrowRight', bubbles: true})); - document.activeElement?.dispatchEvent(new KeyboardEvent('keydown', {key: 'Enter', bubbles: true})); - res.push(items[1].getAttribute('aria-expanded')); + dis('ArrowRight'); + dis('Enter'); + res.push(att()); - document.activeElement?.dispatchEvent(new KeyboardEvent('keydown', {key: 'Enter', bubbles: true})); - res.push(items[1].getAttribute('aria-expanded')); + dis('Enter'); + res.push(att()); - document.activeElement?.dispatchEvent(new KeyboardEvent('keydown', {key: 'ArrowDown', bubbles: true})); - res.push(items[1].getAttribute('aria-expanded')); + dis('ArrowDown'); + res.push(att()); - document.activeElement?.dispatchEvent(new KeyboardEvent('keydown', {key: 'ArrowDown', bubbles: true})); - res.push(document.activeElement?.id === labels[2].getAttribute('for')); + dis('ArrowDown'); + res.push(eq(2)); - document.activeElement?.dispatchEvent(new KeyboardEvent('keydown', {key: 'ArrowUp', bubbles: true})); - res.push(document.activeElement?.id === labels[1].getAttribute('for')); + dis('ArrowUp'); + res.push(eq(1)); - document.activeElement?.dispatchEvent(new KeyboardEvent('keydown', {key: 'ArrowUp', bubbles: true})); - res.push(items[1].getAttribute('aria-expanded')); + dis('ArrowUp'); + res.push(att()); - document.activeElement?.dispatchEvent(new KeyboardEvent('keydown', {key: 'Home', bubbles: true})); - res.push(document.activeElement?.id === labels[0].getAttribute('for')); + dis('Home'); + res.push(eq(0)); - document.activeElement?.dispatchEvent(new KeyboardEvent('keydown', {key: 'End', bubbles: true})); - res.push(document.activeElement?.id === labels[3].getAttribute('for')); + dis('End'); + res.push(eq(3)); return res; }) diff --git a/src/core/component/directives/aria/test/unit/simple.ts b/src/core/component/directives/aria/test/unit/simple.ts index 649d116a97..ff9cab8333 100644 --- a/src/core/component/directives/aria/test/unit/simple.ts +++ b/src/core/component/directives/aria/test/unit/simple.ts @@ -1,5 +1,3 @@ -// @ts-check - /*! * V4Fire Client Core * https://github.com/V4Fire/Client @@ -7,6 +5,7 @@ * Released under the MIT license * https://github.com/V4Fire/Client/blob/master/LICENSE */ + import type { JSHandle, Page } from 'playwright'; import type iBlock from 'super/i-block/i-block'; diff --git a/src/traits/i-access/i-access.ts b/src/traits/i-access/i-access.ts index 37d03c86f9..c49aa0bdec 100644 --- a/src/traits/i-access/i-access.ts +++ b/src/traits/i-access/i-access.ts @@ -231,7 +231,7 @@ export default abstract class iAccess { /** @see [[iAccess.getNextFocusableElement]] */ static getNextFocusableElement: AddSelf = - (component, step, el?): CanUndef => { + (component, step, el?): CanUndef => { if (document.activeElement == null) { return; } @@ -404,7 +404,7 @@ export default abstract class iAccess { * @param step * @param el - a context to search, if not set, document will be used */ - getNextFocusableElement(step: 1 | -1, el?: T): CanUndef { + getNextFocusableElement(step: 1 | -1, el?: T): CanUndef { return Object.throw(); } @@ -417,7 +417,7 @@ export default abstract class iAccess { } /** - * Find all focusable elements except disabled ones. Search includes the specified element + * Find all focusable elements except disabled ones. Search includes the specified element. * @param el - a context to search, if not set, component will be used */ findAllFocusableElements(el?: T): IterableIterator> { From 91d2ca1a7c741879d83f79a27a4db9d74cf49a6b Mon Sep 17 00:00:00 2001 From: Aleksandr Bunin Date: Mon, 15 Aug 2022 11:13:37 +0300 Subject: [PATCH 097/185] upd deps --- package-lock.json | 115 ++++++++++++++++++++++++++++++++-------------- 1 file changed, 80 insertions(+), 35 deletions(-) diff --git a/package-lock.json b/package-lock.json index 7754150686..ff998f3981 100644 --- a/package-lock.json +++ b/package-lock.json @@ -39,7 +39,7 @@ "@types/jasmine": "3.10.3", "@types/semver": "7.3.10", "@types/webpack": "5.28.0", - "@v4fire/core": "3.86.2", + "@v4fire/core": "3.87.0", "@v4fire/linters": "1.9.0", "husky": "7.0.4", "nyc": "15.1.0", @@ -123,7 +123,7 @@ "webpack-cli": "4.9.2" }, "peerDependencies": { - "@v4fire/core": "^3.86.2", + "@v4fire/core": "^3.87.0", "webpack": "^5.70.0" } }, @@ -3971,9 +3971,9 @@ } }, "node_modules/@v4fire/core": { - "version": "3.86.2", - "resolved": "https://registry.npmjs.org/@v4fire/core/-/core-3.86.2.tgz", - "integrity": "sha512-Xu/SKvKkV/XBT+CRXkOVak1KMelBNZseQY9Kh2HD5ZUZ3tyuYfyr5NtwEzCY3rVg+ouh716lQ4s5WFg1u7MJRQ==", + "version": "3.87.0", + "resolved": "https://registry.npmjs.org/@v4fire/core/-/core-3.87.0.tgz", + "integrity": "sha512-NbfSuOWxMldX6IsJ3vuklPB5h13O0Idxnjfwd0j3zCQk2x429TIlCXZuFOJsTdYu6eCBlUc2hfwO1BNtktOHCA==", "dev": true, "dependencies": { "@swc/core": "1.2.153", @@ -4038,7 +4038,7 @@ "through2": "4.0.2", "tsc-alias": "1.6.1", "tsconfig": "7.0.0", - "typedoc": "0.22.12", + "typedoc": "0.22.13", "typescript": "4.6.2", "upath": "2.0.1" } @@ -16192,9 +16192,9 @@ } }, "node_modules/jsonc-parser": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.0.0.tgz", - "integrity": "sha512-fQzRfAbIBnR0IQvftw9FJveWiHp72Fg20giDrHz6TdfB12UH/uue0D3hm57UB5KgAVuniLMCaS8P1IMj9NR7cA==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.1.0.tgz", + "integrity": "sha512-DRf0QjnNeCUds3xTjKlQQ3DpJD51GvDjJfnxUVWg6PZTo2otSm+slzNAxU/35hF8/oJIKoG9slq30JYOsF2azg==", "dev": true, "optional": true }, @@ -17032,9 +17032,9 @@ } }, "node_modules/marked": { - "version": "4.0.17", - "resolved": "https://registry.npmjs.org/marked/-/marked-4.0.17.tgz", - "integrity": "sha512-Wfk0ATOK5iPxM4ptrORkFemqroz0ZDxp5MWfYA7H/F+wO17NRWV5Ypxi6p3g2Xmw2bKeiYOl6oVnLHKxBA0VhA==", + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/marked/-/marked-4.0.18.tgz", + "integrity": "sha512-wbLDJ7Zh0sqA0Vdg6aqlbT+yPxqLblpAZh1mK2+AO2twQkPywvvqQNfEPVwSSRjZ7dZcdeVBIAgiO7MMp3Dszw==", "dev": true, "optional": true, "bin": { @@ -25025,17 +25025,17 @@ } }, "node_modules/typedoc": { - "version": "0.22.12", - "resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.22.12.tgz", - "integrity": "sha512-FcyC+YuaOpr3rB9QwA1IHOi9KnU2m50sPJW5vcNRPCIdecp+3bFkh7Rq5hBU1Fyn29UR2h4h/H7twZHWDhL0sw==", + "version": "0.22.13", + "resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.22.13.tgz", + "integrity": "sha512-NHNI7Dr6JHa/I3+c62gdRNXBIyX7P33O9TafGLd07ur3MqzcKgwTvpg18EtvCLHJyfeSthAtCLpM7WkStUmDuQ==", "dev": true, "optional": true, "dependencies": { "glob": "^7.2.0", "lunr": "^2.3.9", - "marked": "^4.0.10", - "minimatch": "^3.0.4", - "shiki": "^0.10.0" + "marked": "^4.0.12", + "minimatch": "^5.0.1", + "shiki": "^0.10.1" }, "bin": { "typedoc": "bin/typedoc" @@ -25044,7 +25044,30 @@ "node": ">= 12.10.0" }, "peerDependencies": { - "typescript": "4.0.x || 4.1.x || 4.2.x || 4.3.x || 4.4.x || 4.5.x" + "typescript": "4.0.x || 4.1.x || 4.2.x || 4.3.x || 4.4.x || 4.5.x || 4.6.x" + } + }, + "node_modules/typedoc/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "optional": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/typedoc/node_modules/minimatch": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.0.tgz", + "integrity": "sha512-9TPBGGak4nHfGZsPBohm9AWg6NoT7QTCehS3BIJABslyZbzxfV78QM2Y6+i741OPZIafFAaiiEMh5OyIrJPgtg==", + "dev": true, + "optional": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" } }, "node_modules/typescript": { @@ -29283,9 +29306,9 @@ } }, "@v4fire/core": { - "version": "3.86.2", - "resolved": "https://registry.npmjs.org/@v4fire/core/-/core-3.86.2.tgz", - "integrity": "sha512-Xu/SKvKkV/XBT+CRXkOVak1KMelBNZseQY9Kh2HD5ZUZ3tyuYfyr5NtwEzCY3rVg+ouh716lQ4s5WFg1u7MJRQ==", + "version": "3.87.0", + "resolved": "https://registry.npmjs.org/@v4fire/core/-/core-3.87.0.tgz", + "integrity": "sha512-NbfSuOWxMldX6IsJ3vuklPB5h13O0Idxnjfwd0j3zCQk2x429TIlCXZuFOJsTdYu6eCBlUc2hfwO1BNtktOHCA==", "dev": true, "requires": { "@babel/core": "7.17.5", @@ -29344,7 +29367,7 @@ "tsconfig": "7.0.0", "tsconfig-paths": "3.13.0", "tslib": "2.3.1", - "typedoc": "0.22.12", + "typedoc": "0.22.13", "typescript": "4.6.2", "upath": "2.0.1", "w3c-xmlserializer": "2.0.0" @@ -38871,9 +38894,9 @@ } }, "jsonc-parser": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.0.0.tgz", - "integrity": "sha512-fQzRfAbIBnR0IQvftw9FJveWiHp72Fg20giDrHz6TdfB12UH/uue0D3hm57UB5KgAVuniLMCaS8P1IMj9NR7cA==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.1.0.tgz", + "integrity": "sha512-DRf0QjnNeCUds3xTjKlQQ3DpJD51GvDjJfnxUVWg6PZTo2otSm+slzNAxU/35hF8/oJIKoG9slq30JYOsF2azg==", "dev": true, "optional": true }, @@ -39575,9 +39598,9 @@ } }, "marked": { - "version": "4.0.17", - "resolved": "https://registry.npmjs.org/marked/-/marked-4.0.17.tgz", - "integrity": "sha512-Wfk0ATOK5iPxM4ptrORkFemqroz0ZDxp5MWfYA7H/F+wO17NRWV5Ypxi6p3g2Xmw2bKeiYOl6oVnLHKxBA0VhA==", + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/marked/-/marked-4.0.18.tgz", + "integrity": "sha512-wbLDJ7Zh0sqA0Vdg6aqlbT+yPxqLblpAZh1mK2+AO2twQkPywvvqQNfEPVwSSRjZ7dZcdeVBIAgiO7MMp3Dszw==", "dev": true, "optional": true }, @@ -45778,17 +45801,39 @@ } }, "typedoc": { - "version": "0.22.12", - "resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.22.12.tgz", - "integrity": "sha512-FcyC+YuaOpr3rB9QwA1IHOi9KnU2m50sPJW5vcNRPCIdecp+3bFkh7Rq5hBU1Fyn29UR2h4h/H7twZHWDhL0sw==", + "version": "0.22.13", + "resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.22.13.tgz", + "integrity": "sha512-NHNI7Dr6JHa/I3+c62gdRNXBIyX7P33O9TafGLd07ur3MqzcKgwTvpg18EtvCLHJyfeSthAtCLpM7WkStUmDuQ==", "dev": true, "optional": true, "requires": { "glob": "^7.2.0", "lunr": "^2.3.9", - "marked": "^4.0.10", - "minimatch": "^3.0.4", - "shiki": "^0.10.0" + "marked": "^4.0.12", + "minimatch": "^5.0.1", + "shiki": "^0.10.1" + }, + "dependencies": { + "brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "optional": true, + "requires": { + "balanced-match": "^1.0.0" + } + }, + "minimatch": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.0.tgz", + "integrity": "sha512-9TPBGGak4nHfGZsPBohm9AWg6NoT7QTCehS3BIJABslyZbzxfV78QM2Y6+i741OPZIafFAaiiEMh5OyIrJPgtg==", + "dev": true, + "optional": true, + "requires": { + "brace-expansion": "^2.0.1" + } + } } }, "typescript": { From 7c067412f5139be83235ead9448e2bec32a3f8d0 Mon Sep 17 00:00:00 2001 From: Aleksandr Bunin Date: Mon, 15 Aug 2022 14:02:15 +0300 Subject: [PATCH 098/185] add AccessibleElement type --- index.d.ts | 2 ++ .../aria/roles-engines/combobox/index.ts | 2 +- .../aria/roles-engines/tab/index.ts | 6 ++--- .../aria/roles-engines/treeitem/index.ts | 10 ++++--- src/traits/i-access/i-access.ts | 26 ++++++++++--------- 5 files changed, 27 insertions(+), 19 deletions(-) diff --git a/index.d.ts b/index.d.ts index 81ccb3bde4..22949f9f7b 100644 --- a/index.d.ts +++ b/index.d.ts @@ -215,3 +215,5 @@ interface TouchGesturePoint extends Partial { x: number; y: number; } + +type AccessibleElement = Element & HTMLOrSVGElement; diff --git a/src/core/component/directives/aria/roles-engines/combobox/index.ts b/src/core/component/directives/aria/roles-engines/combobox/index.ts index 456714dfca..708fab3488 100644 --- a/src/core/component/directives/aria/roles-engines/combobox/index.ts +++ b/src/core/component/directives/aria/roles-engines/combobox/index.ts @@ -32,7 +32,7 @@ export class ComboboxEngine extends AriaRoleEngine { const {el} = this; - this.el = this.ctx?.findFocusableElement() ?? el; + this.el = this.ctx?.findFocusableElement() ?? el; this.params = options.params; } diff --git a/src/core/component/directives/aria/roles-engines/tab/index.ts b/src/core/component/directives/aria/roles-engines/tab/index.ts index 43429b08cb..4f866e7bb0 100644 --- a/src/core/component/directives/aria/roles-engines/tab/index.ts +++ b/src/core/component/directives/aria/roles-engines/tab/index.ts @@ -65,7 +65,7 @@ export class TabEngine extends AriaRoleEngine { */ protected moveFocusToFirstTab(): void { const - firstTab = this.ctx?.findFocusableElement(); + firstTab = this.ctx?.findFocusableElement(); firstTab?.focus(); } @@ -75,14 +75,14 @@ export class TabEngine extends AriaRoleEngine { */ protected moveFocusToLastTab(): void { const - tabs = this.ctx?.findAllFocusableElements(); + tabs = this.ctx?.findAllFocusableElements(); if (tabs == null) { return; } let - lastTab: CanUndef; + lastTab: CanUndef; for (const tab of tabs) { lastTab = tab; diff --git a/src/core/component/directives/aria/roles-engines/treeitem/index.ts b/src/core/component/directives/aria/roles-engines/treeitem/index.ts index 578b2a897a..995317384d 100644 --- a/src/core/component/directives/aria/roles-engines/treeitem/index.ts +++ b/src/core/component/directives/aria/roles-engines/treeitem/index.ts @@ -65,7 +65,7 @@ export class TreeitemEngine extends AriaRoleEngine { * Changes focus from the current focused element to the passed one * @param el */ - protected focusNext(el: HTMLElement): void { + protected focusNext(el: AccessibleElement): void { this.ctx?.removeAllFromTabSequence(this.el); this.ctx?.restoreAllToTabSequence(el); @@ -143,10 +143,14 @@ export class TreeitemEngine extends AriaRoleEngine { */ protected setFocusToLastItem(): void { const - items = >this.ctx?.findAllFocusableElements(this.params.rootElement); + items = this.ctx?.findAllFocusableElements(this.params.rootElement); + + if (items == null) { + return; + } let - lastItem: CanUndef; + lastItem: CanUndef; for (const item of items) { if (item.offsetWidth > 0 || item.offsetHeight > 0) { diff --git a/src/traits/i-access/i-access.ts b/src/traits/i-access/i-access.ts index c49aa0bdec..0d953e5a82 100644 --- a/src/traits/i-access/i-access.ts +++ b/src/traits/i-access/i-access.ts @@ -180,7 +180,7 @@ export default abstract class iAccess { areElementsRemoved = false; const - focusableElems = >this.findAllFocusableElements(component, ctx); + focusableElems = this.findAllFocusableElements(component, ctx); for (const focusableEl of focusableElems) { if (!focusableEl.hasAttribute('data-tabindex')) { @@ -231,7 +231,7 @@ export default abstract class iAccess { /** @see [[iAccess.getNextFocusableElement]] */ static getNextFocusableElement: AddSelf = - (component, step, el?): CanUndef => { + (component, step, el?): CanUndef => { if (document.activeElement == null) { return; } @@ -239,7 +239,7 @@ export default abstract class iAccess { const ctx = el ?? document.documentElement, focusableElems = >this.findAllFocusableElements(component, ctx), - visibleFocusable: HTMLElement[] = []; + visibleFocusable: AccessibleElement[] = []; for (const focusableEl of focusableElems) { if ( @@ -252,7 +252,7 @@ export default abstract class iAccess { } const - index = visibleFocusable.indexOf(document.activeElement); + index = visibleFocusable.indexOf(document.activeElement); if (index > -1) { return visibleFocusable[index + step]; @@ -261,13 +261,13 @@ export default abstract class iAccess { /** @see [[iAccess.findFocusableElement]] */ static findFocusableElement: AddSelf = - (component, el?) => { + (component, el?): CanUndef => { const - ctx = el ?? component.$el, + ctx = el ?? component.$el, focusableElems = this.findAllFocusableElements(component, ctx); for (const focusableEl of focusableElems) { - if (!focusableEl?.hasAttribute('disabled')) { + if (!focusableEl.hasAttribute('disabled')) { return focusableEl; } } @@ -275,7 +275,7 @@ export default abstract class iAccess { /** @see [[iAccess.findAllFocusableElements]] */ static findAllFocusableElements: AddSelf = - (component, el?): IterableIterator> => { + (component, el?): IterableIterator => { const ctx = el ?? component.$el, focusableElems = ctx?.querySelectorAll(FOCUSABLE_SELECTOR); @@ -287,7 +287,9 @@ export default abstract class iAccess { focusableIter = sequence(focusableIter, intoIter([el])); } - function* createFocusableWithoutDisabled(iter: IterableIterator): IterableIterator { + function* createFocusableWithoutDisabled( + iter: IterableIterator + ): IterableIterator { for (const iterEl of iter) { if (!iterEl.hasAttribute('disabled')) { yield iterEl; @@ -404,7 +406,7 @@ export default abstract class iAccess { * @param step * @param el - a context to search, if not set, document will be used */ - getNextFocusableElement(step: 1 | -1, el?: T): CanUndef { + getNextFocusableElement(step: 1 | -1, el?: T): CanUndef { return Object.throw(); } @@ -412,7 +414,7 @@ export default abstract class iAccess { * Find focusable element except disabled ones * @param el - a context to search, if not set, component will be used */ - findFocusableElement(el?: T): CanUndef { + findFocusableElement(el?: T): CanUndef { return Object.throw(); } @@ -420,7 +422,7 @@ export default abstract class iAccess { * Find all focusable elements except disabled ones. Search includes the specified element. * @param el - a context to search, if not set, component will be used */ - findAllFocusableElements(el?: T): IterableIterator> { + findAllFocusableElements(el?: T): IterableIterator { return Object.throw(); } } From 3d0d5bdb04964de20642d0614fce6c54d63837fa Mon Sep 17 00:00:00 2001 From: kobezzza Date: Tue, 16 Aug 2022 10:34:05 +0300 Subject: [PATCH 099/185] refactor: small refactoring of new properties --- src/base/b-list/b-list.ts | 60 +++++++++++++++++++-------------------- src/base/b-tree/b-tree.ts | 47 +++++++++++++++--------------- 2 files changed, 53 insertions(+), 54 deletions(-) diff --git a/src/base/b-list/b-list.ts b/src/base/b-list/b-list.ts index 04b3a322f3..55a2a48c02 100644 --- a/src/base/b-list/b-list.ts +++ b/src/base/b-list/b-list.ts @@ -36,8 +36,7 @@ export * from 'base/b-list/interface'; export const $$ = symbolGenerator(); -interface bList extends Trait { -} +interface bList extends Trait {} /** * Component to create a list of tabs/links @@ -268,14 +267,6 @@ class bList extends iData implements iVisible, iWidth, iItems, iAccess { protected activeStore!: this['Active']; - /** - * True if the component is used as a tablist - */ - @computed({dependencies: ['items']}) - protected get isTablist(): boolean { - return this.items.some((el) => el.href === undefined); - } - /** * A link to the active item element. * If the component is switched to the `multiple` mode, the getter will return an array of elements. @@ -307,6 +298,14 @@ class bList extends iData implements iVisible, iWidth, iItems, iAccess { }); } + /** + * True if the component is used as a tablist + */ + @computed({dependencies: ['items']}) + protected get isTablist(): boolean { + return this.items.some((el) => el.href === undefined); + } + /** * Returns true if the specified value is active * @param value @@ -716,40 +715,39 @@ class bList extends iData implements iVisible, iWidth, iItems, iAccess { const isActive = this.isActive.bind(this, item?.value); - const bindChangeEvent = (cb: Function) => { - this.on('change', () => { - if (Object.isSet(this.active)) { - cb(this.block?.elements('link', {active: true})); - - } else { - cb(this.block?.element('link', {active: true})); - } - }); - }; - const tablistConfig = { isMultiple: this.multiple, orientation: this.orientation }; const tabConfig = { - hasDefaultSelectedTabs: this.active != null, - isFirst: i === 0, orientation: this.orientation, - '@change': bindChangeEvent, + + isFirst: i === 0, + hasDefaultSelectedTabs: this.active != null, get isSelected() { return isActive(); - } + }, + + '@change': bindChangeEvent.bind(this) }; switch (role) { - case 'tablist': - return tablistConfig; - case 'tab': - return tabConfig; - default: - return {}; + case 'tablist': return tablistConfig; + case 'tab': return tabConfig; + default: return {}; + } + + function bindChangeEvent(this: bList, cb: Function) { + this.on('change', () => { + if (Object.isSet(this.active)) { + cb(this.block?.elements('link', {active: true})); + + } else { + cb(this.block?.element('link', {active: true})); + } + }); } } diff --git a/src/base/b-tree/b-tree.ts b/src/base/b-tree/b-tree.ts index 013cda0154..6212c0eabc 100644 --- a/src/base/b-tree/b-tree.ts +++ b/src/base/b-tree/b-tree.ts @@ -261,8 +261,8 @@ class bTree extends iData implements iItems, iAccess { * Returns a dictionary with configurations for the `v-aria` directive used as a treeitem * * @param role - * @param item - tab item data - * @param i - tab item position index + * @param item - tree item data + * @param i - tree item position index */ protected getAriaConfig(role: 'treeitem', item: this['Item'], i: number): Dictionary @@ -271,21 +271,6 @@ class bTree extends iData implements iItems, iAccess { getFoldedMod = this.getFoldedModById.bind(this, item?.id), root = () => this.top?.$el ?? this.$el; - const toggleFold = (target: HTMLElement, value?: boolean): void => { - const - mod = this.block?.getElMod(target, 'node', 'folded'); - - if (mod == null) { - return; - } - - const - newVal = value ? value : mod === 'false'; - - this.block?.setElMod(target, 'node', 'folded', newVal); - this.emit('fold', target, item, newVal); - }; - const treeConfig = { isRoot: this.top == null, orientation: this.orientation, @@ -295,13 +280,8 @@ class bTree extends iData implements iItems, iAccess { }; const treeitemConfig = { - isFirstRootItem: this.top == null && i === 0, orientation: this.orientation, - toggleFold, - - get rootElement() { - return root(); - }, + isFirstRootItem: this.top == null && i === 0, get isExpanded() { return getFoldedMod() === 'false'; @@ -309,6 +289,12 @@ class bTree extends iData implements iItems, iAccess { get isExpandable() { return item?.children != null; + }, + + toggleFold: toggleFold.bind(this), + + get rootElement() { + return root(); } }; @@ -317,6 +303,21 @@ class bTree extends iData implements iItems, iAccess { case 'treeitem': return treeitemConfig; default: return {}; } + + function toggleFold(this: bTree, target: HTMLElement, value?: boolean) { + const + mod = this.block?.getElMod(target, 'node', 'folded'); + + if (mod == null) { + return; + } + + const + newVal = value ? value : mod === 'false'; + + this.block?.setElMod(target, 'node', 'folded', newVal); + this.emit('fold', target, item, newVal); + } } /** From 660f79a0371024c5aa43e540d72db76f5483908c Mon Sep 17 00:00:00 2001 From: kobezzza Date: Tue, 16 Aug 2022 10:34:11 +0300 Subject: [PATCH 100/185] chore: updated changelog --- src/base/b-list/CHANGELOG.md | 9 +++++---- src/base/b-tree/CHANGELOG.md | 8 +++++--- src/base/b-window/CHANGELOG.md | 7 +++---- src/form/b-checkbox/CHANGELOG.md | 2 +- src/form/b-select/CHANGELOG.md | 2 +- src/traits/i-access/CHANGELOG.md | 2 +- src/traits/i-open/CHANGELOG.md | 2 +- 7 files changed, 17 insertions(+), 15 deletions(-) diff --git a/src/base/b-list/CHANGELOG.md b/src/base/b-list/CHANGELOG.md index bd365587c4..f4399e1a93 100644 --- a/src/base/b-list/CHANGELOG.md +++ b/src/base/b-list/CHANGELOG.md @@ -9,16 +9,17 @@ Changelog > - :house: [Internal] > - :nail_care: [Polish] -## v3.0.0-rc.??? (2022-??-??) +## v3.?.? (2022-??-??) #### :rocket: New Feature -* Added a new directive `v-aria` * Added a new prop `orientation` -* Added `isTablist` -* Added `getAriaConfig` * Now the component derives `iAccess` +#### :house: Internal + +* Improved component accessibility + ## v3.0.0-rc.211 (2021-07-21) #### :boom: Breaking Change diff --git a/src/base/b-tree/CHANGELOG.md b/src/base/b-tree/CHANGELOG.md index 9aeec86d84..51d3724336 100644 --- a/src/base/b-tree/CHANGELOG.md +++ b/src/base/b-tree/CHANGELOG.md @@ -9,15 +9,17 @@ Changelog > - :house: [Internal] > - :nail_care: [Polish] -## v3.0.0-rc.??? (2022-??-??) +## v3.?.? (2022-??-??) #### :rocket: New Feature -* Added a new directive `v-aria` * Added a new prop `orientation` -* Added `getAriaConfig` * Now the component derives `iAccess` +#### :house: Internal + +* Improved component accessibility + ## v3.0.0-rc.164 (2021-03-22) #### :house: Internal diff --git a/src/base/b-window/CHANGELOG.md b/src/base/b-window/CHANGELOG.md index 4fe7a2cff7..32ca2a33f5 100644 --- a/src/base/b-window/CHANGELOG.md +++ b/src/base/b-window/CHANGELOG.md @@ -9,12 +9,11 @@ Changelog > - :house: [Internal] > - :nail_care: [Polish] -## v3.?.? (2022-0?-??) +## v3.?.? (2022-??-??) -#### :rocket: New Feature +#### :house: Internal -* Added a new directive `v-aria` -* Added a new directive `v-id` +* Improved component accessibility ## v3.0.0-rc.211 (2021-07-21) diff --git a/src/form/b-checkbox/CHANGELOG.md b/src/form/b-checkbox/CHANGELOG.md index 5a05e47de2..38d8ea8b5c 100644 --- a/src/form/b-checkbox/CHANGELOG.md +++ b/src/form/b-checkbox/CHANGELOG.md @@ -9,7 +9,7 @@ Changelog > - :house: [Internal] > - :nail_care: [Polish] -## v3.0.0-rc.??? (2022-??-??) +## v3.?.? (2022-??-??) #### :bug: Bug Fix diff --git a/src/form/b-select/CHANGELOG.md b/src/form/b-select/CHANGELOG.md index 334f6805f1..f336cc6855 100644 --- a/src/form/b-select/CHANGELOG.md +++ b/src/form/b-select/CHANGELOG.md @@ -9,7 +9,7 @@ Changelog > - :house: [Internal] > - :nail_care: [Polish] -## v3.0.0-rc.??? (2022-??-??) +## v3.?.? (2022-??-??) #### :rocket: New Feature diff --git a/src/traits/i-access/CHANGELOG.md b/src/traits/i-access/CHANGELOG.md index f4cab58731..5823c3f158 100644 --- a/src/traits/i-access/CHANGELOG.md +++ b/src/traits/i-access/CHANGELOG.md @@ -9,7 +9,7 @@ Changelog > - :house: [Internal] > - :nail_care: [Polish] -## v3.0.0-rc.??? (2022-??-??) +## v3.?.? (2022-??-??) #### :rocket: New Feature diff --git a/src/traits/i-open/CHANGELOG.md b/src/traits/i-open/CHANGELOG.md index 7f2f3680c6..3473a34026 100644 --- a/src/traits/i-open/CHANGELOG.md +++ b/src/traits/i-open/CHANGELOG.md @@ -9,7 +9,7 @@ Changelog > - :house: [Internal] > - :nail_care: [Polish] -## v3.0.0-rc.??? (2022-??-??) +## v3.?.? (2022-??-??) #### :rocket: New Feature From 0a5fedde8bc883538d0e3444d87510fd8a7b3fa2 Mon Sep 17 00:00:00 2001 From: kobezzza Date: Tue, 16 Aug 2022 11:11:40 +0300 Subject: [PATCH 101/185] fix: added support for updating directive value --- src/core/component/directives/id/index.ts | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/src/core/component/directives/id/index.ts b/src/core/component/directives/id/index.ts index 7e1c4a51ab..49e1214828 100644 --- a/src/core/component/directives/id/index.ts +++ b/src/core/component/directives/id/index.ts @@ -17,16 +17,28 @@ import type iBlock from 'super/i-block/i-block'; ComponentEngine.directive('id', { inserted(el: HTMLElement, binding: VNodeDirective, vnode: VNode): void { const - ctx = Object.cast(vnode.fakeContext), - {modifiers: mod} = binding; + ctx = Object.cast(vnode.fakeContext); - if (mod?.preserve != null && el.hasAttribute('id')) { + if (el.hasAttribute('id') && binding.modifiers?.preserve != null) { + el.setAttribute('data-v-id-preserve', 'true'); return; } + el.setAttribute('id', ctx.dom.getId(binding.value)); + }, + + update(el: HTMLElement, binding: VNodeDirective, vnode: VNode) { const - id = ctx.dom.getId(binding.value); + ctx = Object.cast(vnode.fakeContext); + + if (el.hasAttribute('data-v-id-preserve')) { + return; + } + + el.setAttribute('id', ctx.dom.getId(binding.value)); + }, - el.setAttribute('id', id); + unbind(el: HTMLElement) { + el.removeAttribute('data-v-id-preserve'); } }); From 9a4c4f3e8b0e1328328a2589ad94d5571e9d01f9 Mon Sep 17 00:00:00 2001 From: kobezzza Date: Tue, 16 Aug 2022 11:11:59 +0300 Subject: [PATCH 102/185] doc: improved doc --- src/core/component/directives/id/README.md | 42 +++++++++++++++++----- 1 file changed, 33 insertions(+), 9 deletions(-) diff --git a/src/core/component/directives/id/README.md b/src/core/component/directives/id/README.md index 58c7b90465..8f53efd3dd 100644 --- a/src/core/component/directives/id/README.md +++ b/src/core/component/directives/id/README.md @@ -1,26 +1,50 @@ -# core/component/directives/aria +# core/component/directives/id -This module provides a directive for easy adding of id attribute. +This module provides a directive to easily add an id attribute to an element. -## Usage +``` +< div v-id = 'title' +``` + +## Why is this directive needed? +A page cannot have two or more elements with the same id attribute at the same time. But when we design or edit a +component markup and add such an attribute, we cannot be sure that the name is not already used by other components. +To solve this problem, any component has the `dom.getId` method. + +``` +< div :id = dom.getId('title') ``` -< &__foo v-id = 'title' + +This method returns the passed identifier, plus the unique ID of the component within which the method is called. +Thus, we only need to guarantee the uniqueness of the identifier within one template, and not all components. +The problem is solved, but now our template has become more "dirty" due to the addition of syntactic noise with the +method call and other stuff. This directive just solves this problem. +``` +< div v-id = 'title' ``` The same as -``` -< &__foo :id = dom.getId('title') +``` +< div :id = dom.getId('title') ``` ## Modifiers -1. `preserve` means that if there is already an id attribute on the element, -the directive will left it and will not set another one +### preserve +This modifier means that if the element already has an id attribute, then the directive will leave it and won't overwrite it + +``` +< div id = my-div1 | v-id = 'title1' +< div id = my-div2 | v-id.preserve = 'title2' ``` -< &__foo v-id.preserve = 'title' +Will turn into + +```html +

    +
    ``` From 87ccfe14ee3ebbd3e13ff1a5730b6f67359c7f63 Mon Sep 17 00:00:00 2001 From: kobezzza Date: Tue, 16 Aug 2022 11:12:58 +0300 Subject: [PATCH 103/185] chore: updated changelog --- src/core/component/directives/aria/CHANGELOG.md | 2 +- .../directives/aria/roles-engines/combobox/CHANGELOG.md | 2 +- .../directives/aria/roles-engines/controls/CHANGELOG.md | 2 +- .../component/directives/aria/roles-engines/dialog/CHANGELOG.md | 2 +- .../directives/aria/roles-engines/listbox/CHANGELOG.md | 2 +- .../component/directives/aria/roles-engines/option/CHANGELOG.md | 2 +- .../component/directives/aria/roles-engines/tab/CHANGELOG.md | 2 +- .../directives/aria/roles-engines/tablist/CHANGELOG.md | 2 +- .../directives/aria/roles-engines/tabpanel/CHANGELOG.md | 2 +- .../component/directives/aria/roles-engines/tree/CHANGELOG.md | 2 +- .../directives/aria/roles-engines/treeitem/CHANGELOG.md | 2 +- src/core/component/directives/id/CHANGELOG.md | 2 +- 12 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/core/component/directives/aria/CHANGELOG.md b/src/core/component/directives/aria/CHANGELOG.md index ba2980fb55..6e314edf2d 100644 --- a/src/core/component/directives/aria/CHANGELOG.md +++ b/src/core/component/directives/aria/CHANGELOG.md @@ -9,7 +9,7 @@ Changelog > - :house: [Internal] > - :nail_care: [Polish] -## v?.??.? (2022-0?-??) +## v3.?.? (2022-??-??) #### :rocket: New Feature diff --git a/src/core/component/directives/aria/roles-engines/combobox/CHANGELOG.md b/src/core/component/directives/aria/roles-engines/combobox/CHANGELOG.md index ba2980fb55..6e314edf2d 100644 --- a/src/core/component/directives/aria/roles-engines/combobox/CHANGELOG.md +++ b/src/core/component/directives/aria/roles-engines/combobox/CHANGELOG.md @@ -9,7 +9,7 @@ Changelog > - :house: [Internal] > - :nail_care: [Polish] -## v?.??.? (2022-0?-??) +## v3.?.? (2022-??-??) #### :rocket: New Feature diff --git a/src/core/component/directives/aria/roles-engines/controls/CHANGELOG.md b/src/core/component/directives/aria/roles-engines/controls/CHANGELOG.md index ba2980fb55..6e314edf2d 100644 --- a/src/core/component/directives/aria/roles-engines/controls/CHANGELOG.md +++ b/src/core/component/directives/aria/roles-engines/controls/CHANGELOG.md @@ -9,7 +9,7 @@ Changelog > - :house: [Internal] > - :nail_care: [Polish] -## v?.??.? (2022-0?-??) +## v3.?.? (2022-??-??) #### :rocket: New Feature diff --git a/src/core/component/directives/aria/roles-engines/dialog/CHANGELOG.md b/src/core/component/directives/aria/roles-engines/dialog/CHANGELOG.md index ba2980fb55..6e314edf2d 100644 --- a/src/core/component/directives/aria/roles-engines/dialog/CHANGELOG.md +++ b/src/core/component/directives/aria/roles-engines/dialog/CHANGELOG.md @@ -9,7 +9,7 @@ Changelog > - :house: [Internal] > - :nail_care: [Polish] -## v?.??.? (2022-0?-??) +## v3.?.? (2022-??-??) #### :rocket: New Feature diff --git a/src/core/component/directives/aria/roles-engines/listbox/CHANGELOG.md b/src/core/component/directives/aria/roles-engines/listbox/CHANGELOG.md index ba2980fb55..6e314edf2d 100644 --- a/src/core/component/directives/aria/roles-engines/listbox/CHANGELOG.md +++ b/src/core/component/directives/aria/roles-engines/listbox/CHANGELOG.md @@ -9,7 +9,7 @@ Changelog > - :house: [Internal] > - :nail_care: [Polish] -## v?.??.? (2022-0?-??) +## v3.?.? (2022-??-??) #### :rocket: New Feature diff --git a/src/core/component/directives/aria/roles-engines/option/CHANGELOG.md b/src/core/component/directives/aria/roles-engines/option/CHANGELOG.md index ba2980fb55..6e314edf2d 100644 --- a/src/core/component/directives/aria/roles-engines/option/CHANGELOG.md +++ b/src/core/component/directives/aria/roles-engines/option/CHANGELOG.md @@ -9,7 +9,7 @@ Changelog > - :house: [Internal] > - :nail_care: [Polish] -## v?.??.? (2022-0?-??) +## v3.?.? (2022-??-??) #### :rocket: New Feature diff --git a/src/core/component/directives/aria/roles-engines/tab/CHANGELOG.md b/src/core/component/directives/aria/roles-engines/tab/CHANGELOG.md index ba2980fb55..6e314edf2d 100644 --- a/src/core/component/directives/aria/roles-engines/tab/CHANGELOG.md +++ b/src/core/component/directives/aria/roles-engines/tab/CHANGELOG.md @@ -9,7 +9,7 @@ Changelog > - :house: [Internal] > - :nail_care: [Polish] -## v?.??.? (2022-0?-??) +## v3.?.? (2022-??-??) #### :rocket: New Feature diff --git a/src/core/component/directives/aria/roles-engines/tablist/CHANGELOG.md b/src/core/component/directives/aria/roles-engines/tablist/CHANGELOG.md index ba2980fb55..6e314edf2d 100644 --- a/src/core/component/directives/aria/roles-engines/tablist/CHANGELOG.md +++ b/src/core/component/directives/aria/roles-engines/tablist/CHANGELOG.md @@ -9,7 +9,7 @@ Changelog > - :house: [Internal] > - :nail_care: [Polish] -## v?.??.? (2022-0?-??) +## v3.?.? (2022-??-??) #### :rocket: New Feature diff --git a/src/core/component/directives/aria/roles-engines/tabpanel/CHANGELOG.md b/src/core/component/directives/aria/roles-engines/tabpanel/CHANGELOG.md index ba2980fb55..6e314edf2d 100644 --- a/src/core/component/directives/aria/roles-engines/tabpanel/CHANGELOG.md +++ b/src/core/component/directives/aria/roles-engines/tabpanel/CHANGELOG.md @@ -9,7 +9,7 @@ Changelog > - :house: [Internal] > - :nail_care: [Polish] -## v?.??.? (2022-0?-??) +## v3.?.? (2022-??-??) #### :rocket: New Feature diff --git a/src/core/component/directives/aria/roles-engines/tree/CHANGELOG.md b/src/core/component/directives/aria/roles-engines/tree/CHANGELOG.md index ba2980fb55..6e314edf2d 100644 --- a/src/core/component/directives/aria/roles-engines/tree/CHANGELOG.md +++ b/src/core/component/directives/aria/roles-engines/tree/CHANGELOG.md @@ -9,7 +9,7 @@ Changelog > - :house: [Internal] > - :nail_care: [Polish] -## v?.??.? (2022-0?-??) +## v3.?.? (2022-??-??) #### :rocket: New Feature diff --git a/src/core/component/directives/aria/roles-engines/treeitem/CHANGELOG.md b/src/core/component/directives/aria/roles-engines/treeitem/CHANGELOG.md index ba2980fb55..6e314edf2d 100644 --- a/src/core/component/directives/aria/roles-engines/treeitem/CHANGELOG.md +++ b/src/core/component/directives/aria/roles-engines/treeitem/CHANGELOG.md @@ -9,7 +9,7 @@ Changelog > - :house: [Internal] > - :nail_care: [Polish] -## v?.??.? (2022-0?-??) +## v3.?.? (2022-??-??) #### :rocket: New Feature diff --git a/src/core/component/directives/id/CHANGELOG.md b/src/core/component/directives/id/CHANGELOG.md index ba2980fb55..6e314edf2d 100644 --- a/src/core/component/directives/id/CHANGELOG.md +++ b/src/core/component/directives/id/CHANGELOG.md @@ -9,7 +9,7 @@ Changelog > - :house: [Internal] > - :nail_care: [Polish] -## v?.??.? (2022-0?-??) +## v3.?.? (2022-??-??) #### :rocket: New Feature From d1c3e1e54539d36fe50d699a81fb7e501c629751 Mon Sep 17 00:00:00 2001 From: kobezzza Date: Tue, 16 Aug 2022 11:17:58 +0300 Subject: [PATCH 104/185] chore: added new test & refactoring --- .../directives/id/test/unit/functional.ts | 22 ++++++++++++------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/src/core/component/directives/id/test/unit/functional.ts b/src/core/component/directives/id/test/unit/functional.ts index 7c992d0aee..d832c3f6dc 100644 --- a/src/core/component/directives/id/test/unit/functional.ts +++ b/src/core/component/directives/id/test/unit/functional.ts @@ -7,6 +7,7 @@ * Released under the MIT license * https://github.com/V4Fire/Client/blob/master/LICENSE */ + import type { JSHandle, Page } from 'playwright'; import type bDummy from 'dummies/b-dummy/b-dummy'; @@ -18,16 +19,25 @@ test.describe('v-id', () => { await demoPage.goto(); }); - test('id is added', async ({page}) => { - const target = await init(page); - const id = await target.evaluate((ctx) => ctx.$root.unsafe.dom.getId('dummy')); + test('should add an id to the element', async ({page}) => { + const + target = await init(page), + id = await target.evaluate((ctx) => ctx.$root.unsafe.dom.getId('dummy')); test.expect( await target.evaluate((ctx) => ctx.$el?.id) ).toBe(id); }); - test('preserve mod', async ({page}) => { + test('should not preserve the original element id', async ({page}) => { + const target = await init(page, {'v-id': 'dummy', id: 'foo'}); + + test.expect( + await target.evaluate((ctx) => ctx.$el?.id) + ).toBe('dummy'); + }); + + test('should preserve the original element id', async ({page}) => { const target = await init(page, {'v-id.preserve': 'dummy', id: 'foo'}); test.expect( @@ -35,10 +45,6 @@ test.describe('v-id', () => { ).toBe('foo'); }); - /** - * @param page - * @param attrs - */ async function init(page: Page, attrs: Dictionary = {}): Promise> { return Component.createComponent(page, 'b-dummy', { attrs: {'v-id': 'dummy', ...attrs} From b61891ccc682d13f12ad07fe72a457756bfc170b Mon Sep 17 00:00:00 2001 From: kobezzza Date: Tue, 16 Aug 2022 11:45:37 +0300 Subject: [PATCH 105/185] refactor: better naming --- src/traits/i-open/i-open.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/traits/i-open/i-open.ts b/src/traits/i-open/i-open.ts index 93ef25a1a6..efce316887 100644 --- a/src/traits/i-open/i-open.ts +++ b/src/traits/i-open/i-open.ts @@ -135,7 +135,7 @@ export default abstract class iOpen { } /** - * Checks if the component realize current trait + * Checks if the passed object realize the current trait * @param obj */ static is(obj: unknown): obj is iOpen { @@ -143,8 +143,8 @@ export default abstract class iOpen { return false; } - const dict = Object.cast(obj); - return Object.isFunction(dict.open) && Object.isFunction(dict.close); + const unsafe = Object.cast(obj); + return Object.isFunction(unsafe.open) && Object.isFunction(unsafe.close); } /** From dfe95d5a7fbe4e49ffd5fa7232124f346c8db119 Mon Sep 17 00:00:00 2001 From: kobezzza Date: Tue, 16 Aug 2022 11:49:23 +0300 Subject: [PATCH 106/185] chore: updated changelog --- src/traits/i-open/CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/traits/i-open/CHANGELOG.md b/src/traits/i-open/CHANGELOG.md index 3473a34026..68b7bf8e06 100644 --- a/src/traits/i-open/CHANGELOG.md +++ b/src/traits/i-open/CHANGELOG.md @@ -13,7 +13,7 @@ Changelog #### :rocket: New Feature -* Added `is` +* Added a new static method `is` ## v3.0.0-rc.184 (2021-05-12) From 8ce0f577ccb2098d1f192884462ccea3416d6b24 Mon Sep 17 00:00:00 2001 From: kobezzza Date: Tue, 16 Aug 2022 11:49:39 +0300 Subject: [PATCH 107/185] doc: added doc for new helpers --- src/traits/i-open/README.md | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/src/traits/i-open/README.md b/src/traits/i-open/README.md index 73d15dc17a..6600e47bcc 100644 --- a/src/traits/i-open/README.md +++ b/src/traits/i-open/README.md @@ -133,7 +133,23 @@ export default class bButton implements iOpen { ## Helpers -The trait provides a bunch of helper functions to initialize event listeners. +The trait provides a bunch of helper functions to work with it. + +### is + +Checks if the passed object realize the current trait. + +```typescript +import iOpen from 'traits/i-open/i-open'; + +export default class bButton { + created() { + if (iOpen.is(this)) { + this.open(); + } + } +} +``` ### initCloseHelpers From a05a3feaa5bdaa8954a75a952ef4d591df6d4fd4 Mon Sep 17 00:00:00 2001 From: kobezzza Date: Wed, 17 Aug 2022 08:50:01 +0300 Subject: [PATCH 108/185] refactor: moved `is` upper --- src/traits/i-open/i-open.ts | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/src/traits/i-open/i-open.ts b/src/traits/i-open/i-open.ts index efce316887..adaabb2cff 100644 --- a/src/traits/i-open/i-open.ts +++ b/src/traits/i-open/i-open.ts @@ -66,6 +66,19 @@ export default abstract class iOpen { } }; + /** + * Checks if the passed object realize the current trait + * @param obj + */ + static is(obj: unknown): obj is iOpen { + if (Object.isPrimitive(obj)) { + return false; + } + + const unsafe = Object.cast(obj); + return Object.isFunction(unsafe.open) && Object.isFunction(unsafe.close); + } + /** * Initialize default event listeners to close a component by a keyboard or mouse * @@ -134,19 +147,6 @@ export default abstract class iOpen { }); } - /** - * Checks if the passed object realize the current trait - * @param obj - */ - static is(obj: unknown): obj is iOpen { - if (Object.isPrimitive(obj)) { - return false; - } - - const unsafe = Object.cast(obj); - return Object.isFunction(unsafe.open) && Object.isFunction(unsafe.close); - } - /** * Opens the component * @param args From a9171a609e37fae58ccca1372e6228dd7c128945 Mon Sep 17 00:00:00 2001 From: kobezzza Date: Wed, 17 Aug 2022 08:51:43 +0300 Subject: [PATCH 109/185] refactor: better doc & review new methods --- src/traits/i-access/i-access.ts | 186 ++++++++++++++++---------------- 1 file changed, 94 insertions(+), 92 deletions(-) diff --git a/src/traits/i-access/i-access.ts b/src/traits/i-access/i-access.ts index 0d953e5a82..f4fb85d585 100644 --- a/src/traits/i-access/i-access.ts +++ b/src/traits/i-access/i-access.ts @@ -14,8 +14,9 @@ */ import SyncPromise from 'core/promise/sync'; -import { sequence } from 'core/iter/combinators'; + import { intoIter } from 'core/iter'; +import { sequence } from 'core/iter/combinators'; import type iBlock from 'super/i-block/i-block'; import type { ModsDecl, ModEvent } from 'super/i-block/i-block'; @@ -54,6 +55,19 @@ export default abstract class iAccess { static blur: AddSelf = (component) => SyncPromise.resolve(component.setMod('focused', false)); + /** + * Checks if the component realize current trait + * @param obj + */ + static is(obj: unknown): obj is iAccess { + if (Object.isPrimitive(obj)) { + return false; + } + + const dict = Object.cast(obj); + return Object.isFunction(dict.removeAllFromTabSequence) && Object.isFunction(dict.getNextFocusableElement); + } + /** * Returns true if the component in focus * @param component @@ -168,26 +182,23 @@ export default abstract class iAccess { /** @see [[iAccess.removeAllFromTabSequence]] */ static removeAllFromTabSequence: AddSelf = - (component, el?): boolean => { - const - ctx = el ?? component.$el; - - if (ctx == null) { - return false; - } - + (component, searchCtx = component.$el): boolean => { let areElementsRemoved = false; + if (searchCtx == null) { + return areElementsRemoved; + } + const - focusableElems = this.findAllFocusableElements(component, ctx); + focusableEls = this.findAllFocusableElements(component, searchCtx); - for (const focusableEl of focusableElems) { - if (!focusableEl.hasAttribute('data-tabindex')) { - focusableEl.setAttribute('data-tabindex', String(focusableEl.tabIndex)); + for (const el of focusableEls) { + if (!el.hasAttribute('data-tabindex')) { + el.setAttribute('data-tabindex', String(el.tabIndex)); } - focusableEl.tabIndex = -1; + el.tabIndex = -1; areElementsRemoved = true; } @@ -196,32 +207,28 @@ export default abstract class iAccess { /** @see [[iAccess.restoreAllToTabSequence]] */ static restoreAllToTabSequence: AddSelf = - (component, el?): boolean => { - const - ctx = el ?? component.$el; - - if (ctx == null) { - return false; - } - + (component, searchCtx = component.$el): boolean => { let areElementsRestored = false; + if (searchCtx == null) { + return areElementsRestored; + } + let - removedElems = intoIter(ctx.querySelectorAll('[data-tabindex]')); + removedEls = intoIter(searchCtx.querySelectorAll('[data-tabindex]')); - if (el?.hasAttribute('data-tabindex')) { - removedElems = sequence(removedElems, intoIter([el])); + if (searchCtx.hasAttribute('data-tabindex')) { + removedEls = sequence(removedEls, intoIter([searchCtx])); } - for (const elem of removedElems) { + for (const elem of removedEls) { const originalTabIndex = elem.getAttribute('data-tabindex'); if (originalTabIndex != null) { elem.tabIndex = Number(originalTabIndex); elem.removeAttribute('data-tabindex'); - areElementsRestored = true; } } @@ -231,74 +238,63 @@ export default abstract class iAccess { /** @see [[iAccess.getNextFocusableElement]] */ static getNextFocusableElement: AddSelf = - (component, step, el?): CanUndef => { + (component, step, searchCtx = document.documentElement): AccessibleElement | null => { if (document.activeElement == null) { - return; + return null; } const - ctx = el ?? document.documentElement, - focusableElems = >this.findAllFocusableElements(component, ctx), - visibleFocusable: AccessibleElement[] = []; + focusableEls = >this.findAllFocusableElements(component, searchCtx), + visibleFocusableEls: AccessibleElement[] = []; - for (const focusableEl of focusableElems) { + for (const el of focusableEls) { if ( - focusableEl.offsetWidth > 0 || - focusableEl.offsetHeight > 0 || - focusableEl === document.activeElement + el.offsetWidth > 0 || + el.offsetHeight > 0 || + el === document.activeElement ) { - visibleFocusable.push(focusableEl); + visibleFocusableEls.push(el); } } const - index = visibleFocusable.indexOf(document.activeElement); + index = visibleFocusableEls.indexOf(document.activeElement); if (index > -1) { - return visibleFocusable[index + step]; + return visibleFocusableEls[index + step] ?? null; } + + return null; }; /** @see [[iAccess.findFocusableElement]] */ static findFocusableElement: AddSelf = - (component, el?): CanUndef => { + (component, searchCtx?): AccessibleElement | null => { const - ctx = el ?? component.$el, - focusableElems = this.findAllFocusableElements(component, ctx); + search = this.findAllFocusableElements(component, searchCtx).next(); - for (const focusableEl of focusableElems) { - if (!focusableEl.hasAttribute('disabled')) { - return focusableEl; - } + if (search.done) { + return null; } + + return search.value; }; /** @see [[iAccess.findAllFocusableElements]] */ static findAllFocusableElements: AddSelf = - (component, el?): IterableIterator => { + (component, searchCtx = component.$el): IterableIterator => { const - ctx = el ?? component.$el, - focusableElems = ctx?.querySelectorAll(FOCUSABLE_SELECTOR); + accessibleEls = searchCtx?.querySelectorAll(FOCUSABLE_SELECTOR); let - focusableIter = intoIter(focusableElems ?? []); - - if (ctx?.matches(FOCUSABLE_SELECTOR)) { - focusableIter = sequence(focusableIter, intoIter([el])); - } + searchIter = intoIter(accessibleEls ?? []); - function* createFocusableWithoutDisabled( - iter: IterableIterator - ): IterableIterator { - for (const iterEl of iter) { - if (!iterEl.hasAttribute('disabled')) { - yield iterEl; - } - } + if (searchCtx?.matches(FOCUSABLE_SELECTOR)) { + searchIter = sequence(searchIter, intoIter([searchCtx])); } const - focusableWithoutDisabled = createFocusableWithoutDisabled(focusableIter); + focusableWithoutDisabled = filterDisabledElements(searchIter); return { [Symbol.iterator]() { @@ -307,20 +303,17 @@ export default abstract class iAccess { next: focusableWithoutDisabled.next.bind(focusableWithoutDisabled) }; - }; - /** - * Checks if the component realize current trait - * @param obj - */ - static is(obj: unknown): obj is iAccess { - if (Object.isPrimitive(obj)) { - return false; - } - - const dict = Object.cast(obj); - return Object.isFunction(dict.removeAllFromTabSequence) && Object.isFunction(dict.getNextFocusableElement); - } + function* filterDisabledElements( + iter: IterableIterator + ): IterableIterator { + for (const el of iter) { + if (!el.hasAttribute('disabled')) { + yield el; + } + } + } + }; /** * A Boolean attribute which, if present, indicates that the component should automatically @@ -383,46 +376,55 @@ export default abstract class iAccess { * Removes all children of the specified element that can be focused from the Tab toggle sequence. * In effect, these elements are set to -1 for the tabindex attribute. * - * @param el - a context to search, if not set, the root element of the component will be used + * @param [searchCtx] - a context to search, if not set, the component root element will be used */ - removeAllFromTabSequence(el?: Element): boolean { + removeAllFromTabSequence(searchCtx?: Element): boolean { return Object.throw(); } /** - * Reverts all children of the specified element that can be focused to the Tab toggle sequence. - * This method is used to restore the state of elements to the state - * they had before 'removeAllFromTabSequence' was applied. + * Restores all children of the specified element that can be focused to the Tab toggle sequence. + * This method is used to restore the state of elements to the state they had before `removeAllFromTabSequence` was + * applied. * - * @param el - a context to search, if not set, the root element of the component will be used + * @param [searchCtx] - a context to search, if not set, the component root element will be used */ - restoreAllToTabSequence(el?: Element): boolean { + restoreAllToTabSequence(searchCtx?: Element): boolean { return Object.throw(); } /** - * Gets a next or previous focusable element via the step parameter from the current focused element + * Returns the next (or previous) element to which focus will be switched by pressing Tab. + * The method takes a "step" parameter, i.e. you can control the Tab sequence direction. For example, + * by setting the step to `-1` you will get an element that will be switched to focus by pressing Shift+Tab. * * @param step - * @param el - a context to search, if not set, document will be used + * @param [searchCtx] - a context to search, if not set, document will be used */ - getNextFocusableElement(step: 1 | -1, el?: T): CanUndef { + getNextFocusableElement( + step: 1 | -1, + searchCtx?: Element + ): T | null { return Object.throw(); } /** - * Find focusable element except disabled ones - * @param el - a context to search, if not set, component will be used + * Finds the first non-disabled focusable element from the passed context to search and returns it. + * The element that is the search context is also taken into account in the search. + * + * @param [searchCtx] - a context to search, if not set, the component root element will be used */ - findFocusableElement(el?: T): CanUndef { + findFocusableElement(searchCtx?: Element): T | null { return Object.throw(); } /** - * Find all focusable elements except disabled ones. Search includes the specified element. - * @param el - a context to search, if not set, component will be used + * Finds all non-disabled focusable elements and returns an iterator with the found ones. + * The element that is the search context is also taken into account in the search. + * + * @param [searchCtx] - a context to search, if not set, the component root element will be used */ - findAllFocusableElements(el?: T): IterableIterator { + findAllFocusableElements(searchCtx?: Element): IterableIterator { return Object.throw(); } } From dff281a6c1b09deccda028c1db48396c7972dcb4 Mon Sep 17 00:00:00 2001 From: Aleksandr Bunin Date: Tue, 16 Aug 2022 12:55:51 +0300 Subject: [PATCH 110/185] fix test --- src/core/component/directives/id/test/unit/functional.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/core/component/directives/id/test/unit/functional.ts b/src/core/component/directives/id/test/unit/functional.ts index d832c3f6dc..9f1a0108c9 100644 --- a/src/core/component/directives/id/test/unit/functional.ts +++ b/src/core/component/directives/id/test/unit/functional.ts @@ -30,11 +30,13 @@ test.describe('v-id', () => { }); test('should not preserve the original element id', async ({page}) => { - const target = await init(page, {'v-id': 'dummy', id: 'foo'}); + const + target = await init(page), + id = await target.evaluate((ctx) => ctx.$root.unsafe.dom.getId('dummy')); test.expect( await target.evaluate((ctx) => ctx.$el?.id) - ).toBe('dummy'); + ).toBe(id); }); test('should preserve the original element id', async ({page}) => { From e515928c6c9d6c7510c5ece170d3eceb12558673 Mon Sep 17 00:00:00 2001 From: Aleksandr Bunin Date: Tue, 16 Aug 2022 13:13:12 +0300 Subject: [PATCH 111/185] fix getFoldedMod usage --- src/base/b-tree/b-tree.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/base/b-tree/b-tree.ts b/src/base/b-tree/b-tree.ts index 6212c0eabc..b716069e3b 100644 --- a/src/base/b-tree/b-tree.ts +++ b/src/base/b-tree/b-tree.ts @@ -268,7 +268,7 @@ class bTree extends iData implements iItems, iAccess { protected getAriaConfig(role: 'tree' | 'treeitem', item?: this['Item'], i?: number): Dictionary { const - getFoldedMod = this.getFoldedModById.bind(this, item?.id), + getFoldedMod = this.getFoldedModById.bind(this, item?.id ?? ''), root = () => this.top?.$el ?? this.$el; const treeConfig = { From ce621e444c7b73925646d654b0e8916d7152b27c Mon Sep 17 00:00:00 2001 From: Aleksandr Bunin Date: Tue, 16 Aug 2022 15:08:17 +0300 Subject: [PATCH 112/185] refactoring getFoldedMod --- src/base/b-tree/b-tree.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/base/b-tree/b-tree.ts b/src/base/b-tree/b-tree.ts index b716069e3b..5cf73f1c29 100644 --- a/src/base/b-tree/b-tree.ts +++ b/src/base/b-tree/b-tree.ts @@ -31,7 +31,8 @@ export * from 'base/b-tree/interface'; export const $$ = symbolGenerator(); -interface bTree extends Trait {} +interface bTree extends Trait { +} /** * Component to render tree of any elements @@ -267,8 +268,11 @@ class bTree extends iData implements iItems, iAccess { protected getAriaConfig(role: 'treeitem', item: this['Item'], i: number): Dictionary protected getAriaConfig(role: 'tree' | 'treeitem', item?: this['Item'], i?: number): Dictionary { + const getFoldedMod = (item?.id != null) ? + this.getFoldedModById.bind(this, item.id) : + () => ''; + const - getFoldedMod = this.getFoldedModById.bind(this, item?.id ?? ''), root = () => this.top?.$el ?? this.$el; const treeConfig = { From 5ade7f62b5bcf2b68db4c292281a57a30500e17d Mon Sep 17 00:00:00 2001 From: kobezzza Date: Wed, 17 Aug 2022 09:08:22 +0300 Subject: [PATCH 113/185] chore: fixed jsdoc --- src/traits/i-access/i-access.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/traits/i-access/i-access.ts b/src/traits/i-access/i-access.ts index f4fb85d585..c3860c74d5 100644 --- a/src/traits/i-access/i-access.ts +++ b/src/traits/i-access/i-access.ts @@ -56,7 +56,7 @@ export default abstract class iAccess { (component) => SyncPromise.resolve(component.setMod('focused', false)); /** - * Checks if the component realize current trait + * Checks if the passed object realize the current trait * @param obj */ static is(obj: unknown): obj is iAccess { From 04d99b865c08f25a5bf91f20e37969e054975016 Mon Sep 17 00:00:00 2001 From: kobezzza Date: Wed, 17 Aug 2022 09:13:53 +0300 Subject: [PATCH 114/185] :art: --- src/base/b-tree/b-tree.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/base/b-tree/b-tree.ts b/src/base/b-tree/b-tree.ts index 5cf73f1c29..1109d07416 100644 --- a/src/base/b-tree/b-tree.ts +++ b/src/base/b-tree/b-tree.ts @@ -31,8 +31,7 @@ export * from 'base/b-tree/interface'; export const $$ = symbolGenerator(); -interface bTree extends Trait { -} +interface bTree extends Trait {} /** * Component to render tree of any elements From d842a9cb86b0137bde01ad2e42596b0fe6c38ec0 Mon Sep 17 00:00:00 2001 From: kobezzza Date: Wed, 17 Aug 2022 09:16:01 +0300 Subject: [PATCH 115/185] :art: --- src/super/i-input/i-input.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/super/i-input/i-input.ts b/src/super/i-input/i-input.ts index df9e60e4e6..7ac86b8d17 100644 --- a/src/super/i-input/i-input.ts +++ b/src/super/i-input/i-input.ts @@ -15,8 +15,8 @@ import symbolGenerator from 'core/symbol'; import SyncPromise from 'core/promise/sync'; import { Option } from 'core/prelude/structures'; - import { derive } from 'core/functools/trait'; + import iAccess from 'traits/i-access/i-access'; import iVisible from 'traits/i-visible/i-visible'; From b57b1691359011c649dd26900b13a217ecf5420c Mon Sep 17 00:00:00 2001 From: kobezzza Date: Wed, 17 Aug 2022 09:17:26 +0300 Subject: [PATCH 116/185] chore: updated version wildcard --- src/super/i-input/CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/super/i-input/CHANGELOG.md b/src/super/i-input/CHANGELOG.md index 672b1738a9..c97e0a31ae 100644 --- a/src/super/i-input/CHANGELOG.md +++ b/src/super/i-input/CHANGELOG.md @@ -9,7 +9,7 @@ Changelog > - :house: [Internal] > - :nail_care: [Polish] -## v3.0.0-rc.??? (2021-??-??) +## ## v3.?.? (2022-??-??) #### :rocket: New Feature From 0d17cc735bfad759a1c1ec397dd08ae714e31068 Mon Sep 17 00:00:00 2001 From: kobezzza Date: Wed, 17 Aug 2022 09:27:25 +0300 Subject: [PATCH 117/185] refactor: simple refactoring of new methods --- src/form/b-select/b-select.ts | 84 +++++++++++++++++------------------ 1 file changed, 40 insertions(+), 44 deletions(-) diff --git a/src/form/b-select/b-select.ts b/src/form/b-select/b-select.ts index 0b39a564fb..0c4424a2b4 100644 --- a/src/form/b-select/b-select.ts +++ b/src/form/b-select/b-select.ts @@ -865,7 +865,6 @@ class bSelect extends iInputText implements iOpenToggle, iItems { protected override initModEvents(): void { super.initModEvents(); - iOpenToggle.initModEvents(this); this.sync.mod('native', 'native', Boolean); @@ -873,6 +872,46 @@ class bSelect extends iInputText implements iOpenToggle, iItems { this.sync.mod('opened', 'multiple', Boolean); } + /** + * Returns a dictionary with configurations for the `v-aria` directive used as a combobox + * @param role + */ + protected getAriaConfig(role: 'combobox'): Dictionary; + + /** + * Returns a dictionary with configurations for the `v-aria` directive used as an option + * + * @param role + * @param item - option item data + */ + protected getAriaConfig(role: 'option', item: this['Item']): Dictionary; + + protected getAriaConfig(role: 'combobox' | 'option', item?: this['Item']): Dictionary { + const + isSelected = this.isSelected.bind(this, item?.value); + + const comboboxConfig = { + isMultiple: this.multiple, + '@change': (cb) => this.localEmitter.on('el.mod.set.*.marked.*', ({link}) => cb(link)), + '@close': (cb) => this.on('close', cb), + '@open': (cb) => this.on('open', () => this.$nextTick(() => cb(this.selectedElement))) + }; + + const optionConfig = { + get isSelected() { + return isSelected(); + }, + + '@change': (cb) => this.on('actionChange', () => cb(isSelected())) + }; + + switch (role) { + case 'combobox': return comboboxConfig; + case 'option': return optionConfig; + default: return {}; + } + } + protected override beforeDestroy(): void { super.beforeDestroy(); @@ -932,49 +971,6 @@ class bSelect extends iInputText implements iOpenToggle, iItems { return false; } - /** - * Returns a dictionary with options for aria directive for combobox role - * @param role - */ - protected getAriaConfig(role: 'combobox'): Dictionary; - - /** - * Returns a dictionary with options for aria directive for option role - * - * @param role - * @param item - */ - protected getAriaConfig(role: 'option', item: this['Item']): Dictionary; - - protected getAriaConfig(role: 'combobox' | 'option', item?: this['Item']): Dictionary { - const - event = 'el.mod.set.*.marked.*', - isSelected = this.isSelected.bind(this, item?.value); - - const - comboboxConfig = { - isMultiple: this.multiple, - '@change': (cb) => this.localEmitter.on(event, ({link}) => cb(link)), - '@close': (cb) => this.on('close', cb), - '@open': (cb) => this.on('open', () => { - void this.$nextTick(() => cb(this.selectedElement)); - }) - }; - - const optionConfig = { - get isSelected() { - return isSelected(); - }, - '@change': (cb) => this.on('actionChange', () => cb(isSelected())) - }; - - switch (role) { - case 'combobox': return comboboxConfig; - case 'option': return optionConfig; - default: return {}; - } - } - /** * Handler: typing text into a helper text input to search select options * From 21911c9fd8c2eb65b5bb80236c9450c9b7dbb2c6 Mon Sep 17 00:00:00 2001 From: kobezzza Date: Wed, 17 Aug 2022 09:34:02 +0300 Subject: [PATCH 118/185] chore: updated changelog --- src/form/b-select/CHANGELOG.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/form/b-select/CHANGELOG.md b/src/form/b-select/CHANGELOG.md index f336cc6855..38f06bd26d 100644 --- a/src/form/b-select/CHANGELOG.md +++ b/src/form/b-select/CHANGELOG.md @@ -13,14 +13,15 @@ Changelog #### :rocket: New Feature -* Added a new directive `v-aria` -* Added a new directive `v-id` -* Added `getAriaConfig` * Now the component derives `iAccess` #### :bug: Bug Fix -* Fixed the component to emit `iOpen` events +* Fixed a bug due to which the component did not emit `iOpen` events + +#### :house: Internal + +* Improved component accessibility ## v3.5.3 (2021-10-06) From 846b51a6124d2f57b528bc18066c062fc673c1ad Mon Sep 17 00:00:00 2001 From: kobezzza Date: Wed, 17 Aug 2022 14:06:45 +0300 Subject: [PATCH 119/185] doc: improved doc --- src/core/component/directives/aria/README.md | 113 +++++++++++++++---- 1 file changed, 92 insertions(+), 21 deletions(-) diff --git a/src/core/component/directives/aria/README.md b/src/core/component/directives/aria/README.md index aabae564b4..df3a69195f 100644 --- a/src/core/component/directives/aria/README.md +++ b/src/core/component/directives/aria/README.md @@ -1,39 +1,110 @@ # core/component/directives/aria -This module provides a directive to add aria attributes and logic to elements through single API. +This module provides a directive to add aria attributes and logic to elements through a single API. -## Usage +``` +< div v-aria = {labelledby: dom.getId('title')} + +/// The same as + +< div :aria-labelledby = dom.getId('title') +``` + +The [Aria](https://www.w3.org/TR/wai-aria) specification consists of a set of entities called roles. +For example, [Tablist](https://www.w3.org/TR/wai-aria/#tablist) or [Combobox](https://www.w3.org/TR/wai-aria/#combobox). +Therefore, the directive also consists of many engines, each of which implements a particular role. + +``` +< div v-aria:combobox = {...} +``` + +## Why is this directive needed? + +Accessibility is an important part of a modern web application. +However, the implementation of a particular role can be quite a challenge, due to the presence of a large number of nuances. +On the other hand, there are many unrelated components that can logically implement the same ARIA role. +Therefore, we need the ability to share this code between components and not enforce coupling between them. +In addition, ARIA roles are heavily DOM bound, so we need the ability to inject in the component markup. +It turns out that using the directive is the most optimal solution for this task. + +## List of supported roles + +All roles supported by the directive are located in the `roles` sub-folder. + +Each role is named after the appropriate name from the ARIA specification. +Each role can accept its own set of options, which are described in its documentation. + +* [Combobox](https://www.w3.org/TR/wai-aria/#combobox) +* [Dialog](https://www.w3.org/TR/wai-aria/#dialog) +* [Listbox](https://www.w3.org/TR/wai-aria/#listbox) +* [Option](https://www.w3.org/TR/wai-aria/#option) +* [Tab](https://www.w3.org/TR/wai-aria/#tab) +* [Tablist](https://www.w3.org/TR/wai-aria/#tablist) +* [Tabpanel](https://www.w3.org/TR/wai-aria/#tabpanel) +* [Tree](https://www.w3.org/TR/wai-aria/#tree) +* [Treeitem](https://www.w3.org/TR/wai-aria/#treeitem) +* [Controls](https://www.w3.org/TR/wai-aria/#aria-controls) + +## Available options + +### [label] + +Defines a string value that labels the current element. +See [this](https://www.w3.org/TR/wai-aria/#aria-label) for more information. ``` -< &__foo v-aria.#bla +< input type = text | v-aria = {labelledby: 'Billing Name'} +``` + +### [labelledby] + +Identifies the element (or elements) that labels the current element. +See [this](https://www.w3.org/TR/wai-aria/#aria-labelledby) for more information. -< &__foo v-aria = {label: 'title'} ``` +< #billing + Billing + +< #name + Name -## Available modifiers: +< input type = text | v-aria = {labelledby: 'billing name'} -- .#[string] (ex. '.#title') +< #address + Address -Example +< input type = text | v-aria = {labelledby: 'billing address'} ``` -< v-aria.#title -the same as -< v-aria = {labelledby: dom.getId('title')} +### [description] + +Defines a string value that describes or annotates the current element. +See [this](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes/aria-description) for more information. + +### [describedby] + +Identifies the element (or elements) that describes the object. +See [this](https://www.w3.org/TR/wai-aria/#aria-describedby) for more information. + ``` +< button v-aria = {describedby: 'trash-desc'} + Move to trash -## Available values: -Parameters passed to the directive are expected to always be object type. Any directive handle common keys: -- label -Expects string as 'title' to the specified element +< p#trash-desc + Items in the trash will be permanently removed after 30 days. +``` -- labelledby -Expects string as an id of the element. This element is a 'title' of to the specified element +## Modifiers -- description -Expects string as expanded 'description' to the specified element +### `#` -- describedby -Expects string as an id of the element. This element is an expanded 'description' to the specified element +This modifier represents a snippet for more convenient setting of the `labelledby` attribute. +Note that the identifier passed in this way is automatically associated with the component within which the directive is used. -Also, there are specific role keys. For info go to [`core/component/directives/role-engines/`](core/component/directives/role-engines/). +``` +< div v-aria.#title + +/// The same as + +< div v-aria = {labelledby: dom.getId('title')} +``` From 145b4ebd0114903d0ee99f9b46d1bf4349963ac5 Mon Sep 17 00:00:00 2001 From: kobezzza Date: Wed, 17 Aug 2022 14:07:08 +0300 Subject: [PATCH 120/185] chore: updated versrion wildcard --- src/core/component/render-function/CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/component/render-function/CHANGELOG.md b/src/core/component/render-function/CHANGELOG.md index b80bbe6ed6..78b125050e 100644 --- a/src/core/component/render-function/CHANGELOG.md +++ b/src/core/component/render-function/CHANGELOG.md @@ -9,7 +9,7 @@ Changelog > - :house: [Internal] > - :nail_care: [Polish] -## v3.??.? (2022-??-??) +## v3.?.? (2022-??-??) #### :bug: Bug Fix From 05794abfdd99af01db0e38f364449c7cdc7a0e35 Mon Sep 17 00:00:00 2001 From: kobezzza Date: Wed, 17 Aug 2022 14:12:27 +0300 Subject: [PATCH 121/185] doc: added a new example --- src/core/component/directives/aria/README.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/core/component/directives/aria/README.md b/src/core/component/directives/aria/README.md index df3a69195f..375596c8c0 100644 --- a/src/core/component/directives/aria/README.md +++ b/src/core/component/directives/aria/README.md @@ -81,6 +81,14 @@ See [this](https://www.w3.org/TR/wai-aria/#aria-labelledby) for more information Defines a string value that describes or annotates the current element. See [this](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes/aria-description) for more information. +``` +< div role = application | v-aria = {label: 'calendar', decription: 'Game schedule for the Boston Red Sox 2021 Season'} + < h1 + Red Sox 2021 + + ... +``` + ### [describedby] Identifies the element (or elements) that describes the object. From 27bd6c2fa2f01d6dfb19cdf5a5544441f34584a8 Mon Sep 17 00:00:00 2001 From: Aleksandr Bunin Date: Wed, 17 Aug 2022 13:38:16 +0300 Subject: [PATCH 122/185] fix iAccess --- .../aria/roles-engines/tab/index.ts | 2 +- .../aria/roles-engines/treeitem/index.ts | 6 +- src/traits/i-access/CHANGELOG.md | 10 +- src/traits/i-access/README.md | 104 +++++++++++++++++- src/traits/i-access/i-access.ts | 34 +++--- 5 files changed, 127 insertions(+), 29 deletions(-) diff --git a/src/core/component/directives/aria/roles-engines/tab/index.ts b/src/core/component/directives/aria/roles-engines/tab/index.ts index 4f866e7bb0..2cbe0d3be5 100644 --- a/src/core/component/directives/aria/roles-engines/tab/index.ts +++ b/src/core/component/directives/aria/roles-engines/tab/index.ts @@ -75,7 +75,7 @@ export class TabEngine extends AriaRoleEngine { */ protected moveFocusToLastTab(): void { const - tabs = this.ctx?.findAllFocusableElements(); + tabs = this.ctx?.findFocusableElements(); if (tabs == null) { return; diff --git a/src/core/component/directives/aria/roles-engines/treeitem/index.ts b/src/core/component/directives/aria/roles-engines/treeitem/index.ts index 995317384d..a02a0e6905 100644 --- a/src/core/component/directives/aria/roles-engines/treeitem/index.ts +++ b/src/core/component/directives/aria/roles-engines/treeitem/index.ts @@ -143,7 +143,7 @@ export class TreeitemEngine extends AriaRoleEngine { */ protected setFocusToLastItem(): void { const - items = this.ctx?.findAllFocusableElements(this.params.rootElement); + items = this.ctx?.findFocusableElements(this.params.rootElement); if (items == null) { return; @@ -153,9 +153,7 @@ export class TreeitemEngine extends AriaRoleEngine { lastItem: CanUndef; for (const item of items) { - if (item.offsetWidth > 0 || item.offsetHeight > 0) { - lastItem = item; - } + lastItem = item; } if (lastItem != null) { diff --git a/src/traits/i-access/CHANGELOG.md b/src/traits/i-access/CHANGELOG.md index 5823c3f158..dda083d6fa 100644 --- a/src/traits/i-access/CHANGELOG.md +++ b/src/traits/i-access/CHANGELOG.md @@ -13,10 +13,12 @@ Changelog #### :rocket: New Feature -* Added `removeAllFromTabSequence` -* Added `restoreAllToTabSequence` -* Added `getNextFocusableElement` -* Added `is` +* Added a new method `removeAllFromTabSequence` +* Added a new method `restoreAllToTabSequence` +* Added a new method `getNextFocusableElement` +* Added a new method `findFocusableElement` +* Added a new method `findFocusableElements` +* Added a new static method `is` ## v3.0.0-rc.211 (2021-07-21) diff --git a/src/traits/i-access/README.md b/src/traits/i-access/README.md index a28a6ad64e..69b4b39305 100644 --- a/src/traits/i-access/README.md +++ b/src/traits/i-access/README.md @@ -80,7 +80,7 @@ The trait specifies a getter to determine when the component in focus or not. ### isFocused -True if the component in focus. +True if the component is in focus. The getter has the default implementation via a static method `iAccess.isFocused`. ```typescript @@ -98,6 +98,21 @@ export default class bButton implements iAccess { The trait specifies a bunch of methods to implement. +### is + +True if the component realize `iAccess` trait. + +```typescript +import iAccess from 'traits/i-access/i-access'; + +export default class bButton implements iAccess { + /** @see iAccess.is */ + is(): boolean { + return iAccess.is(this); + } +} +``` + ### enable Enables the component. @@ -162,6 +177,93 @@ export default class bButton implements iAccess { } ``` +### removeAllFromTabSequence + +Removes all children of the specified element that can be focused from the Tab toggle sequence. +In effect, these elements are set to -1 for the tabindex attribute. +The method has the default implementation. + +```typescript +import iAccess from 'traits/i-access/i-access'; + +export default class bButton implements iAccess { + /** @see iAccess.removeAllFromTabSequence */ + removeAllFromTabSequence(): boolean { + return iAccess.removeAllFromTabSequence(this); + } +} +``` + +### restoreAllToTabSequence + +Restores all children of the specified element that can be focused to the Tab toggle sequence. +This method is used to restore the state of elements to the state they had before `removeAllFromTabSequence` was +applied. +The method has the default implementation. + +```typescript +import iAccess from 'traits/i-access/i-access'; + +export default class bButton implements iAccess { + /** @see iAccess.restoreAllToTabSequence */ + restoreAllToTabSequence(): boolean { + return iAccess.restoreAllToTabSequence(this); + } +} +``` + +### getNextFocusableElement + +Returns the next (or previous) element to which focus will be switched by pressing Tab. +The method takes a "step" parameter, i.e. you can control the Tab sequence direction. For example, +by setting the step to `-1` you will get an element that will be switched to focus by pressing Shift+Tab. +The method has the default implementation. + +```typescript +import iAccess from 'traits/i-access/i-access'; + +export default class bButton implements iAccess { + /** @see iAccess.getNextFocusableElement */ + getNextFocusableElement(): AccessibleElement | null { + return iAccess.getNextFocusableElement(this); + } +} +``` + +### findFocusableElement + +Finds the first non-disabled visible focusable element from the passed context to search and returns it. +The element that is the search context is also taken into account in the search. +The method has the default implementation. + +```typescript +import iAccess from 'traits/i-access/i-access'; + +export default class bButton implements iAccess { + /** @see iAccess.findFocusableElement */ + findFocusableElement(): AccessibleElement | null { + return iAccess.findFocusableElement(this); + } +} +``` + +### findFocusableElements + +Finds all non-disabled visible focusable elements and returns an iterator with the found ones. +The element that is the search context is also taken into account in the search. +The method has the default implementation. + +```typescript +import iAccess from 'traits/i-access/i-access'; + +export default class bButton implements iAccess { + /** @see iAccess.findFocusableElements */ + findFocusableElements(): IterableIterator { + return iAccess.findFocusableElements(this); + } +} +``` + ## Helpers The trait provides a bunch of helper functions to initialize event listeners. diff --git a/src/traits/i-access/i-access.ts b/src/traits/i-access/i-access.ts index c3860c74d5..bbdcfc4a47 100644 --- a/src/traits/i-access/i-access.ts +++ b/src/traits/i-access/i-access.ts @@ -191,7 +191,7 @@ export default abstract class iAccess { } const - focusableEls = this.findAllFocusableElements(component, searchCtx); + focusableEls = this.findFocusableElements(component, searchCtx); for (const el of focusableEls) { if (!el.hasAttribute('data-tabindex')) { @@ -244,18 +244,10 @@ export default abstract class iAccess { } const - focusableEls = >this.findAllFocusableElements(component, searchCtx), - visibleFocusableEls: AccessibleElement[] = []; + focusableEls = this.findFocusableElements(component, searchCtx), + visibleFocusableEls: AccessibleElement[] = [...focusableEls]; - for (const el of focusableEls) { - if ( - el.offsetWidth > 0 || - el.offsetHeight > 0 || - el === document.activeElement - ) { - visibleFocusableEls.push(el); - } - } + visibleFocusableEls.sort((el1, el2) => el2.tabIndex - el1.tabIndex); const index = visibleFocusableEls.indexOf(document.activeElement); @@ -271,7 +263,7 @@ export default abstract class iAccess { static findFocusableElement: AddSelf = (component, searchCtx?): AccessibleElement | null => { const - search = this.findAllFocusableElements(component, searchCtx).next(); + search = this.findFocusableElements(component, searchCtx).next(); if (search.done) { return null; @@ -280,8 +272,8 @@ export default abstract class iAccess { return search.value; }; - /** @see [[iAccess.findAllFocusableElements]] */ - static findAllFocusableElements: AddSelf = + /** @see [[iAccess.findFocusableElements]] */ + static findFocusableElements: AddSelf = (component, searchCtx = component.$el): IterableIterator => { const accessibleEls = searchCtx?.querySelectorAll(FOCUSABLE_SELECTOR); @@ -308,7 +300,11 @@ export default abstract class iAccess { iter: IterableIterator ): IterableIterator { for (const el of iter) { - if (!el.hasAttribute('disabled')) { + if ( + !el.hasAttribute('disabled') || + el.getAttribute('visibility') !== 'hidden' || + el.getAttribute('display') !== 'none' + ) { yield el; } } @@ -409,7 +405,7 @@ export default abstract class iAccess { } /** - * Finds the first non-disabled focusable element from the passed context to search and returns it. + * Finds the first non-disabled visible focusable element from the passed context to search and returns it. * The element that is the search context is also taken into account in the search. * * @param [searchCtx] - a context to search, if not set, the component root element will be used @@ -419,12 +415,12 @@ export default abstract class iAccess { } /** - * Finds all non-disabled focusable elements and returns an iterator with the found ones. + * Finds all non-disabled visible focusable elements and returns an iterator with the found ones. * The element that is the search context is also taken into account in the search. * * @param [searchCtx] - a context to search, if not set, the component root element will be used */ - findAllFocusableElements(searchCtx?: Element): IterableIterator { + findFocusableElements(searchCtx?: Element): IterableIterator { return Object.throw(); } } From d3acc0dfeb2c69dbb09444143c7cca3d22d3450e Mon Sep 17 00:00:00 2001 From: Aleksandr Bunin Date: Wed, 17 Aug 2022 14:29:42 +0300 Subject: [PATCH 123/185] fix changlelog --- src/form/b-checkbox/CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/form/b-checkbox/CHANGELOG.md b/src/form/b-checkbox/CHANGELOG.md index 38d8ea8b5c..c75caea52f 100644 --- a/src/form/b-checkbox/CHANGELOG.md +++ b/src/form/b-checkbox/CHANGELOG.md @@ -15,6 +15,10 @@ Changelog * Added `label` tag with `for` attribute to label and `id` to nativeInput in template +#### :house: Internal + +* Improved component accessibility + ## v3.0.0-rc.199 (2021-06-16) #### :boom: Breaking Change From 09827ff19c3dd6ab4c2809cf4f0613ddfe395e0c Mon Sep 17 00:00:00 2001 From: Aleksandr Bunin Date: Wed, 17 Aug 2022 17:15:52 +0300 Subject: [PATCH 124/185] b-list doc --- src/base/b-list/README.md | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/src/base/b-list/README.md b/src/base/b-list/README.md index b8f089efa9..666afa3c82 100644 --- a/src/base/b-list/README.md +++ b/src/base/b-list/README.md @@ -10,13 +10,13 @@ If you need a more complex layout, provide it via a slot or by using `item/itemP * The component extends [[iData]]. -* The component implements [[iVisible]], [[iWidth]], [[iItems]] traits. +* The component implements [[iAccess]], [[iVisible]], [[iWidth]], [[iItems]] traits. * The component is used as functional if there is no provided the `dataProvider` prop. * The component supports tooltips. -* The component uses `aria` attributes. +* The component is accessible. * By default, the list will be created using `
      ` and `
    • ` tags. @@ -243,6 +243,10 @@ By default, if the component is switched to the `multiple` mode, this value is s Initial additional attributes are provided to an "internal" (native) list tag. +#### [orientation = `horizontal`] + +The component view orientation. + ### Fields #### items @@ -333,3 +337,14 @@ class Test extends iData { } } ``` + +## Accessibility + +If the component is used as a list of tabs it will implement an ARIA role [tablist](https://www.w3.org/TR/wai-aria/#tablist). +All the accessible functionality included in this [widget](https://www.w3.org/WAI/ARIA/apg/patterns/tabpanel/) is supported. + +When the component is used as a list of links it bases on internal HTML semantics of list tags. + +The component includes the following roles: +- [tablist](https://www.w3.org/TR/wai-aria/#tablist) +- [tab](https://www.w3.org/TR/wai-aria/#tab) From ea332b62f709e08cbf41ec8bfad4333b8c98ed11 Mon Sep 17 00:00:00 2001 From: Aleksandr Bunin Date: Wed, 17 Aug 2022 17:39:28 +0300 Subject: [PATCH 125/185] b-tree doc --- src/base/b-tree/README.md | 25 ++++++++++++++++++++----- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/src/base/b-tree/README.md b/src/base/b-tree/README.md index e7d5947246..0e22c962fd 100644 --- a/src/base/b-tree/README.md +++ b/src/base/b-tree/README.md @@ -6,10 +6,12 @@ This module provides a component to render a recursive list of elements. * The component extends [[iData]]. -* The component implements the [[iItems]] trait. +* The component implements the [[iAccess]], [[iItems]] trait. * By default, the root tag of the component is `
      `. +* The component is accessible. + ## Features * Recursive rendering of any components. @@ -197,11 +199,11 @@ Also, you can see the implemented traits or the parent component. ### Props -### folded +#### [folded = `true`] If true, then all nested elements are folded by default. -### renderFilter +#### [renderFilter] A common filter to render items via `asyncRender`. It is used to optimize the process of rendering items. @@ -210,7 +212,7 @@ It is used to optimize the process of rendering items. < b-tree :item = 'b-checkbox' | :items = listOfItems | :renderFilter = () => async.idle() ``` -### nestedRenderFilter +#### [nestedRenderFilter] A filter to render nested items via `asyncRender`. It is used to optimize the process of rendering child items. @@ -219,10 +221,23 @@ It is used to optimize the process of rendering child items. < b-tree :item = 'b-checkbox' | :items = listOfItems | :nestedRenderFilter = () => async.idle() ``` -### renderChunks +#### [renderChunks = `5`] Number of chunks to render per tick via `asyncRender`. ``` < b-tree :item = 'b-checkbox' | :items = listOfItems | :renderChunks = 3 ``` + +#### [orientation = `horizontal`] + +The component view orientation. + +## Accessibility + +The component implements an ARIA role [tree](https://www.w3.org/TR/wai-aria/#tree). +Only basic accessible functionality (without optional keyboard combinations) included in this [widget](https://www.w3.org/WAI/ARIA/apg/patterns/treeview/) is supported. + +The component includes the following roles: +- [tree](https://www.w3.org/TR/wai-aria/#tree) +- [treeitem](https://www.w3.org/TR/wai-aria/#treeitem) From 99f668463d3feba34f2e4f9e5f1c1854013b21f3 Mon Sep 17 00:00:00 2001 From: Aleksandr Bunin Date: Wed, 17 Aug 2022 17:59:38 +0300 Subject: [PATCH 126/185] b-window doc --- src/base/b-window/README.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/base/b-window/README.md b/src/base/b-window/README.md index 66652ec797..0a4fcbd5f1 100644 --- a/src/base/b-window/README.md +++ b/src/base/b-window/README.md @@ -18,6 +18,8 @@ This module provides a component to create a modal window. * By default, the root tag of the component is `
      `. +* The component is accessible. + ## Modifiers | Name | Description | Values | Default | @@ -263,3 +265,8 @@ When opening the window, you can specify at which `stage` the component should s < span @click = $refs.window.open('loading') Open the window ``` + +## Accessibility + +The component implements an ARIA role [dialog](https://www.w3.org/TR/wai-aria/#dialog). +All the accessible functionality included in this [widget](https://www.w3.org/WAI/ARIA/apg/patterns/dialogmodal/) is supported. From 4c202f9e9c0f53afda8e20197c0dd77f43ceb43f Mon Sep 17 00:00:00 2001 From: Aleksandr Bunin Date: Wed, 17 Aug 2022 18:02:44 +0300 Subject: [PATCH 127/185] demo changelog --- src/pages/p-v4-components-demo/b-v4-component-demo/CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/p-v4-components-demo/b-v4-component-demo/CHANGELOG.md b/src/pages/p-v4-components-demo/b-v4-component-demo/CHANGELOG.md index edbedc52a3..43fcf88a8d 100644 --- a/src/pages/p-v4-components-demo/b-v4-component-demo/CHANGELOG.md +++ b/src/pages/p-v4-components-demo/b-v4-component-demo/CHANGELOG.md @@ -11,7 +11,7 @@ Changelog ## v3.?.? (2022-0?-??) -#### :rocket: New Feature +#### :house: Internal * Added a new directive `v-id` From 9b9410c857378ac7a219d285cd385e030e74e6e3 Mon Sep 17 00:00:00 2001 From: Aleksandr Bunin Date: Wed, 17 Aug 2022 18:18:07 +0300 Subject: [PATCH 128/185] b-select doc --- src/base/b-list/README.md | 3 +++ src/form/b-select/CHANGELOG.md | 2 ++ src/form/b-select/README.md | 12 ++++++++++++ 3 files changed, 17 insertions(+) diff --git a/src/base/b-list/README.md b/src/base/b-list/README.md index 666afa3c82..4160d137a4 100644 --- a/src/base/b-list/README.md +++ b/src/base/b-list/README.md @@ -348,3 +348,6 @@ When the component is used as a list of links it bases on internal HTML semantic The component includes the following roles: - [tablist](https://www.w3.org/TR/wai-aria/#tablist) - [tab](https://www.w3.org/TR/wai-aria/#tab) + +The widget should also include [tabpanel](https://www.w3.org/TR/wai-aria/#tabpanel) role which is a block that contains the content of each tab. +But the component does not provide such block. So the 'connection' with other component should be set with the help of [`v-aria:controls`](core/component/directives/aria/aria-engines/controls/README.md) diff --git a/src/form/b-select/CHANGELOG.md b/src/form/b-select/CHANGELOG.md index 38f06bd26d..ca67cb2ba4 100644 --- a/src/form/b-select/CHANGELOG.md +++ b/src/form/b-select/CHANGELOG.md @@ -23,6 +23,8 @@ Changelog * Improved component accessibility +* Changed keyboard keys handling according to ARIA specification + ## v3.5.3 (2021-10-06) #### :bug: Bug Fix diff --git a/src/form/b-select/README.md b/src/form/b-select/README.md index e6c7fe2d46..01a493279f 100644 --- a/src/form/b-select/README.md +++ b/src/form/b-select/README.md @@ -17,6 +17,8 @@ The select can contain multiple values. * The component has `skeletonMarker`. +* The component is accessible. + ## Modifiers | Name | Description | Values | Default | @@ -384,3 +386,13 @@ Checks that a component value must be filled. size 20px background-image url("assets/my-icon.svg") ``` + +## Accessibility + +The component implements an ARIA role [combobox](https://www.w3.org/TR/wai-aria/#combobox). +Only basic accessible functionality (without optional keyboard combinations) included in this [widget](https://www.w3.org/WAI/ARIA/apg/patterns/combobox/) is supported. + +The component includes the following roles: +- [combobox](https://www.w3.org/TR/wai-aria/#combobox) +- [listbox](https://www.w3.org/TR/wai-aria/#listbox) +- [option](https://www.w3.org/TR/wai-aria/#option) From 2c169b9e54825898c950369124c71140d4b9670c Mon Sep 17 00:00:00 2001 From: Aleksandr Bunin Date: Wed, 17 Aug 2022 18:58:06 +0300 Subject: [PATCH 129/185] changlog about v-id --- src/form/b-button/CHANGELOG.md | 4 ++-- .../p-v4-components-demo/b-v4-component-demo/CHANGELOG.md | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/form/b-button/CHANGELOG.md b/src/form/b-button/CHANGELOG.md index 95f35cb6b7..e7f3ee866c 100644 --- a/src/form/b-button/CHANGELOG.md +++ b/src/form/b-button/CHANGELOG.md @@ -11,9 +11,9 @@ Changelog ## v3.?.? (2022-0?-??) -#### :rocket: New Feature +#### :house: Internal -* Added a new directive `v-id` +* Now element's `id` attribute is added with `v-id` directive ## v3.0.0-rc.211 (2021-07-21) diff --git a/src/pages/p-v4-components-demo/b-v4-component-demo/CHANGELOG.md b/src/pages/p-v4-components-demo/b-v4-component-demo/CHANGELOG.md index 43fcf88a8d..a8389d7ed5 100644 --- a/src/pages/p-v4-components-demo/b-v4-component-demo/CHANGELOG.md +++ b/src/pages/p-v4-components-demo/b-v4-component-demo/CHANGELOG.md @@ -13,7 +13,7 @@ Changelog #### :house: Internal -* Added a new directive `v-id` +* Now element's `id` attribute is added with `v-id` directive ## v3.0.0-rc.37 (2020-07-20) From d5fcad94e300e15105723715e7949479432829a4 Mon Sep 17 00:00:00 2001 From: Aleksandr Bunin Date: Wed, 17 Aug 2022 19:00:47 +0300 Subject: [PATCH 130/185] b-checkbox changlog --- src/form/b-checkbox/CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/form/b-checkbox/CHANGELOG.md b/src/form/b-checkbox/CHANGELOG.md index c75caea52f..e63c710489 100644 --- a/src/form/b-checkbox/CHANGELOG.md +++ b/src/form/b-checkbox/CHANGELOG.md @@ -13,7 +13,7 @@ Changelog #### :bug: Bug Fix -* Added `label` tag with `for` attribute to label and `id` to nativeInput in template +* The `for` attribute has been added to the label to link to the checkbox #### :house: Internal From f45169e9d97fd2b49cde7ef9ca4ed429495512ff Mon Sep 17 00:00:00 2001 From: Aleksandr Bunin Date: Thu, 18 Aug 2022 01:02:19 +0300 Subject: [PATCH 131/185] add static property params --- .../component/directives/aria/aria-setter.ts | 51 +++++++++++++------ .../aria/roles-engines/combobox/index.ts | 5 ++ .../aria/roles-engines/controls/index.ts | 5 ++ .../aria/roles-engines/interface.ts | 5 ++ .../aria/roles-engines/option/index.ts | 5 ++ .../aria/roles-engines/tab/index.ts | 5 ++ .../aria/roles-engines/tablist/index.ts | 5 ++ .../aria/roles-engines/tree/index.ts | 5 ++ .../aria/roles-engines/treeitem/index.ts | 5 ++ .../directives/aria/test/unit/simple.ts | 26 ++++++---- 10 files changed, 90 insertions(+), 27 deletions(-) diff --git a/src/core/component/directives/aria/aria-setter.ts b/src/core/component/directives/aria/aria-setter.ts index a88f7a9bfe..ffa8002c20 100644 --- a/src/core/component/directives/aria/aria-setter.ts +++ b/src/core/component/directives/aria/aria-setter.ts @@ -9,7 +9,7 @@ import Async from 'core/async'; import type iBlock from 'super/i-block/i-block'; -import * as ariaRoles from 'core/component/directives/aria/roles-engines'; +import * as roles from 'core/component/directives/aria/roles-engines'; import type { DirectiveOptions } from 'core/component/directives/aria/interface'; import type { AriaRoleEngine, EngineOptions } from 'core/component/directives/aria/roles-engines'; @@ -32,6 +32,11 @@ export default class AriaSetter { */ role: CanUndef; + /** + * Role engine params list + */ + roleParams: CanUndef; + constructor(options: DirectiveOptions) { this.options = options; this.async = new Async(); @@ -44,7 +49,8 @@ export default class AriaSetter { * Initiates the base logic of the directive */ init(): void { - this.setAriaLabel(); + this.setAriaLabelledBy(); + this.setAriaAttributes(); this.addEventHandlers(); this.role?.init(); @@ -84,7 +90,8 @@ export default class AriaSetter { engine = this.createEngineName(role), options = this.createRoleOptions(); - this.role = new ariaRoles[engine](options); + this.role = new roles[engine](options); + this.roleParams = roles[engine].params; } /** @@ -113,10 +120,9 @@ export default class AriaSetter { } /** - * Sets aria-label, aria-labelledby, aria-description and aria-describedby attributes to the element - * from directive parameters + * Sets aria-labelledby attribute to the element from directive parameters */ - protected setAriaLabel(): void { + protected setAriaLabelledBy(): void { const {vnode, binding, el} = this.options, {dom} = Object.cast(vnode.fakeContext), @@ -134,18 +140,30 @@ export default class AriaSetter { el.setAttribute('aria-labelledby', id); } - if (params.label != null) { - el.setAttribute('aria-label', params.label); + if (params.labelledby == null) { + return; + } + + if (Object.isArray(params.labelledby)) { + el.setAttribute('aria-labelledby', params.labelledby.join(' ')); - } else if (params.labelledby != null) { + } else { el.setAttribute('aria-labelledby', params.labelledby); } + } - if (params.description != null) { - el.setAttribute('aria-description', params.description); + /** + * Sets aria attributes from passed params except `aria-labelledby` + */ + protected setAriaAttributes(): void { + const + {el, binding} = this.options, + params = binding.value; - } else if (params.describedby != null) { - el.setAttribute('aria-describedby', params.describedby); + for (const key in params) { + if (!this.roleParams?.includes(key) && key !== 'labelledby') { + el.setAttribute(`aria-${key}`, params[key]); + } } } @@ -161,9 +179,6 @@ export default class AriaSetter { const params = this.options.binding.value; - const - getCallbackName = (key: string) => `on-${key.slice(1)}`.camelize(false); - for (const key in params) { if (key.startsWith('@')) { const @@ -191,5 +206,9 @@ export default class AriaSetter { } } } + + function getCallbackName(key: string) { + return `on-${key.slice(1)}`.camelize(false); + } } } diff --git a/src/core/component/directives/aria/roles-engines/combobox/index.ts b/src/core/component/directives/aria/roles-engines/combobox/index.ts index 708fab3488..f9713248bb 100644 --- a/src/core/component/directives/aria/roles-engines/combobox/index.ts +++ b/src/core/component/directives/aria/roles-engines/combobox/index.ts @@ -26,6 +26,11 @@ export class ComboboxEngine extends AriaRoleEngine { /** @see [[AriaRoleEngine.Ctx]] */ override Ctx!: ComponentInterface & iAccess; + /** + * Engine params list + */ + static override params: string[] = ['isMultiple', '@change', '@open', '@close']; + constructor(options: EngineOptions) { super(options); diff --git a/src/core/component/directives/aria/roles-engines/controls/index.ts b/src/core/component/directives/aria/roles-engines/controls/index.ts index 7fed253895..41bcf25dd5 100644 --- a/src/core/component/directives/aria/roles-engines/controls/index.ts +++ b/src/core/component/directives/aria/roles-engines/controls/index.ts @@ -15,6 +15,11 @@ export class ControlsEngine extends AriaRoleEngine { */ override params: ControlsParams; + /** + * Engine params + */ + static override params: string[]; + constructor(options: EngineOptions) { super(options); diff --git a/src/core/component/directives/aria/roles-engines/interface.ts b/src/core/component/directives/aria/roles-engines/interface.ts index 26f1e50576..775c6ff3a0 100644 --- a/src/core/component/directives/aria/roles-engines/interface.ts +++ b/src/core/component/directives/aria/roles-engines/interface.ts @@ -45,6 +45,11 @@ export abstract class AriaRoleEngine { */ async: CanUndef; + /** + * Directive expected params list + */ + static params: string[]; + constructor({el, ctx, modifiers, params, async}: EngineOptions) { this.el = el; this.ctx = ctx; diff --git a/src/core/component/directives/aria/roles-engines/option/index.ts b/src/core/component/directives/aria/roles-engines/option/index.ts index 49322476a5..10fcb4c262 100644 --- a/src/core/component/directives/aria/roles-engines/option/index.ts +++ b/src/core/component/directives/aria/roles-engines/option/index.ts @@ -15,6 +15,11 @@ export class OptionEngine extends AriaRoleEngine { */ override params: OptionParams; + /** + * Engine params list + */ + static override params: string[] = ['isSelected', '@change']; + constructor(options: EngineOptions) { super(options); diff --git a/src/core/component/directives/aria/roles-engines/tab/index.ts b/src/core/component/directives/aria/roles-engines/tab/index.ts index 2cbe0d3be5..513b96cd0a 100644 --- a/src/core/component/directives/aria/roles-engines/tab/index.ts +++ b/src/core/component/directives/aria/roles-engines/tab/index.ts @@ -24,6 +24,11 @@ export class TabEngine extends AriaRoleEngine { /** @see [[AriaRoleEngine.ctx]] */ override ctx?: iBlock & iAccess; + /** + * Engine params list + */ + static override params: string[] = ['isFirst', 'isSelected', 'hasDefaultSelectedTabs', 'orientation', '@change']; + constructor(options: EngineOptions) { super(options); diff --git a/src/core/component/directives/aria/roles-engines/tablist/index.ts b/src/core/component/directives/aria/roles-engines/tablist/index.ts index 83451227bd..5aa6531fba 100644 --- a/src/core/component/directives/aria/roles-engines/tablist/index.ts +++ b/src/core/component/directives/aria/roles-engines/tablist/index.ts @@ -15,6 +15,11 @@ export class TablistEngine extends AriaRoleEngine { */ override params: TablistParams; + /** + * Engine params list + */ + static override params: string[] = ['isMultiple', 'orientation']; + constructor(options: EngineOptions) { super(options); diff --git a/src/core/component/directives/aria/roles-engines/tree/index.ts b/src/core/component/directives/aria/roles-engines/tree/index.ts index 483901f2f7..9bb4cd98e4 100644 --- a/src/core/component/directives/aria/roles-engines/tree/index.ts +++ b/src/core/component/directives/aria/roles-engines/tree/index.ts @@ -15,6 +15,11 @@ export class TreeEngine extends AriaRoleEngine { */ override params: TreeParams; + /** + * Engine params list + */ + static override params: string[] = ['isRoot', 'orientation', '@change']; + constructor(options: EngineOptions) { super(options); diff --git a/src/core/component/directives/aria/roles-engines/treeitem/index.ts b/src/core/component/directives/aria/roles-engines/treeitem/index.ts index a02a0e6905..a96d27a0bb 100644 --- a/src/core/component/directives/aria/roles-engines/treeitem/index.ts +++ b/src/core/component/directives/aria/roles-engines/treeitem/index.ts @@ -24,6 +24,11 @@ export class TreeitemEngine extends AriaRoleEngine { /** @see [[AriaRoleEngine.ctx]] */ override ctx?: iBlock & iAccess; + /** + * Engine params list + */ + static override params: string[] = ['isFirstRootItem', 'isExpandable', 'isExpanded', 'orientation', 'rootElement', 'toggleFold']; + constructor(options: EngineOptions) { super(options); diff --git a/src/core/component/directives/aria/test/unit/simple.ts b/src/core/component/directives/aria/test/unit/simple.ts index ff9cab8333..592cef7b24 100644 --- a/src/core/component/directives/aria/test/unit/simple.ts +++ b/src/core/component/directives/aria/test/unit/simple.ts @@ -17,7 +17,7 @@ test.describe('v-aria', () => { await demoPage.goto(); }); - test('aria-label is added', async ({page}) => { + test('one aria attribute is added', async ({page}) => { const target = await init(page, {'v-aria': {label: 'bla'}}); test.expect( @@ -25,28 +25,32 @@ test.describe('v-aria', () => { ).toBe('bla'); }); - test('aria-labelledby is added', async ({page}) => { - const target = await init(page, {'v-aria': {labelledby: 'bla'}}); + test('two aria attributes are added', async ({page}) => { + const target = await init(page, {'v-aria': {describedby: 'bla', label: 'foo'}}); test.expect( - await target.evaluate((ctx) => ctx.$el?.getAttribute('aria-labelledby')) + await target.evaluate((ctx) => ctx.$el?.getAttribute('aria-describedby')) ).toBe('bla'); + + test.expect( + await target.evaluate((ctx) => ctx.$el?.getAttribute('aria-label')) + ).toBe('foo'); }); - test('aria-description is added', async ({page}) => { - const target = await init(page, {'v-aria': {description: 'bla'}}); + test('aria-labelledby is added by string', async ({page}) => { + const target = await init(page, {'v-aria': {labelledby: 'bla'}}); test.expect( - await target.evaluate((ctx) => ctx.$el?.getAttribute('aria-description')) + await target.evaluate((ctx) => ctx.$el?.getAttribute('aria-labelledby')) ).toBe('bla'); }); - test('aria-describedby is added', async ({page}) => { - const target = await init(page, {'v-aria': {describedby: 'bla'}}); + test('aria-labelledby is added by array', async ({page}) => { + const target = await init(page, {'v-aria': {labelledby: ['bla', 'bar', 'foo']}}); test.expect( - await target.evaluate((ctx) => ctx.$el?.getAttribute('aria-describedby')) - ).toBe('bla'); + await target.evaluate((ctx) => ctx.$el?.getAttribute('aria-labelledby')) + ).toBe('bla bar foo'); }); test('aria-labelledby sugar syntax', async ({page}) => { From 4cd32fe6c1d9d4a0479f46f5829d2cb849f4420d Mon Sep 17 00:00:00 2001 From: Aleksandr Bunin Date: Thu, 18 Aug 2022 01:02:39 +0300 Subject: [PATCH 132/185] fix iaccess --- src/traits/i-access/i-access.ts | 36 ++++++++++++++++++++++----------- 1 file changed, 24 insertions(+), 12 deletions(-) diff --git a/src/traits/i-access/i-access.ts b/src/traits/i-access/i-access.ts index bbdcfc4a47..45c88979ea 100644 --- a/src/traits/i-access/i-access.ts +++ b/src/traits/i-access/i-access.ts @@ -239,24 +239,32 @@ export default abstract class iAccess { /** @see [[iAccess.getNextFocusableElement]] */ static getNextFocusableElement: AddSelf = (component, step, searchCtx = document.documentElement): AccessibleElement | null => { - if (document.activeElement == null) { + const + {activeElement} = document; + + if (activeElement == null) { return null; } const - focusableEls = this.findFocusableElements(component, searchCtx), - visibleFocusableEls: AccessibleElement[] = [...focusableEls]; + focusableEls = [...this.findFocusableElements(component, searchCtx)], + index = focusableEls.indexOf(activeElement); - visibleFocusableEls.sort((el1, el2) => el2.tabIndex - el1.tabIndex); + if (index < 0) { + return null; + } - const - index = visibleFocusableEls.indexOf(document.activeElement); + if (step > 0) { + const next = focusableEls + .slice(index + 1) + .find((el) => el.tabIndex > 0); - if (index > -1) { - return visibleFocusableEls[index + step] ?? null; + if (next != null) { + return next; + } } - return null; + return focusableEls[index + step] ?? null; }; /** @see [[iAccess.findFocusableElement]] */ @@ -300,10 +308,14 @@ export default abstract class iAccess { iter: IterableIterator ): IterableIterator { for (const el of iter) { + const + rect = el.getBoundingClientRect(); + if ( - !el.hasAttribute('disabled') || - el.getAttribute('visibility') !== 'hidden' || - el.getAttribute('display') !== 'none' + !el.hasAttribute('disabled') && + el.getAttribute('visibility') !== 'hidden' && + el.getAttribute('display') !== 'none' && + rect.height > 0 || rect.width > 0 ) { yield el; } From 17fe5449f5cc4b208ba3ff1d3c79126d7731acc4 Mon Sep 17 00:00:00 2001 From: Aleksandr Bunin Date: Thu, 18 Aug 2022 16:26:34 +0300 Subject: [PATCH 133/185] fix iaccess --- .../aria/roles-engines/treeitem/index.ts | 7 +++- .../treeitem/test/unit/treeitem.ts | 6 +-- src/traits/i-access/i-access.ts | 37 ++++++++++++------- 3 files changed, 31 insertions(+), 19 deletions(-) diff --git a/src/core/component/directives/aria/roles-engines/treeitem/index.ts b/src/core/component/directives/aria/roles-engines/treeitem/index.ts index a96d27a0bb..cad2c6ce5a 100644 --- a/src/core/component/directives/aria/roles-engines/treeitem/index.ts +++ b/src/core/component/directives/aria/roles-engines/treeitem/index.ts @@ -137,7 +137,7 @@ export class TreeitemEngine extends AriaRoleEngine { protected setFocusToFirstItem(): void { const firstItem = this.ctx?.findFocusableElement(this.params.rootElement); - +debugger; if (firstItem != null) { this.focusNext(firstItem); } @@ -235,7 +235,10 @@ export class TreeitemEngine extends AriaRoleEngine { break; case KeyCodes.ENTER: - this.params.toggleFold(this.el); + if (this.params.isExpandable) { + this.params.toggleFold(this.el); + } + break; case KeyCodes.HOME: diff --git a/src/core/component/directives/aria/roles-engines/treeitem/test/unit/treeitem.ts b/src/core/component/directives/aria/roles-engines/treeitem/test/unit/treeitem.ts index 00bde5251e..f44947bbf7 100644 --- a/src/core/component/directives/aria/roles-engines/treeitem/test/unit/treeitem.ts +++ b/src/core/component/directives/aria/roles-engines/treeitem/test/unit/treeitem.ts @@ -48,8 +48,6 @@ test.describe('v-aria:treeitem', () => { test('keyboard keys handle on vertical orientation', async ({page}) => { const target = await init(page); - await page.waitForSelector('[role="group"]'); - test.expect( await target.evaluate((ctx) => { if (ctx.unsafe.block == null) { @@ -78,6 +76,8 @@ test.describe('v-aria:treeitem', () => { dis('ArrowUp'); res.push(eq(0)); + dis('Enter'); + res.push(items[0].getAttribute('aria-expanded')); dis('ArrowDown'); dis('Enter'); @@ -106,7 +106,7 @@ test.describe('v-aria:treeitem', () => { return res; }) - ).toEqual([true, true, 'true', 'false', 'true', true, true, 'false', true, true]); + ).toEqual([true, true, null, 'true', 'false', 'true', true, true, 'false', true, true]); }); test('keyboard keys handle on horizontal orientation', async ({page}) => { diff --git a/src/traits/i-access/i-access.ts b/src/traits/i-access/i-access.ts index 45c88979ea..fe8593e930 100644 --- a/src/traits/i-access/i-access.ts +++ b/src/traits/i-access/i-access.ts @@ -247,22 +247,18 @@ export default abstract class iAccess { } const - focusableEls = [...this.findFocusableElements(component, searchCtx)], + focusableEls = [...this.findFocusableElements(component, searchCtx, {native: false})], index = focusableEls.indexOf(activeElement); if (index < 0) { return null; } - if (step > 0) { - const next = focusableEls - .slice(index + 1) - .find((el) => el.tabIndex > 0); - - if (next != null) { - return next; + focusableEls.forEach((el) => { + if (el.tabIndex > 0) { + Object.throw('The tab sequence has an element with tabindex more than 0. The sequence would be different in different browsers. It is strongly recommended not to use tabindexes more than 0.'); } - } + }); return focusableEls[index + step] ?? null; }; @@ -282,7 +278,7 @@ export default abstract class iAccess { /** @see [[iAccess.findFocusableElements]] */ static findFocusableElements: AddSelf = - (component, searchCtx = component.$el): IterableIterator => { + (component, searchCtx = component.$el, opts = {native: true}): IterableIterator => { const accessibleEls = searchCtx?.querySelectorAll(FOCUSABLE_SELECTOR); @@ -314,10 +310,16 @@ export default abstract class iAccess { if ( !el.hasAttribute('disabled') && el.getAttribute('visibility') !== 'hidden' && - el.getAttribute('display') !== 'none' && - rect.height > 0 || rect.width > 0 + el.getAttribute('display') !== 'none' ) { - yield el; + if (!opts.native) { + if (rect.height > 0 || rect.width > 0) { + yield el; + } + + } else { + yield el; + } } } } @@ -429,10 +431,17 @@ export default abstract class iAccess { /** * Finds all non-disabled visible focusable elements and returns an iterator with the found ones. * The element that is the search context is also taken into account in the search. + * Also expects a dictionary with option of filtration invisible elements. + * If native property is set to true, the method filters invisible elements by css properties + * `disabled`, `visible` and `display`. + * Native in false also adds the filtration by element's current visibility on the screen. * * @param [searchCtx] - a context to search, if not set, the component root element will be used + * @param [opts] - dictionary with options of elements' visibility filtration, {native: true} by default */ - findFocusableElements(searchCtx?: Element): IterableIterator { + findFocusableElements< + T extends AccessibleElement = AccessibleElement + >(searchCtx?: Element, opts?: {native: boolean}): IterableIterator { return Object.throw(); } } From db0cbbd7ee360b17a34e8338c7a0aec225aaf389 Mon Sep 17 00:00:00 2001 From: Aleksandr Bunin Date: Thu, 18 Aug 2022 17:50:23 +0300 Subject: [PATCH 134/185] refactoring --- src/base/b-list/b-list.ts | 2 + src/base/b-tree/b-tree.ts | 2 + src/base/b-window/b-window.ts | 2 + .../aria/{aria-setter.ts => adapter.ts} | 86 ++++++------------- src/core/component/directives/aria/index.ts | 10 +-- .../aria/roles-engines/combobox/index.ts | 19 ++-- .../aria/roles-engines/controls/index.ts | 22 ++--- .../aria/roles-engines/dialog/index.ts | 4 +- .../aria/roles-engines/interface.ts | 3 + .../aria/roles-engines/listbox/index.ts | 4 +- .../aria/roles-engines/option/index.ts | 22 ++--- .../aria/roles-engines/tab/index.ts | 22 ++--- .../aria/roles-engines/tablist/index.ts | 22 ++--- .../aria/roles-engines/tabpanel/index.ts | 4 +- .../aria/roles-engines/tree/index.ts | 22 ++--- .../aria/roles-engines/treeitem/index.ts | 26 ++---- src/core/component/directives/index.ts | 2 - src/form/b-checkbox/b-checkbox.ts | 2 + src/form/b-select/b-select.ts | 7 +- 19 files changed, 84 insertions(+), 199 deletions(-) rename src/core/component/directives/aria/{aria-setter.ts => adapter.ts} (68%) diff --git a/src/base/b-list/b-list.ts b/src/base/b-list/b-list.ts index 55a2a48c02..3d6d72a8a5 100644 --- a/src/base/b-list/b-list.ts +++ b/src/base/b-list/b-list.ts @@ -15,6 +15,8 @@ import 'models/demo/list'; //#endif +import 'core/component/directives/aria'; + import symbolGenerator from 'core/symbol'; import SyncPromise from 'core/promise/sync'; diff --git a/src/base/b-tree/b-tree.ts b/src/base/b-tree/b-tree.ts index 1109d07416..8c5fac81a2 100644 --- a/src/base/b-tree/b-tree.ts +++ b/src/base/b-tree/b-tree.ts @@ -15,6 +15,8 @@ import 'models/demo/nested-list'; //#endif +import 'core/component/directives/aria'; + import symbolGenerator from 'core/symbol'; import { derive } from 'core/functools/trait'; diff --git a/src/base/b-window/b-window.ts b/src/base/b-window/b-window.ts index 1b444faaf1..f960ac5cd6 100644 --- a/src/base/b-window/b-window.ts +++ b/src/base/b-window/b-window.ts @@ -11,6 +11,8 @@ * @packageDocumentation */ +import 'core/component/directives/aria'; + import symbolGenerator from 'core/symbol'; import { derive } from 'core/functools/trait'; diff --git a/src/core/component/directives/aria/aria-setter.ts b/src/core/component/directives/aria/adapter.ts similarity index 68% rename from src/core/component/directives/aria/aria-setter.ts rename to src/core/component/directives/aria/adapter.ts index ffa8002c20..8439be4ed9 100644 --- a/src/core/component/directives/aria/aria-setter.ts +++ b/src/core/component/directives/aria/adapter.ts @@ -14,34 +14,30 @@ import type { DirectiveOptions } from 'core/component/directives/aria/interface' import type { AriaRoleEngine, EngineOptions } from 'core/component/directives/aria/roles-engines'; /** - * Class-helper for making base operations for the directive + * An adapter to create an ARIA role instance based on the passed directive options and to add common attributes */ -export default class AriaSetter { +export default class AriaAdapter { /** - * Aria directive options + * Parameters passed from the directive */ - readonly options: DirectiveOptions; + protected readonly options: DirectiveOptions; - /** - * Async instance for aria directive - */ - readonly async: Async; + /** @see [[Async]] */ + protected readonly async: Async = new Async(); /** - * Role engine instance + * An instance of the associated ARIA role */ - role: CanUndef; + protected role: CanUndef; /** * Role engine params list */ - roleParams: CanUndef; + protected roleParams: CanUndef; constructor(options: DirectiveOptions) { this.options = options; - this.async = new Async(); this.setAriaRole(); - this.init(); } @@ -56,18 +52,6 @@ export default class AriaSetter { this.role?.init(); } - /** - * Runs on update directive hook. Removes listeners from component if the component is Functional. - */ - update(): void { - const - ctx = this.options.vnode.fakeContext; - - if (ctx.isFunctional) { - ctx.off(); - } - } - /** * Runs on unbind directive hook. Clears the Async instance. */ @@ -75,48 +59,35 @@ export default class AriaSetter { this.async.clearAll(); } + protected get ctx(): iBlock['unsafe'] { + return Object.cast(this.options.vnode.fakeContext); + } + /** * If the role was passed as a directive argument sets specified engine */ protected setAriaRole(): CanUndef { const - {arg: role} = this.options.binding; + {el, binding} = this.options, + {value, modifiers, arg: role} = binding; if (role == null) { return; } const - engine = this.createEngineName(role), - options = this.createRoleOptions(); + engine = `${role.capitalize()}Engine`; - this.role = new roles[engine](options); - this.roleParams = roles[engine].params; - } - - /** - * Creates an engine name from a passed parameter - * @param role - */ - protected createEngineName(role: string): string { - return `${role.capitalize()}Engine`; - } - - /** - * Creates a dictionary with engine options - */ - protected createRoleOptions(): EngineOptions { - const - {el, binding, vnode} = this.options, - {value, modifiers} = binding; - - return { + const options: EngineOptions = { el, modifiers, params: value, - ctx: Object.cast(vnode.fakeContext), + ctx: this.ctx, async: this.async }; + + this.role = new roles[engine](options); + this.roleParams = roles[engine].params; } /** @@ -124,8 +95,8 @@ export default class AriaSetter { */ protected setAriaLabelledBy(): void { const - {vnode, binding, el} = this.options, - {dom} = Object.cast(vnode.fakeContext), + {binding, el} = this.options, + {dom} = this.ctx, params = Object.isCustomObject(binding.value) ? binding.value : {}; for (const mod in binding.modifiers) { @@ -182,7 +153,7 @@ export default class AriaSetter { for (const key in params) { if (key.startsWith('@')) { const - callbackName = getCallbackName(key); + callbackName = `on-${key.slice(1)}`.camelize(false); if (!Object.isFunction(this.role[callbackName])) { Object.throw('Aria role engine does not contains event handler for passed event name or the type of engine\'s property is not a function'); @@ -199,16 +170,9 @@ export default class AriaSetter { void property.then(callback); } else if (Object.isString(property)) { - const - ctx = this.options.vnode.fakeContext; - - ctx.on(property, callback); + this.ctx.on(property, callback); } } } - - function getCallbackName(key: string) { - return `on-${key.slice(1)}`.camelize(false); - } } } diff --git a/src/core/component/directives/aria/index.ts b/src/core/component/directives/aria/index.ts index 0155ea4e8c..e50f94f653 100644 --- a/src/core/component/directives/aria/index.ts +++ b/src/core/component/directives/aria/index.ts @@ -12,12 +12,12 @@ */ import { ComponentEngine, VNode, VNodeDirective } from 'core/component/engines'; -import AriaSetter from 'core/component/directives/aria/aria-setter'; +import AriaAdapter from 'core/component/directives/aria/adapter'; export * from 'core/component/directives/aria/interface'; const - ariaInstances = new WeakMap(); + ariaInstances = new WeakMap(); ComponentEngine.directive('aria', { inserted(el: HTMLElement, binding: VNodeDirective, vnode: VNode): void { @@ -28,11 +28,7 @@ ComponentEngine.directive('aria', { return; } - ariaInstances.set(el, new AriaSetter({el, binding, vnode})); - }, - - update(el: HTMLElement) { - ariaInstances.get(el)?.update(); + ariaInstances.set(el, new AriaAdapter({el, binding, vnode})); }, unbind(el: HTMLElement) { diff --git a/src/core/component/directives/aria/roles-engines/combobox/index.ts b/src/core/component/directives/aria/roles-engines/combobox/index.ts index f9713248bb..4f9b89a44b 100644 --- a/src/core/component/directives/aria/roles-engines/combobox/index.ts +++ b/src/core/component/directives/aria/roles-engines/combobox/index.ts @@ -13,22 +13,16 @@ import type { ComboboxParams } from 'core/component/directives/aria/roles-engine import { AriaRoleEngine, EngineOptions } from 'core/component/directives/aria/roles-engines/interface'; export class ComboboxEngine extends AriaRoleEngine { - /** - * Engine params - */ - override params: ComboboxParams; + /** @see [[AriaRoleEngine.Prams]] */ + override Params!: ComboboxParams; - /** - * First focusable element inside the element with directive or this element if there is no focusable inside - */ + /** @see [[AriaRoleEngine.el]] */ override el: HTMLElement; /** @see [[AriaRoleEngine.Ctx]] */ override Ctx!: ComponentInterface & iAccess; - /** - * Engine params list - */ + /** @see [[AriaRoleEngine.params]] */ static override params: string[] = ['isMultiple', '@change', '@open', '@close']; constructor(options: EngineOptions) { @@ -38,12 +32,9 @@ export class ComboboxEngine extends AriaRoleEngine { {el} = this; this.el = this.ctx?.findFocusableElement() ?? el; - this.params = options.params; } - /** - * Sets base aria attributes for current role - */ + /* @inheritDoc */ init(): void { this.el.setAttribute('role', 'combobox'); this.el.setAttribute('aria-expanded', 'false'); diff --git a/src/core/component/directives/aria/roles-engines/controls/index.ts b/src/core/component/directives/aria/roles-engines/controls/index.ts index 41bcf25dd5..f0bb7da1f6 100644 --- a/src/core/component/directives/aria/roles-engines/controls/index.ts +++ b/src/core/component/directives/aria/roles-engines/controls/index.ts @@ -7,28 +7,16 @@ */ import type { ControlsParams } from 'core/component/directives/aria/roles-engines/controls/interface'; -import { AriaRoleEngine, EngineOptions } from 'core/component/directives/aria/roles-engines/interface'; +import { AriaRoleEngine } from 'core/component/directives/aria/roles-engines/interface'; export class ControlsEngine extends AriaRoleEngine { - /** - * Engine params - */ - override params: ControlsParams; + /** @see [[AriaRoleEngine.Params]] */ + override Params!: ControlsParams; - /** - * Engine params - */ + /** @see [[AriaRoleEngine.params]] */ static override params: string[]; - constructor(options: EngineOptions) { - super(options); - - this.params = options.params; - } - - /** - * Sets base aria attributes for current role - */ + /* @inheritDoc */ init(): void { const {ctx, modifiers, el} = this, diff --git a/src/core/component/directives/aria/roles-engines/dialog/index.ts b/src/core/component/directives/aria/roles-engines/dialog/index.ts index e1652ea13c..c3f99369de 100644 --- a/src/core/component/directives/aria/roles-engines/dialog/index.ts +++ b/src/core/component/directives/aria/roles-engines/dialog/index.ts @@ -10,9 +10,7 @@ import { AriaRoleEngine } from 'core/component/directives/aria/roles-engines/int import iOpen from 'traits/i-open/i-open'; export class DialogEngine extends AriaRoleEngine { - /** - * Sets base aria attributes for current role - */ + /* @inheritDoc */ init(): void { this.el.setAttribute('role', 'dialog'); this.el.setAttribute('aria-modal', 'true'); diff --git a/src/core/component/directives/aria/roles-engines/interface.ts b/src/core/component/directives/aria/roles-engines/interface.ts index 775c6ff3a0..ad7a391346 100644 --- a/src/core/component/directives/aria/roles-engines/interface.ts +++ b/src/core/component/directives/aria/roles-engines/interface.ts @@ -58,6 +58,9 @@ export abstract class AriaRoleEngine { this.async = async; } + /** + * Sets base aria attributes for current role + */ abstract init(): void; } diff --git a/src/core/component/directives/aria/roles-engines/listbox/index.ts b/src/core/component/directives/aria/roles-engines/listbox/index.ts index 7d451389d5..42bc7dbc89 100644 --- a/src/core/component/directives/aria/roles-engines/listbox/index.ts +++ b/src/core/component/directives/aria/roles-engines/listbox/index.ts @@ -9,9 +9,7 @@ import { AriaRoleEngine } from 'core/component/directives/aria/roles-engines/interface'; export class ListboxEngine extends AriaRoleEngine { - /** - * Sets base aria attributes for current role - */ + /* @inheritDoc */ init(): void { this.el.setAttribute('role', 'listbox'); this.el.setAttribute('tabindex', '-1'); diff --git a/src/core/component/directives/aria/roles-engines/option/index.ts b/src/core/component/directives/aria/roles-engines/option/index.ts index 10fcb4c262..3a31e1802f 100644 --- a/src/core/component/directives/aria/roles-engines/option/index.ts +++ b/src/core/component/directives/aria/roles-engines/option/index.ts @@ -7,28 +7,16 @@ */ import type { OptionParams } from 'core/component/directives/aria/roles-engines/option/interface'; -import { AriaRoleEngine, EngineOptions } from 'core/component/directives/aria/roles-engines/interface'; +import { AriaRoleEngine } from 'core/component/directives/aria/roles-engines/interface'; export class OptionEngine extends AriaRoleEngine { - /** - * Engine params - */ - override params: OptionParams; + /** @see [[AriaRoleEngine.Params]] */ + override Params!: OptionParams; - /** - * Engine params list - */ + /** @see [[AriaRoleEngine.params]] */ static override params: string[] = ['isSelected', '@change']; - constructor(options: EngineOptions) { - super(options); - - this.params = options.params; - } - - /** - * Sets base aria attributes for current role - */ + /* @inheritDoc */ init(): void { this.el.setAttribute('role', 'option'); this.el.setAttribute('aria-selected', String(this.params.isSelected)); diff --git a/src/core/component/directives/aria/roles-engines/tab/index.ts b/src/core/component/directives/aria/roles-engines/tab/index.ts index 513b96cd0a..6516157e40 100644 --- a/src/core/component/directives/aria/roles-engines/tab/index.ts +++ b/src/core/component/directives/aria/roles-engines/tab/index.ts @@ -13,31 +13,19 @@ import type iBlock from 'super/i-block/i-block'; import type iAccess from 'traits/i-access/i-access'; import type { TabParams } from 'core/component/directives/aria/roles-engines/tab/interface'; -import { AriaRoleEngine, EngineOptions, KeyCodes } from 'core/component/directives/aria/roles-engines/interface'; +import { AriaRoleEngine, KeyCodes } from 'core/component/directives/aria/roles-engines/interface'; export class TabEngine extends AriaRoleEngine { - /** - * Engine params - */ - override params: TabParams; + /** @see [[AriaRoleEngine.Params]] */ + override Params!: TabParams; /** @see [[AriaRoleEngine.ctx]] */ override ctx?: iBlock & iAccess; - /** - * Engine params list - */ + /** @see [[AriaRoleEngine.params]] */ static override params: string[] = ['isFirst', 'isSelected', 'hasDefaultSelectedTabs', 'orientation', '@change']; - constructor(options: EngineOptions) { - super(options); - - this.params = options.params; - } - - /** - * Sets base aria attributes for current role - */ + /* @inheritDoc */ init(): void { const {el} = this, diff --git a/src/core/component/directives/aria/roles-engines/tablist/index.ts b/src/core/component/directives/aria/roles-engines/tablist/index.ts index 5aa6531fba..8f43ad2125 100644 --- a/src/core/component/directives/aria/roles-engines/tablist/index.ts +++ b/src/core/component/directives/aria/roles-engines/tablist/index.ts @@ -7,28 +7,16 @@ */ import type { TablistParams } from 'core/component/directives/aria/roles-engines/tablist/interface'; -import { AriaRoleEngine, EngineOptions } from 'core/component/directives/aria/roles-engines/interface'; +import { AriaRoleEngine } from 'core/component/directives/aria/roles-engines/interface'; export class TablistEngine extends AriaRoleEngine { - /** - * Engine params - */ - override params: TablistParams; + /** @see [[AriaRoleEngine.Params]] */ + override Params!: TablistParams; - /** - * Engine params list - */ + /** @see [[AriaRoleEngine.params]] */ static override params: string[] = ['isMultiple', 'orientation']; - constructor(options: EngineOptions) { - super(options); - - this.params = options.params; - } - - /** - * Sets base aria attributes for current role - */ + /* @inheritDoc */ init(): void { const {el, params} = this; diff --git a/src/core/component/directives/aria/roles-engines/tabpanel/index.ts b/src/core/component/directives/aria/roles-engines/tabpanel/index.ts index f49f2661ba..0eea14ef23 100644 --- a/src/core/component/directives/aria/roles-engines/tabpanel/index.ts +++ b/src/core/component/directives/aria/roles-engines/tabpanel/index.ts @@ -9,9 +9,7 @@ import { AriaRoleEngine } from 'core/component/directives/aria/roles-engines/interface'; export class TabpanelEngine extends AriaRoleEngine { - /** - * Sets base aria attributes for current role - */ + /* @inheritDoc */ init(): void { const {el} = this; diff --git a/src/core/component/directives/aria/roles-engines/tree/index.ts b/src/core/component/directives/aria/roles-engines/tree/index.ts index 9bb4cd98e4..1418d1ecdc 100644 --- a/src/core/component/directives/aria/roles-engines/tree/index.ts +++ b/src/core/component/directives/aria/roles-engines/tree/index.ts @@ -7,28 +7,16 @@ */ import type { TreeParams } from 'core/component/directives/aria/roles-engines/tree/interface'; -import { AriaRoleEngine, EngineOptions } from 'core/component/directives/aria/roles-engines/interface'; +import { AriaRoleEngine } from 'core/component/directives/aria/roles-engines/interface'; export class TreeEngine extends AriaRoleEngine { - /** - * Engine params - */ - override params: TreeParams; + /** @see [[AriaRoleEngine.Params]] */ + override Params!: TreeParams; - /** - * Engine params list - */ + /** @see [[AriaRoleEngine.params]] */ static override params: string[] = ['isRoot', 'orientation', '@change']; - constructor(options: EngineOptions) { - super(options); - - this.params = options.params; - } - - /** - * Sets base aria attributes for current role - */ + /* @inheritDoc */ init(): void { const {orientation, isRoot} = this.params; diff --git a/src/core/component/directives/aria/roles-engines/treeitem/index.ts b/src/core/component/directives/aria/roles-engines/treeitem/index.ts index cad2c6ce5a..4b533cc2fb 100644 --- a/src/core/component/directives/aria/roles-engines/treeitem/index.ts +++ b/src/core/component/directives/aria/roles-engines/treeitem/index.ts @@ -13,36 +13,24 @@ import iAccess from 'traits/i-access/i-access'; import type iBlock from 'super/i-block/i-block'; import type { TreeitemParams } from 'core/component/directives/aria/roles-engines/treeitem/interface'; -import { AriaRoleEngine, KeyCodes, EngineOptions } from 'core/component/directives/aria/roles-engines/interface'; +import { AriaRoleEngine, KeyCodes } from 'core/component/directives/aria/roles-engines/interface'; export class TreeitemEngine extends AriaRoleEngine { - /** - * Engine params - */ - override params: TreeitemParams; + /** @see [[AriaRoleEngine.Params]] */ + override Params!: TreeitemParams; /** @see [[AriaRoleEngine.ctx]] */ override ctx?: iBlock & iAccess; - /** - * Engine params list - */ + /** @see [[AriaRoleEngine.params]] */ static override params: string[] = ['isFirstRootItem', 'isExpandable', 'isExpanded', 'orientation', 'rootElement', 'toggleFold']; - constructor(options: EngineOptions) { - super(options); - + /* @inheritDoc */ + init(): void { if (!iAccess.is(this.ctx)) { Object.throw('Treeitem aria directive expects the component to realize iAccess interface'); } - this.params = options.params; - } - - /** - * Sets base aria attributes for current role - */ - init(): void { this.async?.on(this.el, 'keydown', this.onKeyDown.bind(this)); const @@ -137,7 +125,7 @@ export class TreeitemEngine extends AriaRoleEngine { protected setFocusToFirstItem(): void { const firstItem = this.ctx?.findFocusableElement(this.params.rootElement); -debugger; + debugger; if (firstItem != null) { this.focusNext(firstItem); } diff --git a/src/core/component/directives/index.ts b/src/core/component/directives/index.ts index e90a8b1f94..f788c4e911 100644 --- a/src/core/component/directives/index.ts +++ b/src/core/component/directives/index.ts @@ -22,8 +22,6 @@ import 'core/component/directives/image'; import 'core/component/directives/update-on'; //#endif -import 'core/component/directives/aria'; - import 'core/component/directives/hook'; import 'core/component/directives/id'; diff --git a/src/form/b-checkbox/b-checkbox.ts b/src/form/b-checkbox/b-checkbox.ts index 8e712e8ee4..bc4b9420d2 100644 --- a/src/form/b-checkbox/b-checkbox.ts +++ b/src/form/b-checkbox/b-checkbox.ts @@ -15,6 +15,8 @@ import 'models/demo/checkbox'; //#endif +import 'core/component/directives/aria'; + import symbolGenerator from 'core/symbol'; import SyncPromise from 'core/promise/sync'; diff --git a/src/form/b-select/b-select.ts b/src/form/b-select/b-select.ts index 0c4424a2b4..821a7d76a2 100644 --- a/src/form/b-select/b-select.ts +++ b/src/form/b-select/b-select.ts @@ -15,6 +15,8 @@ import 'models/demo/select'; //#endif +import 'core/component/directives/aria'; + import SyncPromise from 'core/promise/sync'; import { derive } from 'core/functools/trait'; @@ -887,8 +889,9 @@ class bSelect extends iInputText implements iOpenToggle, iItems { protected getAriaConfig(role: 'option', item: this['Item']): Dictionary; protected getAriaConfig(role: 'combobox' | 'option', item?: this['Item']): Dictionary { - const - isSelected = this.isSelected.bind(this, item?.value); + const isSelected = item?.value != null ? + this.isSelected.bind(this, item.value) : + () => undefined; const comboboxConfig = { isMultiple: this.multiple, From 49c4bc321b44c461ba89af8d15fae772b3f05abe Mon Sep 17 00:00:00 2001 From: Aleksandr Bunin Date: Thu, 18 Aug 2022 18:21:11 +0300 Subject: [PATCH 135/185] refactoring --- .../component/directives/aria/roles-engines/combobox/index.ts | 2 +- .../component/directives/aria/roles-engines/controls/index.ts | 2 +- .../component/directives/aria/roles-engines/dialog/index.ts | 2 +- .../component/directives/aria/roles-engines/listbox/index.ts | 2 +- .../component/directives/aria/roles-engines/option/index.ts | 2 +- src/core/component/directives/aria/roles-engines/tab/index.ts | 2 +- .../component/directives/aria/roles-engines/tablist/index.ts | 2 +- .../component/directives/aria/roles-engines/tabpanel/index.ts | 2 +- .../component/directives/aria/roles-engines/tree/index.ts | 2 +- .../component/directives/aria/roles-engines/treeitem/index.ts | 4 ++-- 10 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/core/component/directives/aria/roles-engines/combobox/index.ts b/src/core/component/directives/aria/roles-engines/combobox/index.ts index 4f9b89a44b..09e94b92b6 100644 --- a/src/core/component/directives/aria/roles-engines/combobox/index.ts +++ b/src/core/component/directives/aria/roles-engines/combobox/index.ts @@ -34,7 +34,7 @@ export class ComboboxEngine extends AriaRoleEngine { this.el = this.ctx?.findFocusableElement() ?? el; } - /* @inheritDoc */ + /** @inheritDoc */ init(): void { this.el.setAttribute('role', 'combobox'); this.el.setAttribute('aria-expanded', 'false'); diff --git a/src/core/component/directives/aria/roles-engines/controls/index.ts b/src/core/component/directives/aria/roles-engines/controls/index.ts index f0bb7da1f6..6bd4133542 100644 --- a/src/core/component/directives/aria/roles-engines/controls/index.ts +++ b/src/core/component/directives/aria/roles-engines/controls/index.ts @@ -16,7 +16,7 @@ export class ControlsEngine extends AriaRoleEngine { /** @see [[AriaRoleEngine.params]] */ static override params: string[]; - /* @inheritDoc */ + /** @inheritDoc */ init(): void { const {ctx, modifiers, el} = this, diff --git a/src/core/component/directives/aria/roles-engines/dialog/index.ts b/src/core/component/directives/aria/roles-engines/dialog/index.ts index c3f99369de..7dd58ab641 100644 --- a/src/core/component/directives/aria/roles-engines/dialog/index.ts +++ b/src/core/component/directives/aria/roles-engines/dialog/index.ts @@ -10,7 +10,7 @@ import { AriaRoleEngine } from 'core/component/directives/aria/roles-engines/int import iOpen from 'traits/i-open/i-open'; export class DialogEngine extends AriaRoleEngine { - /* @inheritDoc */ + /** @inheritDoc */ init(): void { this.el.setAttribute('role', 'dialog'); this.el.setAttribute('aria-modal', 'true'); diff --git a/src/core/component/directives/aria/roles-engines/listbox/index.ts b/src/core/component/directives/aria/roles-engines/listbox/index.ts index 42bc7dbc89..5ae60ad6b7 100644 --- a/src/core/component/directives/aria/roles-engines/listbox/index.ts +++ b/src/core/component/directives/aria/roles-engines/listbox/index.ts @@ -9,7 +9,7 @@ import { AriaRoleEngine } from 'core/component/directives/aria/roles-engines/interface'; export class ListboxEngine extends AriaRoleEngine { - /* @inheritDoc */ + /** @inheritDoc */ init(): void { this.el.setAttribute('role', 'listbox'); this.el.setAttribute('tabindex', '-1'); diff --git a/src/core/component/directives/aria/roles-engines/option/index.ts b/src/core/component/directives/aria/roles-engines/option/index.ts index 3a31e1802f..ddd256c0ba 100644 --- a/src/core/component/directives/aria/roles-engines/option/index.ts +++ b/src/core/component/directives/aria/roles-engines/option/index.ts @@ -16,7 +16,7 @@ export class OptionEngine extends AriaRoleEngine { /** @see [[AriaRoleEngine.params]] */ static override params: string[] = ['isSelected', '@change']; - /* @inheritDoc */ + /** @inheritDoc */ init(): void { this.el.setAttribute('role', 'option'); this.el.setAttribute('aria-selected', String(this.params.isSelected)); diff --git a/src/core/component/directives/aria/roles-engines/tab/index.ts b/src/core/component/directives/aria/roles-engines/tab/index.ts index 6516157e40..9e7e9e9e40 100644 --- a/src/core/component/directives/aria/roles-engines/tab/index.ts +++ b/src/core/component/directives/aria/roles-engines/tab/index.ts @@ -25,7 +25,7 @@ export class TabEngine extends AriaRoleEngine { /** @see [[AriaRoleEngine.params]] */ static override params: string[] = ['isFirst', 'isSelected', 'hasDefaultSelectedTabs', 'orientation', '@change']; - /* @inheritDoc */ + /** @inheritDoc */ init(): void { const {el} = this, diff --git a/src/core/component/directives/aria/roles-engines/tablist/index.ts b/src/core/component/directives/aria/roles-engines/tablist/index.ts index 8f43ad2125..c04775c5c4 100644 --- a/src/core/component/directives/aria/roles-engines/tablist/index.ts +++ b/src/core/component/directives/aria/roles-engines/tablist/index.ts @@ -16,7 +16,7 @@ export class TablistEngine extends AriaRoleEngine { /** @see [[AriaRoleEngine.params]] */ static override params: string[] = ['isMultiple', 'orientation']; - /* @inheritDoc */ + /** @inheritDoc */ init(): void { const {el, params} = this; diff --git a/src/core/component/directives/aria/roles-engines/tabpanel/index.ts b/src/core/component/directives/aria/roles-engines/tabpanel/index.ts index 0eea14ef23..d9ccd4921c 100644 --- a/src/core/component/directives/aria/roles-engines/tabpanel/index.ts +++ b/src/core/component/directives/aria/roles-engines/tabpanel/index.ts @@ -9,7 +9,7 @@ import { AriaRoleEngine } from 'core/component/directives/aria/roles-engines/interface'; export class TabpanelEngine extends AriaRoleEngine { - /* @inheritDoc */ + /** @inheritDoc */ init(): void { const {el} = this; diff --git a/src/core/component/directives/aria/roles-engines/tree/index.ts b/src/core/component/directives/aria/roles-engines/tree/index.ts index 1418d1ecdc..33c450429d 100644 --- a/src/core/component/directives/aria/roles-engines/tree/index.ts +++ b/src/core/component/directives/aria/roles-engines/tree/index.ts @@ -16,7 +16,7 @@ export class TreeEngine extends AriaRoleEngine { /** @see [[AriaRoleEngine.params]] */ static override params: string[] = ['isRoot', 'orientation', '@change']; - /* @inheritDoc */ + /** @inheritDoc */ init(): void { const {orientation, isRoot} = this.params; diff --git a/src/core/component/directives/aria/roles-engines/treeitem/index.ts b/src/core/component/directives/aria/roles-engines/treeitem/index.ts index 4b533cc2fb..8b10244a51 100644 --- a/src/core/component/directives/aria/roles-engines/treeitem/index.ts +++ b/src/core/component/directives/aria/roles-engines/treeitem/index.ts @@ -25,7 +25,7 @@ export class TreeitemEngine extends AriaRoleEngine { /** @see [[AriaRoleEngine.params]] */ static override params: string[] = ['isFirstRootItem', 'isExpandable', 'isExpanded', 'orientation', 'rootElement', 'toggleFold']; - /* @inheritDoc */ + /** @inheritDoc */ init(): void { if (!iAccess.is(this.ctx)) { Object.throw('Treeitem aria directive expects the component to realize iAccess interface'); @@ -41,7 +41,7 @@ export class TreeitemEngine extends AriaRoleEngine { this.ctx?.restoreAllToTabSequence(this.el); } else { - this.el.tabIndex = 0; + this.el.setAttribute('tabindex', '0'); } } From 96ac9505efecc376d5fa4423169e774251de391e Mon Sep 17 00:00:00 2001 From: Aleksandr Bunin Date: Fri, 19 Aug 2022 16:24:32 +0300 Subject: [PATCH 136/185] refactoring --- src/core/component/directives/aria/adapter.ts | 45 +++++++++++-------- .../aria/roles-engines/combobox/index.ts | 28 +++++------- .../aria/roles-engines/combobox/interface.ts | 14 +++--- .../aria/roles-engines/controls/index.ts | 16 +++---- .../aria/roles-engines/controls/interface.ts | 4 +- .../aria/roles-engines/dialog/index.ts | 4 +- .../aria/roles-engines/interface.ts | 21 ++++----- .../aria/roles-engines/listbox/index.ts | 4 +- .../aria/roles-engines/option/index.ts | 12 ++--- .../aria/roles-engines/option/interface.ts | 6 +-- .../aria/roles-engines/tab/index.ts | 41 +++++++---------- .../aria/roles-engines/tab/interface.ts | 12 ++--- .../aria/roles-engines/tablist/index.ts | 16 +++---- .../aria/roles-engines/tablist/interface.ts | 6 +-- .../aria/roles-engines/tabpanel/index.ts | 2 +- .../aria/roles-engines/tree/index.ts | 14 +++--- .../aria/roles-engines/tree/interface.ts | 8 ++-- .../aria/roles-engines/treeitem/index.ts | 22 ++++----- .../aria/roles-engines/treeitem/interface.ts | 16 ++++--- 19 files changed, 133 insertions(+), 158 deletions(-) diff --git a/src/core/component/directives/aria/adapter.ts b/src/core/component/directives/aria/adapter.ts index 8439be4ed9..6249b2e139 100644 --- a/src/core/component/directives/aria/adapter.ts +++ b/src/core/component/directives/aria/adapter.ts @@ -30,11 +30,6 @@ export default class AriaAdapter { */ protected role: CanUndef; - /** - * Role engine params list - */ - protected roleParams: CanUndef; - constructor(options: DirectiveOptions) { this.options = options; this.setAriaRole(); @@ -87,7 +82,6 @@ export default class AriaAdapter { }; this.role = new roles[engine](options); - this.roleParams = roles[engine].params; } /** @@ -97,7 +91,11 @@ export default class AriaAdapter { const {binding, el} = this.options, {dom} = this.ctx, - params = Object.isCustomObject(binding.value) ? binding.value : {}; + {labelledby} = binding.value ?? {}, + attr = 'aria-labelledby'; + + let + isAttrSet = false; for (const mod in binding.modifiers) { if (!mod.startsWith('#')) { @@ -108,18 +106,17 @@ export default class AriaAdapter { title = mod.slice(1), id = dom.getId(title); - el.setAttribute('aria-labelledby', id); + el.setAttribute(attr, id); + isAttrSet = true; } - if (params.labelledby == null) { - return; + if (labelledby != null) { + el.setAttribute(attr, Object.isArray(labelledby) ? labelledby.join(' ') : labelledby); + isAttrSet = true; } - if (Object.isArray(params.labelledby)) { - el.setAttribute('aria-labelledby', params.labelledby.join(' ')); - - } else { - el.setAttribute('aria-labelledby', params.labelledby); + if (isAttrSet) { + this.async.worker(() => el.removeAttribute(attr)); } } @@ -129,11 +126,23 @@ export default class AriaAdapter { protected setAriaAttributes(): void { const {el, binding} = this.options, - params = binding.value; + params: Dictionary = binding.value; for (const key in params) { - if (!this.roleParams?.includes(key) && key !== 'labelledby') { - el.setAttribute(`aria-${key}`, params[key]); + if (!params.hasOwnProperty(key)) { + continue; + } + + const + roleParams = this.role?.Params, + param = params[key]; + + if (!roleParams?.hasOwnProperty(key) && key !== 'labelledby' && param != null) { + const + attr = `aria-${key}`; + + el.setAttribute(attr, param); + this.async.worker(() => el.removeAttribute(attr)); } } } diff --git a/src/core/component/directives/aria/roles-engines/combobox/index.ts b/src/core/component/directives/aria/roles-engines/combobox/index.ts index 09e94b92b6..7af277460e 100644 --- a/src/core/component/directives/aria/roles-engines/combobox/index.ts +++ b/src/core/component/directives/aria/roles-engines/combobox/index.ts @@ -9,21 +9,13 @@ import type iAccess from 'traits/i-access/i-access'; import type { ComponentInterface } from 'super/i-block/i-block'; -import type { ComboboxParams } from 'core/component/directives/aria/roles-engines/combobox/interface'; +import { ComboboxParams } from 'core/component/directives/aria/roles-engines/combobox/interface'; import { AriaRoleEngine, EngineOptions } from 'core/component/directives/aria/roles-engines/interface'; export class ComboboxEngine extends AriaRoleEngine { - /** @see [[AriaRoleEngine.Prams]] */ - override Params!: ComboboxParams; - - /** @see [[AriaRoleEngine.el]] */ - override el: HTMLElement; - - /** @see [[AriaRoleEngine.Ctx]] */ + override Params: ComboboxParams = new ComboboxParams(); override Ctx!: ComponentInterface & iAccess; - - /** @see [[AriaRoleEngine.params]] */ - static override params: string[] = ['isMultiple', '@change', '@open', '@close']; + override el: HTMLElement; constructor(options: EngineOptions) { super(options); @@ -36,15 +28,15 @@ export class ComboboxEngine extends AriaRoleEngine { /** @inheritDoc */ init(): void { - this.el.setAttribute('role', 'combobox'); - this.el.setAttribute('aria-expanded', 'false'); + this.setAttribute('role', 'combobox'); + this.setAttribute('aria-expanded', 'false'); if (this.params.isMultiple) { - this.el.setAttribute('aria-multiselectable', 'true'); + this.setAttribute('aria-multiselectable', 'true'); } if (this.el.tabIndex < 0) { - this.el.setAttribute('tabindex', '0'); + this.setAttribute('tabindex', '0'); } } @@ -52,7 +44,7 @@ export class ComboboxEngine extends AriaRoleEngine { * Sets or deletes the id of active descendant element */ protected setAriaActive(el?: HTMLElement): void { - this.el.setAttribute('aria-activedescendant', el?.id ?? ''); + this.setAttribute('aria-activedescendant', el?.id ?? ''); } /** @@ -60,7 +52,7 @@ export class ComboboxEngine extends AriaRoleEngine { * @param el */ protected onOpen(el: HTMLElement): void { - this.el.setAttribute('aria-expanded', 'true'); + this.setAttribute('aria-expanded', 'true'); this.setAriaActive(el); } @@ -68,7 +60,7 @@ export class ComboboxEngine extends AriaRoleEngine { * Handler: the option list is closed */ protected onClose(): void { - this.el.setAttribute('aria-expanded', 'false'); + this.setAttribute('aria-expanded', 'false'); this.setAriaActive(); } diff --git a/src/core/component/directives/aria/roles-engines/combobox/interface.ts b/src/core/component/directives/aria/roles-engines/combobox/interface.ts index a319311e58..03ee51ae17 100644 --- a/src/core/component/directives/aria/roles-engines/combobox/interface.ts +++ b/src/core/component/directives/aria/roles-engines/combobox/interface.ts @@ -6,11 +6,13 @@ * https://github.com/V4Fire/Client/blob/master/LICENSE */ -import type { AbstractParams, HandlerAttachment } from 'core/component/directives/aria/roles-engines/interface'; +import type { HandlerAttachment } from 'core/component/directives/aria/roles-engines/interface'; -export interface ComboboxParams extends AbstractParams { - isMultiple: boolean; - '@change': HandlerAttachment; - '@open': HandlerAttachment; - '@close': HandlerAttachment; +const defaultFn = (): void => undefined; + +export class ComboboxParams { + isMultiple: boolean = false; + '@change': HandlerAttachment = defaultFn; + '@open': HandlerAttachment = defaultFn; + '@close': HandlerAttachment = defaultFn; } diff --git a/src/core/component/directives/aria/roles-engines/controls/index.ts b/src/core/component/directives/aria/roles-engines/controls/index.ts index 6bd4133542..adb57ea056 100644 --- a/src/core/component/directives/aria/roles-engines/controls/index.ts +++ b/src/core/component/directives/aria/roles-engines/controls/index.ts @@ -6,15 +6,11 @@ * https://github.com/V4Fire/Client/blob/master/LICENSE */ -import type { ControlsParams } from 'core/component/directives/aria/roles-engines/controls/interface'; +import { ControlsParams } from 'core/component/directives/aria/roles-engines/controls/interface'; import { AriaRoleEngine } from 'core/component/directives/aria/roles-engines/interface'; export class ControlsEngine extends AriaRoleEngine { - /** @see [[AriaRoleEngine.Params]] */ - override Params!: ControlsParams; - - /** @see [[AriaRoleEngine.params]] */ - static override params: string[]; + override Params: ControlsParams = new ControlsParams(); /** @inheritDoc */ init(): void { @@ -45,7 +41,7 @@ export class ControlsEngine extends AriaRoleEngine { elems.forEach((el, i) => { if (Object.isString(forId)) { - el.setAttribute('aria-controls', forId); + this.setAttribute('aria-controls', forId, el); return; } @@ -53,7 +49,7 @@ export class ControlsEngine extends AriaRoleEngine { id = forId[i]; if (Object.isString(id)) { - el.setAttribute('aria-controls', id); + this.setAttribute('aria-controls', id, el); } }); }); @@ -64,7 +60,9 @@ export class ControlsEngine extends AriaRoleEngine { [elId, controlsId] = param, element = el.querySelector(`#${elId}`); - element?.setAttribute('aria-controls', controlsId); + if (element != null) { + this.setAttribute('aria-controls', controlsId, element); + } }); } } diff --git a/src/core/component/directives/aria/roles-engines/controls/interface.ts b/src/core/component/directives/aria/roles-engines/controls/interface.ts index 608bf5c139..c73270ecbb 100644 --- a/src/core/component/directives/aria/roles-engines/controls/interface.ts +++ b/src/core/component/directives/aria/roles-engines/controls/interface.ts @@ -6,6 +6,6 @@ * https://github.com/V4Fire/Client/blob/master/LICENSE */ -export interface ControlsParams { - for: CanArray | Array<[string, string]>; +export class ControlsParams { + for: CanArray | Array<[string, string]> = 'for'; } diff --git a/src/core/component/directives/aria/roles-engines/dialog/index.ts b/src/core/component/directives/aria/roles-engines/dialog/index.ts index 7dd58ab641..d004f5dc4b 100644 --- a/src/core/component/directives/aria/roles-engines/dialog/index.ts +++ b/src/core/component/directives/aria/roles-engines/dialog/index.ts @@ -12,8 +12,8 @@ import iOpen from 'traits/i-open/i-open'; export class DialogEngine extends AriaRoleEngine { /** @inheritDoc */ init(): void { - this.el.setAttribute('role', 'dialog'); - this.el.setAttribute('aria-modal', 'true'); + this.setAttribute('role', 'dialog'); + this.setAttribute('aria-modal', 'true'); if (!iOpen.is(this.ctx)) { Object.throw('Dialog aria directive expects the component to realize iOpen interface'); diff --git a/src/core/component/directives/aria/roles-engines/interface.ts b/src/core/component/directives/aria/roles-engines/interface.ts index ad7a391346..e6e79394b8 100644 --- a/src/core/component/directives/aria/roles-engines/interface.ts +++ b/src/core/component/directives/aria/roles-engines/interface.ts @@ -40,15 +40,8 @@ export abstract class AriaRoleEngine { */ readonly params: this['Params']; - /** - * Async instance - */ - async: CanUndef; - - /** - * Directive expected params list - */ - static params: string[]; + /** @see [[Async]] */ + async: Async; constructor({el, ctx, modifiers, params, async}: EngineOptions) { this.el = el; @@ -62,9 +55,17 @@ export abstract class AriaRoleEngine { * Sets base aria attributes for current role */ abstract init(): void; + + /** + * Sets aria attributes and the `Async` destructor + */ + setAttribute(attr: string, value: string, el: Element = this.el): void { + el.setAttribute(attr, value); + this.async.worker(() => el.removeAttribute(attr)); + } } -export interface AbstractParams {} +interface AbstractParams {} export interface EngineOptions

      { el: HTMLElement; diff --git a/src/core/component/directives/aria/roles-engines/listbox/index.ts b/src/core/component/directives/aria/roles-engines/listbox/index.ts index 5ae60ad6b7..4e28b0c596 100644 --- a/src/core/component/directives/aria/roles-engines/listbox/index.ts +++ b/src/core/component/directives/aria/roles-engines/listbox/index.ts @@ -11,7 +11,7 @@ import { AriaRoleEngine } from 'core/component/directives/aria/roles-engines/int export class ListboxEngine extends AriaRoleEngine { /** @inheritDoc */ init(): void { - this.el.setAttribute('role', 'listbox'); - this.el.setAttribute('tabindex', '-1'); + this.setAttribute('role', 'listbox'); + this.setAttribute('tabindex', '-1'); } } diff --git a/src/core/component/directives/aria/roles-engines/option/index.ts b/src/core/component/directives/aria/roles-engines/option/index.ts index ddd256c0ba..1791c48b4b 100644 --- a/src/core/component/directives/aria/roles-engines/option/index.ts +++ b/src/core/component/directives/aria/roles-engines/option/index.ts @@ -6,20 +6,16 @@ * https://github.com/V4Fire/Client/blob/master/LICENSE */ -import type { OptionParams } from 'core/component/directives/aria/roles-engines/option/interface'; +import { OptionParams } from 'core/component/directives/aria/roles-engines/option/interface'; import { AriaRoleEngine } from 'core/component/directives/aria/roles-engines/interface'; export class OptionEngine extends AriaRoleEngine { - /** @see [[AriaRoleEngine.Params]] */ - override Params!: OptionParams; - - /** @see [[AriaRoleEngine.params]] */ - static override params: string[] = ['isSelected', '@change']; + override Params: OptionParams = new OptionParams(); /** @inheritDoc */ init(): void { - this.el.setAttribute('role', 'option'); - this.el.setAttribute('aria-selected', String(this.params.isSelected)); + this.setAttribute('role', 'option'); + this.setAttribute('aria-selected', String(this.params.isSelected)); } /** diff --git a/src/core/component/directives/aria/roles-engines/option/interface.ts b/src/core/component/directives/aria/roles-engines/option/interface.ts index 4e90c740ae..58d4bffe1f 100644 --- a/src/core/component/directives/aria/roles-engines/option/interface.ts +++ b/src/core/component/directives/aria/roles-engines/option/interface.ts @@ -8,7 +8,7 @@ import type { HandlerAttachment } from 'core/component/directives/aria/roles-engines/interface'; -export interface OptionParams { - isSelected: boolean; - '@change': HandlerAttachment; +export class OptionParams { + isSelected: boolean = false; + '@change': HandlerAttachment = () => undefined; } diff --git a/src/core/component/directives/aria/roles-engines/tab/index.ts b/src/core/component/directives/aria/roles-engines/tab/index.ts index 9e7e9e9e40..ddf9fb43d9 100644 --- a/src/core/component/directives/aria/roles-engines/tab/index.ts +++ b/src/core/component/directives/aria/roles-engines/tab/index.ts @@ -12,18 +12,12 @@ import type iBlock from 'super/i-block/i-block'; import type iAccess from 'traits/i-access/i-access'; -import type { TabParams } from 'core/component/directives/aria/roles-engines/tab/interface'; +import { TabParams } from 'core/component/directives/aria/roles-engines/tab/interface'; import { AriaRoleEngine, KeyCodes } from 'core/component/directives/aria/roles-engines/interface'; export class TabEngine extends AriaRoleEngine { - /** @see [[AriaRoleEngine.Params]] */ - override Params!: TabParams; - - /** @see [[AriaRoleEngine.ctx]] */ - override ctx?: iBlock & iAccess; - - /** @see [[AriaRoleEngine.params]] */ - static override params: string[] = ['isFirst', 'isSelected', 'hasDefaultSelectedTabs', 'orientation', '@change']; + override Params: TabParams = new TabParams(); + override Ctx!: iBlock & iAccess; /** @inheritDoc */ init(): void { @@ -31,26 +25,24 @@ export class TabEngine extends AriaRoleEngine { {el} = this, {isFirst, isSelected, hasDefaultSelectedTabs} = this.params; - el.setAttribute('role', 'tab'); - el.setAttribute('aria-selected', String(isSelected)); + this.setAttribute('role', 'tab'); + this.setAttribute('aria-selected', String(isSelected)); if (isFirst && !hasDefaultSelectedTabs) { if (el.tabIndex < 0) { - el.setAttribute('tabindex', '0'); + this.setAttribute('tabindex', '0'); } } else if (hasDefaultSelectedTabs && isSelected) { if (el.tabIndex < 0) { - el.setAttribute('tabindex', '0'); + this.setAttribute('tabindex', '0'); } } else { - el.setAttribute('tabindex', '-1'); + this.setAttribute('tabindex', '-1'); } - if (this.async != null) { - this.async.on(el, 'keydown', this.onKeydown.bind(this)); - } + this.async.on(el, 'keydown', this.onKeydown.bind(this)); } /** @@ -100,23 +92,20 @@ export class TabEngine extends AriaRoleEngine { * @param active */ protected onChange(active: Element | NodeListOf): void { - const - {el} = this; - - function setAttributes(isSelected: boolean) { - el.setAttribute('aria-selected', String(isSelected)); - el.setAttribute('tabindex', isSelected ? '0' : '-1'); - } + const setAttributes = (isSelected: boolean) => { + this.setAttribute('aria-selected', String(isSelected)); + this.setAttribute('tabindex', isSelected ? '0' : '-1'); + }; if (Object.isArrayLike(active)) { for (let i = 0; i < active.length; i++) { - setAttributes(el === active[i]); + setAttributes(this.el === active[i]); } return; } - setAttributes(el === active); + setAttributes(this.el === active); } /** diff --git a/src/core/component/directives/aria/roles-engines/tab/interface.ts b/src/core/component/directives/aria/roles-engines/tab/interface.ts index e42c5bdc11..190e244b0a 100644 --- a/src/core/component/directives/aria/roles-engines/tab/interface.ts +++ b/src/core/component/directives/aria/roles-engines/tab/interface.ts @@ -8,10 +8,10 @@ import type { HandlerAttachment } from 'core/component/directives/aria/roles-engines/interface'; -export interface TabParams { - isFirst: boolean; - isSelected: boolean; - hasDefaultSelectedTabs: boolean; - orientation: string; - '@change': HandlerAttachment; +export class TabParams { + isFirst: boolean = false; + isSelected: boolean = false; + hasDefaultSelectedTabs: boolean = false; + orientation: string = 'false'; + '@change': HandlerAttachment = () => undefined; } diff --git a/src/core/component/directives/aria/roles-engines/tablist/index.ts b/src/core/component/directives/aria/roles-engines/tablist/index.ts index c04775c5c4..c464b54c37 100644 --- a/src/core/component/directives/aria/roles-engines/tablist/index.ts +++ b/src/core/component/directives/aria/roles-engines/tablist/index.ts @@ -6,29 +6,25 @@ * https://github.com/V4Fire/Client/blob/master/LICENSE */ -import type { TablistParams } from 'core/component/directives/aria/roles-engines/tablist/interface'; +import { TablistParams } from 'core/component/directives/aria/roles-engines/tablist/interface'; import { AriaRoleEngine } from 'core/component/directives/aria/roles-engines/interface'; export class TablistEngine extends AriaRoleEngine { - /** @see [[AriaRoleEngine.Params]] */ - override Params!: TablistParams; - - /** @see [[AriaRoleEngine.params]] */ - static override params: string[] = ['isMultiple', 'orientation']; + override Params: TablistParams = new TablistParams(); /** @inheritDoc */ init(): void { const - {el, params} = this; + {params} = this; - el.setAttribute('role', 'tablist'); + this.setAttribute('role', 'tablist'); if (params.isMultiple) { - el.setAttribute('aria-multiselectable', 'true'); + this.setAttribute('aria-multiselectable', 'true'); } if (params.orientation === 'vertical') { - el.setAttribute('aria-orientation', params.orientation); + this.setAttribute('aria-orientation', params.orientation); } } } diff --git a/src/core/component/directives/aria/roles-engines/tablist/interface.ts b/src/core/component/directives/aria/roles-engines/tablist/interface.ts index ec88e5a93a..6144a59fba 100644 --- a/src/core/component/directives/aria/roles-engines/tablist/interface.ts +++ b/src/core/component/directives/aria/roles-engines/tablist/interface.ts @@ -6,7 +6,7 @@ * https://github.com/V4Fire/Client/blob/master/LICENSE */ -export interface TablistParams { - isMultiple: boolean; - orientation: string; +export class TablistParams { + isMultiple: boolean = false; + orientation: string = 'false'; } diff --git a/src/core/component/directives/aria/roles-engines/tabpanel/index.ts b/src/core/component/directives/aria/roles-engines/tabpanel/index.ts index d9ccd4921c..cd76548539 100644 --- a/src/core/component/directives/aria/roles-engines/tabpanel/index.ts +++ b/src/core/component/directives/aria/roles-engines/tabpanel/index.ts @@ -18,6 +18,6 @@ export class TabpanelEngine extends AriaRoleEngine { Object.throw('Tabpanel aria directive expects "label" or "labelledby" value to be passed'); } - el.setAttribute('role', 'tabpanel'); + this.setAttribute('role', 'tabpanel'); } } diff --git a/src/core/component/directives/aria/roles-engines/tree/index.ts b/src/core/component/directives/aria/roles-engines/tree/index.ts index 33c450429d..92665ae3fa 100644 --- a/src/core/component/directives/aria/roles-engines/tree/index.ts +++ b/src/core/component/directives/aria/roles-engines/tree/index.ts @@ -6,15 +6,11 @@ * https://github.com/V4Fire/Client/blob/master/LICENSE */ -import type { TreeParams } from 'core/component/directives/aria/roles-engines/tree/interface'; +import { TreeParams } from 'core/component/directives/aria/roles-engines/tree/interface'; import { AriaRoleEngine } from 'core/component/directives/aria/roles-engines/interface'; export class TreeEngine extends AriaRoleEngine { - /** @see [[AriaRoleEngine.Params]] */ - override Params!: TreeParams; - - /** @see [[AriaRoleEngine.params]] */ - static override params: string[] = ['isRoot', 'orientation', '@change']; + override Params: TreeParams = new TreeParams(); /** @inheritDoc */ init(): void { @@ -24,7 +20,7 @@ export class TreeEngine extends AriaRoleEngine { this.setRootRole(); if (orientation === 'horizontal' && isRoot) { - this.el.setAttribute('aria-orientation', orientation); + this.setAttribute('aria-orientation', orientation); } } @@ -32,7 +28,7 @@ export class TreeEngine extends AriaRoleEngine { * Sets the role to the element depending on whether the tree is root or nested */ protected setRootRole(): void { - this.el.setAttribute('role', this.params.isRoot ? 'tree' : 'group'); + this.setAttribute('role', this.params.isRoot ? 'tree' : 'group'); } /** @@ -41,6 +37,6 @@ export class TreeEngine extends AriaRoleEngine { * @param isFolded */ protected onChange(el: Element, isFolded: boolean): void { - el.setAttribute('aria-expanded', String(!isFolded)); + this.setAttribute('aria-expanded', String(!isFolded), el); } } diff --git a/src/core/component/directives/aria/roles-engines/tree/interface.ts b/src/core/component/directives/aria/roles-engines/tree/interface.ts index 44b0e9cf3f..bad9ae817c 100644 --- a/src/core/component/directives/aria/roles-engines/tree/interface.ts +++ b/src/core/component/directives/aria/roles-engines/tree/interface.ts @@ -8,8 +8,8 @@ import type { HandlerAttachment } from 'core/component/directives/aria/roles-engines/interface'; -export interface TreeParams { - isRoot: boolean; - orientation: string; - '@change': HandlerAttachment; +export class TreeParams { + isRoot: boolean = false; + orientation: string = 'false'; + '@change': HandlerAttachment = () => undefined; } diff --git a/src/core/component/directives/aria/roles-engines/treeitem/index.ts b/src/core/component/directives/aria/roles-engines/treeitem/index.ts index 8b10244a51..a65884bc12 100644 --- a/src/core/component/directives/aria/roles-engines/treeitem/index.ts +++ b/src/core/component/directives/aria/roles-engines/treeitem/index.ts @@ -12,18 +12,12 @@ import iAccess from 'traits/i-access/i-access'; import type iBlock from 'super/i-block/i-block'; -import type { TreeitemParams } from 'core/component/directives/aria/roles-engines/treeitem/interface'; +import { TreeitemParams } from 'core/component/directives/aria/roles-engines/treeitem/interface'; import { AriaRoleEngine, KeyCodes } from 'core/component/directives/aria/roles-engines/interface'; export class TreeitemEngine extends AriaRoleEngine { - /** @see [[AriaRoleEngine.Params]] */ - override Params!: TreeitemParams; - - /** @see [[AriaRoleEngine.ctx]] */ - override ctx?: iBlock & iAccess; - - /** @see [[AriaRoleEngine.params]] */ - static override params: string[] = ['isFirstRootItem', 'isExpandable', 'isExpanded', 'orientation', 'rootElement', 'toggleFold']; + override Params: TreeitemParams = new TreeitemParams(); + override Ctx!: iBlock & iAccess; /** @inheritDoc */ init(): void { @@ -31,7 +25,7 @@ export class TreeitemEngine extends AriaRoleEngine { Object.throw('Treeitem aria directive expects the component to realize iAccess interface'); } - this.async?.on(this.el, 'keydown', this.onKeyDown.bind(this)); + this.async.on(this.el, 'keydown', this.onKeyDown.bind(this)); const isMuted = this.ctx?.removeAllFromTabSequence(this.el); @@ -41,15 +35,15 @@ export class TreeitemEngine extends AriaRoleEngine { this.ctx?.restoreAllToTabSequence(this.el); } else { - this.el.setAttribute('tabindex', '0'); + this.setAttribute('tabindex', '0'); } } - this.el.setAttribute('role', 'treeitem'); + this.setAttribute('role', 'treeitem'); this.ctx?.$nextTick(() => { if (this.params.isExpandable) { - this.el.setAttribute('aria-expanded', String(this.params.isExpanded)); + this.setAttribute('aria-expanded', String(this.params.isExpanded)); } }); } @@ -125,7 +119,7 @@ export class TreeitemEngine extends AriaRoleEngine { protected setFocusToFirstItem(): void { const firstItem = this.ctx?.findFocusableElement(this.params.rootElement); - debugger; + if (firstItem != null) { this.focusNext(firstItem); } diff --git a/src/core/component/directives/aria/roles-engines/treeitem/interface.ts b/src/core/component/directives/aria/roles-engines/treeitem/interface.ts index 38bfb24418..bcd0834b2d 100644 --- a/src/core/component/directives/aria/roles-engines/treeitem/interface.ts +++ b/src/core/component/directives/aria/roles-engines/treeitem/interface.ts @@ -6,11 +6,13 @@ * https://github.com/V4Fire/Client/blob/master/LICENSE */ -export interface TreeitemParams { - isFirstRootItem: boolean; - isExpandable: boolean; - isExpanded: boolean; - orientation: string; - rootElement: CanUndef; - toggleFold(el: Element, value?: boolean): void; +type FoldToggle = (el: Element, value?: boolean) => void; + +export class TreeitemParams { + isFirstRootItem: boolean = false; + isExpandable: boolean = false; + isExpanded: boolean = false; + orientation: string = 'false'; + rootElement?: HTMLElement = undefined; + toggleFold: FoldToggle = () => undefined; } From fabd2a5ee9ed381174a6d7d499509744d9bd2fb7 Mon Sep 17 00:00:00 2001 From: Aleksandr Bunin Date: Fri, 19 Aug 2022 17:47:12 +0300 Subject: [PATCH 137/185] refactoring --- src/form/b-checkbox/b-checkbox.ss | 10 ---------- src/form/b-checkbox/b-checkbox.styl | 4 +++- src/form/b-checkbox/b-checkbox.ts | 5 ++++- 3 files changed, 7 insertions(+), 12 deletions(-) diff --git a/src/form/b-checkbox/b-checkbox.ss b/src/form/b-checkbox/b-checkbox.ss index 8a92080ac7..a46209e12b 100644 --- a/src/form/b-checkbox/b-checkbox.ss +++ b/src/form/b-checkbox/b-checkbox.ss @@ -14,16 +14,6 @@ - nativeInputType = "'checkbox'" - nativeInputModel = undefined - - block hiddenInput() - += self.nativeInput({ & - elName: 'hidden-input', - id: 'id || dom.getId("input")', - - attrs: { - autocomplete: 'off' - } - }) . - - block rootAttrs - super ? rootAttrs[':-parent-id'] = 'parentId' diff --git a/src/form/b-checkbox/b-checkbox.styl b/src/form/b-checkbox/b-checkbox.styl index 6d1605175d..320bbca008 100644 --- a/src/form/b-checkbox/b-checkbox.styl +++ b/src/form/b-checkbox/b-checkbox.styl @@ -17,7 +17,9 @@ b-checkbox extends i-input contain paint position relative - &__wrapper, &__checkbox, &__label + &__wrapper, + &__checkbox, + &__label cursor pointer &__wrapper diff --git a/src/form/b-checkbox/b-checkbox.ts b/src/form/b-checkbox/b-checkbox.ts index bc4b9420d2..ef9dd10945 100644 --- a/src/form/b-checkbox/b-checkbox.ts +++ b/src/form/b-checkbox/b-checkbox.ts @@ -158,6 +158,9 @@ export default class bCheckbox extends iInput implements iSize { return this.defaultProp; } + @system((ctx) => ctx.sync.link((v: Dictionary) => ({...v, id: ctx.id ?? 'hidden-input'}))) + override attrs?: Dictionary; + /** * True if the checkbox is checked */ @@ -198,7 +201,7 @@ export default class bCheckbox extends iInput implements iSize { @system() protected override valueStore!: this['Value']; - protected override readonly $refs!: {input: HTMLInputElement}; + protected override readonly $refs!: { input: HTMLInputElement }; /** * Checks the checkbox From 6de694ea24241476511f014501976988933803fc Mon Sep 17 00:00:00 2001 From: Aleksandr Bunin Date: Sat, 20 Aug 2022 15:15:05 +0300 Subject: [PATCH 138/185] fixing tests & adding new tests --- src/form/b-checkbox/b-checkbox.ss | 2 +- src/form/b-checkbox/b-checkbox.ts | 2 +- src/form/b-checkbox/test/unit/simple.ts | 32 +++++++++++++++++++++++++ 3 files changed, 34 insertions(+), 2 deletions(-) diff --git a/src/form/b-checkbox/b-checkbox.ss b/src/form/b-checkbox/b-checkbox.ss index a46209e12b..2011b4fd2f 100644 --- a/src/form/b-checkbox/b-checkbox.ss +++ b/src/form/b-checkbox/b-checkbox.ss @@ -39,7 +39,7 @@ - block label < label.&__label & v-if = label || vdom.getSlot('label') | - :for = id || dom.getId('input') + :for = id || dom.getId('hidden-input') . += self.slot('label', {':label': 'label'}) {{ t(label) }} diff --git a/src/form/b-checkbox/b-checkbox.ts b/src/form/b-checkbox/b-checkbox.ts index ef9dd10945..e8f2109db0 100644 --- a/src/form/b-checkbox/b-checkbox.ts +++ b/src/form/b-checkbox/b-checkbox.ts @@ -158,7 +158,7 @@ export default class bCheckbox extends iInput implements iSize { return this.defaultProp; } - @system((ctx) => ctx.sync.link((v: Dictionary) => ({...v, id: ctx.id ?? 'hidden-input'}))) + @system((ctx) => ctx.sync.link((v: Dictionary) => ({...v, id: ctx.id ?? ctx.dom.getId('hidden-input')}))) override attrs?: Dictionary; /** diff --git a/src/form/b-checkbox/test/unit/simple.ts b/src/form/b-checkbox/test/unit/simple.ts index cd0feea7c0..f422aa1dda 100644 --- a/src/form/b-checkbox/test/unit/simple.ts +++ b/src/form/b-checkbox/test/unit/simple.ts @@ -179,6 +179,23 @@ test.describe('b-checkbox simple usage', () => { ).toBeUndefined(); }); + test('checking with id prop', async ({page}) => { + const target = await init(page, {value: 'bar', id: 'foo'}); + + test.expect( + await target.evaluate((ctx) => ctx.unsafe.block!.element('hidden-input')?.id) + ).toBe('foo'); + }); + + test('checking without id prop', async ({page}) => { + const target = await init(page, {value: 'bar'}); + const id = await target.evaluate((ctx) => ctx.unsafe.dom.getId('')); + + test.expect( + await target.evaluate((ctx) => ctx.unsafe.block!.element('hidden-input')?.id) + ).toBe(`${id}hidden-input`); + }); + test('checkbox with a `label` prop', async ({page}) => { const target = await init(page, { label: 'Foo' @@ -197,6 +214,21 @@ test.describe('b-checkbox simple usage', () => { test.expect( await target.evaluate((ctx) => ctx.value) ).toBe(true); + + const id = await target.evaluate((ctx) => ctx.unsafe.dom.getId('')); + + test.expect( + await target.evaluate((ctx) => ctx.unsafe.block!.element('label')?.getAttribute('for')) + ).toBe(`${id}hidden-input`); + + const target2 = await init(page, { + label: 'Foo', + id: 'bla' + }); + + test.expect( + await target2.evaluate((ctx) => ctx.unsafe.block!.element('label')?.getAttribute('for')) + ).toBe('bla'); }); /** From cf6e007073e6a886a2759f8f3c27d44fad124c37 Mon Sep 17 00:00:00 2001 From: Aleksandr Bunin Date: Tue, 23 Aug 2022 11:43:03 +0300 Subject: [PATCH 139/185] =?UTF-8?q?=20=F0=9F=93=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/base/b-tree/b-tree.ts | 2 +- src/core/component/directives/aria/README.md | 25 +++++------ .../directives/aria/roles-engines/README.md | 33 ++++++++++++++ .../aria/roles-engines/combobox/README.md | 26 +++++++++-- .../aria/roles-engines/combobox/index.ts | 6 +-- .../aria/roles-engines/controls/README.md | 31 ++++++------- .../aria/roles-engines/dialog/README.md | 10 +++-- .../aria/roles-engines/interface.ts | 2 +- .../aria/roles-engines/listbox/README.md | 11 +++-- .../aria/roles-engines/option/README.md | 18 ++++++-- .../aria/roles-engines/tab/README.md | 40 +++++++++++++---- .../aria/roles-engines/tablist/README.md | 23 ++++++++-- .../aria/roles-engines/tabpanel/README.md | 21 ++++----- .../aria/roles-engines/tree/README.md | 25 +++++++++-- .../aria/roles-engines/tree/index.ts | 6 +-- .../aria/roles-engines/treeitem/README.md | 44 ++++++++++++------- .../aria/roles-engines/treeitem/interface.ts | 2 +- 17 files changed, 231 insertions(+), 94 deletions(-) create mode 100644 src/core/component/directives/aria/roles-engines/README.md diff --git a/src/base/b-tree/b-tree.ts b/src/base/b-tree/b-tree.ts index 8c5fac81a2..bec9ae954d 100644 --- a/src/base/b-tree/b-tree.ts +++ b/src/base/b-tree/b-tree.ts @@ -280,7 +280,7 @@ class bTree extends iData implements iItems, iAccess { isRoot: this.top == null, orientation: this.orientation, '@change': (cb: Function) => { - this.on('fold', (ctx, el, item, value) => cb(el, value)); + this.on('fold', (ctx, el: Element, item, value: boolean) => cb(el, !value)); } }; diff --git a/src/core/component/directives/aria/README.md b/src/core/component/directives/aria/README.md index 375596c8c0..e1a8b4ed43 100644 --- a/src/core/component/directives/aria/README.md +++ b/src/core/component/directives/aria/README.md @@ -47,6 +47,18 @@ Each role can accept its own set of options, which are described in its document ## Available options +All ARIA attributes could be added in options through short syntax. + +``` +< div v-aria = {label: 'foo', desribedby: 'id1', details: 'id2'} + +/// The same as + +< div :aria-label = 'foo' | :aria-desribedby = 'id1' | :aria-details = 'id2' +``` + +The most common are described below: + ### [label] Defines a string value that labels the current element. @@ -76,19 +88,6 @@ See [this](https://www.w3.org/TR/wai-aria/#aria-labelledby) for more information < input type = text | v-aria = {labelledby: 'billing address'} ``` -### [description] - -Defines a string value that describes or annotates the current element. -See [this](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes/aria-description) for more information. - -``` -< div role = application | v-aria = {label: 'calendar', decription: 'Game schedule for the Boston Red Sox 2021 Season'} - < h1 - Red Sox 2021 - - ... -``` - ### [describedby] Identifies the element (or elements) that describes the object. diff --git a/src/core/component/directives/aria/roles-engines/README.md b/src/core/component/directives/aria/roles-engines/README.md new file mode 100644 index 0000000000..d5e9d13fff --- /dev/null +++ b/src/core/component/directives/aria/roles-engines/README.md @@ -0,0 +1,33 @@ +# core/component/directives/aria/roles-engines/combobox + +This module provides engines for `v-aria` directive. + +## API + +Some roles need to handle components state changes or react to some events (add, delete or change certain attributes). +The fields in directive passed options which name starts with `@` respond for this (ex. `@change`, `@open`). +The certain contract should be followed: +the name of the callback, which should be 'connected' with such field should start with `on` and be named in camelCase style (ex. `onChange`, `onOpen`). + +Directive supports this field type to be function, promise or string (type [`HandlerAttachment`](`core/component/directives/aria/roles-engines/interface.ts`)). +- Function: +expects a callback to be passed. +In this function callback could be added as a listener to certain component events or provide to the callback some component's state. + +``` +< div v-aria:somerole = {'@change': (cb) => on('event', cb)} +``` + +- Promise: +If the field is a `Promise` or a `PromiseLike` object the callback would be passed to `then`. + +- String: +If the field is a `string`, the callback would be added as a listener to component's event similar to the string. + +``` +< div v-aria:somerole = {'@change': 'event'} + +// the same as + +< div v-aria:somerole = {'@change': (cb) => on('event', cb)} +``` diff --git a/src/core/component/directives/aria/roles-engines/combobox/README.md b/src/core/component/directives/aria/roles-engines/combobox/README.md index 57e6f9c96f..8a5792f9df 100644 --- a/src/core/component/directives/aria/roles-engines/combobox/README.md +++ b/src/core/component/directives/aria/roles-engines/combobox/README.md @@ -3,12 +3,30 @@ This module provides an engine for `v-aria` directive. The engine to set `combobox` role attribute. -For more information about attributes go to [`https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/combobox_role`]. -For recommendations how to make accessible widget go to [`https://www.w3.org/WAI/ARIA/apg/patterns/combobox/`]. +The `combobox` role identifies an element as an input that controls another element, such as a `listbox`, that can dynamically pop up to help the user set the value of that input. + +For more information about attributes go to [combobox](`https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/combobox_role`). +For recommendations how to make accessible widget go to [combobox](`https://www.w3.org/WAI/ARIA/apg/patterns/combobox/`). + +## API + +The engine expects specific parameters to be passed. +- `isMultiple`:`boolean`. +If true widget supports multiple selected options. +- `@change`:`HandlerAttachment`, see `core/component/directives/aria/roles-engines/README.md`. +Internal callback `onChange` expects an `Element` to be passed. +- `@open`:`HandlerAttachment`. +Internal callback `onChange` expects an `Element` to be passed. +- `@close`:`HandlerAttachment`. ## Usage ``` -< &__foo v-aria:combobox = {...} - +< div v-aria:combobox = { & + isMultiple: multiple, + '@change': (cb) => on('actionChange', cb), + '@open': 'open', + '@close': 'close' + } +. ``` diff --git a/src/core/component/directives/aria/roles-engines/combobox/index.ts b/src/core/component/directives/aria/roles-engines/combobox/index.ts index 7af277460e..9240a547d0 100644 --- a/src/core/component/directives/aria/roles-engines/combobox/index.ts +++ b/src/core/component/directives/aria/roles-engines/combobox/index.ts @@ -43,7 +43,7 @@ export class ComboboxEngine extends AriaRoleEngine { /** * Sets or deletes the id of active descendant element */ - protected setAriaActive(el?: HTMLElement): void { + protected setAriaActive(el?: Element): void { this.setAttribute('aria-activedescendant', el?.id ?? ''); } @@ -51,7 +51,7 @@ export class ComboboxEngine extends AriaRoleEngine { * Handler: the option list is expanded * @param el */ - protected onOpen(el: HTMLElement): void { + protected onOpen(el: Element): void { this.setAttribute('aria-expanded', 'true'); this.setAriaActive(el); } @@ -68,7 +68,7 @@ export class ComboboxEngine extends AriaRoleEngine { * Handler: active option element was changed * @param el */ - protected onChange(el: HTMLElement): void { + protected onChange(el: Element): void { this.setAriaActive(el); } } diff --git a/src/core/component/directives/aria/roles-engines/controls/README.md b/src/core/component/directives/aria/roles-engines/controls/README.md index 1b3744eddd..c23104c222 100644 --- a/src/core/component/directives/aria/roles-engines/controls/README.md +++ b/src/core/component/directives/aria/roles-engines/controls/README.md @@ -2,17 +2,12 @@ This module provides an engine for `v-aria` directive. -The engine to set aria-controls attribute. -For more information go to [`https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes/aria-controls`]. +The engine is used to set `aria-controls` attribute. +The global `aria-controls` property identifies the element (or elements) whose contents or presence are controlled by the element on which this attribute is set. -## Usage - -``` -< &__foo v-aria:controls = {...} - -``` +For more information go to [controls](`https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes/aria-controls`). -## How to use +## API Directive can be added to any tag that includes tag with needed role. Role should be passed as a modifier. ID or IDs are passed as value. @@ -25,10 +20,10 @@ If element controls several elements `for` should be passed as a string with IDs Example: ``` -< &__foo v-aria:controls.tab = {for: 'id1 id2 id3'} +< div v-aria:controls.tab = {for: 'id1 id2 id3'} -the same as -< &__foo +// the same as +< div < button aria-controls = "id1 id2 id3" role = "tab" ``` @@ -39,12 +34,18 @@ The second one is an id of an element to set as value in aria-controls attribute Example: ``` -< &__foo v-aria:controls = {for: [[id1, id3], [id2, id4]]} +< div v-aria:controls = {for: [[id1, id3], [id2, id4]]} < span :id = "id1" < span :id = "id2" -the same as -< &__foo +// the same as +< div < span :id = "id1" aria-controls = "id3" < span :id = "id2" aria-controls = "id4" ``` + +## Usage + +``` +< div v-aria:controls = {...} +``` diff --git a/src/core/component/directives/aria/roles-engines/dialog/README.md b/src/core/component/directives/aria/roles-engines/dialog/README.md index 342b5b3e6c..2540480c79 100644 --- a/src/core/component/directives/aria/roles-engines/dialog/README.md +++ b/src/core/component/directives/aria/roles-engines/dialog/README.md @@ -3,14 +3,16 @@ This module provides an engine for `v-aria` directive. The engine to set `dialog` role attribute. -For more information go to [`https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/dialog_role`]. +The `dialog` role is used to mark up an HTML based application dialog or window that separates content or UI from the rest of the web application or page. -Expects `iOpen` trait to be realized. +For more information go to [dialog](`https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/dialog_role`). +## API + +The engine expects the component to realize the`iOpen` trait. ## Usage ``` -< &__foo v-aria:dialog - +< div v-aria:dialog ``` diff --git a/src/core/component/directives/aria/roles-engines/interface.ts b/src/core/component/directives/aria/roles-engines/interface.ts index e6e79394b8..f82549b390 100644 --- a/src/core/component/directives/aria/roles-engines/interface.ts +++ b/src/core/component/directives/aria/roles-engines/interface.ts @@ -75,7 +75,7 @@ export interface EngineOptions

      void; +export type HandlerAttachment = ((cb: Function) => void) | Promise | string; export const enum KeyCodes { ENTER = 'Enter', diff --git a/src/core/component/directives/aria/roles-engines/listbox/README.md b/src/core/component/directives/aria/roles-engines/listbox/README.md index fdd53494f0..8083d3a1fe 100644 --- a/src/core/component/directives/aria/roles-engines/listbox/README.md +++ b/src/core/component/directives/aria/roles-engines/listbox/README.md @@ -3,12 +3,15 @@ This module provides an engine for `v-aria` directive. The engine to set `listbox` role attribute. -For more information go to [`https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/listbox_role`]. -For recommendations how to make accessible widget go to [`https://www.w3.org/WAI/ARIA/apg/patterns/listbox/`]. +The `listbox` role is used for lists from which a user may select one or more items which are static and may contain images. + +For more information go to [listbox](`https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/listbox_role`). +For recommendations how to make accessible widget go to [listbox](`https://www.w3.org/WAI/ARIA/apg/patterns/listbox/`). + +Widget `listbox` also contains elements with role `option` (see specified engine) ## Usage ``` -< &__foo v-aria:listbox = {...} - +< div v-aria:listbox = {...} ``` diff --git a/src/core/component/directives/aria/roles-engines/option/README.md b/src/core/component/directives/aria/roles-engines/option/README.md index cdf2fdcf28..c3141835a3 100644 --- a/src/core/component/directives/aria/roles-engines/option/README.md +++ b/src/core/component/directives/aria/roles-engines/option/README.md @@ -3,11 +3,23 @@ This module provides an engine for `v-aria` directive. The engine to set `option` role attribute. -For more information go to [`https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/option_role`]. +The option role is used for selectable items in a `listbox`. + +For more information go to [option](`https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/option_role`). + +## API + +The engine expects specific parameters to be passed. +- `isSelected`: `boolean`. +If true current option is selected by default. +- `@change`:`HandlerAttachment`, see `core/component/directives/aria/roles-engines/README.md`. +Internal callback `onChange` expects an `boolean` value if current option is selected. ## Usage ``` -< &__foo v-aria:option - +< div v-aria:option = { & + isSelected: el.active + '@change': (cb) => on('actionChange', () => cb(el.active)) + } ``` diff --git a/src/core/component/directives/aria/roles-engines/tab/README.md b/src/core/component/directives/aria/roles-engines/tab/README.md index 0395e3bad9..95e1c8e4c0 100644 --- a/src/core/component/directives/aria/roles-engines/tab/README.md +++ b/src/core/component/directives/aria/roles-engines/tab/README.md @@ -3,19 +3,26 @@ This module provides an engine for `v-aria` directive. The engine to set `tab` role attribute. -For more information go to [`https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/tab_role`]. -For recommendations how to make accessible widget go to [`https://www.w3.org/WAI/ARIA/apg/patterns/tabpanel/`]. +The ARIA tab role indicates an interactive element inside a `tablist` that, when activated, displays its associated `tabpanel`. -## Usage +For more information go to [tab](`https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/tab_role`). +For recommendations how to make accessible widget go to [tab](`https://www.w3.org/WAI/ARIA/apg/patterns/tabpanel/`). -``` -< &__foo v-aria:tab = {...} +## API -``` +The engine expects specific parameters to be passed. +- `isFirst`: `boolean`. +If true current tab is the first one in the list of tabs. +- `isSelected`: `boolean`. +If true current tab is active. +- `hasDefaultSelectedTabs`: `boolean`. +If true there are active tabs in the tablist widget by default. +- `orientation`: `string`. +The tablist widget view orientation. +- `@change`:`HandlerAttachment`, see `core/component/directives/aria/roles-engines/README.md`. +Internal callback `onChange` expects an `Element` or `NodeListOf` to be passed. -## How to use - -Tabs expect the `controls` role engine to be added in addition. ID passed to `controls` engine should be the id of the element with role `tabpanel`. +In addition, tabs expect the `controls` role engine to be added. An id passed to `controls` engine should be the id of the element with role `tabpanel`. Example: ``` @@ -25,3 +32,18 @@ Example: < span :id = 'id2' // content ``` + +The engine expects the component to realize`iAccess` trait. + +## Usage + +``` +< div v-aria:tab = { & + isFirst: i === 0, + isSelected: el.active, + hasDefaultSelectedTabs: items.some((el) => !!el.active), + orientation: orientation, + '@change': (cb) => cb(el.active) + } +. +``` diff --git a/src/core/component/directives/aria/roles-engines/tablist/README.md b/src/core/component/directives/aria/roles-engines/tablist/README.md index 078d4fd7e6..e3a123639c 100644 --- a/src/core/component/directives/aria/roles-engines/tablist/README.md +++ b/src/core/component/directives/aria/roles-engines/tablist/README.md @@ -3,12 +3,27 @@ This module provides an engine for `v-aria` directive. The engine to set `tablist` role attribute. -For more information go to [`https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/tablist_role`]. -For recommendations how to make accessible widget go to [`https://www.w3.org/WAI/ARIA/apg/patterns/tabpanel/`]. +The `tablist` role identifies the element that serves as the container for a set of `tabs`. The `tab` content are referred to as `tabpanel` elements. + +For more information go to [tablist](`https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/tablist_role`). +For recommendations how to make accessible widget go to [tablist](`https://www.w3.org/WAI/ARIA/apg/patterns/tabpanel/`). + +## API + +The engine expects specific parameters to be passed. +- `isMultiple`:`boolean`. +If true widget supports multiple selected options. +- `orientation`: `string`. +The tablist widget view orientation. + +The engine expects the component to realize`iAccess` trait. ## Usage ``` -< &__foo v-aria:tablist = {...} - +< div v-aria:tablist = { & + isMultiple: multiple; + orientation: orientation; + } +. ``` diff --git a/src/core/component/directives/aria/roles-engines/tabpanel/README.md b/src/core/component/directives/aria/roles-engines/tabpanel/README.md index a60f4f322d..f4a6bba828 100644 --- a/src/core/component/directives/aria/roles-engines/tabpanel/README.md +++ b/src/core/component/directives/aria/roles-engines/tabpanel/README.md @@ -3,19 +3,14 @@ This module provides an engine for `v-aria` directive. The engine to set `tabpanel` role attribute. -For more information go to [`https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/tabpanel_role`]. -For recommendations how to make accessible widget go to [`https://www.w3.org/WAI/ARIA/apg/patterns/tabpanel/`]. +The ARIA `tabpanel` is a container for the resources of layered content associated with a `tab`. -## Usage - -``` -< &__foo v-aria:tabpanel = {...} - -``` +For more information go to [tabpanel](`https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/tabpanel_role`). +For recommendations how to make accessible widget go to [tabpanel](`https://www.w3.org/WAI/ARIA/apg/patterns/tabpanel/`). -## How to use +## API -Expects `label` or `labelledby` params to be passed. +The engine expects `label` or `labelledby` params to be passed. Example: ``` @@ -23,3 +18,9 @@ Example: < span :id = 'id1' // content ``` + +## Usage + +``` +< div v-aria:tabpanel = {label: 'content'} +``` diff --git a/src/core/component/directives/aria/roles-engines/tree/README.md b/src/core/component/directives/aria/roles-engines/tree/README.md index f18c69c773..73f926850c 100644 --- a/src/core/component/directives/aria/roles-engines/tree/README.md +++ b/src/core/component/directives/aria/roles-engines/tree/README.md @@ -3,13 +3,30 @@ This module provides an engine for `v-aria` directive. The engine to set `tree` role attribute. -For more information go to [`https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/tree_role`]. +A `tree` is a widget that allows the user to select one or more items from a hierarchically organized collection. -For recommendations how to make accessible widget go to [`https://www.w3.org/WAI/ARIA/apg/patterns/treeview/`]. +For more information go to [tree](`https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/tree_role`). +For recommendations how to make accessible widget go to [tree](`https://www.w3.org/WAI/ARIA/apg/patterns/treeview/`). + +## API + +The engine expects specific parameters to be passed. +- `isRoot`: `boolean`. +If true current tree is the root tree in the component. +- `orientation`: `string`. +The tablist widget view orientation. +- `@change`:`HandlerAttachment`, see `core/component/directives/aria/roles-engines/README.md`. +Internal callback `onChange` expects an `Element` and `boolean` value if current tree is expanded. + +The engine expects the component to realize`iAccess` trait. ## Usage ``` -< &__foo v-aria:tree = {...} - +< div v-aria:tree = { & + isRoot: boolean = false; + orientation: string = 'false'; + '@change': HandlerAttachment = () => undefined; + } +. ``` diff --git a/src/core/component/directives/aria/roles-engines/tree/index.ts b/src/core/component/directives/aria/roles-engines/tree/index.ts index 92665ae3fa..3611b14510 100644 --- a/src/core/component/directives/aria/roles-engines/tree/index.ts +++ b/src/core/component/directives/aria/roles-engines/tree/index.ts @@ -34,9 +34,9 @@ export class TreeEngine extends AriaRoleEngine { /** * Handler: treeitem was expanded or closed * @param el - * @param isFolded + * @param isExpanded */ - protected onChange(el: Element, isFolded: boolean): void { - this.setAttribute('aria-expanded', String(!isFolded), el); + protected onChange(el: Element, isExpanded: boolean): void { + this.setAttribute('aria-expanded', String(isExpanded), el); } } diff --git a/src/core/component/directives/aria/roles-engines/treeitem/README.md b/src/core/component/directives/aria/roles-engines/treeitem/README.md index 3c262ae318..59f4cb951f 100644 --- a/src/core/component/directives/aria/roles-engines/treeitem/README.md +++ b/src/core/component/directives/aria/roles-engines/treeitem/README.md @@ -3,25 +3,39 @@ This module provides an engine for `v-aria` directive. The engine to set `treeitem` role attribute. -For more information go to [`https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/treeitem_role`]. +A `treeitem` is an item in a `tree`. -For recommendations how to make accessible widget go to [`https://www.w3.org/WAI/ARIA/apg/patterns/treeview/`]. +For more information go to [treeitem](`https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/treeitem_role`). +For recommendations how to make accessible widget go to [treeitem](`https://www.w3.org/WAI/ARIA/apg/patterns/treeview/`). -Expects `iAccess` trait to be realized. +## API + +The engine expects specific parameters to be passed. +- `isFirstRootItem`: `boolean`. +If true the item is first one in the root tree. +- `isExpandable`: `boolean`. +If true the item has children and can be expanded. +- `isExpanded`: `boolean`. +If true the item is expanded in the current moment. +- `orientation`: `string`. +The tablist widget view orientation. +- `rootElement`: `Element`. +The link to the root tree element. +- `toggleFold`: `function`. +The function to toggle the expandable item. + +The engine expects the component to realize`iAccess` trait. ## Usage ``` -< &__foo v-aria:treeitem = {...} - +< div v-aria:treeitem = { & + isFirstRootItem: el === top; + isExpandable: el.children != null; + isExpanded: !el.folded; + orientation: 'orientation'; + rootElement?: top; + toggleFold: () => ...; + } +. ``` - -## Adding new role engines -When creating a new role engine which handles some components events the contract of passed params types and naming should be respected. - -The name of handlers in engine should be like `onChange`, `onOpen`, etc. -The name of property in passed params should be like `@change`, `@open`, etc. -Types of the property on passed params could be: -- `Function` that accepts callback parameter; -- `Promise`, so the handler will be passed in `.then` method; -- `String` that is the name of component's event, so the handler will be added as a listener to this event. diff --git a/src/core/component/directives/aria/roles-engines/treeitem/interface.ts b/src/core/component/directives/aria/roles-engines/treeitem/interface.ts index bcd0834b2d..c9ec8aedd1 100644 --- a/src/core/component/directives/aria/roles-engines/treeitem/interface.ts +++ b/src/core/component/directives/aria/roles-engines/treeitem/interface.ts @@ -13,6 +13,6 @@ export class TreeitemParams { isExpandable: boolean = false; isExpanded: boolean = false; orientation: string = 'false'; - rootElement?: HTMLElement = undefined; + rootElement?: Element = undefined; toggleFold: FoldToggle = () => undefined; } From 894261ec4fb424a7c3fc66b2ad313b1d678a6c90 Mon Sep 17 00:00:00 2001 From: Andrey Kobets Date: Fri, 9 Sep 2022 12:59:32 +0300 Subject: [PATCH 140/185] doc: added more examples --- src/base/b-list/README.md | 64 +++++++++++++++++++++++++++------------ src/base/b-list/b-list.ts | 7 +++-- 2 files changed, 48 insertions(+), 23 deletions(-) diff --git a/src/base/b-list/README.md b/src/base/b-list/README.md index 4160d137a4..07610605a4 100644 --- a/src/base/b-list/README.md +++ b/src/base/b-list/README.md @@ -30,6 +30,43 @@ If you need a more complex layout, provide it via a slot or by using `item/itemP * Dynamic data loading. +## Accessibility + +The component supports two standard logical roles. + +### List of links + +If the component is used as a list of links, then standard HTML link semantics will be used. +That is, links can be navigated using the Tab key, etc. + +### List of tabs + +If the component is used as a list of tabs it will implement the ARIA [tablist](https://www.w3.org/TR/wai-aria/#tablist) role. +All available features included in this [widget] (https://www.w3.org/WAI/ARIA/apg/patterns/tabpanel/) are supported. + +Please note that the component does not support the ability to set the content of the tabs. +You need to do it yourself using the ARIA [tabpanel](https://www.w3.org/TR/wai-aria/#tabpanel) role. + +``` +< b-list :items = [ & + {label: 'First tab', id: 'tab-1', controls: 'panel-1'}, + {label: 'Second tag', id: 'tab-2', controls: 'panel-2'}, + {label: 'Third tab', id: 'tab-3', controls: 'panel-3'} +] . + +< div id = panel-1" | v-aria:tabpanel = {labelledby: 'tab-1'} + < p + Content for the first panel + +< div id = panel-2" | v-aria:tabpanel = {labelledby: 'tab-2'} + < p + Content for the second panel + +< div id = panel-3" | v-aria:tabpanel = {labelledby: 'tab-3'} + < p + Content for the third panel +``` + ## Modifiers | Name | Description | Values | Default | @@ -213,13 +250,18 @@ Also, you can see the implemented traits or the parent component. ### Props +#### [orientation = `horizontal`] + +Indicates whether the component orientation is `horizontal`, `vertical`, or unknown/ambiguous. +This props affects the ARIA component role. + #### [listTag = `'ul'`] -A type of the list' root tag. +A type of the list root tag. #### [listElTag = `'li'`] -A type of list' element tags. +A type of list element tags. #### [activeProp] @@ -243,10 +285,6 @@ By default, if the component is switched to the `multiple` mode, this value is s Initial additional attributes are provided to an "internal" (native) list tag. -#### [orientation = `horizontal`] - -The component view orientation. - ### Fields #### items @@ -337,17 +375,3 @@ class Test extends iData { } } ``` - -## Accessibility - -If the component is used as a list of tabs it will implement an ARIA role [tablist](https://www.w3.org/TR/wai-aria/#tablist). -All the accessible functionality included in this [widget](https://www.w3.org/WAI/ARIA/apg/patterns/tabpanel/) is supported. - -When the component is used as a list of links it bases on internal HTML semantics of list tags. - -The component includes the following roles: -- [tablist](https://www.w3.org/TR/wai-aria/#tablist) -- [tab](https://www.w3.org/TR/wai-aria/#tab) - -The widget should also include [tabpanel](https://www.w3.org/TR/wai-aria/#tabpanel) role which is a block that contains the content of each tab. -But the component does not provide such block. So the 'connection' with other component should be set with the help of [`v-aria:controls`](core/component/directives/aria/aria-engines/controls/README.md) diff --git a/src/base/b-list/b-list.ts b/src/base/b-list/b-list.ts index 3d6d72a8a5..cb19dbde9f 100644 --- a/src/base/b-list/b-list.ts +++ b/src/base/b-list/b-list.ts @@ -88,13 +88,13 @@ class bList extends iData implements iVisible, iWidth, iItems, iAccess { readonly itemProps?: iItems['itemProps']; /** - * Type of the list' root tag + * Type of the list root tag */ @prop(String) readonly listTag: string = 'ul'; /** - * Type of list' element tags + * Type of list element tags */ @prop(String) readonly listElTag: string = 'li'; @@ -120,7 +120,8 @@ class bList extends iData implements iVisible, iWidth, iItems, iAccess { readonly multiple: boolean = false; /** - * The component view orientation + * Indicates whether the component orientation is `horizontal`, `vertical`, or unknown/ambiguous. + * This props affects the ARIA component role. */ @prop(String) readonly orientation: Orientation = 'horizontal'; From 0cbe29f69770832bb57a684dcdcd553cccfe7b81 Mon Sep 17 00:00:00 2001 From: Andrey Kobets Date: Fri, 9 Sep 2022 13:01:10 +0300 Subject: [PATCH 141/185] chore: moved down a18ly charter --- src/base/b-list/README.md | 38 +++++++++++++++++++------------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/src/base/b-list/README.md b/src/base/b-list/README.md index 07610605a4..aebef4b582 100644 --- a/src/base/b-list/README.md +++ b/src/base/b-list/README.md @@ -30,6 +30,25 @@ If you need a more complex layout, provide it via a slot or by using `item/itemP * Dynamic data loading. +## Modifiers + +| Name | Description | Values | Default | +|--------------|------------------------|-----------|---------| +| `hideLabels` | Item labels are hidden | `boolean` | `false` | + +Also, you can see the parent component and the component traits. + +## Events + +| EventName | Description | Payload description | Payload | +|-------------------|-----------------------------------------------------------------------------------------------------------------------------|---------------------------------------|----------| +| `change` | An active item of the component has been changed | Active value or a set of active items | `Active` | +| `immediateChange` | An active item of the component has been changed (the event can fire at component initializing if `activeProp` is provided) | Active value or a set of active items | `Active` | +| `actionChange` | An active item of the component has been changed due to some user action | Active value or a set of active items | `Active` | +| `itemsChange` | A list of items has been changed | List of items | `Items` | + +Also, you can see the parent component and the component traits. + ## Accessibility The component supports two standard logical roles. @@ -67,25 +86,6 @@ You need to do it yourself using the ARIA [tabpanel](https://www.w3.org/TR/wai-a Content for the third panel ``` -## Modifiers - -| Name | Description | Values | Default | -|--------------|------------------------|-----------|---------| -| `hideLabels` | Item labels are hidden | `boolean` | `false` | - -Also, you can see the parent component and the component traits. - -## Events - -| EventName | Description | Payload description | Payload | -|-------------------|-----------------------------------------------------------------------------------------------------------------------------|---------------------------------------|----------| -| `change` | An active item of the component has been changed | Active value or a set of active items | `Active` | -| `immediateChange` | An active item of the component has been changed (the event can fire at component initializing if `activeProp` is provided) | Active value or a set of active items | `Active` | -| `actionChange` | An active item of the component has been changed due to some user action | Active value or a set of active items | `Active` | -| `itemsChange` | A list of items has been changed | List of items | `Items` | - -Also, you can see the parent component and the component traits. - ## Associated types The component has associated type to specify active component item: **Active**. From f8e304b953c7e1402fe6184e90b990f41362a8f8 Mon Sep 17 00:00:00 2001 From: Andrey Kobets Date: Fri, 9 Sep 2022 13:05:39 +0300 Subject: [PATCH 142/185] chore: removed redundant chars from the example --- src/base/b-list/README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/base/b-list/README.md b/src/base/b-list/README.md index aebef4b582..f3f10c7e48 100644 --- a/src/base/b-list/README.md +++ b/src/base/b-list/README.md @@ -73,15 +73,15 @@ You need to do it yourself using the ARIA [tabpanel](https://www.w3.org/TR/wai-a {label: 'Third tab', id: 'tab-3', controls: 'panel-3'} ] . -< div id = panel-1" | v-aria:tabpanel = {labelledby: 'tab-1'} +< div id = panel-1 | v-aria:tabpanel = {labelledby: 'tab-1'} < p Content for the first panel -< div id = panel-2" | v-aria:tabpanel = {labelledby: 'tab-2'} +< div id = panel-2 | v-aria:tabpanel = {labelledby: 'tab-2'} < p Content for the second panel -< div id = panel-3" | v-aria:tabpanel = {labelledby: 'tab-3'} +< div id = panel-3 | v-aria:tabpanel = {labelledby: 'tab-3'} < p Content for the third panel ``` From 497425e88b557ba59be43a653f9197cb0fcb70a7 Mon Sep 17 00:00:00 2001 From: Aleksandr Bunin Date: Tue, 13 Sep 2022 11:37:52 +0300 Subject: [PATCH 143/185] refactoring list --- src/base/b-list/README.md | 8 +-- src/base/b-list/b-list.ss | 2 + src/base/b-list/interface.ts | 10 ++++ .../aria/roles-engines/controls/README.md | 57 ++++++++++++++++++- 4 files changed, 72 insertions(+), 5 deletions(-) diff --git a/src/base/b-list/README.md b/src/base/b-list/README.md index f3f10c7e48..762243b079 100644 --- a/src/base/b-list/README.md +++ b/src/base/b-list/README.md @@ -61,7 +61,7 @@ That is, links can be navigated using the Tab key, etc. ### List of tabs If the component is used as a list of tabs it will implement the ARIA [tablist](https://www.w3.org/TR/wai-aria/#tablist) role. -All available features included in this [widget] (https://www.w3.org/WAI/ARIA/apg/patterns/tabpanel/) are supported. +All available features included in this [widget](https://www.w3.org/WAI/ARIA/apg/patterns/tabpanel/) are supported. Please note that the component does not support the ability to set the content of the tabs. You need to do it yourself using the ARIA [tabpanel](https://www.w3.org/TR/wai-aria/#tabpanel) role. @@ -73,15 +73,15 @@ You need to do it yourself using the ARIA [tabpanel](https://www.w3.org/TR/wai-a {label: 'Third tab', id: 'tab-3', controls: 'panel-3'} ] . -< div id = panel-1 | v-aria:tabpanel = {labelledby: 'tab-1'} +< div id = 'panel-1' | v-aria:tabpanel = {labelledby: 'tab-1'} < p Content for the first panel -< div id = panel-2 | v-aria:tabpanel = {labelledby: 'tab-2'} +< div id = 'panel-2' | v-aria:tabpanel = {labelledby: 'tab-2'} < p Content for the second panel -< div id = panel-3 | v-aria:tabpanel = {labelledby: 'tab-3'} +< div id = 'panel-3' | v-aria:tabpanel = {labelledby: 'tab-3'} < p Content for the third panel ``` diff --git a/src/base/b-list/b-list.ss b/src/base/b-list/b-list.ss index 323280bec9..74fc4a5021 100644 --- a/src/base/b-list/b-list.ss +++ b/src/base/b-list/b-list.ss @@ -30,6 +30,7 @@ < tag & :is = el.href !== undefined ? 'a' : 'button' | + :id = el.id | :href = el.href | :value = el.value | @@ -50,6 +51,7 @@ :v-attrs = isTablist ? { 'v-aria:tab': getAriaConfig('tab', el, i), + 'v-aria:controls': el.controls, ...el.attrs } : el.attrs diff --git a/src/base/b-list/interface.ts b/src/base/b-list/interface.ts index 4139904270..0e573dc07c 100644 --- a/src/base/b-list/interface.ts +++ b/src/base/b-list/interface.ts @@ -97,6 +97,16 @@ export interface Item extends Dictionary { * Map of additional attributes of the item */ attrs?: Dictionary; + + /** + * The id of the item + */ + id?: string; + + /** + * The id of the element which is controlled by the item in ARIA terms + */ + controls?: string; } export type Items = Item[]; diff --git a/src/core/component/directives/aria/roles-engines/controls/README.md b/src/core/component/directives/aria/roles-engines/controls/README.md index c23104c222..7c93ac3832 100644 --- a/src/core/component/directives/aria/roles-engines/controls/README.md +++ b/src/core/component/directives/aria/roles-engines/controls/README.md @@ -46,6 +46,61 @@ Example: ## Usage +Example with `b-list`: + +``` +< b-list :items = [ & + {label: 'First tab', id: 'tab-1', controls: 'panel-1'}, + {label: 'Second tag', id: 'tab-2', controls: 'panel-2'}, + {label: 'Third tab', id: 'tab-3', controls: 'panel-3'} +] . + +< div id = 'panel-1' | v-aria:tabpanel = {labelledby: 'tab-1'} + < p + Content for the first panel + +< div id = 'panel-2' | v-aria:tabpanel = {labelledby: 'tab-2'} + < p + Content for the second panel + +< div id = 'panel-3' | v-aria:tabpanel = {labelledby: 'tab-3'} + < p + Content for the third panel +``` + +Example with custom tab list: + ``` -< div v-aria:controls = {...} +< div.custom-page + < div v-aria.tablist = {label: 'Sample Tabs'} + < span & + id = 'tab-1' | + v-aria.tab = {...} | + v-aria.controls = {for: 'panel-1'} + First Tab + + < span & + id = 'tab-2' | + v-aria.tab = {...} | + v-aria.controls = {for: 'panel-2'} + Second Tab + + < span & + id = 'tab-3' | + v-aria.tab = {...} | + v-aria.controls = {for: 'panel-3'} + Third Tab + + < div id = 'panel-1' | v-aria.tabpanel = {labelledby = 'tab-1'} + < p + Content for the first panel + + < div id = 'panel-2' | v-aria.tabpanel = {labelledby = 'tab-2'} + < p + Content for the second panel + + < div id = 'panel-3' | v-aria.tabpanel = {labelledby = 'tab-3'} + < p + Content for the third panel + ``` From 7520a4483083d96489fbabd81ff32548e054ebd9 Mon Sep 17 00:00:00 2001 From: Aleksandr Bunin Date: Tue, 13 Sep 2022 11:59:21 +0300 Subject: [PATCH 144/185] improve docs --- .../aria/roles-engines/controls/README.md | 6 +++--- .../aria/roles-engines/tab/README.md | 14 ++++++++++++++ .../aria/roles-engines/tabpanel/README.md | 19 +++++++++++-------- 3 files changed, 28 insertions(+), 11 deletions(-) diff --git a/src/core/component/directives/aria/roles-engines/controls/README.md b/src/core/component/directives/aria/roles-engines/controls/README.md index 7c93ac3832..d7d41dfec2 100644 --- a/src/core/component/directives/aria/roles-engines/controls/README.md +++ b/src/core/component/directives/aria/roles-engines/controls/README.md @@ -75,19 +75,19 @@ Example with custom tab list: < div v-aria.tablist = {label: 'Sample Tabs'} < span & id = 'tab-1' | - v-aria.tab = {...} | + v-aria.tab = {...config} | v-aria.controls = {for: 'panel-1'} First Tab < span & id = 'tab-2' | - v-aria.tab = {...} | + v-aria.tab = {...config} | v-aria.controls = {for: 'panel-2'} Second Tab < span & id = 'tab-3' | - v-aria.tab = {...} | + v-aria.tab = {...config} | v-aria.controls = {for: 'panel-3'} Third Tab diff --git a/src/core/component/directives/aria/roles-engines/tab/README.md b/src/core/component/directives/aria/roles-engines/tab/README.md index 95e1c8e4c0..29e1d0d3b3 100644 --- a/src/core/component/directives/aria/roles-engines/tab/README.md +++ b/src/core/component/directives/aria/roles-engines/tab/README.md @@ -37,6 +37,7 @@ The engine expects the component to realize`iAccess` trait. ## Usage +Example of passing parameters: ``` < div v-aria:tab = { & isFirst: i === 0, @@ -47,3 +48,16 @@ The engine expects the component to realize`iAccess` trait. } . ``` + +Example of external usage: +``` +< div & + id = 'tab-1' | + v-aria:tab = {...config} | + v-aria:controls = {for: 'tabpanel-1'} + Tab + +< div id = 'tabpanel-1' | v-aria:tabpanel = {labelledby: 'tab-1'} + < p + Content for the panel +``` diff --git a/src/core/component/directives/aria/roles-engines/tabpanel/README.md b/src/core/component/directives/aria/roles-engines/tabpanel/README.md index f4a6bba828..2149453537 100644 --- a/src/core/component/directives/aria/roles-engines/tabpanel/README.md +++ b/src/core/component/directives/aria/roles-engines/tabpanel/README.md @@ -10,17 +10,20 @@ For recommendations how to make accessible widget go to [tabpanel](`https://www. ## API -The engine expects `label` or `labelledby` params to be passed. +The engine expects `label` or `labelledby` params to be passed and the element `id` -Example: -``` -< v-aria:tabpanel = {labelledby: 'id1'} - < span :id = 'id1' - // content -``` ## Usage +Example: ``` -< div v-aria:tabpanel = {label: 'content'} +< div & + id = 'tab-1' | + v-aria:tab = {...config} | + v-aria:controls = {for: 'tabpanel-1'} + Tab + +< div id = 'tabpanel-1' | v-aria:tabpanel = {labelledby: 'tab-1'} + < p + Content for the panel ``` From 9577f666881b638a0f8d980f5d98cece8091e505 Mon Sep 17 00:00:00 2001 From: Aleksandr Bunin Date: Tue, 13 Sep 2022 12:04:24 +0300 Subject: [PATCH 145/185] rename folder --- src/core/component/directives/aria/adapter.ts | 4 ++-- .../directives/aria/roles-engines/index.ts | 20 ------------------- .../aria/{roles-engines => roles}/README.md | 4 ++-- .../combobox/CHANGELOG.md | 0 .../combobox/README.md | 4 ++-- .../combobox/index.ts | 4 ++-- .../combobox/interface.ts | 2 +- .../combobox/test/unit/combobox.ts | 0 .../controls/CHANGELOG.md | 0 .../controls/README.md | 2 +- .../controls/index.ts | 4 ++-- .../controls/interface.ts | 0 .../controls/test/unit/controls.ts | 0 .../dialog/CHANGELOG.md | 0 .../{roles-engines => roles}/dialog/README.md | 2 +- .../{roles-engines => roles}/dialog/index.ts | 2 +- .../dialog/test/unit/dialog.ts | 0 .../component/directives/aria/roles/index.ts | 20 +++++++++++++++++++ .../{roles-engines => roles}/interface.ts | 0 .../listbox/CHANGELOG.md | 0 .../listbox/README.md | 2 +- .../{roles-engines => roles}/listbox/index.ts | 2 +- .../listbox/test/unit/listbox.ts | 0 .../option/CHANGELOG.md | 0 .../{roles-engines => roles}/option/README.md | 4 ++-- .../{roles-engines => roles}/option/index.ts | 4 ++-- .../option/interface.ts | 2 +- .../option/test/unit/option.ts | 0 .../{roles-engines => roles}/tab/CHANGELOG.md | 0 .../{roles-engines => roles}/tab/README.md | 4 ++-- .../{roles-engines => roles}/tab/index.ts | 4 ++-- .../{roles-engines => roles}/tab/interface.ts | 2 +- .../tab/test/unit/tab.ts | 0 .../tablist/CHANGELOG.md | 0 .../tablist/README.md | 2 +- .../{roles-engines => roles}/tablist/index.ts | 4 ++-- .../tablist/interface.ts | 0 .../tablist/test/unit/tablist.ts | 0 .../tabpanel/CHANGELOG.md | 0 .../tabpanel/README.md | 2 +- .../tabpanel/index.ts | 2 +- .../tabpanel/test/unit/tabpanel.ts | 0 .../tree/CHANGELOG.md | 0 .../{roles-engines => roles}/tree/README.md | 4 ++-- .../{roles-engines => roles}/tree/index.ts | 4 ++-- .../tree/interface.ts | 2 +- .../tree/test/unit/tree.ts | 0 .../treeitem/CHANGELOG.md | 0 .../treeitem/README.md | 2 +- .../treeitem/index.ts | 4 ++-- .../treeitem/interface.ts | 0 .../treeitem/test/unit/treeitem.ts | 0 52 files changed, 59 insertions(+), 59 deletions(-) delete mode 100644 src/core/component/directives/aria/roles-engines/index.ts rename src/core/component/directives/aria/{roles-engines => roles}/README.md (93%) rename src/core/component/directives/aria/{roles-engines => roles}/combobox/CHANGELOG.md (100%) rename src/core/component/directives/aria/{roles-engines => roles}/combobox/README.md (92%) rename src/core/component/directives/aria/{roles-engines => roles}/combobox/index.ts (95%) rename src/core/component/directives/aria/{roles-engines => roles}/combobox/interface.ts (93%) rename src/core/component/directives/aria/{roles-engines => roles}/combobox/test/unit/combobox.ts (100%) rename src/core/component/directives/aria/{roles-engines => roles}/controls/CHANGELOG.md (100%) rename src/core/component/directives/aria/{roles-engines => roles}/controls/README.md (98%) rename src/core/component/directives/aria/{roles-engines => roles}/controls/index.ts (96%) rename src/core/component/directives/aria/{roles-engines => roles}/controls/interface.ts (100%) rename src/core/component/directives/aria/{roles-engines => roles}/controls/test/unit/controls.ts (100%) rename src/core/component/directives/aria/{roles-engines => roles}/dialog/CHANGELOG.md (100%) rename src/core/component/directives/aria/{roles-engines => roles}/dialog/README.md (90%) rename src/core/component/directives/aria/{roles-engines => roles}/dialog/index.ts (95%) rename src/core/component/directives/aria/{roles-engines => roles}/dialog/test/unit/dialog.ts (100%) create mode 100644 src/core/component/directives/aria/roles/index.ts rename src/core/component/directives/aria/{roles-engines => roles}/interface.ts (100%) rename src/core/component/directives/aria/{roles-engines => roles}/listbox/CHANGELOG.md (100%) rename src/core/component/directives/aria/{roles-engines => roles}/listbox/README.md (91%) rename src/core/component/directives/aria/{roles-engines => roles}/listbox/index.ts (94%) rename src/core/component/directives/aria/{roles-engines => roles}/listbox/test/unit/listbox.ts (100%) rename src/core/component/directives/aria/{roles-engines => roles}/option/CHANGELOG.md (100%) rename src/core/component/directives/aria/{roles-engines => roles}/option/README.md (89%) rename src/core/component/directives/aria/{roles-engines => roles}/option/index.ts (92%) rename src/core/component/directives/aria/{roles-engines => roles}/option/interface.ts (91%) rename src/core/component/directives/aria/{roles-engines => roles}/option/test/unit/option.ts (100%) rename src/core/component/directives/aria/{roles-engines => roles}/tab/CHANGELOG.md (100%) rename src/core/component/directives/aria/{roles-engines => roles}/tab/README.md (95%) rename src/core/component/directives/aria/{roles-engines => roles}/tab/index.ts (96%) rename src/core/component/directives/aria/{roles-engines => roles}/tab/interface.ts (92%) rename src/core/component/directives/aria/{roles-engines => roles}/tab/test/unit/tab.ts (100%) rename src/core/component/directives/aria/{roles-engines => roles}/tablist/CHANGELOG.md (100%) rename src/core/component/directives/aria/{roles-engines => roles}/tablist/README.md (94%) rename src/core/component/directives/aria/{roles-engines => roles}/tablist/index.ts (92%) rename src/core/component/directives/aria/{roles-engines => roles}/tablist/interface.ts (100%) rename src/core/component/directives/aria/{roles-engines => roles}/tablist/test/unit/tablist.ts (100%) rename src/core/component/directives/aria/{roles-engines => roles}/tabpanel/CHANGELOG.md (100%) rename src/core/component/directives/aria/{roles-engines => roles}/tabpanel/README.md (93%) rename src/core/component/directives/aria/{roles-engines => roles}/tabpanel/index.ts (95%) rename src/core/component/directives/aria/{roles-engines => roles}/tabpanel/test/unit/tabpanel.ts (100%) rename src/core/component/directives/aria/{roles-engines => roles}/tree/CHANGELOG.md (100%) rename src/core/component/directives/aria/{roles-engines => roles}/tree/README.md (92%) rename src/core/component/directives/aria/{roles-engines => roles}/tree/index.ts (95%) rename src/core/component/directives/aria/{roles-engines => roles}/tree/interface.ts (91%) rename src/core/component/directives/aria/{roles-engines => roles}/tree/test/unit/tree.ts (100%) rename src/core/component/directives/aria/{roles-engines => roles}/treeitem/CHANGELOG.md (100%) rename src/core/component/directives/aria/{roles-engines => roles}/treeitem/README.md (95%) rename src/core/component/directives/aria/{roles-engines => roles}/treeitem/index.ts (98%) rename src/core/component/directives/aria/{roles-engines => roles}/treeitem/interface.ts (100%) rename src/core/component/directives/aria/{roles-engines => roles}/treeitem/test/unit/treeitem.ts (100%) diff --git a/src/core/component/directives/aria/adapter.ts b/src/core/component/directives/aria/adapter.ts index 6249b2e139..d3a92c05d4 100644 --- a/src/core/component/directives/aria/adapter.ts +++ b/src/core/component/directives/aria/adapter.ts @@ -9,9 +9,9 @@ import Async from 'core/async'; import type iBlock from 'super/i-block/i-block'; -import * as roles from 'core/component/directives/aria/roles-engines'; +import * as roles from 'core/component/directives/aria/roles'; import type { DirectiveOptions } from 'core/component/directives/aria/interface'; -import type { AriaRoleEngine, EngineOptions } from 'core/component/directives/aria/roles-engines'; +import type { AriaRoleEngine, EngineOptions } from 'core/component/directives/aria/roles'; /** * An adapter to create an ARIA role instance based on the passed directive options and to add common attributes diff --git a/src/core/component/directives/aria/roles-engines/index.ts b/src/core/component/directives/aria/roles-engines/index.ts deleted file mode 100644 index cbb9f1bbce..0000000000 --- a/src/core/component/directives/aria/roles-engines/index.ts +++ /dev/null @@ -1,20 +0,0 @@ -/*! - * V4Fire Client Core - * https://github.com/V4Fire/Client - * - * Released under the MIT license - * https://github.com/V4Fire/Client/blob/master/LICENSE - */ - -export * from 'core/component/directives/aria/roles-engines/dialog'; -export * from 'core/component/directives/aria/roles-engines/tablist'; -export * from 'core/component/directives/aria/roles-engines/tab'; -export * from 'core/component/directives/aria/roles-engines/tabpanel'; -export * from 'core/component/directives/aria/roles-engines/controls'; -export * from 'core/component/directives/aria/roles-engines/combobox'; -export * from 'core/component/directives/aria/roles-engines/listbox'; -export * from 'core/component/directives/aria/roles-engines/option'; -export * from 'core/component/directives/aria/roles-engines/tree'; -export * from 'core/component/directives/aria/roles-engines/treeitem'; - -export { AriaRoleEngine, EngineOptions } from 'core/component/directives/aria/roles-engines/interface'; diff --git a/src/core/component/directives/aria/roles-engines/README.md b/src/core/component/directives/aria/roles/README.md similarity index 93% rename from src/core/component/directives/aria/roles-engines/README.md rename to src/core/component/directives/aria/roles/README.md index d5e9d13fff..3dd4966a8c 100644 --- a/src/core/component/directives/aria/roles-engines/README.md +++ b/src/core/component/directives/aria/roles/README.md @@ -1,4 +1,4 @@ -# core/component/directives/aria/roles-engines/combobox +# core/component/directives/aria/roles/combobox This module provides engines for `v-aria` directive. @@ -9,7 +9,7 @@ The fields in directive passed options which name starts with `@` respond for th The certain contract should be followed: the name of the callback, which should be 'connected' with such field should start with `on` and be named in camelCase style (ex. `onChange`, `onOpen`). -Directive supports this field type to be function, promise or string (type [`HandlerAttachment`](`core/component/directives/aria/roles-engines/interface.ts`)). +Directive supports this field type to be function, promise or string (type [`HandlerAttachment`](`core/component/directives/aria/roles/interface.ts`)). - Function: expects a callback to be passed. In this function callback could be added as a listener to certain component events or provide to the callback some component's state. diff --git a/src/core/component/directives/aria/roles-engines/combobox/CHANGELOG.md b/src/core/component/directives/aria/roles/combobox/CHANGELOG.md similarity index 100% rename from src/core/component/directives/aria/roles-engines/combobox/CHANGELOG.md rename to src/core/component/directives/aria/roles/combobox/CHANGELOG.md diff --git a/src/core/component/directives/aria/roles-engines/combobox/README.md b/src/core/component/directives/aria/roles/combobox/README.md similarity index 92% rename from src/core/component/directives/aria/roles-engines/combobox/README.md rename to src/core/component/directives/aria/roles/combobox/README.md index 8a5792f9df..c3b03ad2c7 100644 --- a/src/core/component/directives/aria/roles-engines/combobox/README.md +++ b/src/core/component/directives/aria/roles/combobox/README.md @@ -1,4 +1,4 @@ -# core/component/directives/aria/roles-engines/combobox +# core/component/directives/aria/roles/combobox This module provides an engine for `v-aria` directive. @@ -13,7 +13,7 @@ For recommendations how to make accessible widget go to [combobox](`https://www. The engine expects specific parameters to be passed. - `isMultiple`:`boolean`. If true widget supports multiple selected options. -- `@change`:`HandlerAttachment`, see `core/component/directives/aria/roles-engines/README.md`. +- `@change`:`HandlerAttachment`, see `core/component/directives/aria/roles/README.md`. Internal callback `onChange` expects an `Element` to be passed. - `@open`:`HandlerAttachment`. Internal callback `onChange` expects an `Element` to be passed. diff --git a/src/core/component/directives/aria/roles-engines/combobox/index.ts b/src/core/component/directives/aria/roles/combobox/index.ts similarity index 95% rename from src/core/component/directives/aria/roles-engines/combobox/index.ts rename to src/core/component/directives/aria/roles/combobox/index.ts index 9240a547d0..8a42bc6a95 100644 --- a/src/core/component/directives/aria/roles-engines/combobox/index.ts +++ b/src/core/component/directives/aria/roles/combobox/index.ts @@ -9,8 +9,8 @@ import type iAccess from 'traits/i-access/i-access'; import type { ComponentInterface } from 'super/i-block/i-block'; -import { ComboboxParams } from 'core/component/directives/aria/roles-engines/combobox/interface'; -import { AriaRoleEngine, EngineOptions } from 'core/component/directives/aria/roles-engines/interface'; +import { ComboboxParams } from 'core/component/directives/aria/roles/combobox/interface'; +import { AriaRoleEngine, EngineOptions } from 'core/component/directives/aria/roles/interface'; export class ComboboxEngine extends AriaRoleEngine { override Params: ComboboxParams = new ComboboxParams(); diff --git a/src/core/component/directives/aria/roles-engines/combobox/interface.ts b/src/core/component/directives/aria/roles/combobox/interface.ts similarity index 93% rename from src/core/component/directives/aria/roles-engines/combobox/interface.ts rename to src/core/component/directives/aria/roles/combobox/interface.ts index 03ee51ae17..b83a6ce5c0 100644 --- a/src/core/component/directives/aria/roles-engines/combobox/interface.ts +++ b/src/core/component/directives/aria/roles/combobox/interface.ts @@ -6,7 +6,7 @@ * https://github.com/V4Fire/Client/blob/master/LICENSE */ -import type { HandlerAttachment } from 'core/component/directives/aria/roles-engines/interface'; +import type { HandlerAttachment } from 'core/component/directives/aria/roles/interface'; const defaultFn = (): void => undefined; diff --git a/src/core/component/directives/aria/roles-engines/combobox/test/unit/combobox.ts b/src/core/component/directives/aria/roles/combobox/test/unit/combobox.ts similarity index 100% rename from src/core/component/directives/aria/roles-engines/combobox/test/unit/combobox.ts rename to src/core/component/directives/aria/roles/combobox/test/unit/combobox.ts diff --git a/src/core/component/directives/aria/roles-engines/controls/CHANGELOG.md b/src/core/component/directives/aria/roles/controls/CHANGELOG.md similarity index 100% rename from src/core/component/directives/aria/roles-engines/controls/CHANGELOG.md rename to src/core/component/directives/aria/roles/controls/CHANGELOG.md diff --git a/src/core/component/directives/aria/roles-engines/controls/README.md b/src/core/component/directives/aria/roles/controls/README.md similarity index 98% rename from src/core/component/directives/aria/roles-engines/controls/README.md rename to src/core/component/directives/aria/roles/controls/README.md index d7d41dfec2..344ac2f38f 100644 --- a/src/core/component/directives/aria/roles-engines/controls/README.md +++ b/src/core/component/directives/aria/roles/controls/README.md @@ -1,4 +1,4 @@ -# core/component/directives/aria/roles-engines/controls +# core/component/directives/aria/roles/controls This module provides an engine for `v-aria` directive. diff --git a/src/core/component/directives/aria/roles-engines/controls/index.ts b/src/core/component/directives/aria/roles/controls/index.ts similarity index 96% rename from src/core/component/directives/aria/roles-engines/controls/index.ts rename to src/core/component/directives/aria/roles/controls/index.ts index adb57ea056..d7ca127bbb 100644 --- a/src/core/component/directives/aria/roles-engines/controls/index.ts +++ b/src/core/component/directives/aria/roles/controls/index.ts @@ -6,8 +6,8 @@ * https://github.com/V4Fire/Client/blob/master/LICENSE */ -import { ControlsParams } from 'core/component/directives/aria/roles-engines/controls/interface'; -import { AriaRoleEngine } from 'core/component/directives/aria/roles-engines/interface'; +import { ControlsParams } from 'core/component/directives/aria/roles/controls/interface'; +import { AriaRoleEngine } from 'core/component/directives/aria/roles/interface'; export class ControlsEngine extends AriaRoleEngine { override Params: ControlsParams = new ControlsParams(); diff --git a/src/core/component/directives/aria/roles-engines/controls/interface.ts b/src/core/component/directives/aria/roles/controls/interface.ts similarity index 100% rename from src/core/component/directives/aria/roles-engines/controls/interface.ts rename to src/core/component/directives/aria/roles/controls/interface.ts diff --git a/src/core/component/directives/aria/roles-engines/controls/test/unit/controls.ts b/src/core/component/directives/aria/roles/controls/test/unit/controls.ts similarity index 100% rename from src/core/component/directives/aria/roles-engines/controls/test/unit/controls.ts rename to src/core/component/directives/aria/roles/controls/test/unit/controls.ts diff --git a/src/core/component/directives/aria/roles-engines/dialog/CHANGELOG.md b/src/core/component/directives/aria/roles/dialog/CHANGELOG.md similarity index 100% rename from src/core/component/directives/aria/roles-engines/dialog/CHANGELOG.md rename to src/core/component/directives/aria/roles/dialog/CHANGELOG.md diff --git a/src/core/component/directives/aria/roles-engines/dialog/README.md b/src/core/component/directives/aria/roles/dialog/README.md similarity index 90% rename from src/core/component/directives/aria/roles-engines/dialog/README.md rename to src/core/component/directives/aria/roles/dialog/README.md index 2540480c79..b97f8463a5 100644 --- a/src/core/component/directives/aria/roles-engines/dialog/README.md +++ b/src/core/component/directives/aria/roles/dialog/README.md @@ -1,4 +1,4 @@ -# core/component/directives/aria/roles-engines/dialog +# core/component/directives/aria/roles/dialog This module provides an engine for `v-aria` directive. diff --git a/src/core/component/directives/aria/roles-engines/dialog/index.ts b/src/core/component/directives/aria/roles/dialog/index.ts similarity index 95% rename from src/core/component/directives/aria/roles-engines/dialog/index.ts rename to src/core/component/directives/aria/roles/dialog/index.ts index d004f5dc4b..14f57631f5 100644 --- a/src/core/component/directives/aria/roles-engines/dialog/index.ts +++ b/src/core/component/directives/aria/roles/dialog/index.ts @@ -6,7 +6,7 @@ * https://github.com/V4Fire/Client/blob/master/LICENSE */ -import { AriaRoleEngine } from 'core/component/directives/aria/roles-engines/interface'; +import { AriaRoleEngine } from 'core/component/directives/aria/roles/interface'; import iOpen from 'traits/i-open/i-open'; export class DialogEngine extends AriaRoleEngine { diff --git a/src/core/component/directives/aria/roles-engines/dialog/test/unit/dialog.ts b/src/core/component/directives/aria/roles/dialog/test/unit/dialog.ts similarity index 100% rename from src/core/component/directives/aria/roles-engines/dialog/test/unit/dialog.ts rename to src/core/component/directives/aria/roles/dialog/test/unit/dialog.ts diff --git a/src/core/component/directives/aria/roles/index.ts b/src/core/component/directives/aria/roles/index.ts new file mode 100644 index 0000000000..792503f849 --- /dev/null +++ b/src/core/component/directives/aria/roles/index.ts @@ -0,0 +1,20 @@ +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ + +export * from 'core/component/directives/aria/roles/dialog'; +export * from 'core/component/directives/aria/roles/tablist'; +export * from 'core/component/directives/aria/roles/tab'; +export * from 'core/component/directives/aria/roles/tabpanel'; +export * from 'core/component/directives/aria/roles/controls'; +export * from 'core/component/directives/aria/roles/combobox'; +export * from 'core/component/directives/aria/roles/listbox'; +export * from 'core/component/directives/aria/roles/option'; +export * from 'core/component/directives/aria/roles/tree'; +export * from 'core/component/directives/aria/roles/treeitem'; + +export { AriaRoleEngine, EngineOptions } from 'core/component/directives/aria/roles/interface'; diff --git a/src/core/component/directives/aria/roles-engines/interface.ts b/src/core/component/directives/aria/roles/interface.ts similarity index 100% rename from src/core/component/directives/aria/roles-engines/interface.ts rename to src/core/component/directives/aria/roles/interface.ts diff --git a/src/core/component/directives/aria/roles-engines/listbox/CHANGELOG.md b/src/core/component/directives/aria/roles/listbox/CHANGELOG.md similarity index 100% rename from src/core/component/directives/aria/roles-engines/listbox/CHANGELOG.md rename to src/core/component/directives/aria/roles/listbox/CHANGELOG.md diff --git a/src/core/component/directives/aria/roles-engines/listbox/README.md b/src/core/component/directives/aria/roles/listbox/README.md similarity index 91% rename from src/core/component/directives/aria/roles-engines/listbox/README.md rename to src/core/component/directives/aria/roles/listbox/README.md index 8083d3a1fe..46beafb742 100644 --- a/src/core/component/directives/aria/roles-engines/listbox/README.md +++ b/src/core/component/directives/aria/roles/listbox/README.md @@ -1,4 +1,4 @@ -# core/component/directives/aria/roles-engines/listbox +# core/component/directives/aria/roles/listbox This module provides an engine for `v-aria` directive. diff --git a/src/core/component/directives/aria/roles-engines/listbox/index.ts b/src/core/component/directives/aria/roles/listbox/index.ts similarity index 94% rename from src/core/component/directives/aria/roles-engines/listbox/index.ts rename to src/core/component/directives/aria/roles/listbox/index.ts index 4e28b0c596..2bd9df6db6 100644 --- a/src/core/component/directives/aria/roles-engines/listbox/index.ts +++ b/src/core/component/directives/aria/roles/listbox/index.ts @@ -6,7 +6,7 @@ * https://github.com/V4Fire/Client/blob/master/LICENSE */ -import { AriaRoleEngine } from 'core/component/directives/aria/roles-engines/interface'; +import { AriaRoleEngine } from 'core/component/directives/aria/roles/interface'; export class ListboxEngine extends AriaRoleEngine { /** @inheritDoc */ diff --git a/src/core/component/directives/aria/roles-engines/listbox/test/unit/listbox.ts b/src/core/component/directives/aria/roles/listbox/test/unit/listbox.ts similarity index 100% rename from src/core/component/directives/aria/roles-engines/listbox/test/unit/listbox.ts rename to src/core/component/directives/aria/roles/listbox/test/unit/listbox.ts diff --git a/src/core/component/directives/aria/roles-engines/option/CHANGELOG.md b/src/core/component/directives/aria/roles/option/CHANGELOG.md similarity index 100% rename from src/core/component/directives/aria/roles-engines/option/CHANGELOG.md rename to src/core/component/directives/aria/roles/option/CHANGELOG.md diff --git a/src/core/component/directives/aria/roles-engines/option/README.md b/src/core/component/directives/aria/roles/option/README.md similarity index 89% rename from src/core/component/directives/aria/roles-engines/option/README.md rename to src/core/component/directives/aria/roles/option/README.md index c3141835a3..5717ff3f82 100644 --- a/src/core/component/directives/aria/roles-engines/option/README.md +++ b/src/core/component/directives/aria/roles/option/README.md @@ -1,4 +1,4 @@ -# core/component/directives/aria/roles-engines/option +# core/component/directives/aria/roles/option This module provides an engine for `v-aria` directive. @@ -12,7 +12,7 @@ For more information go to [option](`https://developer.mozilla.org/en-US/docs/We The engine expects specific parameters to be passed. - `isSelected`: `boolean`. If true current option is selected by default. -- `@change`:`HandlerAttachment`, see `core/component/directives/aria/roles-engines/README.md`. +- `@change`:`HandlerAttachment`, see `core/component/directives/aria/roles/README.md`. Internal callback `onChange` expects an `boolean` value if current option is selected. ## Usage diff --git a/src/core/component/directives/aria/roles-engines/option/index.ts b/src/core/component/directives/aria/roles/option/index.ts similarity index 92% rename from src/core/component/directives/aria/roles-engines/option/index.ts rename to src/core/component/directives/aria/roles/option/index.ts index 1791c48b4b..140c242e90 100644 --- a/src/core/component/directives/aria/roles-engines/option/index.ts +++ b/src/core/component/directives/aria/roles/option/index.ts @@ -6,8 +6,8 @@ * https://github.com/V4Fire/Client/blob/master/LICENSE */ -import { OptionParams } from 'core/component/directives/aria/roles-engines/option/interface'; -import { AriaRoleEngine } from 'core/component/directives/aria/roles-engines/interface'; +import { OptionParams } from 'core/component/directives/aria/roles/option/interface'; +import { AriaRoleEngine } from 'core/component/directives/aria/roles/interface'; export class OptionEngine extends AriaRoleEngine { override Params: OptionParams = new OptionParams(); diff --git a/src/core/component/directives/aria/roles-engines/option/interface.ts b/src/core/component/directives/aria/roles/option/interface.ts similarity index 91% rename from src/core/component/directives/aria/roles-engines/option/interface.ts rename to src/core/component/directives/aria/roles/option/interface.ts index 58d4bffe1f..c825c44888 100644 --- a/src/core/component/directives/aria/roles-engines/option/interface.ts +++ b/src/core/component/directives/aria/roles/option/interface.ts @@ -6,7 +6,7 @@ * https://github.com/V4Fire/Client/blob/master/LICENSE */ -import type { HandlerAttachment } from 'core/component/directives/aria/roles-engines/interface'; +import type { HandlerAttachment } from 'core/component/directives/aria/roles/interface'; export class OptionParams { isSelected: boolean = false; diff --git a/src/core/component/directives/aria/roles-engines/option/test/unit/option.ts b/src/core/component/directives/aria/roles/option/test/unit/option.ts similarity index 100% rename from src/core/component/directives/aria/roles-engines/option/test/unit/option.ts rename to src/core/component/directives/aria/roles/option/test/unit/option.ts diff --git a/src/core/component/directives/aria/roles-engines/tab/CHANGELOG.md b/src/core/component/directives/aria/roles/tab/CHANGELOG.md similarity index 100% rename from src/core/component/directives/aria/roles-engines/tab/CHANGELOG.md rename to src/core/component/directives/aria/roles/tab/CHANGELOG.md diff --git a/src/core/component/directives/aria/roles-engines/tab/README.md b/src/core/component/directives/aria/roles/tab/README.md similarity index 95% rename from src/core/component/directives/aria/roles-engines/tab/README.md rename to src/core/component/directives/aria/roles/tab/README.md index 29e1d0d3b3..2f393e551e 100644 --- a/src/core/component/directives/aria/roles-engines/tab/README.md +++ b/src/core/component/directives/aria/roles/tab/README.md @@ -1,4 +1,4 @@ -# core/component/directives/aria/roles-engines/tab +# core/component/directives/aria/roles/tab This module provides an engine for `v-aria` directive. @@ -19,7 +19,7 @@ If true current tab is active. If true there are active tabs in the tablist widget by default. - `orientation`: `string`. The tablist widget view orientation. -- `@change`:`HandlerAttachment`, see `core/component/directives/aria/roles-engines/README.md`. +- `@change`:`HandlerAttachment`, see `core/component/directives/aria/roles/README.md`. Internal callback `onChange` expects an `Element` or `NodeListOf` to be passed. In addition, tabs expect the `controls` role engine to be added. An id passed to `controls` engine should be the id of the element with role `tabpanel`. diff --git a/src/core/component/directives/aria/roles-engines/tab/index.ts b/src/core/component/directives/aria/roles/tab/index.ts similarity index 96% rename from src/core/component/directives/aria/roles-engines/tab/index.ts rename to src/core/component/directives/aria/roles/tab/index.ts index ddf9fb43d9..3c3e522290 100644 --- a/src/core/component/directives/aria/roles-engines/tab/index.ts +++ b/src/core/component/directives/aria/roles/tab/index.ts @@ -12,8 +12,8 @@ import type iBlock from 'super/i-block/i-block'; import type iAccess from 'traits/i-access/i-access'; -import { TabParams } from 'core/component/directives/aria/roles-engines/tab/interface'; -import { AriaRoleEngine, KeyCodes } from 'core/component/directives/aria/roles-engines/interface'; +import { TabParams } from 'core/component/directives/aria/roles/tab/interface'; +import { AriaRoleEngine, KeyCodes } from 'core/component/directives/aria/roles/interface'; export class TabEngine extends AriaRoleEngine { override Params: TabParams = new TabParams(); diff --git a/src/core/component/directives/aria/roles-engines/tab/interface.ts b/src/core/component/directives/aria/roles/tab/interface.ts similarity index 92% rename from src/core/component/directives/aria/roles-engines/tab/interface.ts rename to src/core/component/directives/aria/roles/tab/interface.ts index 190e244b0a..ec70ef6d2e 100644 --- a/src/core/component/directives/aria/roles-engines/tab/interface.ts +++ b/src/core/component/directives/aria/roles/tab/interface.ts @@ -6,7 +6,7 @@ * https://github.com/V4Fire/Client/blob/master/LICENSE */ -import type { HandlerAttachment } from 'core/component/directives/aria/roles-engines/interface'; +import type { HandlerAttachment } from 'core/component/directives/aria/roles/interface'; export class TabParams { isFirst: boolean = false; diff --git a/src/core/component/directives/aria/roles-engines/tab/test/unit/tab.ts b/src/core/component/directives/aria/roles/tab/test/unit/tab.ts similarity index 100% rename from src/core/component/directives/aria/roles-engines/tab/test/unit/tab.ts rename to src/core/component/directives/aria/roles/tab/test/unit/tab.ts diff --git a/src/core/component/directives/aria/roles-engines/tablist/CHANGELOG.md b/src/core/component/directives/aria/roles/tablist/CHANGELOG.md similarity index 100% rename from src/core/component/directives/aria/roles-engines/tablist/CHANGELOG.md rename to src/core/component/directives/aria/roles/tablist/CHANGELOG.md diff --git a/src/core/component/directives/aria/roles-engines/tablist/README.md b/src/core/component/directives/aria/roles/tablist/README.md similarity index 94% rename from src/core/component/directives/aria/roles-engines/tablist/README.md rename to src/core/component/directives/aria/roles/tablist/README.md index e3a123639c..91954684ea 100644 --- a/src/core/component/directives/aria/roles-engines/tablist/README.md +++ b/src/core/component/directives/aria/roles/tablist/README.md @@ -1,4 +1,4 @@ -# core/component/directives/aria/roles-engines/tablist +# core/component/directives/aria/roles/tablist This module provides an engine for `v-aria` directive. diff --git a/src/core/component/directives/aria/roles-engines/tablist/index.ts b/src/core/component/directives/aria/roles/tablist/index.ts similarity index 92% rename from src/core/component/directives/aria/roles-engines/tablist/index.ts rename to src/core/component/directives/aria/roles/tablist/index.ts index c464b54c37..ec78c07fd7 100644 --- a/src/core/component/directives/aria/roles-engines/tablist/index.ts +++ b/src/core/component/directives/aria/roles/tablist/index.ts @@ -6,8 +6,8 @@ * https://github.com/V4Fire/Client/blob/master/LICENSE */ -import { TablistParams } from 'core/component/directives/aria/roles-engines/tablist/interface'; -import { AriaRoleEngine } from 'core/component/directives/aria/roles-engines/interface'; +import { TablistParams } from 'core/component/directives/aria/roles/tablist/interface'; +import { AriaRoleEngine } from 'core/component/directives/aria/roles/interface'; export class TablistEngine extends AriaRoleEngine { override Params: TablistParams = new TablistParams(); diff --git a/src/core/component/directives/aria/roles-engines/tablist/interface.ts b/src/core/component/directives/aria/roles/tablist/interface.ts similarity index 100% rename from src/core/component/directives/aria/roles-engines/tablist/interface.ts rename to src/core/component/directives/aria/roles/tablist/interface.ts diff --git a/src/core/component/directives/aria/roles-engines/tablist/test/unit/tablist.ts b/src/core/component/directives/aria/roles/tablist/test/unit/tablist.ts similarity index 100% rename from src/core/component/directives/aria/roles-engines/tablist/test/unit/tablist.ts rename to src/core/component/directives/aria/roles/tablist/test/unit/tablist.ts diff --git a/src/core/component/directives/aria/roles-engines/tabpanel/CHANGELOG.md b/src/core/component/directives/aria/roles/tabpanel/CHANGELOG.md similarity index 100% rename from src/core/component/directives/aria/roles-engines/tabpanel/CHANGELOG.md rename to src/core/component/directives/aria/roles/tabpanel/CHANGELOG.md diff --git a/src/core/component/directives/aria/roles-engines/tabpanel/README.md b/src/core/component/directives/aria/roles/tabpanel/README.md similarity index 93% rename from src/core/component/directives/aria/roles-engines/tabpanel/README.md rename to src/core/component/directives/aria/roles/tabpanel/README.md index 2149453537..a3b7180666 100644 --- a/src/core/component/directives/aria/roles-engines/tabpanel/README.md +++ b/src/core/component/directives/aria/roles/tabpanel/README.md @@ -1,4 +1,4 @@ -# core/component/directives/aria/roles-engines/tabpanel +# core/component/directives/aria/roles/tabpanel This module provides an engine for `v-aria` directive. diff --git a/src/core/component/directives/aria/roles-engines/tabpanel/index.ts b/src/core/component/directives/aria/roles/tabpanel/index.ts similarity index 95% rename from src/core/component/directives/aria/roles-engines/tabpanel/index.ts rename to src/core/component/directives/aria/roles/tabpanel/index.ts index cd76548539..69d23640d4 100644 --- a/src/core/component/directives/aria/roles-engines/tabpanel/index.ts +++ b/src/core/component/directives/aria/roles/tabpanel/index.ts @@ -6,7 +6,7 @@ * https://github.com/V4Fire/Client/blob/master/LICENSE */ -import { AriaRoleEngine } from 'core/component/directives/aria/roles-engines/interface'; +import { AriaRoleEngine } from 'core/component/directives/aria/roles/interface'; export class TabpanelEngine extends AriaRoleEngine { /** @inheritDoc */ diff --git a/src/core/component/directives/aria/roles-engines/tabpanel/test/unit/tabpanel.ts b/src/core/component/directives/aria/roles/tabpanel/test/unit/tabpanel.ts similarity index 100% rename from src/core/component/directives/aria/roles-engines/tabpanel/test/unit/tabpanel.ts rename to src/core/component/directives/aria/roles/tabpanel/test/unit/tabpanel.ts diff --git a/src/core/component/directives/aria/roles-engines/tree/CHANGELOG.md b/src/core/component/directives/aria/roles/tree/CHANGELOG.md similarity index 100% rename from src/core/component/directives/aria/roles-engines/tree/CHANGELOG.md rename to src/core/component/directives/aria/roles/tree/CHANGELOG.md diff --git a/src/core/component/directives/aria/roles-engines/tree/README.md b/src/core/component/directives/aria/roles/tree/README.md similarity index 92% rename from src/core/component/directives/aria/roles-engines/tree/README.md rename to src/core/component/directives/aria/roles/tree/README.md index 73f926850c..0c0cdc1305 100644 --- a/src/core/component/directives/aria/roles-engines/tree/README.md +++ b/src/core/component/directives/aria/roles/tree/README.md @@ -1,4 +1,4 @@ -# core/component/directives/aria/roles-engines/tree +# core/component/directives/aria/roles/tree This module provides an engine for `v-aria` directive. @@ -15,7 +15,7 @@ The engine expects specific parameters to be passed. If true current tree is the root tree in the component. - `orientation`: `string`. The tablist widget view orientation. -- `@change`:`HandlerAttachment`, see `core/component/directives/aria/roles-engines/README.md`. +- `@change`:`HandlerAttachment`, see `core/component/directives/aria/roles/README.md`. Internal callback `onChange` expects an `Element` and `boolean` value if current tree is expanded. The engine expects the component to realize`iAccess` trait. diff --git a/src/core/component/directives/aria/roles-engines/tree/index.ts b/src/core/component/directives/aria/roles/tree/index.ts similarity index 95% rename from src/core/component/directives/aria/roles-engines/tree/index.ts rename to src/core/component/directives/aria/roles/tree/index.ts index 3611b14510..91292045e7 100644 --- a/src/core/component/directives/aria/roles-engines/tree/index.ts +++ b/src/core/component/directives/aria/roles/tree/index.ts @@ -6,8 +6,8 @@ * https://github.com/V4Fire/Client/blob/master/LICENSE */ -import { TreeParams } from 'core/component/directives/aria/roles-engines/tree/interface'; -import { AriaRoleEngine } from 'core/component/directives/aria/roles-engines/interface'; +import { TreeParams } from 'core/component/directives/aria/roles/tree/interface'; +import { AriaRoleEngine } from 'core/component/directives/aria/roles/interface'; export class TreeEngine extends AriaRoleEngine { override Params: TreeParams = new TreeParams(); diff --git a/src/core/component/directives/aria/roles-engines/tree/interface.ts b/src/core/component/directives/aria/roles/tree/interface.ts similarity index 91% rename from src/core/component/directives/aria/roles-engines/tree/interface.ts rename to src/core/component/directives/aria/roles/tree/interface.ts index bad9ae817c..88500cf918 100644 --- a/src/core/component/directives/aria/roles-engines/tree/interface.ts +++ b/src/core/component/directives/aria/roles/tree/interface.ts @@ -6,7 +6,7 @@ * https://github.com/V4Fire/Client/blob/master/LICENSE */ -import type { HandlerAttachment } from 'core/component/directives/aria/roles-engines/interface'; +import type { HandlerAttachment } from 'core/component/directives/aria/roles/interface'; export class TreeParams { isRoot: boolean = false; diff --git a/src/core/component/directives/aria/roles-engines/tree/test/unit/tree.ts b/src/core/component/directives/aria/roles/tree/test/unit/tree.ts similarity index 100% rename from src/core/component/directives/aria/roles-engines/tree/test/unit/tree.ts rename to src/core/component/directives/aria/roles/tree/test/unit/tree.ts diff --git a/src/core/component/directives/aria/roles-engines/treeitem/CHANGELOG.md b/src/core/component/directives/aria/roles/treeitem/CHANGELOG.md similarity index 100% rename from src/core/component/directives/aria/roles-engines/treeitem/CHANGELOG.md rename to src/core/component/directives/aria/roles/treeitem/CHANGELOG.md diff --git a/src/core/component/directives/aria/roles-engines/treeitem/README.md b/src/core/component/directives/aria/roles/treeitem/README.md similarity index 95% rename from src/core/component/directives/aria/roles-engines/treeitem/README.md rename to src/core/component/directives/aria/roles/treeitem/README.md index 59f4cb951f..3621b43255 100644 --- a/src/core/component/directives/aria/roles-engines/treeitem/README.md +++ b/src/core/component/directives/aria/roles/treeitem/README.md @@ -1,4 +1,4 @@ -# core/component/directives/aria/roles-engines/treeitem +# core/component/directives/aria/roles/treeitem This module provides an engine for `v-aria` directive. diff --git a/src/core/component/directives/aria/roles-engines/treeitem/index.ts b/src/core/component/directives/aria/roles/treeitem/index.ts similarity index 98% rename from src/core/component/directives/aria/roles-engines/treeitem/index.ts rename to src/core/component/directives/aria/roles/treeitem/index.ts index a65884bc12..b97146f3f0 100644 --- a/src/core/component/directives/aria/roles-engines/treeitem/index.ts +++ b/src/core/component/directives/aria/roles/treeitem/index.ts @@ -12,8 +12,8 @@ import iAccess from 'traits/i-access/i-access'; import type iBlock from 'super/i-block/i-block'; -import { TreeitemParams } from 'core/component/directives/aria/roles-engines/treeitem/interface'; -import { AriaRoleEngine, KeyCodes } from 'core/component/directives/aria/roles-engines/interface'; +import { TreeitemParams } from 'core/component/directives/aria/roles/treeitem/interface'; +import { AriaRoleEngine, KeyCodes } from 'core/component/directives/aria/roles/interface'; export class TreeitemEngine extends AriaRoleEngine { override Params: TreeitemParams = new TreeitemParams(); diff --git a/src/core/component/directives/aria/roles-engines/treeitem/interface.ts b/src/core/component/directives/aria/roles/treeitem/interface.ts similarity index 100% rename from src/core/component/directives/aria/roles-engines/treeitem/interface.ts rename to src/core/component/directives/aria/roles/treeitem/interface.ts diff --git a/src/core/component/directives/aria/roles-engines/treeitem/test/unit/treeitem.ts b/src/core/component/directives/aria/roles/treeitem/test/unit/treeitem.ts similarity index 100% rename from src/core/component/directives/aria/roles-engines/treeitem/test/unit/treeitem.ts rename to src/core/component/directives/aria/roles/treeitem/test/unit/treeitem.ts From 3dfc28a4bcb19aeaf801c7b4f347099faf779b72 Mon Sep 17 00:00:00 2001 From: Aleksandr Bunin Date: Tue, 13 Sep 2022 12:25:20 +0300 Subject: [PATCH 146/185] improve doc --- .../directives/aria/roles/controls/README.md | 35 ++++++++++++------- 1 file changed, 23 insertions(+), 12 deletions(-) diff --git a/src/core/component/directives/aria/roles/controls/README.md b/src/core/component/directives/aria/roles/controls/README.md index 344ac2f38f..64b07d3195 100644 --- a/src/core/component/directives/aria/roles/controls/README.md +++ b/src/core/component/directives/aria/roles/controls/README.md @@ -20,11 +20,18 @@ If element controls several elements `for` should be passed as a string with IDs Example: ``` -< div v-aria:controls.tab = {for: 'id1 id2 id3'} +< div id = 'tab' | v-aria:tablist | v-aria:controls.tab = {for: 'id1 id2 id3'} + < div v-aria:tab + Tab -// the same as -< div - < button aria-controls = "id1 id2 id3" role = "tab" +< div id = 'id1' | v-aria:tabpanel = {labelledby: 'tab'} + Content + +< div id = 'id2' | v-aria:tabpanel = {labelledby: 'tab'} + Content + +< div id = 'id3' | v-aria:tabpanel = {labelledby: 'tab'} + Content ``` 2. To pass value `for` as an array of tuples. @@ -34,14 +41,18 @@ The second one is an id of an element to set as value in aria-controls attribute Example: ``` -< div v-aria:controls = {for: [[id1, id3], [id2, id4]]} - < span :id = "id1" - < span :id = "id2" - -// the same as -< div - < span :id = "id1" aria-controls = "id3" - < span :id = "id2" aria-controls = "id4" +< div v-aria:tablist | v-aria:controls = {for: [['tab-1', 'panel-1'], ['tab-2', 'panel-2']]} + < div id = 'tab-1' | v-aria:tab + First tab + + < div id = 'tab-2' | v-aria:tab + Second tab + +< div id = 'panel-1' | v-aria:tabpanel = {labelledby: 'tab-1'} + Content for the first panel + +< div id = 'panel-2' | v-aria:tabpanel = {labelledby: 'tab-2'} + Content for the second panel ``` ## Usage From c796ea9f6153b626e16063a7fad5f2c0fa373412 Mon Sep 17 00:00:00 2001 From: Aleksandr Bunin Date: Tue, 13 Sep 2022 15:58:38 +0300 Subject: [PATCH 147/185] fix v-attrs --- src/core/component/render-function/const.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/component/render-function/const.ts b/src/core/component/render-function/const.ts index 18dc3de3d9..54503e5348 100644 --- a/src/core/component/render-function/const.ts +++ b/src/core/component/render-function/const.ts @@ -7,4 +7,4 @@ */ export const - vAttrsRgxp = /(v-(.*?))(?::(.*?))?(?:\.(.*))?$/; + vAttrsRgxp = /(v-(.*?)(?::(.*?))?)(?:\.(.*))?$/; From 439be038edf5b7f5aaee8db2131e2f7882160a2e Mon Sep 17 00:00:00 2001 From: Aleksandr Bunin Date: Tue, 13 Sep 2022 15:58:54 +0300 Subject: [PATCH 148/185] fix tests --- src/base/b-list/b-list.ss | 2 +- .../directives/aria/roles/controls/index.ts | 11 +++++++- .../aria/roles/controls/test/unit/controls.ts | 4 +-- .../aria/roles/tab/test/unit/tab.ts | 26 ++++++++++++++++--- 4 files changed, 36 insertions(+), 7 deletions(-) diff --git a/src/base/b-list/b-list.ss b/src/base/b-list/b-list.ss index 74fc4a5021..f76202e5df 100644 --- a/src/base/b-list/b-list.ss +++ b/src/base/b-list/b-list.ss @@ -51,7 +51,7 @@ :v-attrs = isTablist ? { 'v-aria:tab': getAriaConfig('tab', el, i), - 'v-aria:controls': el.controls, + 'v-aria:controls': {for: el.controls}, ...el.attrs } : el.attrs diff --git a/src/core/component/directives/aria/roles/controls/index.ts b/src/core/component/directives/aria/roles/controls/index.ts index d7ca127bbb..d792e93bf9 100644 --- a/src/core/component/directives/aria/roles/controls/index.ts +++ b/src/core/component/directives/aria/roles/controls/index.ts @@ -54,7 +54,10 @@ export class ControlsEngine extends AriaRoleEngine { }); }); - } else if (isForPropArrayOfTuples) { + return; + } + + if (isForPropArrayOfTuples) { forId.forEach((param) => { const [elId, controlsId] = param, @@ -64,6 +67,12 @@ export class ControlsEngine extends AriaRoleEngine { this.setAttribute('aria-controls', controlsId, element); } }); + + return; + } + + if (Object.isString(forId)) { + this.setAttribute('aria-controls', forId, el); } } } diff --git a/src/core/component/directives/aria/roles/controls/test/unit/controls.ts b/src/core/component/directives/aria/roles/controls/test/unit/controls.ts index b43c742542..deefaee410 100644 --- a/src/core/component/directives/aria/roles/controls/test/unit/controls.ts +++ b/src/core/component/directives/aria/roles/controls/test/unit/controls.ts @@ -149,8 +149,8 @@ test.describe('v-aria:controls', () => { attrs: { [directive]: ariaConfig, items: [ - {label: 'foo', value: 0, attrs: {id: 'id1'}}, - {label: 'bla', value: 1, attrs: {id: 'id2'}} + {label: 'foo', value: 0, id: 'id1'}, + {label: 'bla', value: 1, id: 'id2'} ] } }); diff --git a/src/core/component/directives/aria/roles/tab/test/unit/tab.ts b/src/core/component/directives/aria/roles/tab/test/unit/tab.ts index ee1725eb04..25a587a89b 100644 --- a/src/core/component/directives/aria/roles/tab/test/unit/tab.ts +++ b/src/core/component/directives/aria/roles/tab/test/unit/tab.ts @@ -40,6 +40,26 @@ test.describe('v-aria:tab', () => { ).toEqual(['tab', 'tab', 'tab']); }); + test('aria-controls is set', async ({page}) => { + const target = await init(page); + + test.expect( + await target.evaluate((ctx) => { + if (ctx.unsafe.block == null) { + return; + } + + const tabs = ctx.unsafe.block.elements('link'); + + const res: Array> = []; + + tabs.forEach((el) => res.push(el.getAttribute('aria-controls'))); + + return res; + }) + ).toEqual(['id4', 'id5', 'id6']); + }); + test('has active value', async ({page}) => { const target = await init(page, {active: 1}); @@ -213,9 +233,9 @@ test.describe('v-aria:tab', () => { attrs: { 'data-id': 'target', items: [ - {label: 'Male', value: 0, attrs: {id: 'id1'}}, - {label: 'Female', value: 1, attrs: {id: 'id2'}}, - {label: 'Other', value: 2, attrs: {id: 'id3'}} + {label: 'Male', value: 0, id: 'id1', controls: 'id4'}, + {label: 'Female', value: 1, id: 'id2', controls: 'id5'}, + {label: 'Other', value: 2, id: 'id3', controls: 'id6'} ], ...attrs } From 65c07bc10d35ec02f5751d25b48cd780c74c0ace Mon Sep 17 00:00:00 2001 From: kobezzza Date: Wed, 14 Sep 2022 12:41:52 +0300 Subject: [PATCH 149/185] chore: removed roles-engines --- .../directives/aria/roles-engines/README.md | 33 --- .../aria/roles-engines/combobox/CHANGELOG.md | 16 -- .../aria/roles-engines/combobox/README.md | 32 --- .../aria/roles-engines/combobox/index.ts | 74 ------ .../aria/roles-engines/combobox/interface.ts | 18 -- .../combobox/test/unit/combobox.ts | 130 ---------- .../aria/roles-engines/controls/CHANGELOG.md | 16 -- .../aria/roles-engines/controls/README.md | 51 ---- .../aria/roles-engines/controls/index.ts | 69 ----- .../aria/roles-engines/controls/interface.ts | 11 - .../controls/test/unit/controls.ts | 158 ------------ .../aria/roles-engines/dialog/CHANGELOG.md | 16 -- .../aria/roles-engines/dialog/README.md | 18 -- .../aria/roles-engines/dialog/index.ts | 22 -- .../roles-engines/dialog/test/unit/dialog.ts | 50 ---- .../directives/aria/roles-engines/index.ts | 20 -- .../aria/roles-engines/interface.ts | 88 ------- .../aria/roles-engines/listbox/CHANGELOG.md | 16 -- .../aria/roles-engines/listbox/README.md | 17 -- .../aria/roles-engines/listbox/index.ts | 17 -- .../listbox/test/unit/listbox.ts | 65 ----- .../aria/roles-engines/option/CHANGELOG.md | 16 -- .../aria/roles-engines/option/README.md | 25 -- .../aria/roles-engines/option/index.ts | 28 -- .../aria/roles-engines/option/interface.ts | 14 - .../roles-engines/option/test/unit/option.ts | 116 --------- .../aria/roles-engines/tab/CHANGELOG.md | 16 -- .../aria/roles-engines/tab/README.md | 49 ---- .../aria/roles-engines/tab/index.ts | 167 ------------ .../aria/roles-engines/tab/interface.ts | 17 -- .../aria/roles-engines/tab/test/unit/tab.ts | 225 ---------------- .../aria/roles-engines/tablist/CHANGELOG.md | 16 -- .../aria/roles-engines/tablist/README.md | 29 --- .../aria/roles-engines/tablist/index.ts | 30 --- .../aria/roles-engines/tablist/interface.ts | 12 - .../tablist/test/unit/tablist.ts | 71 ------ .../aria/roles-engines/tabpanel/CHANGELOG.md | 16 -- .../aria/roles-engines/tabpanel/README.md | 26 -- .../aria/roles-engines/tabpanel/index.ts | 23 -- .../tabpanel/test/unit/tabpanel.ts | 48 ---- .../aria/roles-engines/tree/CHANGELOG.md | 16 -- .../aria/roles-engines/tree/README.md | 32 --- .../aria/roles-engines/tree/index.ts | 42 --- .../aria/roles-engines/tree/interface.ts | 15 -- .../aria/roles-engines/tree/test/unit/tree.ts | 102 -------- .../aria/roles-engines/treeitem/CHANGELOG.md | 16 -- .../aria/roles-engines/treeitem/README.md | 41 --- .../aria/roles-engines/treeitem/index.ts | 241 ------------------ .../aria/roles-engines/treeitem/interface.ts | 18 -- .../treeitem/test/unit/treeitem.ts | 197 -------------- 50 files changed, 2601 deletions(-) delete mode 100644 src/core/component/directives/aria/roles-engines/README.md delete mode 100644 src/core/component/directives/aria/roles-engines/combobox/CHANGELOG.md delete mode 100644 src/core/component/directives/aria/roles-engines/combobox/README.md delete mode 100644 src/core/component/directives/aria/roles-engines/combobox/index.ts delete mode 100644 src/core/component/directives/aria/roles-engines/combobox/interface.ts delete mode 100644 src/core/component/directives/aria/roles-engines/combobox/test/unit/combobox.ts delete mode 100644 src/core/component/directives/aria/roles-engines/controls/CHANGELOG.md delete mode 100644 src/core/component/directives/aria/roles-engines/controls/README.md delete mode 100644 src/core/component/directives/aria/roles-engines/controls/index.ts delete mode 100644 src/core/component/directives/aria/roles-engines/controls/interface.ts delete mode 100644 src/core/component/directives/aria/roles-engines/controls/test/unit/controls.ts delete mode 100644 src/core/component/directives/aria/roles-engines/dialog/CHANGELOG.md delete mode 100644 src/core/component/directives/aria/roles-engines/dialog/README.md delete mode 100644 src/core/component/directives/aria/roles-engines/dialog/index.ts delete mode 100644 src/core/component/directives/aria/roles-engines/dialog/test/unit/dialog.ts delete mode 100644 src/core/component/directives/aria/roles-engines/index.ts delete mode 100644 src/core/component/directives/aria/roles-engines/interface.ts delete mode 100644 src/core/component/directives/aria/roles-engines/listbox/CHANGELOG.md delete mode 100644 src/core/component/directives/aria/roles-engines/listbox/README.md delete mode 100644 src/core/component/directives/aria/roles-engines/listbox/index.ts delete mode 100644 src/core/component/directives/aria/roles-engines/listbox/test/unit/listbox.ts delete mode 100644 src/core/component/directives/aria/roles-engines/option/CHANGELOG.md delete mode 100644 src/core/component/directives/aria/roles-engines/option/README.md delete mode 100644 src/core/component/directives/aria/roles-engines/option/index.ts delete mode 100644 src/core/component/directives/aria/roles-engines/option/interface.ts delete mode 100644 src/core/component/directives/aria/roles-engines/option/test/unit/option.ts delete mode 100644 src/core/component/directives/aria/roles-engines/tab/CHANGELOG.md delete mode 100644 src/core/component/directives/aria/roles-engines/tab/README.md delete mode 100644 src/core/component/directives/aria/roles-engines/tab/index.ts delete mode 100644 src/core/component/directives/aria/roles-engines/tab/interface.ts delete mode 100644 src/core/component/directives/aria/roles-engines/tab/test/unit/tab.ts delete mode 100644 src/core/component/directives/aria/roles-engines/tablist/CHANGELOG.md delete mode 100644 src/core/component/directives/aria/roles-engines/tablist/README.md delete mode 100644 src/core/component/directives/aria/roles-engines/tablist/index.ts delete mode 100644 src/core/component/directives/aria/roles-engines/tablist/interface.ts delete mode 100644 src/core/component/directives/aria/roles-engines/tablist/test/unit/tablist.ts delete mode 100644 src/core/component/directives/aria/roles-engines/tabpanel/CHANGELOG.md delete mode 100644 src/core/component/directives/aria/roles-engines/tabpanel/README.md delete mode 100644 src/core/component/directives/aria/roles-engines/tabpanel/index.ts delete mode 100644 src/core/component/directives/aria/roles-engines/tabpanel/test/unit/tabpanel.ts delete mode 100644 src/core/component/directives/aria/roles-engines/tree/CHANGELOG.md delete mode 100644 src/core/component/directives/aria/roles-engines/tree/README.md delete mode 100644 src/core/component/directives/aria/roles-engines/tree/index.ts delete mode 100644 src/core/component/directives/aria/roles-engines/tree/interface.ts delete mode 100644 src/core/component/directives/aria/roles-engines/tree/test/unit/tree.ts delete mode 100644 src/core/component/directives/aria/roles-engines/treeitem/CHANGELOG.md delete mode 100644 src/core/component/directives/aria/roles-engines/treeitem/README.md delete mode 100644 src/core/component/directives/aria/roles-engines/treeitem/index.ts delete mode 100644 src/core/component/directives/aria/roles-engines/treeitem/interface.ts delete mode 100644 src/core/component/directives/aria/roles-engines/treeitem/test/unit/treeitem.ts diff --git a/src/core/component/directives/aria/roles-engines/README.md b/src/core/component/directives/aria/roles-engines/README.md deleted file mode 100644 index d5e9d13fff..0000000000 --- a/src/core/component/directives/aria/roles-engines/README.md +++ /dev/null @@ -1,33 +0,0 @@ -# core/component/directives/aria/roles-engines/combobox - -This module provides engines for `v-aria` directive. - -## API - -Some roles need to handle components state changes or react to some events (add, delete or change certain attributes). -The fields in directive passed options which name starts with `@` respond for this (ex. `@change`, `@open`). -The certain contract should be followed: -the name of the callback, which should be 'connected' with such field should start with `on` and be named in camelCase style (ex. `onChange`, `onOpen`). - -Directive supports this field type to be function, promise or string (type [`HandlerAttachment`](`core/component/directives/aria/roles-engines/interface.ts`)). -- Function: -expects a callback to be passed. -In this function callback could be added as a listener to certain component events or provide to the callback some component's state. - -``` -< div v-aria:somerole = {'@change': (cb) => on('event', cb)} -``` - -- Promise: -If the field is a `Promise` or a `PromiseLike` object the callback would be passed to `then`. - -- String: -If the field is a `string`, the callback would be added as a listener to component's event similar to the string. - -``` -< div v-aria:somerole = {'@change': 'event'} - -// the same as - -< div v-aria:somerole = {'@change': (cb) => on('event', cb)} -``` diff --git a/src/core/component/directives/aria/roles-engines/combobox/CHANGELOG.md b/src/core/component/directives/aria/roles-engines/combobox/CHANGELOG.md deleted file mode 100644 index 6e314edf2d..0000000000 --- a/src/core/component/directives/aria/roles-engines/combobox/CHANGELOG.md +++ /dev/null @@ -1,16 +0,0 @@ -Changelog -========= - -> **Tags:** -> - :boom: [Breaking Change] -> - :rocket: [New Feature] -> - :bug: [Bug Fix] -> - :memo: [Documentation] -> - :house: [Internal] -> - :nail_care: [Polish] - -## v3.?.? (2022-??-??) - -#### :rocket: New Feature - -* Initial release diff --git a/src/core/component/directives/aria/roles-engines/combobox/README.md b/src/core/component/directives/aria/roles-engines/combobox/README.md deleted file mode 100644 index 8a5792f9df..0000000000 --- a/src/core/component/directives/aria/roles-engines/combobox/README.md +++ /dev/null @@ -1,32 +0,0 @@ -# core/component/directives/aria/roles-engines/combobox - -This module provides an engine for `v-aria` directive. - -The engine to set `combobox` role attribute. -The `combobox` role identifies an element as an input that controls another element, such as a `listbox`, that can dynamically pop up to help the user set the value of that input. - -For more information about attributes go to [combobox](`https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/combobox_role`). -For recommendations how to make accessible widget go to [combobox](`https://www.w3.org/WAI/ARIA/apg/patterns/combobox/`). - -## API - -The engine expects specific parameters to be passed. -- `isMultiple`:`boolean`. -If true widget supports multiple selected options. -- `@change`:`HandlerAttachment`, see `core/component/directives/aria/roles-engines/README.md`. -Internal callback `onChange` expects an `Element` to be passed. -- `@open`:`HandlerAttachment`. -Internal callback `onChange` expects an `Element` to be passed. -- `@close`:`HandlerAttachment`. - -## Usage - -``` -< div v-aria:combobox = { & - isMultiple: multiple, - '@change': (cb) => on('actionChange', cb), - '@open': 'open', - '@close': 'close' - } -. -``` diff --git a/src/core/component/directives/aria/roles-engines/combobox/index.ts b/src/core/component/directives/aria/roles-engines/combobox/index.ts deleted file mode 100644 index 9240a547d0..0000000000 --- a/src/core/component/directives/aria/roles-engines/combobox/index.ts +++ /dev/null @@ -1,74 +0,0 @@ -/*! - * V4Fire Client Core - * https://github.com/V4Fire/Client - * - * Released under the MIT license - * https://github.com/V4Fire/Client/blob/master/LICENSE - */ - -import type iAccess from 'traits/i-access/i-access'; -import type { ComponentInterface } from 'super/i-block/i-block'; - -import { ComboboxParams } from 'core/component/directives/aria/roles-engines/combobox/interface'; -import { AriaRoleEngine, EngineOptions } from 'core/component/directives/aria/roles-engines/interface'; - -export class ComboboxEngine extends AriaRoleEngine { - override Params: ComboboxParams = new ComboboxParams(); - override Ctx!: ComponentInterface & iAccess; - override el: HTMLElement; - - constructor(options: EngineOptions) { - super(options); - - const - {el} = this; - - this.el = this.ctx?.findFocusableElement() ?? el; - } - - /** @inheritDoc */ - init(): void { - this.setAttribute('role', 'combobox'); - this.setAttribute('aria-expanded', 'false'); - - if (this.params.isMultiple) { - this.setAttribute('aria-multiselectable', 'true'); - } - - if (this.el.tabIndex < 0) { - this.setAttribute('tabindex', '0'); - } - } - - /** - * Sets or deletes the id of active descendant element - */ - protected setAriaActive(el?: Element): void { - this.setAttribute('aria-activedescendant', el?.id ?? ''); - } - - /** - * Handler: the option list is expanded - * @param el - */ - protected onOpen(el: Element): void { - this.setAttribute('aria-expanded', 'true'); - this.setAriaActive(el); - } - - /** - * Handler: the option list is closed - */ - protected onClose(): void { - this.setAttribute('aria-expanded', 'false'); - this.setAriaActive(); - } - - /** - * Handler: active option element was changed - * @param el - */ - protected onChange(el: Element): void { - this.setAriaActive(el); - } -} diff --git a/src/core/component/directives/aria/roles-engines/combobox/interface.ts b/src/core/component/directives/aria/roles-engines/combobox/interface.ts deleted file mode 100644 index 03ee51ae17..0000000000 --- a/src/core/component/directives/aria/roles-engines/combobox/interface.ts +++ /dev/null @@ -1,18 +0,0 @@ -/*! - * V4Fire Client Core - * https://github.com/V4Fire/Client - * - * Released under the MIT license - * https://github.com/V4Fire/Client/blob/master/LICENSE - */ - -import type { HandlerAttachment } from 'core/component/directives/aria/roles-engines/interface'; - -const defaultFn = (): void => undefined; - -export class ComboboxParams { - isMultiple: boolean = false; - '@change': HandlerAttachment = defaultFn; - '@open': HandlerAttachment = defaultFn; - '@close': HandlerAttachment = defaultFn; -} diff --git a/src/core/component/directives/aria/roles-engines/combobox/test/unit/combobox.ts b/src/core/component/directives/aria/roles-engines/combobox/test/unit/combobox.ts deleted file mode 100644 index 1e92ffca7c..0000000000 --- a/src/core/component/directives/aria/roles-engines/combobox/test/unit/combobox.ts +++ /dev/null @@ -1,130 +0,0 @@ -/*! - * V4Fire Client Core - * https://github.com/V4Fire/Client - * - * Released under the MIT license - * https://github.com/V4Fire/Client/blob/master/LICENSE - */ - -import type { JSHandle, Page } from 'playwright'; -import type iBlock from 'super/i-block/i-block'; - -import test from 'tests/config/unit/test'; -import Component from 'tests/helpers/component'; - -test.describe('v-aria:combobox', () => { - test.beforeEach(async ({demoPage}) => { - await demoPage.goto(); - }); - - const - selector = '[data-id="target"]'; - - /** - * Initial attributes - */ - test('role is set', async ({page}) => { - const target = await init(page); - - test.expect( - await target.evaluate((ctx) => ctx.unsafe.block?.element('input')?.getAttribute('role')) - ).toBe('combobox'); - }); - - test('aria-expanded is set to false', async ({page}) => { - const target = await init(page); - - test.expect( - await target.evaluate((ctx) => ctx.unsafe.block?.element('input')?.getAttribute('aria-expanded')) - ).toBe('false'); - }); - - test('aria-multiselectable is set', async ({page}) => { - const target = await init(page, {multiple: true}); - - test.expect( - await target.evaluate((ctx) => ctx.unsafe.block?.element('input')?.getAttribute('aria-multiselectable')) - ).toBe('true'); - }); - - test('element\'s tabindex is 0', async ({page}) => { - const target = await init(page); - - test.expect( - await target.evaluate((ctx) => { - const input = ctx.unsafe.block?.element('input'); - return input.tabIndex; - }) - ).toBe(0); - }); - - /** - * Handling events - */ - test('select is opened with no preselected option', async ({page}) => { - const target = await init(page); - - await page.click(selector); - - test.expect( - await target.evaluate((ctx) => ctx.unsafe.block?.element('input')?.getAttribute('aria-expanded')) - ).toBe('true'); - - test.expect( - await target.evaluate((ctx) => ctx.unsafe.block?.element('input')?.getAttribute('aria-activedescendant')) - ).toBe(''); - }); - - test('select is opened with preselected option', async ({page}) => { - const target = await init(page, {value: 1}); - - await page.focus('input'); - - const id = await target.evaluate((ctx) => ctx.unsafe.dom.getId('1')); - - test.expect( - await target.evaluate((ctx) => ctx.unsafe.block?.element('input')?.getAttribute('aria-expanded')) - ).toBe('true'); - - test.expect( - await target.evaluate((ctx) => ctx.unsafe.block?.element('input')?.getAttribute('aria-activedescendant')) - ).toBe(id); - }); - - test('select is opened and closed', async ({page}) => { - const target = await init(page, {value: 1}); - - await page.focus('input'); - - test.expect( - await target.evaluate((ctx) => ctx.unsafe.block?.element('input')?.getAttribute('aria-expanded')) - ).toBe('true'); - - await page.click('body'); - - test.expect( - await target.evaluate((ctx) => ctx.unsafe.block?.element('input')?.getAttribute('aria-expanded')) - ).toBe('false'); - - test.expect( - await target.evaluate((ctx) => ctx.unsafe.block?.element('input')?.getAttribute('aria-activedescendant')) - ).toBe(''); - }); - - /** - * @param page - * @param attrs - */ - async function init(page: Page, attrs: Dictionary = {}): Promise> { - return Component.createComponent(page, 'b-select', { - attrs: { - 'data-id': 'target', - items: [ - {label: 'foo', value: 0}, - {label: 'bar', value: 1} - ], - ...attrs - } - }); - } -}); diff --git a/src/core/component/directives/aria/roles-engines/controls/CHANGELOG.md b/src/core/component/directives/aria/roles-engines/controls/CHANGELOG.md deleted file mode 100644 index 6e314edf2d..0000000000 --- a/src/core/component/directives/aria/roles-engines/controls/CHANGELOG.md +++ /dev/null @@ -1,16 +0,0 @@ -Changelog -========= - -> **Tags:** -> - :boom: [Breaking Change] -> - :rocket: [New Feature] -> - :bug: [Bug Fix] -> - :memo: [Documentation] -> - :house: [Internal] -> - :nail_care: [Polish] - -## v3.?.? (2022-??-??) - -#### :rocket: New Feature - -* Initial release diff --git a/src/core/component/directives/aria/roles-engines/controls/README.md b/src/core/component/directives/aria/roles-engines/controls/README.md deleted file mode 100644 index c23104c222..0000000000 --- a/src/core/component/directives/aria/roles-engines/controls/README.md +++ /dev/null @@ -1,51 +0,0 @@ -# core/component/directives/aria/roles-engines/controls - -This module provides an engine for `v-aria` directive. - -The engine is used to set `aria-controls` attribute. -The global `aria-controls` property identifies the element (or elements) whose contents or presence are controlled by the element on which this attribute is set. - -For more information go to [controls](`https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes/aria-controls`). - -## API - -Directive can be added to any tag that includes tag with needed role. Role should be passed as a modifier. -ID or IDs are passed as value. -ID could be single or multiple written in string with space between. - -There are two ways to use this engine: -1. To add role as a modifier to which passed IDs in `for` value should be added. `for` could be `string` or `string[]`. -If element controls several elements `for` should be passed as a string with IDs separated with space. -(!) Notice that this role attribute should already be added to the element. The engine does not set passed role to any element. - -Example: -``` -< div v-aria:controls.tab = {for: 'id1 id2 id3'} - -// the same as -< div - < button aria-controls = "id1 id2 id3" role = "tab" -``` - -2. To pass value `for` as an array of tuples. -First id in a tuple is an id of an element to add the aria attributes. -The second one is an id of an element to set as value in aria-controls attribute. -(!) Notice that id attribute should already be added to the element. The engine does not set passed ids to any element. - -Example: -``` -< div v-aria:controls = {for: [[id1, id3], [id2, id4]]} - < span :id = "id1" - < span :id = "id2" - -// the same as -< div - < span :id = "id1" aria-controls = "id3" - < span :id = "id2" aria-controls = "id4" -``` - -## Usage - -``` -< div v-aria:controls = {...} -``` diff --git a/src/core/component/directives/aria/roles-engines/controls/index.ts b/src/core/component/directives/aria/roles-engines/controls/index.ts deleted file mode 100644 index adb57ea056..0000000000 --- a/src/core/component/directives/aria/roles-engines/controls/index.ts +++ /dev/null @@ -1,69 +0,0 @@ -/*! - * V4Fire Client Core - * https://github.com/V4Fire/Client - * - * Released under the MIT license - * https://github.com/V4Fire/Client/blob/master/LICENSE - */ - -import { ControlsParams } from 'core/component/directives/aria/roles-engines/controls/interface'; -import { AriaRoleEngine } from 'core/component/directives/aria/roles-engines/interface'; - -export class ControlsEngine extends AriaRoleEngine { - override Params: ControlsParams = new ControlsParams(); - - /** @inheritDoc */ - init(): void { - const - {ctx, modifiers, el} = this, - {for: forId} = this.params; - - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - if (forId == null) { - Object.throw('Controls aria directive expects the id of controlling elements to be passed as "for" prop'); - return; - } - - const - isForPropArray = Object.isArray(forId), - isForPropArrayOfTuples = isForPropArray && Object.isArray(forId[0]); - - if (modifiers != null && Object.size(modifiers) > 0) { - ctx?.$nextTick().then(() => { - const - roleName = Object.keys(modifiers)[0], - elems = el.querySelectorAll(`[role=${roleName}]`); - - if (isForPropArray && forId.length !== elems.length) { - Object.throw('Controls aria directive expects prop "for" length to be equal to amount of elements with specified role or string type'); - return; - } - - elems.forEach((el, i) => { - if (Object.isString(forId)) { - this.setAttribute('aria-controls', forId, el); - return; - } - - const - id = forId[i]; - - if (Object.isString(id)) { - this.setAttribute('aria-controls', id, el); - } - }); - }); - - } else if (isForPropArrayOfTuples) { - forId.forEach((param) => { - const - [elId, controlsId] = param, - element = el.querySelector(`#${elId}`); - - if (element != null) { - this.setAttribute('aria-controls', controlsId, element); - } - }); - } - } -} diff --git a/src/core/component/directives/aria/roles-engines/controls/interface.ts b/src/core/component/directives/aria/roles-engines/controls/interface.ts deleted file mode 100644 index c73270ecbb..0000000000 --- a/src/core/component/directives/aria/roles-engines/controls/interface.ts +++ /dev/null @@ -1,11 +0,0 @@ -/*! - * V4Fire Client Core - * https://github.com/V4Fire/Client - * - * Released under the MIT license - * https://github.com/V4Fire/Client/blob/master/LICENSE - */ - -export class ControlsParams { - for: CanArray | Array<[string, string]> = 'for'; -} diff --git a/src/core/component/directives/aria/roles-engines/controls/test/unit/controls.ts b/src/core/component/directives/aria/roles-engines/controls/test/unit/controls.ts deleted file mode 100644 index b43c742542..0000000000 --- a/src/core/component/directives/aria/roles-engines/controls/test/unit/controls.ts +++ /dev/null @@ -1,158 +0,0 @@ -/*! - * V4Fire Client Core - * https://github.com/V4Fire/Client - * - * Released under the MIT license - * https://github.com/V4Fire/Client/blob/master/LICENSE - */ - -import type { JSHandle, Page } from 'playwright'; -import type iBlock from 'super/i-block/i-block'; - -import test from 'tests/config/unit/test'; -import Component from 'tests/helpers/component'; - -test.describe('v-aria:controls', () => { - test.beforeEach(async ({demoPage}) => { - await demoPage.goto(); - }); - - /** - * With modifiers - */ - test('modifiers. "for" is a string', async ({page}) => { - const target = await init(page, {for: 'id3'}); - - test.expect( - await target.evaluate((ctx) => { - if (ctx.unsafe.block == null) { - return; - } - - const [el1, el2] = Array.from(ctx.unsafe.block.elements('link')); - - return el1.getAttribute('aria-controls') === 'id3' && el2.getAttribute('aria-controls') === 'id3'; - }) - ).toBe(true); - }); - - test('modifiers. "for" is an array', async ({page}) => { - const target = await init(page, {for: ['id3', 'id4']}); - - test.expect( - await target.evaluate((ctx) => { - if (ctx.unsafe.block == null) { - return; - } - - const [el1, el2]: HTMLElement[] = Array.from(ctx.unsafe.block.elements('link')); - - return [el1.getAttribute('aria-controls'), el2.getAttribute('aria-controls')]; - }) - ).toEqual(['id3', 'id4']); - }); - - test('modifiers. "for" is an array with wrong length', async ({page}) => { - const target = await init(page, {for: ['id3', 'id4', 'id5']}); - - test.expect( - await target.evaluate((ctx) => { - if (ctx.unsafe.block == null) { - return; - } - - const [el1, el2]: HTMLElement[] = Array.from(ctx.unsafe.block.elements('link')); - - return [el1.hasAttribute('aria-controls'), el2.hasAttribute('aria-controls')]; - }) - ).toEqual([false, false]); - }); - - test('modifiers. no "for" value passed', async ({page}) => { - const target = await init(page); - - test.expect( - await target.evaluate((ctx) => { - if (ctx.unsafe.block == null) { - return; - } - - const [el1, el2]: HTMLElement[] = Array.from(ctx.unsafe.block.elements('link')); - - return [el1.hasAttribute('aria-controls'), el2.hasAttribute('aria-controls')]; - }) - ).toEqual([false, false]); - }); - - /** - * With 'for' param as an array of tuples - */ - test('tuples', async ({page}) => { - const target = await init(page, {for: [['id1', 'id3'], ['id2', 'id4']]}, 'v-aria:controls'); - - test.expect( - await target.evaluate((ctx) => { - if (ctx.unsafe.block == null) { - return; - } - - const [el1, el2]: HTMLElement[] = Array.from(ctx.unsafe.block.elements('link')); - - return [el1.getAttribute('aria-controls'), el2.getAttribute('aria-controls')]; - }) - ).toEqual(['id3', 'id4']); - }); - - test('tuples. wrong ids', async ({page}) => { - const target = await init(page, {for: [['id5', 'id6'], ['id3', 'id8']]}, 'v-aria:controls'); - - test.expect( - await target.evaluate((ctx) => { - if (ctx.unsafe.block == null) { - return; - } - - const [el1, el2]: HTMLElement[] = Array.from(ctx.unsafe.block.elements('link')); - - return [el1.hasAttribute('aria-controls'), el2.hasAttribute('aria-controls')]; - }) - ).toEqual([false, false]); - }); - - test('no params passed', async ({page}) => { - const target = await init(page); - - test.expect( - await target.evaluate((ctx) => { - if (ctx.unsafe.block == null) { - return; - } - - const [el1, el2]: HTMLElement[] = Array.from(ctx.unsafe.block.elements('link')); - - return [el1.hasAttribute('aria-controls'), el2.hasAttribute('aria-controls')]; - }) - ).toEqual([false, false]); - }); - - /** - * @param page - * @param ariaConfig - * @param directive - */ - async function init( - page: Page, - ariaConfig: Dictionary = {}, - directive: string = 'v-aria:controls.tab' - ): Promise> { - return Component.createComponent(page, 'b-list', { - attrs: { - [directive]: ariaConfig, - items: [ - {label: 'foo', value: 0, attrs: {id: 'id1'}}, - {label: 'bla', value: 1, attrs: {id: 'id2'}} - ] - } - }); - } -}); diff --git a/src/core/component/directives/aria/roles-engines/dialog/CHANGELOG.md b/src/core/component/directives/aria/roles-engines/dialog/CHANGELOG.md deleted file mode 100644 index 6e314edf2d..0000000000 --- a/src/core/component/directives/aria/roles-engines/dialog/CHANGELOG.md +++ /dev/null @@ -1,16 +0,0 @@ -Changelog -========= - -> **Tags:** -> - :boom: [Breaking Change] -> - :rocket: [New Feature] -> - :bug: [Bug Fix] -> - :memo: [Documentation] -> - :house: [Internal] -> - :nail_care: [Polish] - -## v3.?.? (2022-??-??) - -#### :rocket: New Feature - -* Initial release diff --git a/src/core/component/directives/aria/roles-engines/dialog/README.md b/src/core/component/directives/aria/roles-engines/dialog/README.md deleted file mode 100644 index 2540480c79..0000000000 --- a/src/core/component/directives/aria/roles-engines/dialog/README.md +++ /dev/null @@ -1,18 +0,0 @@ -# core/component/directives/aria/roles-engines/dialog - -This module provides an engine for `v-aria` directive. - -The engine to set `dialog` role attribute. -The `dialog` role is used to mark up an HTML based application dialog or window that separates content or UI from the rest of the web application or page. - -For more information go to [dialog](`https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/dialog_role`). - -## API - -The engine expects the component to realize the`iOpen` trait. - -## Usage - -``` -< div v-aria:dialog -``` diff --git a/src/core/component/directives/aria/roles-engines/dialog/index.ts b/src/core/component/directives/aria/roles-engines/dialog/index.ts deleted file mode 100644 index d004f5dc4b..0000000000 --- a/src/core/component/directives/aria/roles-engines/dialog/index.ts +++ /dev/null @@ -1,22 +0,0 @@ -/*! - * V4Fire Client Core - * https://github.com/V4Fire/Client - * - * Released under the MIT license - * https://github.com/V4Fire/Client/blob/master/LICENSE - */ - -import { AriaRoleEngine } from 'core/component/directives/aria/roles-engines/interface'; -import iOpen from 'traits/i-open/i-open'; - -export class DialogEngine extends AriaRoleEngine { - /** @inheritDoc */ - init(): void { - this.setAttribute('role', 'dialog'); - this.setAttribute('aria-modal', 'true'); - - if (!iOpen.is(this.ctx)) { - Object.throw('Dialog aria directive expects the component to realize iOpen interface'); - } - } -} diff --git a/src/core/component/directives/aria/roles-engines/dialog/test/unit/dialog.ts b/src/core/component/directives/aria/roles-engines/dialog/test/unit/dialog.ts deleted file mode 100644 index 5031e0debe..0000000000 --- a/src/core/component/directives/aria/roles-engines/dialog/test/unit/dialog.ts +++ /dev/null @@ -1,50 +0,0 @@ -/*! - * V4Fire Client Core - * https://github.com/V4Fire/Client - * - * Released under the MIT license - * https://github.com/V4Fire/Client/blob/master/LICENSE - */ - -import type { JSHandle, Page } from 'playwright'; -import type iBlock from 'super/i-block/i-block'; - -import test from 'tests/config/unit/test'; -import Component from 'tests/helpers/component'; - -test.describe('v-aria:dialog', () => { - test.beforeEach(async ({demoPage}) => { - await demoPage.goto(); - }); - - test('role is set', async ({page}) => { - const target = await init(page); - - test.expect( - await target.evaluate((ctx) => { - const el = ctx.unsafe.block?.element('window'); - - return el?.getAttribute('role'); - }) - ).toBe('dialog'); - }); - - test('aria-modal is set', async ({page}) => { - const target = await init(page); - - test.expect( - await target.evaluate((ctx) => { - const el = ctx.unsafe.block?.element('window'); - - return el?.getAttribute('aria-modal'); - }) - ).toBe('true'); - }); - - /** - * @param page - */ - async function init(page: Page): Promise> { - return Component.createComponent(page, 'b-window'); - } -}); diff --git a/src/core/component/directives/aria/roles-engines/index.ts b/src/core/component/directives/aria/roles-engines/index.ts deleted file mode 100644 index cbb9f1bbce..0000000000 --- a/src/core/component/directives/aria/roles-engines/index.ts +++ /dev/null @@ -1,20 +0,0 @@ -/*! - * V4Fire Client Core - * https://github.com/V4Fire/Client - * - * Released under the MIT license - * https://github.com/V4Fire/Client/blob/master/LICENSE - */ - -export * from 'core/component/directives/aria/roles-engines/dialog'; -export * from 'core/component/directives/aria/roles-engines/tablist'; -export * from 'core/component/directives/aria/roles-engines/tab'; -export * from 'core/component/directives/aria/roles-engines/tabpanel'; -export * from 'core/component/directives/aria/roles-engines/controls'; -export * from 'core/component/directives/aria/roles-engines/combobox'; -export * from 'core/component/directives/aria/roles-engines/listbox'; -export * from 'core/component/directives/aria/roles-engines/option'; -export * from 'core/component/directives/aria/roles-engines/tree'; -export * from 'core/component/directives/aria/roles-engines/treeitem'; - -export { AriaRoleEngine, EngineOptions } from 'core/component/directives/aria/roles-engines/interface'; diff --git a/src/core/component/directives/aria/roles-engines/interface.ts b/src/core/component/directives/aria/roles-engines/interface.ts deleted file mode 100644 index f82549b390..0000000000 --- a/src/core/component/directives/aria/roles-engines/interface.ts +++ /dev/null @@ -1,88 +0,0 @@ -/*! - * V4Fire Client Core - * https://github.com/V4Fire/Client - * - * Released under the MIT license - * https://github.com/V4Fire/Client/blob/master/LICENSE - */ - -import type Async from 'core/async'; -import type { ComponentInterface } from 'super/i-block/i-block'; - -export abstract class AriaRoleEngine { - /** - * Type: directive passed params - */ - readonly Params!: AbstractParams; - - /** - * Type: component on which the directive is set - */ - readonly Ctx!: ComponentInterface; - - /** - * Element on which the directive is set - */ - readonly el: HTMLElement; - - /** - * Component on which the directive is set - */ - readonly ctx?: this['Ctx']; - - /** - * Directive passed modifiers - */ - readonly modifiers?: Dictionary; - - /** - * Directive passed params - */ - readonly params: this['Params']; - - /** @see [[Async]] */ - async: Async; - - constructor({el, ctx, modifiers, params, async}: EngineOptions) { - this.el = el; - this.ctx = ctx; - this.modifiers = modifiers; - this.params = params; - this.async = async; - } - - /** - * Sets base aria attributes for current role - */ - abstract init(): void; - - /** - * Sets aria attributes and the `Async` destructor - */ - setAttribute(attr: string, value: string, el: Element = this.el): void { - el.setAttribute(attr, value); - this.async.worker(() => el.removeAttribute(attr)); - } -} - -interface AbstractParams {} - -export interface EngineOptions

      { - el: HTMLElement; - ctx?: C; - modifiers?: Dictionary; - params: P; - async: Async; -} - -export type HandlerAttachment = ((cb: Function) => void) | Promise | string; - -export const enum KeyCodes { - ENTER = 'Enter', - END = 'End', - HOME = 'Home', - LEFT = 'ArrowLeft', - UP = 'ArrowUp', - RIGHT = 'ArrowRight', - DOWN = 'ArrowDown' -} diff --git a/src/core/component/directives/aria/roles-engines/listbox/CHANGELOG.md b/src/core/component/directives/aria/roles-engines/listbox/CHANGELOG.md deleted file mode 100644 index 6e314edf2d..0000000000 --- a/src/core/component/directives/aria/roles-engines/listbox/CHANGELOG.md +++ /dev/null @@ -1,16 +0,0 @@ -Changelog -========= - -> **Tags:** -> - :boom: [Breaking Change] -> - :rocket: [New Feature] -> - :bug: [Bug Fix] -> - :memo: [Documentation] -> - :house: [Internal] -> - :nail_care: [Polish] - -## v3.?.? (2022-??-??) - -#### :rocket: New Feature - -* Initial release diff --git a/src/core/component/directives/aria/roles-engines/listbox/README.md b/src/core/component/directives/aria/roles-engines/listbox/README.md deleted file mode 100644 index 8083d3a1fe..0000000000 --- a/src/core/component/directives/aria/roles-engines/listbox/README.md +++ /dev/null @@ -1,17 +0,0 @@ -# core/component/directives/aria/roles-engines/listbox - -This module provides an engine for `v-aria` directive. - -The engine to set `listbox` role attribute. -The `listbox` role is used for lists from which a user may select one or more items which are static and may contain images. - -For more information go to [listbox](`https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/listbox_role`). -For recommendations how to make accessible widget go to [listbox](`https://www.w3.org/WAI/ARIA/apg/patterns/listbox/`). - -Widget `listbox` also contains elements with role `option` (see specified engine) - -## Usage - -``` -< div v-aria:listbox = {...} -``` diff --git a/src/core/component/directives/aria/roles-engines/listbox/index.ts b/src/core/component/directives/aria/roles-engines/listbox/index.ts deleted file mode 100644 index 4e28b0c596..0000000000 --- a/src/core/component/directives/aria/roles-engines/listbox/index.ts +++ /dev/null @@ -1,17 +0,0 @@ -/*! - * V4Fire Client Core - * https://github.com/V4Fire/Client - * - * Released under the MIT license - * https://github.com/V4Fire/Client/blob/master/LICENSE - */ - -import { AriaRoleEngine } from 'core/component/directives/aria/roles-engines/interface'; - -export class ListboxEngine extends AriaRoleEngine { - /** @inheritDoc */ - init(): void { - this.setAttribute('role', 'listbox'); - this.setAttribute('tabindex', '-1'); - } -} diff --git a/src/core/component/directives/aria/roles-engines/listbox/test/unit/listbox.ts b/src/core/component/directives/aria/roles-engines/listbox/test/unit/listbox.ts deleted file mode 100644 index d4fd92d3cb..0000000000 --- a/src/core/component/directives/aria/roles-engines/listbox/test/unit/listbox.ts +++ /dev/null @@ -1,65 +0,0 @@ -/*! - * V4Fire Client Core - * https://github.com/V4Fire/Client - * - * Released under the MIT license - * https://github.com/V4Fire/Client/blob/master/LICENSE - */ - -import type { JSHandle, Page } from 'playwright'; -import type iBlock from 'super/i-block/i-block'; - -import test from 'tests/config/unit/test'; -import Component from 'tests/helpers/component'; - -test.describe('v-aria:listbox', () => { - test.beforeEach(async ({demoPage}) => { - await demoPage.goto(); - }); - - const - selector = '[data-id="target"]'; - - test('role is set', async ({page}) => { - const target = await init(page); - - await page.click(selector); - - test.expect( - await target.evaluate((ctx) => { - const el = ctx.unsafe.block?.element('dropdown'); - - return el?.getAttribute('role'); - }) - ).toBe('listbox'); - }); - - test('tabindex is -1', async ({page}) => { - const target = await init(page); - - await page.click(selector); - - test.expect( - await target.evaluate((ctx) => { - const el = ctx.unsafe.block?.element('dropdown'); - - return el?.getAttribute('tabindex'); - }) - ).toBe('-1'); - }); - - /** - * @param page - */ - async function init(page: Page): Promise> { - return Component.createComponent(page, 'b-select', { - attrs: { - 'data-id': 'target', - items: [ - {label: 'foo', value: 0}, - {label: 'bar', value: 1} - ] - } - }); - } -}); diff --git a/src/core/component/directives/aria/roles-engines/option/CHANGELOG.md b/src/core/component/directives/aria/roles-engines/option/CHANGELOG.md deleted file mode 100644 index 6e314edf2d..0000000000 --- a/src/core/component/directives/aria/roles-engines/option/CHANGELOG.md +++ /dev/null @@ -1,16 +0,0 @@ -Changelog -========= - -> **Tags:** -> - :boom: [Breaking Change] -> - :rocket: [New Feature] -> - :bug: [Bug Fix] -> - :memo: [Documentation] -> - :house: [Internal] -> - :nail_care: [Polish] - -## v3.?.? (2022-??-??) - -#### :rocket: New Feature - -* Initial release diff --git a/src/core/component/directives/aria/roles-engines/option/README.md b/src/core/component/directives/aria/roles-engines/option/README.md deleted file mode 100644 index c3141835a3..0000000000 --- a/src/core/component/directives/aria/roles-engines/option/README.md +++ /dev/null @@ -1,25 +0,0 @@ -# core/component/directives/aria/roles-engines/option - -This module provides an engine for `v-aria` directive. - -The engine to set `option` role attribute. -The option role is used for selectable items in a `listbox`. - -For more information go to [option](`https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/option_role`). - -## API - -The engine expects specific parameters to be passed. -- `isSelected`: `boolean`. -If true current option is selected by default. -- `@change`:`HandlerAttachment`, see `core/component/directives/aria/roles-engines/README.md`. -Internal callback `onChange` expects an `boolean` value if current option is selected. - -## Usage - -``` -< div v-aria:option = { & - isSelected: el.active - '@change': (cb) => on('actionChange', () => cb(el.active)) - } -``` diff --git a/src/core/component/directives/aria/roles-engines/option/index.ts b/src/core/component/directives/aria/roles-engines/option/index.ts deleted file mode 100644 index 1791c48b4b..0000000000 --- a/src/core/component/directives/aria/roles-engines/option/index.ts +++ /dev/null @@ -1,28 +0,0 @@ -/*! - * V4Fire Client Core - * https://github.com/V4Fire/Client - * - * Released under the MIT license - * https://github.com/V4Fire/Client/blob/master/LICENSE - */ - -import { OptionParams } from 'core/component/directives/aria/roles-engines/option/interface'; -import { AriaRoleEngine } from 'core/component/directives/aria/roles-engines/interface'; - -export class OptionEngine extends AriaRoleEngine { - override Params: OptionParams = new OptionParams(); - - /** @inheritDoc */ - init(): void { - this.setAttribute('role', 'option'); - this.setAttribute('aria-selected', String(this.params.isSelected)); - } - - /** - * Handler: selected option changes - * @param isSelected - */ - protected onChange(isSelected: boolean): void { - this.el.setAttribute('aria-selected', String(isSelected)); - } -} diff --git a/src/core/component/directives/aria/roles-engines/option/interface.ts b/src/core/component/directives/aria/roles-engines/option/interface.ts deleted file mode 100644 index 58d4bffe1f..0000000000 --- a/src/core/component/directives/aria/roles-engines/option/interface.ts +++ /dev/null @@ -1,14 +0,0 @@ -/*! - * V4Fire Client Core - * https://github.com/V4Fire/Client - * - * Released under the MIT license - * https://github.com/V4Fire/Client/blob/master/LICENSE - */ - -import type { HandlerAttachment } from 'core/component/directives/aria/roles-engines/interface'; - -export class OptionParams { - isSelected: boolean = false; - '@change': HandlerAttachment = () => undefined; -} diff --git a/src/core/component/directives/aria/roles-engines/option/test/unit/option.ts b/src/core/component/directives/aria/roles-engines/option/test/unit/option.ts deleted file mode 100644 index f4b912d6d0..0000000000 --- a/src/core/component/directives/aria/roles-engines/option/test/unit/option.ts +++ /dev/null @@ -1,116 +0,0 @@ -/*! - * V4Fire Client Core - * https://github.com/V4Fire/Client - * - * Released under the MIT license - * https://github.com/V4Fire/Client/blob/master/LICENSE - */ - -import type { JSHandle, Page } from 'playwright'; -import type iBlock from 'super/i-block/i-block'; - -import test from 'tests/config/unit/test'; -import Component from 'tests/helpers/component'; - -test.describe('v-aria:option', () => { - test.beforeEach(async ({demoPage}) => { - await demoPage.goto(); - }); - - const - selector = '[data-id="target"]'; - - test('role is set', async ({page}) => { - const target = await init(page); - - await page.click(selector); - - test.expect( - await target.evaluate((ctx) => { - if (ctx.unsafe.block == null) { - return; - } - - const [el1, el2]: HTMLElement[] = Array.from(ctx.unsafe.block.elements('item')); - - return [el1.getAttribute('role'), el2.getAttribute('role')]; - }) - ).toEqual(['option', 'option']); - }); - - test('has no preselected value', async ({page}) => { - const target = await init(page); - - await page.click(selector); - - test.expect( - await target.evaluate((ctx) => { - if (ctx.unsafe.block == null) { - return; - } - - const [el1, el2]: HTMLElement[] = Array.from(ctx.unsafe.block.elements('item')); - - return [el1.getAttribute('aria-selected'), el2.getAttribute('aria-selected')]; - }) - ).toEqual(['false', 'false']); - }); - - test('options with preselected value', async ({page}) => { - const target = await init(page, {value: 0}); - - await page.click(selector); - - test.expect( - await target.evaluate((ctx) => { - if (ctx.unsafe.block == null) { - return; - } - - const [el1, el2]: HTMLElement[] = Array.from(ctx.unsafe.block.elements('item')); - - return [el1.getAttribute('aria-selected'), el2.getAttribute('aria-selected')]; - }) - ).toEqual(['true', 'false']); - }); - - test('selected option changed', async ({page}) => { - const target = await init(page, {value: 0}); - - await page.click(selector); - - test.expect( - await target.evaluate((ctx) => { - if (ctx.unsafe.block == null) { - return; - } - - const input = ctx.unsafe.block.element('input'); - const [el1, el2]: HTMLElement[] = Array.from(ctx.unsafe.block.elements('item')); - - input?.dispatchEvent(new KeyboardEvent('keydown', {key: 'ArrowDown', bubbles: true})); - - input?.dispatchEvent(new KeyboardEvent('keydown', {key: 'Enter', bubbles: true})); - - return [el1.getAttribute('aria-selected'), el2.getAttribute('aria-selected')]; - }) - ).toEqual(['false', 'true']); - }); - - /** - * @param page - * @param attrs - */ - async function init(page: Page, attrs: Dictionary = {}): Promise> { - return Component.createComponent(page, 'b-select', { - attrs: { - 'data-id': 'target', - items: [ - {label: 'foo', value: 0, attrs: {id: 'item1'}}, - {label: 'bar', value: 1, attrs: {id: 'item2'}} - ], - ...attrs - } - }); - } -}); diff --git a/src/core/component/directives/aria/roles-engines/tab/CHANGELOG.md b/src/core/component/directives/aria/roles-engines/tab/CHANGELOG.md deleted file mode 100644 index 6e314edf2d..0000000000 --- a/src/core/component/directives/aria/roles-engines/tab/CHANGELOG.md +++ /dev/null @@ -1,16 +0,0 @@ -Changelog -========= - -> **Tags:** -> - :boom: [Breaking Change] -> - :rocket: [New Feature] -> - :bug: [Bug Fix] -> - :memo: [Documentation] -> - :house: [Internal] -> - :nail_care: [Polish] - -## v3.?.? (2022-??-??) - -#### :rocket: New Feature - -* Initial release diff --git a/src/core/component/directives/aria/roles-engines/tab/README.md b/src/core/component/directives/aria/roles-engines/tab/README.md deleted file mode 100644 index 95e1c8e4c0..0000000000 --- a/src/core/component/directives/aria/roles-engines/tab/README.md +++ /dev/null @@ -1,49 +0,0 @@ -# core/component/directives/aria/roles-engines/tab - -This module provides an engine for `v-aria` directive. - -The engine to set `tab` role attribute. -The ARIA tab role indicates an interactive element inside a `tablist` that, when activated, displays its associated `tabpanel`. - -For more information go to [tab](`https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/tab_role`). -For recommendations how to make accessible widget go to [tab](`https://www.w3.org/WAI/ARIA/apg/patterns/tabpanel/`). - -## API - -The engine expects specific parameters to be passed. -- `isFirst`: `boolean`. -If true current tab is the first one in the list of tabs. -- `isSelected`: `boolean`. -If true current tab is active. -- `hasDefaultSelectedTabs`: `boolean`. -If true there are active tabs in the tablist widget by default. -- `orientation`: `string`. -The tablist widget view orientation. -- `@change`:`HandlerAttachment`, see `core/component/directives/aria/roles-engines/README.md`. -Internal callback `onChange` expects an `Element` or `NodeListOf` to be passed. - -In addition, tabs expect the `controls` role engine to be added. An id passed to `controls` engine should be the id of the element with role `tabpanel`. - -Example: -``` -< button v-aria:tab | v-aria:controls = {for: 'id1'} - -< v-aria:tabpanel = {labelledby: 'id2'} | :id = 'id1' - < span :id = 'id2' - // content -``` - -The engine expects the component to realize`iAccess` trait. - -## Usage - -``` -< div v-aria:tab = { & - isFirst: i === 0, - isSelected: el.active, - hasDefaultSelectedTabs: items.some((el) => !!el.active), - orientation: orientation, - '@change': (cb) => cb(el.active) - } -. -``` diff --git a/src/core/component/directives/aria/roles-engines/tab/index.ts b/src/core/component/directives/aria/roles-engines/tab/index.ts deleted file mode 100644 index ddf9fb43d9..0000000000 --- a/src/core/component/directives/aria/roles-engines/tab/index.ts +++ /dev/null @@ -1,167 +0,0 @@ -/*! - * V4Fire Client Core - * https://github.com/V4Fire/Client - * - * Released under the MIT license - * https://github.com/V4Fire/Client/blob/master/LICENSE - * - * This software or document includes material copied from or derived from ["Example of Tabs with Manual Activation", https://www.w3.org/WAI/ARIA/apg/example-index/tabs/tabs-manual.html]. - * Copyright © [2022] W3C® (MIT, ERCIM, Keio, Beihang). - */ - -import type iBlock from 'super/i-block/i-block'; -import type iAccess from 'traits/i-access/i-access'; - -import { TabParams } from 'core/component/directives/aria/roles-engines/tab/interface'; -import { AriaRoleEngine, KeyCodes } from 'core/component/directives/aria/roles-engines/interface'; - -export class TabEngine extends AriaRoleEngine { - override Params: TabParams = new TabParams(); - override Ctx!: iBlock & iAccess; - - /** @inheritDoc */ - init(): void { - const - {el} = this, - {isFirst, isSelected, hasDefaultSelectedTabs} = this.params; - - this.setAttribute('role', 'tab'); - this.setAttribute('aria-selected', String(isSelected)); - - if (isFirst && !hasDefaultSelectedTabs) { - if (el.tabIndex < 0) { - this.setAttribute('tabindex', '0'); - } - - } else if (hasDefaultSelectedTabs && isSelected) { - if (el.tabIndex < 0) { - this.setAttribute('tabindex', '0'); - } - - } else { - this.setAttribute('tabindex', '-1'); - } - - this.async.on(el, 'keydown', this.onKeydown.bind(this)); - } - - /** - * Moves focus to the first tab in tablist - */ - protected moveFocusToFirstTab(): void { - const - firstTab = this.ctx?.findFocusableElement(); - - firstTab?.focus(); - } - - /** - * Moves focus to the last tab in tablist - */ - protected moveFocusToLastTab(): void { - const - tabs = this.ctx?.findFocusableElements(); - - if (tabs == null) { - return; - } - - let - lastTab: CanUndef; - - for (const tab of tabs) { - lastTab = tab; - } - - lastTab?.focus(); - } - - /** - * Moves focus to the next or previous focusable element via the step parameter - * @param step - */ - protected moveFocus(step: 1 | -1): void { - const - focusable = this.ctx?.getNextFocusableElement(step); - - focusable?.focus(); - } - - /** - * Handler: active tab changes - * @param active - */ - protected onChange(active: Element | NodeListOf): void { - const setAttributes = (isSelected: boolean) => { - this.setAttribute('aria-selected', String(isSelected)); - this.setAttribute('tabindex', isSelected ? '0' : '-1'); - }; - - if (Object.isArrayLike(active)) { - for (let i = 0; i < active.length; i++) { - setAttributes(this.el === active[i]); - } - - return; - } - - setAttributes(this.el === active); - } - - /** - * Handler: keyboard event - */ - protected onKeydown(e: Event): void { - const - evt = (e), - isVertical = this.params.orientation === 'vertical'; - - switch (evt.key) { - case KeyCodes.LEFT: - if (isVertical) { - return; - } - - this.moveFocus(-1); - break; - - case KeyCodes.UP: - if (isVertical) { - this.moveFocus(-1); - break; - } - - return; - - case KeyCodes.RIGHT: - if (isVertical) { - return; - } - - this.moveFocus(1); - break; - - case KeyCodes.DOWN: - if (isVertical) { - this.moveFocus(1); - break; - } - - return; - - case KeyCodes.HOME: - this.moveFocusToFirstTab(); - break; - - case KeyCodes.END: - this.moveFocusToLastTab(); - break; - - default: - return; - } - - e.stopPropagation(); - e.preventDefault(); - } -} diff --git a/src/core/component/directives/aria/roles-engines/tab/interface.ts b/src/core/component/directives/aria/roles-engines/tab/interface.ts deleted file mode 100644 index 190e244b0a..0000000000 --- a/src/core/component/directives/aria/roles-engines/tab/interface.ts +++ /dev/null @@ -1,17 +0,0 @@ -/*! - * V4Fire Client Core - * https://github.com/V4Fire/Client - * - * Released under the MIT license - * https://github.com/V4Fire/Client/blob/master/LICENSE - */ - -import type { HandlerAttachment } from 'core/component/directives/aria/roles-engines/interface'; - -export class TabParams { - isFirst: boolean = false; - isSelected: boolean = false; - hasDefaultSelectedTabs: boolean = false; - orientation: string = 'false'; - '@change': HandlerAttachment = () => undefined; -} diff --git a/src/core/component/directives/aria/roles-engines/tab/test/unit/tab.ts b/src/core/component/directives/aria/roles-engines/tab/test/unit/tab.ts deleted file mode 100644 index ee1725eb04..0000000000 --- a/src/core/component/directives/aria/roles-engines/tab/test/unit/tab.ts +++ /dev/null @@ -1,225 +0,0 @@ -/*! - * V4Fire Client Core - * https://github.com/V4Fire/Client - * - * Released under the MIT license - * https://github.com/V4Fire/Client/blob/master/LICENSE - */ - -import type { JSHandle, Page } from 'playwright'; -import type iBlock from 'super/i-block/i-block'; - -import test from 'tests/config/unit/test'; -import Component from 'tests/helpers/component'; - -test.describe('v-aria:tab', () => { - test.beforeEach(async ({demoPage}) => { - await demoPage.goto(); - }); - - const - selector = '[data-id="target"]'; - - test('role is set', async ({page}) => { - const target = await init(page); - - test.expect( - await target.evaluate((ctx) => { - if (ctx.unsafe.block == null) { - return; - } - - const tabs = ctx.unsafe.block.elements('link'); - - const res: Array> = []; - - tabs.forEach((el) => res.push(el.getAttribute('role'))); - - return res; - }) - ).toEqual(['tab', 'tab', 'tab']); - }); - - test('has active value', async ({page}) => { - const target = await init(page, {active: 1}); - - await page.focus(selector); - - test.expect( - await target.evaluate((ctx) => { - if (ctx.unsafe.block == null) { - return; - } - - const tabs = ctx.unsafe.block.elements('link'); - - const res: Array> = []; - - tabs.forEach((el) => res.push(el.getAttribute('aria-selected'))); - - return res; - }) - ).toEqual(['false', 'true', 'false']); - }); - - test('tabindexes are set without active item', async ({page}) => { - const target = await init(page); - - test.expect( - await target.evaluate((ctx) => { - if (ctx.unsafe.block == null) { - return; - } - - const tabs: NodeListOf = ctx.unsafe.block.elements('link'); - - const res: number[] = []; - - tabs.forEach((el) => res.push(el.tabIndex)); - - return res; - }) - ).toEqual([0, -1, -1]); - }); - - test('tabindexes are set with active item', async ({page}) => { - const target = await init(page, {active: 1}); - - test.expect( - await target.evaluate((ctx) => { - if (ctx.unsafe.block == null) { - return; - } - - const tabs: NodeListOf = ctx.unsafe.block.elements('link'); - - const res: number[] = []; - - tabs.forEach((el) => res.push(el.tabIndex)); - - return res; - }) - ).toEqual([-1, 0, -1]); - }); - - test('active item changed', async ({page}) => { - const target = await init(page); - - test.expect( - await target.evaluate((ctx) => { - if (ctx.unsafe.block == null) { - return; - } - - let tabs: NodeListOf = ctx.unsafe.block.elements('link'); - - tabs[1].click(); - - const res: Array<[number, Nullable]> = []; - - tabs = ctx.unsafe.block.elements('link'); - - tabs.forEach((el, i) => res[i] = [el.tabIndex, el.ariaSelected]); - - return res; - }) - ).toEqual([ - [-1, 'false'], - [0, 'true'], - [-1, 'false'] - ]); - }); - - test('keyboard keys handle on horizontal orientation', async ({page}) => { - const target = await init(page); - - test.expect( - await target.evaluate((ctx) => { - if (ctx.unsafe.block == null) { - return; - } - - const - res: Array> = [], - tab: CanUndef = ctx.unsafe.block.element('link'); - - tab?.focus(); - tab?.dispatchEvent(new KeyboardEvent('keydown', {key: 'ArrowRight'})); - res.push(document.activeElement?.id); - - document.activeElement?.dispatchEvent(new KeyboardEvent('keydown', {key: 'ArrowLeft'})); - res.push(document.activeElement?.id); - - document.activeElement?.dispatchEvent(new KeyboardEvent('keydown', {key: 'ArrowUp'})); - res.push(document.activeElement?.id); - - document.activeElement?.dispatchEvent(new KeyboardEvent('keydown', {key: 'ArrowDown'})); - res.push(document.activeElement?.id); - - document.activeElement?.dispatchEvent(new KeyboardEvent('keydown', {key: 'End'})); - res.push(document.activeElement?.id); - - document.activeElement?.dispatchEvent(new KeyboardEvent('keydown', {key: 'Home'})); - res.push(document.activeElement?.id); - - return res; - }) - ).toEqual(['id2', 'id1', 'id1', 'id1', 'id3', 'id1']); - }); - - test('keyboard keys handle on vertical orientation', async ({page}) => { - const target = await init(page, {orientation: 'vertical'}); - - test.expect( - await target.evaluate((ctx) => { - if (ctx.unsafe.block == null) { - return; - } - - const - res: Array> = [], - tab: CanUndef = ctx.unsafe.block.element('link'); - - tab?.focus(); - tab?.dispatchEvent(new KeyboardEvent('keydown', {key: 'ArrowRight'})); - res.push(document.activeElement?.id); - - document.activeElement?.dispatchEvent(new KeyboardEvent('keydown', {key: 'ArrowLeft'})); - res.push(document.activeElement?.id); - - document.activeElement?.dispatchEvent(new KeyboardEvent('keydown', {key: 'ArrowDown'})); - res.push(document.activeElement?.id); - - document.activeElement?.dispatchEvent(new KeyboardEvent('keydown', {key: 'ArrowUp'})); - res.push(document.activeElement?.id); - - document.activeElement?.dispatchEvent(new KeyboardEvent('keydown', {key: 'End'})); - res.push(document.activeElement?.id); - - document.activeElement?.dispatchEvent(new KeyboardEvent('keydown', {key: 'Home'})); - res.push(document.activeElement?.id); - - return res; - }) - ).toEqual(['id1', 'id1', 'id2', 'id1', 'id3', 'id1']); - }); - - /** - * @param page - * @param attrs - */ - async function init(page: Page, attrs: Dictionary = {}): Promise> { - return Component.createComponent(page, 'b-list', { - attrs: { - 'data-id': 'target', - items: [ - {label: 'Male', value: 0, attrs: {id: 'id1'}}, - {label: 'Female', value: 1, attrs: {id: 'id2'}}, - {label: 'Other', value: 2, attrs: {id: 'id3'}} - ], - ...attrs - } - }); - } -}); - diff --git a/src/core/component/directives/aria/roles-engines/tablist/CHANGELOG.md b/src/core/component/directives/aria/roles-engines/tablist/CHANGELOG.md deleted file mode 100644 index 6e314edf2d..0000000000 --- a/src/core/component/directives/aria/roles-engines/tablist/CHANGELOG.md +++ /dev/null @@ -1,16 +0,0 @@ -Changelog -========= - -> **Tags:** -> - :boom: [Breaking Change] -> - :rocket: [New Feature] -> - :bug: [Bug Fix] -> - :memo: [Documentation] -> - :house: [Internal] -> - :nail_care: [Polish] - -## v3.?.? (2022-??-??) - -#### :rocket: New Feature - -* Initial release diff --git a/src/core/component/directives/aria/roles-engines/tablist/README.md b/src/core/component/directives/aria/roles-engines/tablist/README.md deleted file mode 100644 index e3a123639c..0000000000 --- a/src/core/component/directives/aria/roles-engines/tablist/README.md +++ /dev/null @@ -1,29 +0,0 @@ -# core/component/directives/aria/roles-engines/tablist - -This module provides an engine for `v-aria` directive. - -The engine to set `tablist` role attribute. -The `tablist` role identifies the element that serves as the container for a set of `tabs`. The `tab` content are referred to as `tabpanel` elements. - -For more information go to [tablist](`https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/tablist_role`). -For recommendations how to make accessible widget go to [tablist](`https://www.w3.org/WAI/ARIA/apg/patterns/tabpanel/`). - -## API - -The engine expects specific parameters to be passed. -- `isMultiple`:`boolean`. -If true widget supports multiple selected options. -- `orientation`: `string`. -The tablist widget view orientation. - -The engine expects the component to realize`iAccess` trait. - -## Usage - -``` -< div v-aria:tablist = { & - isMultiple: multiple; - orientation: orientation; - } -. -``` diff --git a/src/core/component/directives/aria/roles-engines/tablist/index.ts b/src/core/component/directives/aria/roles-engines/tablist/index.ts deleted file mode 100644 index c464b54c37..0000000000 --- a/src/core/component/directives/aria/roles-engines/tablist/index.ts +++ /dev/null @@ -1,30 +0,0 @@ -/*! - * V4Fire Client Core - * https://github.com/V4Fire/Client - * - * Released under the MIT license - * https://github.com/V4Fire/Client/blob/master/LICENSE - */ - -import { TablistParams } from 'core/component/directives/aria/roles-engines/tablist/interface'; -import { AriaRoleEngine } from 'core/component/directives/aria/roles-engines/interface'; - -export class TablistEngine extends AriaRoleEngine { - override Params: TablistParams = new TablistParams(); - - /** @inheritDoc */ - init(): void { - const - {params} = this; - - this.setAttribute('role', 'tablist'); - - if (params.isMultiple) { - this.setAttribute('aria-multiselectable', 'true'); - } - - if (params.orientation === 'vertical') { - this.setAttribute('aria-orientation', params.orientation); - } - } -} diff --git a/src/core/component/directives/aria/roles-engines/tablist/interface.ts b/src/core/component/directives/aria/roles-engines/tablist/interface.ts deleted file mode 100644 index 6144a59fba..0000000000 --- a/src/core/component/directives/aria/roles-engines/tablist/interface.ts +++ /dev/null @@ -1,12 +0,0 @@ -/*! - * V4Fire Client Core - * https://github.com/V4Fire/Client - * - * Released under the MIT license - * https://github.com/V4Fire/Client/blob/master/LICENSE - */ - -export class TablistParams { - isMultiple: boolean = false; - orientation: string = 'false'; -} diff --git a/src/core/component/directives/aria/roles-engines/tablist/test/unit/tablist.ts b/src/core/component/directives/aria/roles-engines/tablist/test/unit/tablist.ts deleted file mode 100644 index fd4872facb..0000000000 --- a/src/core/component/directives/aria/roles-engines/tablist/test/unit/tablist.ts +++ /dev/null @@ -1,71 +0,0 @@ -/*! - * V4Fire Client Core - * https://github.com/V4Fire/Client - * - * Released under the MIT license - * https://github.com/V4Fire/Client/blob/master/LICENSE - */ - -import type { JSHandle, Page } from 'playwright'; -import type iBlock from 'super/i-block/i-block'; - -import test from 'tests/config/unit/test'; -import Component from 'tests/helpers/component'; - -test.describe('v-aria:tablist', () => { - test.beforeEach(async ({demoPage}) => { - await demoPage.goto(); - }); - - test('role is set', async ({page}) => { - const target = await init(page); - - test.expect( - await target.evaluate((ctx) => { - const el = ctx.unsafe.block?.element('wrapper'); - - return el?.getAttribute('role'); - }) - ).toBe('tablist'); - }); - - test('multiselectable is set', async ({page}) => { - const target = await init(page, {multiple: true}); - - test.expect( - await target.evaluate((ctx) => { - const el = ctx.unsafe.block?.element('wrapper'); - - return el?.getAttribute('aria-multiselectable'); - }) - ).toBe('true'); - }); - - test('orientation is set', async ({page}) => { - const target = await init(page, {orientation: 'vertical'}); - - test.expect( - await target.evaluate((ctx) => { - const el = ctx.unsafe.block?.element('wrapper'); - - return el?.getAttribute('aria-orientation'); - }) - ).toBe('vertical'); - }); - - /** - * @param page - * @param attrs - */ - async function init(page: Page, attrs: Dictionary = {}): Promise> { - return Component.createComponent(page, 'b-list', { - attrs: { - items: [ - {label: 'foo', value: 0}, - {label: 'bar', value: 1} - ], - ...attrs - } - }); - } -}); diff --git a/src/core/component/directives/aria/roles-engines/tabpanel/CHANGELOG.md b/src/core/component/directives/aria/roles-engines/tabpanel/CHANGELOG.md deleted file mode 100644 index 6e314edf2d..0000000000 --- a/src/core/component/directives/aria/roles-engines/tabpanel/CHANGELOG.md +++ /dev/null @@ -1,16 +0,0 @@ -Changelog -========= - -> **Tags:** -> - :boom: [Breaking Change] -> - :rocket: [New Feature] -> - :bug: [Bug Fix] -> - :memo: [Documentation] -> - :house: [Internal] -> - :nail_care: [Polish] - -## v3.?.? (2022-??-??) - -#### :rocket: New Feature - -* Initial release diff --git a/src/core/component/directives/aria/roles-engines/tabpanel/README.md b/src/core/component/directives/aria/roles-engines/tabpanel/README.md deleted file mode 100644 index f4a6bba828..0000000000 --- a/src/core/component/directives/aria/roles-engines/tabpanel/README.md +++ /dev/null @@ -1,26 +0,0 @@ -# core/component/directives/aria/roles-engines/tabpanel - -This module provides an engine for `v-aria` directive. - -The engine to set `tabpanel` role attribute. -The ARIA `tabpanel` is a container for the resources of layered content associated with a `tab`. - -For more information go to [tabpanel](`https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/tabpanel_role`). -For recommendations how to make accessible widget go to [tabpanel](`https://www.w3.org/WAI/ARIA/apg/patterns/tabpanel/`). - -## API - -The engine expects `label` or `labelledby` params to be passed. - -Example: -``` -< v-aria:tabpanel = {labelledby: 'id1'} - < span :id = 'id1' - // content -``` - -## Usage - -``` -< div v-aria:tabpanel = {label: 'content'} -``` diff --git a/src/core/component/directives/aria/roles-engines/tabpanel/index.ts b/src/core/component/directives/aria/roles-engines/tabpanel/index.ts deleted file mode 100644 index cd76548539..0000000000 --- a/src/core/component/directives/aria/roles-engines/tabpanel/index.ts +++ /dev/null @@ -1,23 +0,0 @@ -/*! - * V4Fire Client Core - * https://github.com/V4Fire/Client - * - * Released under the MIT license - * https://github.com/V4Fire/Client/blob/master/LICENSE - */ - -import { AriaRoleEngine } from 'core/component/directives/aria/roles-engines/interface'; - -export class TabpanelEngine extends AriaRoleEngine { - /** @inheritDoc */ - init(): void { - const - {el} = this; - - if (!el.hasAttribute('aria-label') && !el.hasAttribute('aria-labelledby')) { - Object.throw('Tabpanel aria directive expects "label" or "labelledby" value to be passed'); - } - - this.setAttribute('role', 'tabpanel'); - } -} diff --git a/src/core/component/directives/aria/roles-engines/tabpanel/test/unit/tabpanel.ts b/src/core/component/directives/aria/roles-engines/tabpanel/test/unit/tabpanel.ts deleted file mode 100644 index 72e2422067..0000000000 --- a/src/core/component/directives/aria/roles-engines/tabpanel/test/unit/tabpanel.ts +++ /dev/null @@ -1,48 +0,0 @@ -/*! - * V4Fire Client Core - * https://github.com/V4Fire/Client - * - * Released under the MIT license - * https://github.com/V4Fire/Client/blob/master/LICENSE - */ - -import type { JSHandle, Page } from 'playwright'; -import type iBlock from 'super/i-block/i-block'; - -import test from 'tests/config/unit/test'; -import Component from 'tests/helpers/component'; - -test.describe('v-aria:tabpanel', () => { - test.beforeEach(async ({demoPage}) => { - await demoPage.goto(); - }); - - test('role is set', async ({page}) => { - const target = await init(page, {}); - - test.expect( - await target.evaluate((ctx) => ctx.$el?.getAttribute('role')) - ).toBe('tabpanel'); - }); - - test('no label passed', async ({page}) => { - const target = await init(page, {'v-aria:tabpanel': {}}); - - test.expect( - await target.evaluate((ctx) => ctx.$el?.hasAttribute('role')) - ).toBe(false); - }); - - /** - * @param page - * @param attrs - */ - async function init(page: Page, attrs: Dictionary = {}): Promise> { - return Component.createComponent(page, 'b-dummy', { - attrs: { - 'v-aria:tabpanel': {label: 'foo'}, - ...attrs - } - }); - } -}); diff --git a/src/core/component/directives/aria/roles-engines/tree/CHANGELOG.md b/src/core/component/directives/aria/roles-engines/tree/CHANGELOG.md deleted file mode 100644 index 6e314edf2d..0000000000 --- a/src/core/component/directives/aria/roles-engines/tree/CHANGELOG.md +++ /dev/null @@ -1,16 +0,0 @@ -Changelog -========= - -> **Tags:** -> - :boom: [Breaking Change] -> - :rocket: [New Feature] -> - :bug: [Bug Fix] -> - :memo: [Documentation] -> - :house: [Internal] -> - :nail_care: [Polish] - -## v3.?.? (2022-??-??) - -#### :rocket: New Feature - -* Initial release diff --git a/src/core/component/directives/aria/roles-engines/tree/README.md b/src/core/component/directives/aria/roles-engines/tree/README.md deleted file mode 100644 index 73f926850c..0000000000 --- a/src/core/component/directives/aria/roles-engines/tree/README.md +++ /dev/null @@ -1,32 +0,0 @@ -# core/component/directives/aria/roles-engines/tree - -This module provides an engine for `v-aria` directive. - -The engine to set `tree` role attribute. -A `tree` is a widget that allows the user to select one or more items from a hierarchically organized collection. - -For more information go to [tree](`https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/tree_role`). -For recommendations how to make accessible widget go to [tree](`https://www.w3.org/WAI/ARIA/apg/patterns/treeview/`). - -## API - -The engine expects specific parameters to be passed. -- `isRoot`: `boolean`. -If true current tree is the root tree in the component. -- `orientation`: `string`. -The tablist widget view orientation. -- `@change`:`HandlerAttachment`, see `core/component/directives/aria/roles-engines/README.md`. -Internal callback `onChange` expects an `Element` and `boolean` value if current tree is expanded. - -The engine expects the component to realize`iAccess` trait. - -## Usage - -``` -< div v-aria:tree = { & - isRoot: boolean = false; - orientation: string = 'false'; - '@change': HandlerAttachment = () => undefined; - } -. -``` diff --git a/src/core/component/directives/aria/roles-engines/tree/index.ts b/src/core/component/directives/aria/roles-engines/tree/index.ts deleted file mode 100644 index 3611b14510..0000000000 --- a/src/core/component/directives/aria/roles-engines/tree/index.ts +++ /dev/null @@ -1,42 +0,0 @@ -/*! - * V4Fire Client Core - * https://github.com/V4Fire/Client - * - * Released under the MIT license - * https://github.com/V4Fire/Client/blob/master/LICENSE - */ - -import { TreeParams } from 'core/component/directives/aria/roles-engines/tree/interface'; -import { AriaRoleEngine } from 'core/component/directives/aria/roles-engines/interface'; - -export class TreeEngine extends AriaRoleEngine { - override Params: TreeParams = new TreeParams(); - - /** @inheritDoc */ - init(): void { - const - {orientation, isRoot} = this.params; - - this.setRootRole(); - - if (orientation === 'horizontal' && isRoot) { - this.setAttribute('aria-orientation', orientation); - } - } - - /** - * Sets the role to the element depending on whether the tree is root or nested - */ - protected setRootRole(): void { - this.setAttribute('role', this.params.isRoot ? 'tree' : 'group'); - } - - /** - * Handler: treeitem was expanded or closed - * @param el - * @param isExpanded - */ - protected onChange(el: Element, isExpanded: boolean): void { - this.setAttribute('aria-expanded', String(isExpanded), el); - } -} diff --git a/src/core/component/directives/aria/roles-engines/tree/interface.ts b/src/core/component/directives/aria/roles-engines/tree/interface.ts deleted file mode 100644 index bad9ae817c..0000000000 --- a/src/core/component/directives/aria/roles-engines/tree/interface.ts +++ /dev/null @@ -1,15 +0,0 @@ -/*! - * V4Fire Client Core - * https://github.com/V4Fire/Client - * - * Released under the MIT license - * https://github.com/V4Fire/Client/blob/master/LICENSE - */ - -import type { HandlerAttachment } from 'core/component/directives/aria/roles-engines/interface'; - -export class TreeParams { - isRoot: boolean = false; - orientation: string = 'false'; - '@change': HandlerAttachment = () => undefined; -} diff --git a/src/core/component/directives/aria/roles-engines/tree/test/unit/tree.ts b/src/core/component/directives/aria/roles-engines/tree/test/unit/tree.ts deleted file mode 100644 index e040da54ed..0000000000 --- a/src/core/component/directives/aria/roles-engines/tree/test/unit/tree.ts +++ /dev/null @@ -1,102 +0,0 @@ -/*! - * V4Fire Client Core - * https://github.com/V4Fire/Client - * - * Released under the MIT license - * https://github.com/V4Fire/Client/blob/master/LICENSE - */ - -import type { JSHandle, Page } from 'playwright'; -import type iBlock from 'super/i-block/i-block'; - -import test from 'tests/config/unit/test'; -import Component from 'tests/helpers/component'; - -test.describe('v-aria:option', () => { - test.beforeEach(async ({demoPage}) => { - await demoPage.goto(); - }); - - test('role is set', async ({page}) => { - const target = await init(page); - - await page.waitForSelector('[role="group"]'); - - test.expect( - await target.evaluate(() => { - const - roots = document.querySelectorAll('[role="tree"]'), - groups = document.querySelectorAll('[role="group"]'); - - return [roots.length, groups.length]; - }) - ).toEqual([1, 2]); - }); - - test('orientation is set', async ({page}) => { - const target = await init(page, {orientation: 'horizontal'}); - - test.expect( - await target.evaluate((ctx) => { - const el = ctx.unsafe.block?.element('root'); - - return el?.getAttribute('aria-orientation'); - }) - ).toBe('horizontal'); - }); - - test('treeitem is expanded', async ({page}) => { - const target = await init(page); - - test.expect( - await target.evaluate((ctx) => { - const - fold: CanUndef = ctx.unsafe.block?.element('fold'), - items = ctx.unsafe.block?.elements('node'), - expandableItem = items?.[1]; - - const - res: Array>> = []; - - fold?.click(); - res.push(expandableItem?.getAttribute('aria-expanded')); - - fold?.click(); - res.push(expandableItem?.getAttribute('aria-expanded')); - - return res; - }) - ).toEqual(['true', 'false']); - }); - - /** - * @param page - * @param attrs - */ - async function init(page: Page, attrs: Dictionary = {}): Promise> { - return Component.createComponent(page, 'b-tree', { - attrs: { - item: 'b-checkbox', - items: [ - {id: 'bar', label: 'bar', attrs: {id: 'bar'}}, - { - id: 'foo', - label: 'foo', - children: [ - {id: 'fooone', label: 'foo1'}, - {id: 'footwo', label: 'foo2'}, - { - id: 'foothree', - label: 'foo3', - children: [{id: 'foothreeone', label: 'foo4'}] - }, - {id: 'foosix', label: 'foo5'} - ] - } - ], - ...attrs - } - }); - } -}); - diff --git a/src/core/component/directives/aria/roles-engines/treeitem/CHANGELOG.md b/src/core/component/directives/aria/roles-engines/treeitem/CHANGELOG.md deleted file mode 100644 index 6e314edf2d..0000000000 --- a/src/core/component/directives/aria/roles-engines/treeitem/CHANGELOG.md +++ /dev/null @@ -1,16 +0,0 @@ -Changelog -========= - -> **Tags:** -> - :boom: [Breaking Change] -> - :rocket: [New Feature] -> - :bug: [Bug Fix] -> - :memo: [Documentation] -> - :house: [Internal] -> - :nail_care: [Polish] - -## v3.?.? (2022-??-??) - -#### :rocket: New Feature - -* Initial release diff --git a/src/core/component/directives/aria/roles-engines/treeitem/README.md b/src/core/component/directives/aria/roles-engines/treeitem/README.md deleted file mode 100644 index 59f4cb951f..0000000000 --- a/src/core/component/directives/aria/roles-engines/treeitem/README.md +++ /dev/null @@ -1,41 +0,0 @@ -# core/component/directives/aria/roles-engines/treeitem - -This module provides an engine for `v-aria` directive. - -The engine to set `treeitem` role attribute. -A `treeitem` is an item in a `tree`. - -For more information go to [treeitem](`https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/treeitem_role`). -For recommendations how to make accessible widget go to [treeitem](`https://www.w3.org/WAI/ARIA/apg/patterns/treeview/`). - -## API - -The engine expects specific parameters to be passed. -- `isFirstRootItem`: `boolean`. -If true the item is first one in the root tree. -- `isExpandable`: `boolean`. -If true the item has children and can be expanded. -- `isExpanded`: `boolean`. -If true the item is expanded in the current moment. -- `orientation`: `string`. -The tablist widget view orientation. -- `rootElement`: `Element`. -The link to the root tree element. -- `toggleFold`: `function`. -The function to toggle the expandable item. - -The engine expects the component to realize`iAccess` trait. - -## Usage - -``` -< div v-aria:treeitem = { & - isFirstRootItem: el === top; - isExpandable: el.children != null; - isExpanded: !el.folded; - orientation: 'orientation'; - rootElement?: top; - toggleFold: () => ...; - } -. -``` diff --git a/src/core/component/directives/aria/roles-engines/treeitem/index.ts b/src/core/component/directives/aria/roles-engines/treeitem/index.ts deleted file mode 100644 index a65884bc12..0000000000 --- a/src/core/component/directives/aria/roles-engines/treeitem/index.ts +++ /dev/null @@ -1,241 +0,0 @@ -/*! - * V4Fire Client Core - * https://github.com/V4Fire/Client - * - * Released under the MIT license - * https://github.com/V4Fire/Client/blob/master/LICENSE - * - * This software or document includes material copied from or derived from ["Example of Tabs with Manual Activation", https://www.w3.org/WAI/ARIA/apg/example-index/tabs/tabs-manual.html]. - * Copyright © [2022] W3C® (MIT, ERCIM, Keio, Beihang). - */ - -import iAccess from 'traits/i-access/i-access'; -import type iBlock from 'super/i-block/i-block'; - -import { TreeitemParams } from 'core/component/directives/aria/roles-engines/treeitem/interface'; -import { AriaRoleEngine, KeyCodes } from 'core/component/directives/aria/roles-engines/interface'; - -export class TreeitemEngine extends AriaRoleEngine { - override Params: TreeitemParams = new TreeitemParams(); - override Ctx!: iBlock & iAccess; - - /** @inheritDoc */ - init(): void { - if (!iAccess.is(this.ctx)) { - Object.throw('Treeitem aria directive expects the component to realize iAccess interface'); - } - - this.async.on(this.el, 'keydown', this.onKeyDown.bind(this)); - - const - isMuted = this.ctx?.removeAllFromTabSequence(this.el); - - if (this.params.isFirstRootItem) { - if (isMuted) { - this.ctx?.restoreAllToTabSequence(this.el); - - } else { - this.setAttribute('tabindex', '0'); - } - } - - this.setAttribute('role', 'treeitem'); - - this.ctx?.$nextTick(() => { - if (this.params.isExpandable) { - this.setAttribute('aria-expanded', String(this.params.isExpanded)); - } - }); - } - - /** - * Changes focus from the current focused element to the passed one - * @param el - */ - protected focusNext(el: AccessibleElement): void { - this.ctx?.removeAllFromTabSequence(this.el); - this.ctx?.restoreAllToTabSequence(el); - - el.focus(); - } - - /** - * Moves focus to the next or previous focusable element via the step parameter - * @param step - */ - protected moveFocus(step: 1 | -1): void { - const - nextEl = this.ctx?.getNextFocusableElement(step); - - if (nextEl != null) { - this.focusNext(nextEl); - } - } - - /** - * Expands the treeitem - */ - protected openFold(): void { - this.params.toggleFold(this.el, false); - } - - /** - * Closes the treeitem - */ - protected closeFold(): void { - this.params.toggleFold(this.el, true); - } - - /** - * Moves focus to the parent treeitem - */ - protected focusParent(): void { - let - parent = this.el.parentElement; - - while (parent != null) { - if (parent.getAttribute('role') === 'treeitem') { - break; - } - - parent = parent.parentElement; - } - - if (parent == null) { - return; - } - - const - focusableParent = this.ctx?.findFocusableElement(parent); - - if (focusableParent != null) { - this.focusNext(focusableParent); - } - } - - /** - * Moves focus to the first visible treeitem - */ - protected setFocusToFirstItem(): void { - const - firstItem = this.ctx?.findFocusableElement(this.params.rootElement); - - if (firstItem != null) { - this.focusNext(firstItem); - } - } - - /** - * Moves focus to the last visible treeitem - */ - protected setFocusToLastItem(): void { - const - items = this.ctx?.findFocusableElements(this.params.rootElement); - - if (items == null) { - return; - } - - let - lastItem: CanUndef; - - for (const item of items) { - lastItem = item; - } - - if (lastItem != null) { - this.focusNext(lastItem); - } - } - - /** - * Handler: keyboard event - */ - protected onKeyDown(e: KeyboardEvent): void { - if (e.altKey || e.ctrlKey || e.metaKey || e.shiftKey) { - return; - } - - const - isHorizontal = this.params.orientation === 'horizontal'; - - const open = () => { - if (this.params.isExpandable) { - if (this.params.isExpanded) { - this.moveFocus(1); - - } else { - this.openFold(); - } - } - }; - - const close = () => { - if (this.params.isExpandable && this.params.isExpanded) { - this.closeFold(); - - } else { - this.focusParent(); - } - }; - - switch (e.key) { - case KeyCodes.UP: - if (isHorizontal) { - close(); - break; - } - - this.moveFocus(-1); - break; - - case KeyCodes.DOWN: - if (isHorizontal) { - open(); - break; - } - - this.moveFocus(1); - break; - - case KeyCodes.RIGHT: - if (isHorizontal) { - this.moveFocus(1); - break; - } - - open(); - break; - - case KeyCodes.LEFT: - if (isHorizontal) { - this.moveFocus(-1); - break; - } - - close(); - break; - - case KeyCodes.ENTER: - if (this.params.isExpandable) { - this.params.toggleFold(this.el); - } - - break; - - case KeyCodes.HOME: - this.setFocusToFirstItem(); - break; - - case KeyCodes.END: - this.setFocusToLastItem(); - break; - - default: - return; - } - - e.stopPropagation(); - e.preventDefault(); - } -} diff --git a/src/core/component/directives/aria/roles-engines/treeitem/interface.ts b/src/core/component/directives/aria/roles-engines/treeitem/interface.ts deleted file mode 100644 index c9ec8aedd1..0000000000 --- a/src/core/component/directives/aria/roles-engines/treeitem/interface.ts +++ /dev/null @@ -1,18 +0,0 @@ -/*! - * V4Fire Client Core - * https://github.com/V4Fire/Client - * - * Released under the MIT license - * https://github.com/V4Fire/Client/blob/master/LICENSE - */ - -type FoldToggle = (el: Element, value?: boolean) => void; - -export class TreeitemParams { - isFirstRootItem: boolean = false; - isExpandable: boolean = false; - isExpanded: boolean = false; - orientation: string = 'false'; - rootElement?: Element = undefined; - toggleFold: FoldToggle = () => undefined; -} diff --git a/src/core/component/directives/aria/roles-engines/treeitem/test/unit/treeitem.ts b/src/core/component/directives/aria/roles-engines/treeitem/test/unit/treeitem.ts deleted file mode 100644 index f44947bbf7..0000000000 --- a/src/core/component/directives/aria/roles-engines/treeitem/test/unit/treeitem.ts +++ /dev/null @@ -1,197 +0,0 @@ -/*! - * V4Fire Client Core - * https://github.com/V4Fire/Client - * - * Released under the MIT license - * https://github.com/V4Fire/Client/blob/master/LICENSE - */ - -import type { JSHandle, Page } from 'playwright'; -import type iBlock from 'super/i-block/i-block'; - -import test from 'tests/config/unit/test'; -import Component from 'tests/helpers/component'; - -test.describe('v-aria:treeitem', () => { - test.beforeEach(async ({demoPage}) => { - await demoPage.goto(); - }); - - test('role is set', async ({page}) => { - const target = await init(page); - - test.expect( - await target.evaluate((ctx) => { - const el = ctx.unsafe.block?.element('node'); - - return el?.getAttribute('role'); - }) - ).toBe('treeitem'); - }); - - test('aria-expanded is set', async ({page}) => { - const target = await init(page); - - await page.waitForSelector('[role="group"]'); - - test.expect( - await target.evaluate((ctx) => { - const - items = ctx.unsafe.block?.elements('node'), - expandableItem = items?.[1]; - - return expandableItem?.getAttribute('aria-expanded'); - }) - ).toBe('false'); - }); - - test('keyboard keys handle on vertical orientation', async ({page}) => { - const target = await init(page); - - test.expect( - await target.evaluate((ctx) => { - if (ctx.unsafe.block == null) { - return; - } - - const - input = document.querySelector('input'), - items = ctx.unsafe.block.elements('node'), - labels = document.querySelectorAll('label'); - - const - res: Array> = []; - - const - eq = (index: number) => document.activeElement?.id === labels[index].getAttribute('for'), - att = (): Nullable => items[1].getAttribute('aria-expanded'), - dis = (key: string) => document.activeElement?.dispatchEvent( - new KeyboardEvent('keydown', {key, bubbles: true}) - ); - - input?.focus(); - - input?.dispatchEvent(new KeyboardEvent('keydown', {key: 'ArrowDown', bubbles: true})); - res.push(eq(1)); - - dis('ArrowUp'); - res.push(eq(0)); - dis('Enter'); - res.push(items[0].getAttribute('aria-expanded')); - - dis('ArrowDown'); - dis('Enter'); - res.push(att()); - - dis('Enter'); - res.push(att()); - - dis('ArrowRight'); - res.push(att()); - - dis('ArrowRight'); - res.push(eq(2)); - - dis('ArrowLeft'); - res.push(eq(1)); - - dis('ArrowLeft'); - res.push(att()); - - dis('Home'); - res.push(eq(0)); - - dis('End'); - res.push(eq(3)); - - return res; - }) - ).toEqual([true, true, null, 'true', 'false', 'true', true, true, 'false', true, true]); - }); - - test('keyboard keys handle on horizontal orientation', async ({page}) => { - const target = await init(page, {orientation: 'horizontal'}); - - await page.waitForSelector('[role="group"]'); - - test.expect( - await target.evaluate((ctx) => { - if (ctx.unsafe.block == null) { - return; - } - - const - input = document.querySelector('input'), - items = ctx.unsafe.block.elements('node'), - labels = document.querySelectorAll('label'); - - const - res: Array> = []; - - const - eq = (index: number) => document.activeElement?.id === labels[index].getAttribute('for'), - att = (): Nullable => items[1].getAttribute('aria-expanded'), - dis = (key: string) => document.activeElement?.dispatchEvent( - new KeyboardEvent('keydown', {key, bubbles: true}) - ); - - input?.focus(); - - input?.dispatchEvent(new KeyboardEvent('keydown', {key: 'ArrowRight', bubbles: true})); - res.push(eq(1)); - - dis('ArrowLeft'); - res.push(eq(0)); - - dis('ArrowRight'); - dis('Enter'); - res.push(att()); - - dis('Enter'); - res.push(att()); - - dis('ArrowDown'); - res.push(att()); - - dis('ArrowDown'); - res.push(eq(2)); - - dis('ArrowUp'); - res.push(eq(1)); - - dis('ArrowUp'); - res.push(att()); - - dis('Home'); - res.push(eq(0)); - - dis('End'); - res.push(eq(3)); - - return res; - }) - ).toEqual([true, true, 'true', 'false', 'true', true, true, 'false', true, true]); - }); - - /** - * @param page - * @param attrs - */ - async function init(page: Page, attrs: Dictionary = {}): Promise> { - return Component.createComponent(page, 'b-tree', { - attrs: { - item: 'b-checkbox', - items: [ - {id: 'bar', label: 'bar'}, - { - id: 'foo', - label: 'foo', - children: [{id: 'fooone', label: 'foo1'}] - }, - {id: 'bla', label: 'bla'} - ], - ...attrs - } - }); - } -}); From 1f65216e986e6aa0d9af546698d2a98c449f245a Mon Sep 17 00:00:00 2001 From: kobezzza Date: Wed, 14 Sep 2022 12:42:33 +0300 Subject: [PATCH 150/185] refactor: renamed interface `AriaRoleEngine` to `AriaRole` --- src/core/component/directives/aria/adapter.ts | 8 ++++---- .../component/directives/aria/roles/combobox/index.ts | 4 ++-- .../component/directives/aria/roles/controls/index.ts | 4 ++-- src/core/component/directives/aria/roles/dialog/index.ts | 4 ++-- src/core/component/directives/aria/roles/index.ts | 2 +- src/core/component/directives/aria/roles/interface.ts | 4 ++-- src/core/component/directives/aria/roles/listbox/index.ts | 4 ++-- src/core/component/directives/aria/roles/option/index.ts | 4 ++-- src/core/component/directives/aria/roles/tab/index.ts | 4 ++-- src/core/component/directives/aria/roles/tablist/index.ts | 4 ++-- .../component/directives/aria/roles/tabpanel/index.ts | 4 ++-- src/core/component/directives/aria/roles/tree/index.ts | 4 ++-- .../component/directives/aria/roles/treeitem/index.ts | 4 ++-- 13 files changed, 27 insertions(+), 27 deletions(-) diff --git a/src/core/component/directives/aria/adapter.ts b/src/core/component/directives/aria/adapter.ts index d3a92c05d4..38d12599a6 100644 --- a/src/core/component/directives/aria/adapter.ts +++ b/src/core/component/directives/aria/adapter.ts @@ -11,7 +11,7 @@ import type iBlock from 'super/i-block/i-block'; import * as roles from 'core/component/directives/aria/roles'; import type { DirectiveOptions } from 'core/component/directives/aria/interface'; -import type { AriaRoleEngine, EngineOptions } from 'core/component/directives/aria/roles'; +import type { AriaRole, EngineOptions } from 'core/component/directives/aria/roles'; /** * An adapter to create an ARIA role instance based on the passed directive options and to add common attributes @@ -28,7 +28,7 @@ export default class AriaAdapter { /** * An instance of the associated ARIA role */ - protected role: CanUndef; + protected role: CanUndef; constructor(options: DirectiveOptions) { this.options = options; @@ -61,7 +61,7 @@ export default class AriaAdapter { /** * If the role was passed as a directive argument sets specified engine */ - protected setAriaRole(): CanUndef { + protected setAriaRole(): CanUndef { const {el, binding} = this.options, {value, modifiers, arg: role} = binding; @@ -73,7 +73,7 @@ export default class AriaAdapter { const engine = `${role.capitalize()}Engine`; - const options: EngineOptions = { + const options: EngineOptions = { el, modifiers, params: value, diff --git a/src/core/component/directives/aria/roles/combobox/index.ts b/src/core/component/directives/aria/roles/combobox/index.ts index 8a42bc6a95..34624f7308 100644 --- a/src/core/component/directives/aria/roles/combobox/index.ts +++ b/src/core/component/directives/aria/roles/combobox/index.ts @@ -10,9 +10,9 @@ import type iAccess from 'traits/i-access/i-access'; import type { ComponentInterface } from 'super/i-block/i-block'; import { ComboboxParams } from 'core/component/directives/aria/roles/combobox/interface'; -import { AriaRoleEngine, EngineOptions } from 'core/component/directives/aria/roles/interface'; +import { AriaRole, EngineOptions } from 'core/component/directives/aria/roles/interface'; -export class ComboboxEngine extends AriaRoleEngine { +export class ComboboxEngine extends AriaRole { override Params: ComboboxParams = new ComboboxParams(); override Ctx!: ComponentInterface & iAccess; override el: HTMLElement; diff --git a/src/core/component/directives/aria/roles/controls/index.ts b/src/core/component/directives/aria/roles/controls/index.ts index d792e93bf9..4b6395b1d9 100644 --- a/src/core/component/directives/aria/roles/controls/index.ts +++ b/src/core/component/directives/aria/roles/controls/index.ts @@ -7,9 +7,9 @@ */ import { ControlsParams } from 'core/component/directives/aria/roles/controls/interface'; -import { AriaRoleEngine } from 'core/component/directives/aria/roles/interface'; +import { AriaRole } from 'core/component/directives/aria/roles/interface'; -export class ControlsEngine extends AriaRoleEngine { +export class ControlsEngine extends AriaRole { override Params: ControlsParams = new ControlsParams(); /** @inheritDoc */ diff --git a/src/core/component/directives/aria/roles/dialog/index.ts b/src/core/component/directives/aria/roles/dialog/index.ts index 14f57631f5..9bc9618707 100644 --- a/src/core/component/directives/aria/roles/dialog/index.ts +++ b/src/core/component/directives/aria/roles/dialog/index.ts @@ -6,10 +6,10 @@ * https://github.com/V4Fire/Client/blob/master/LICENSE */ -import { AriaRoleEngine } from 'core/component/directives/aria/roles/interface'; +import { AriaRole } from 'core/component/directives/aria/roles/interface'; import iOpen from 'traits/i-open/i-open'; -export class DialogEngine extends AriaRoleEngine { +export class DialogEngine extends AriaRole { /** @inheritDoc */ init(): void { this.setAttribute('role', 'dialog'); diff --git a/src/core/component/directives/aria/roles/index.ts b/src/core/component/directives/aria/roles/index.ts index 792503f849..a3c6527402 100644 --- a/src/core/component/directives/aria/roles/index.ts +++ b/src/core/component/directives/aria/roles/index.ts @@ -17,4 +17,4 @@ export * from 'core/component/directives/aria/roles/option'; export * from 'core/component/directives/aria/roles/tree'; export * from 'core/component/directives/aria/roles/treeitem'; -export { AriaRoleEngine, EngineOptions } from 'core/component/directives/aria/roles/interface'; +export { AriaRole, EngineOptions } from 'core/component/directives/aria/roles/interface'; diff --git a/src/core/component/directives/aria/roles/interface.ts b/src/core/component/directives/aria/roles/interface.ts index f82549b390..9b1746749f 100644 --- a/src/core/component/directives/aria/roles/interface.ts +++ b/src/core/component/directives/aria/roles/interface.ts @@ -9,7 +9,7 @@ import type Async from 'core/async'; import type { ComponentInterface } from 'super/i-block/i-block'; -export abstract class AriaRoleEngine { +export abstract class AriaRole { /** * Type: directive passed params */ @@ -43,7 +43,7 @@ export abstract class AriaRoleEngine { /** @see [[Async]] */ async: Async; - constructor({el, ctx, modifiers, params, async}: EngineOptions) { + constructor({el, ctx, modifiers, params, async}: EngineOptions) { this.el = el; this.ctx = ctx; this.modifiers = modifiers; diff --git a/src/core/component/directives/aria/roles/listbox/index.ts b/src/core/component/directives/aria/roles/listbox/index.ts index 2bd9df6db6..abad8bcdd3 100644 --- a/src/core/component/directives/aria/roles/listbox/index.ts +++ b/src/core/component/directives/aria/roles/listbox/index.ts @@ -6,9 +6,9 @@ * https://github.com/V4Fire/Client/blob/master/LICENSE */ -import { AriaRoleEngine } from 'core/component/directives/aria/roles/interface'; +import { AriaRole } from 'core/component/directives/aria/roles/interface'; -export class ListboxEngine extends AriaRoleEngine { +export class ListboxEngine extends AriaRole { /** @inheritDoc */ init(): void { this.setAttribute('role', 'listbox'); diff --git a/src/core/component/directives/aria/roles/option/index.ts b/src/core/component/directives/aria/roles/option/index.ts index 140c242e90..e0173d7e36 100644 --- a/src/core/component/directives/aria/roles/option/index.ts +++ b/src/core/component/directives/aria/roles/option/index.ts @@ -7,9 +7,9 @@ */ import { OptionParams } from 'core/component/directives/aria/roles/option/interface'; -import { AriaRoleEngine } from 'core/component/directives/aria/roles/interface'; +import { AriaRole } from 'core/component/directives/aria/roles/interface'; -export class OptionEngine extends AriaRoleEngine { +export class OptionEngine extends AriaRole { override Params: OptionParams = new OptionParams(); /** @inheritDoc */ diff --git a/src/core/component/directives/aria/roles/tab/index.ts b/src/core/component/directives/aria/roles/tab/index.ts index 3c3e522290..921f1597f5 100644 --- a/src/core/component/directives/aria/roles/tab/index.ts +++ b/src/core/component/directives/aria/roles/tab/index.ts @@ -13,9 +13,9 @@ import type iBlock from 'super/i-block/i-block'; import type iAccess from 'traits/i-access/i-access'; import { TabParams } from 'core/component/directives/aria/roles/tab/interface'; -import { AriaRoleEngine, KeyCodes } from 'core/component/directives/aria/roles/interface'; +import { AriaRole, KeyCodes } from 'core/component/directives/aria/roles/interface'; -export class TabEngine extends AriaRoleEngine { +export class TabEngine extends AriaRole { override Params: TabParams = new TabParams(); override Ctx!: iBlock & iAccess; diff --git a/src/core/component/directives/aria/roles/tablist/index.ts b/src/core/component/directives/aria/roles/tablist/index.ts index ec78c07fd7..1dcd175634 100644 --- a/src/core/component/directives/aria/roles/tablist/index.ts +++ b/src/core/component/directives/aria/roles/tablist/index.ts @@ -7,9 +7,9 @@ */ import { TablistParams } from 'core/component/directives/aria/roles/tablist/interface'; -import { AriaRoleEngine } from 'core/component/directives/aria/roles/interface'; +import { AriaRole } from 'core/component/directives/aria/roles/interface'; -export class TablistEngine extends AriaRoleEngine { +export class TablistEngine extends AriaRole { override Params: TablistParams = new TablistParams(); /** @inheritDoc */ diff --git a/src/core/component/directives/aria/roles/tabpanel/index.ts b/src/core/component/directives/aria/roles/tabpanel/index.ts index 69d23640d4..e174bdb348 100644 --- a/src/core/component/directives/aria/roles/tabpanel/index.ts +++ b/src/core/component/directives/aria/roles/tabpanel/index.ts @@ -6,9 +6,9 @@ * https://github.com/V4Fire/Client/blob/master/LICENSE */ -import { AriaRoleEngine } from 'core/component/directives/aria/roles/interface'; +import { AriaRole } from 'core/component/directives/aria/roles/interface'; -export class TabpanelEngine extends AriaRoleEngine { +export class TabpanelEngine extends AriaRole { /** @inheritDoc */ init(): void { const diff --git a/src/core/component/directives/aria/roles/tree/index.ts b/src/core/component/directives/aria/roles/tree/index.ts index 91292045e7..3a83447ce8 100644 --- a/src/core/component/directives/aria/roles/tree/index.ts +++ b/src/core/component/directives/aria/roles/tree/index.ts @@ -7,9 +7,9 @@ */ import { TreeParams } from 'core/component/directives/aria/roles/tree/interface'; -import { AriaRoleEngine } from 'core/component/directives/aria/roles/interface'; +import { AriaRole } from 'core/component/directives/aria/roles/interface'; -export class TreeEngine extends AriaRoleEngine { +export class TreeEngine extends AriaRole { override Params: TreeParams = new TreeParams(); /** @inheritDoc */ diff --git a/src/core/component/directives/aria/roles/treeitem/index.ts b/src/core/component/directives/aria/roles/treeitem/index.ts index b97146f3f0..e60fc804ef 100644 --- a/src/core/component/directives/aria/roles/treeitem/index.ts +++ b/src/core/component/directives/aria/roles/treeitem/index.ts @@ -13,9 +13,9 @@ import iAccess from 'traits/i-access/i-access'; import type iBlock from 'super/i-block/i-block'; import { TreeitemParams } from 'core/component/directives/aria/roles/treeitem/interface'; -import { AriaRoleEngine, KeyCodes } from 'core/component/directives/aria/roles/interface'; +import { AriaRole, KeyCodes } from 'core/component/directives/aria/roles/interface'; -export class TreeitemEngine extends AriaRoleEngine { +export class TreeitemEngine extends AriaRole { override Params: TreeitemParams = new TreeitemParams(); override Ctx!: iBlock & iAccess; From 8631286d2d6056a6fadb29639840ceccee096933 Mon Sep 17 00:00:00 2001 From: kobezzza Date: Wed, 14 Sep 2022 14:57:12 +0300 Subject: [PATCH 151/185] refactor: renamed all `Aria` chunks to `ARIA`; removed redundant `Engine` postfixes --- .../directives/aria/roles/combobox/index.ts | 12 ++++++------ .../directives/aria/roles/controls/index.ts | 4 ++-- .../component/directives/aria/roles/dialog/index.ts | 4 ++-- src/core/component/directives/aria/roles/index.ts | 2 +- .../component/directives/aria/roles/interface.ts | 4 ++-- .../component/directives/aria/roles/listbox/index.ts | 4 ++-- .../component/directives/aria/roles/option/index.ts | 4 ++-- .../component/directives/aria/roles/tab/index.ts | 4 ++-- .../component/directives/aria/roles/tablist/index.ts | 4 ++-- .../directives/aria/roles/tabpanel/index.ts | 4 ++-- .../component/directives/aria/roles/tree/index.ts | 4 ++-- .../directives/aria/roles/treeitem/index.ts | 4 ++-- 12 files changed, 27 insertions(+), 27 deletions(-) diff --git a/src/core/component/directives/aria/roles/combobox/index.ts b/src/core/component/directives/aria/roles/combobox/index.ts index 34624f7308..c2a2f31605 100644 --- a/src/core/component/directives/aria/roles/combobox/index.ts +++ b/src/core/component/directives/aria/roles/combobox/index.ts @@ -10,9 +10,9 @@ import type iAccess from 'traits/i-access/i-access'; import type { ComponentInterface } from 'super/i-block/i-block'; import { ComboboxParams } from 'core/component/directives/aria/roles/combobox/interface'; -import { AriaRole, EngineOptions } from 'core/component/directives/aria/roles/interface'; +import { ARIARole, EngineOptions } from 'core/component/directives/aria/roles/interface'; -export class ComboboxEngine extends AriaRole { +export class Combobox extends ARIARole { override Params: ComboboxParams = new ComboboxParams(); override Ctx!: ComponentInterface & iAccess; override el: HTMLElement; @@ -43,7 +43,7 @@ export class ComboboxEngine extends AriaRole { /** * Sets or deletes the id of active descendant element */ - protected setAriaActive(el?: Element): void { + protected setARIAActive(el?: Element): void { this.setAttribute('aria-activedescendant', el?.id ?? ''); } @@ -53,7 +53,7 @@ export class ComboboxEngine extends AriaRole { */ protected onOpen(el: Element): void { this.setAttribute('aria-expanded', 'true'); - this.setAriaActive(el); + this.setARIAActive(el); } /** @@ -61,7 +61,7 @@ export class ComboboxEngine extends AriaRole { */ protected onClose(): void { this.setAttribute('aria-expanded', 'false'); - this.setAriaActive(); + this.setARIAActive(); } /** @@ -69,6 +69,6 @@ export class ComboboxEngine extends AriaRole { * @param el */ protected onChange(el: Element): void { - this.setAriaActive(el); + this.setARIAActive(el); } } diff --git a/src/core/component/directives/aria/roles/controls/index.ts b/src/core/component/directives/aria/roles/controls/index.ts index 4b6395b1d9..0fdc19af68 100644 --- a/src/core/component/directives/aria/roles/controls/index.ts +++ b/src/core/component/directives/aria/roles/controls/index.ts @@ -7,9 +7,9 @@ */ import { ControlsParams } from 'core/component/directives/aria/roles/controls/interface'; -import { AriaRole } from 'core/component/directives/aria/roles/interface'; +import { ARIARole } from 'core/component/directives/aria/roles/interface'; -export class ControlsEngine extends AriaRole { +export class Controls extends ARIARole { override Params: ControlsParams = new ControlsParams(); /** @inheritDoc */ diff --git a/src/core/component/directives/aria/roles/dialog/index.ts b/src/core/component/directives/aria/roles/dialog/index.ts index 9bc9618707..adac9eda02 100644 --- a/src/core/component/directives/aria/roles/dialog/index.ts +++ b/src/core/component/directives/aria/roles/dialog/index.ts @@ -6,10 +6,10 @@ * https://github.com/V4Fire/Client/blob/master/LICENSE */ -import { AriaRole } from 'core/component/directives/aria/roles/interface'; +import { ARIARole } from 'core/component/directives/aria/roles/interface'; import iOpen from 'traits/i-open/i-open'; -export class DialogEngine extends AriaRole { +export class Dialog extends ARIARole { /** @inheritDoc */ init(): void { this.setAttribute('role', 'dialog'); diff --git a/src/core/component/directives/aria/roles/index.ts b/src/core/component/directives/aria/roles/index.ts index a3c6527402..c25539a631 100644 --- a/src/core/component/directives/aria/roles/index.ts +++ b/src/core/component/directives/aria/roles/index.ts @@ -17,4 +17,4 @@ export * from 'core/component/directives/aria/roles/option'; export * from 'core/component/directives/aria/roles/tree'; export * from 'core/component/directives/aria/roles/treeitem'; -export { AriaRole, EngineOptions } from 'core/component/directives/aria/roles/interface'; +export { ARIARole, EngineOptions } from 'core/component/directives/aria/roles/interface'; diff --git a/src/core/component/directives/aria/roles/interface.ts b/src/core/component/directives/aria/roles/interface.ts index 9b1746749f..8c9eda8e2e 100644 --- a/src/core/component/directives/aria/roles/interface.ts +++ b/src/core/component/directives/aria/roles/interface.ts @@ -9,7 +9,7 @@ import type Async from 'core/async'; import type { ComponentInterface } from 'super/i-block/i-block'; -export abstract class AriaRole { +export abstract class ARIARole { /** * Type: directive passed params */ @@ -43,7 +43,7 @@ export abstract class AriaRole { /** @see [[Async]] */ async: Async; - constructor({el, ctx, modifiers, params, async}: EngineOptions) { + constructor({el, ctx, modifiers, params, async}: EngineOptions) { this.el = el; this.ctx = ctx; this.modifiers = modifiers; diff --git a/src/core/component/directives/aria/roles/listbox/index.ts b/src/core/component/directives/aria/roles/listbox/index.ts index abad8bcdd3..0742c095c6 100644 --- a/src/core/component/directives/aria/roles/listbox/index.ts +++ b/src/core/component/directives/aria/roles/listbox/index.ts @@ -6,9 +6,9 @@ * https://github.com/V4Fire/Client/blob/master/LICENSE */ -import { AriaRole } from 'core/component/directives/aria/roles/interface'; +import { ARIARole } from 'core/component/directives/aria/roles/interface'; -export class ListboxEngine extends AriaRole { +export class Listbox extends ARIARole { /** @inheritDoc */ init(): void { this.setAttribute('role', 'listbox'); diff --git a/src/core/component/directives/aria/roles/option/index.ts b/src/core/component/directives/aria/roles/option/index.ts index e0173d7e36..f7dd79ec29 100644 --- a/src/core/component/directives/aria/roles/option/index.ts +++ b/src/core/component/directives/aria/roles/option/index.ts @@ -7,9 +7,9 @@ */ import { OptionParams } from 'core/component/directives/aria/roles/option/interface'; -import { AriaRole } from 'core/component/directives/aria/roles/interface'; +import { ARIARole } from 'core/component/directives/aria/roles/interface'; -export class OptionEngine extends AriaRole { +export class Option extends ARIARole { override Params: OptionParams = new OptionParams(); /** @inheritDoc */ diff --git a/src/core/component/directives/aria/roles/tab/index.ts b/src/core/component/directives/aria/roles/tab/index.ts index 921f1597f5..457a16a9e5 100644 --- a/src/core/component/directives/aria/roles/tab/index.ts +++ b/src/core/component/directives/aria/roles/tab/index.ts @@ -13,9 +13,9 @@ import type iBlock from 'super/i-block/i-block'; import type iAccess from 'traits/i-access/i-access'; import { TabParams } from 'core/component/directives/aria/roles/tab/interface'; -import { AriaRole, KeyCodes } from 'core/component/directives/aria/roles/interface'; +import { ARIARole, KeyCodes } from 'core/component/directives/aria/roles/interface'; -export class TabEngine extends AriaRole { +export class Tab extends ARIARole { override Params: TabParams = new TabParams(); override Ctx!: iBlock & iAccess; diff --git a/src/core/component/directives/aria/roles/tablist/index.ts b/src/core/component/directives/aria/roles/tablist/index.ts index 1dcd175634..3e0449b84c 100644 --- a/src/core/component/directives/aria/roles/tablist/index.ts +++ b/src/core/component/directives/aria/roles/tablist/index.ts @@ -7,9 +7,9 @@ */ import { TablistParams } from 'core/component/directives/aria/roles/tablist/interface'; -import { AriaRole } from 'core/component/directives/aria/roles/interface'; +import { ARIARole } from 'core/component/directives/aria/roles/interface'; -export class TablistEngine extends AriaRole { +export class Tablist extends ARIARole { override Params: TablistParams = new TablistParams(); /** @inheritDoc */ diff --git a/src/core/component/directives/aria/roles/tabpanel/index.ts b/src/core/component/directives/aria/roles/tabpanel/index.ts index e174bdb348..306b1202e6 100644 --- a/src/core/component/directives/aria/roles/tabpanel/index.ts +++ b/src/core/component/directives/aria/roles/tabpanel/index.ts @@ -6,9 +6,9 @@ * https://github.com/V4Fire/Client/blob/master/LICENSE */ -import { AriaRole } from 'core/component/directives/aria/roles/interface'; +import { ARIARole } from 'core/component/directives/aria/roles/interface'; -export class TabpanelEngine extends AriaRole { +export class Tabpanel extends ARIARole { /** @inheritDoc */ init(): void { const diff --git a/src/core/component/directives/aria/roles/tree/index.ts b/src/core/component/directives/aria/roles/tree/index.ts index 3a83447ce8..7eb1d63d60 100644 --- a/src/core/component/directives/aria/roles/tree/index.ts +++ b/src/core/component/directives/aria/roles/tree/index.ts @@ -7,9 +7,9 @@ */ import { TreeParams } from 'core/component/directives/aria/roles/tree/interface'; -import { AriaRole } from 'core/component/directives/aria/roles/interface'; +import { ARIARole } from 'core/component/directives/aria/roles/interface'; -export class TreeEngine extends AriaRole { +export class Tree extends ARIARole { override Params: TreeParams = new TreeParams(); /** @inheritDoc */ diff --git a/src/core/component/directives/aria/roles/treeitem/index.ts b/src/core/component/directives/aria/roles/treeitem/index.ts index e60fc804ef..d5c672deb1 100644 --- a/src/core/component/directives/aria/roles/treeitem/index.ts +++ b/src/core/component/directives/aria/roles/treeitem/index.ts @@ -13,9 +13,9 @@ import iAccess from 'traits/i-access/i-access'; import type iBlock from 'super/i-block/i-block'; import { TreeitemParams } from 'core/component/directives/aria/roles/treeitem/interface'; -import { AriaRole, KeyCodes } from 'core/component/directives/aria/roles/interface'; +import { ARIARole, KeyCodes } from 'core/component/directives/aria/roles/interface'; -export class TreeitemEngine extends AriaRole { +export class Treeitem extends ARIARole { override Params: TreeitemParams = new TreeitemParams(); override Ctx!: iBlock & iAccess; From 98f2dafd56e46015eb6b54b8617e4e01dc9cbda9 Mon Sep 17 00:00:00 2001 From: kobezzza Date: Wed, 14 Sep 2022 14:57:43 +0300 Subject: [PATCH 152/185] chore: simple stylish fixes --- src/core/component/directives/aria/README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/core/component/directives/aria/README.md b/src/core/component/directives/aria/README.md index e1a8b4ed43..78d0ed91cc 100644 --- a/src/core/component/directives/aria/README.md +++ b/src/core/component/directives/aria/README.md @@ -1,6 +1,6 @@ # core/component/directives/aria -This module provides a directive to add aria attributes and logic to elements through a single API. +This module provides a directive to add aria attributes and logic to elements via a common API. ``` < div v-aria = {labelledby: dom.getId('title')} @@ -10,7 +10,7 @@ This module provides a directive to add aria attributes and logic to elements th < div :aria-labelledby = dom.getId('title') ``` -The [Aria](https://www.w3.org/TR/wai-aria) specification consists of a set of entities called roles. +The [ARIA](https://www.w3.org/TR/wai-aria) specification consists of a set of entities called roles. For example, [Tablist](https://www.w3.org/TR/wai-aria/#tablist) or [Combobox](https://www.w3.org/TR/wai-aria/#combobox). Therefore, the directive also consists of many engines, each of which implements a particular role. @@ -47,7 +47,7 @@ Each role can accept its own set of options, which are described in its document ## Available options -All ARIA attributes could be added in options through short syntax. +Any ARIA attributes could be added in options through the short syntax. ``` < div v-aria = {label: 'foo', desribedby: 'id1', details: 'id2'} From 98275b201eb3254a72a3e41b0b50639ea8961d68 Mon Sep 17 00:00:00 2001 From: kobezzza Date: Wed, 14 Sep 2022 14:58:49 +0300 Subject: [PATCH 153/185] refactor: simplify adapter API --- src/core/component/directives/aria/adapter.ts | 180 +++++++++--------- 1 file changed, 86 insertions(+), 94 deletions(-) diff --git a/src/core/component/directives/aria/adapter.ts b/src/core/component/directives/aria/adapter.ts index 38d12599a6..7303303b2c 100644 --- a/src/core/component/directives/aria/adapter.ts +++ b/src/core/component/directives/aria/adapter.ts @@ -11,16 +11,16 @@ import type iBlock from 'super/i-block/i-block'; import * as roles from 'core/component/directives/aria/roles'; import type { DirectiveOptions } from 'core/component/directives/aria/interface'; -import type { AriaRole, EngineOptions } from 'core/component/directives/aria/roles'; +import type { ARIARole, EngineOptions } from 'core/component/directives/aria/roles'; /** - * An adapter to create an ARIA role instance based on the passed directive options and to add common attributes + * An adapter to create an ARIA role instance based on the passed directive params and to add common attributes */ -export default class AriaAdapter { +export default class ARIAAdapter { /** - * Parameters passed from the directive + * Parameters passed from the associated directive */ - protected readonly options: DirectiveOptions; + protected readonly params: DirectiveOptions; /** @see [[Async]] */ protected readonly async: Async = new Async(); @@ -28,52 +28,55 @@ export default class AriaAdapter { /** * An instance of the associated ARIA role */ - protected role: CanUndef; + protected role: ARIARole | null = null; - constructor(options: DirectiveOptions) { - this.options = options; - this.setAriaRole(); + /** + * A context of the associated directive + */ + protected get ctx(): iBlock['unsafe'] { + return Object.cast(this.params.vnode.fakeContext); + } + + /** + * @param params - parameters passed from the associated directive + */ + constructor(params: DirectiveOptions) { + this.params = params; + this.createRole(); this.init(); } /** - * Initiates the base logic of the directive + * Initializes the adapter according to the associated directive parameters */ init(): void { - this.setAriaLabelledBy(); - this.setAriaAttributes(); - this.addEventHandlers(); - + this.setAttributes(); + this.attachRoleHandlers(); this.role?.init(); } /** - * Runs on unbind directive hook. Clears the Async instance. + * Destroys the adapter and cancels all tied threads */ destroy(): void { - this.async.clearAll(); - } - - protected get ctx(): iBlock['unsafe'] { - return Object.cast(this.options.vnode.fakeContext); + this.async.clearAll().locked = true; } /** - * If the role was passed as a directive argument sets specified engine + * Creates an ARIA role based on the associated directive parameters. + * If the role is not explicitly specified in the parameters, then the common strategy will be used. + * The method returns an instance of the created role or null. */ - protected setAriaRole(): CanUndef { + protected createRole(): ARIARole | null { const - {el, binding} = this.options, + {el, binding} = this.params, {value, modifiers, arg: role} = binding; if (role == null) { - return; + return null; } - const - engine = `${role.capitalize()}Engine`; - - const options: EngineOptions = { + const params: EngineOptions = { el, modifiers, params: value, @@ -81,107 +84,96 @@ export default class AriaAdapter { async: this.async }; - this.role = new roles[engine](options); + return this.role = new roles[role.capitalize()](params); } /** - * Sets aria-labelledby attribute to the element from directive parameters + * Sets the ARIA attributes passed in the associated directive parameters */ - protected setAriaLabelledBy(): void { + protected setAttributes(): void { const - {binding, el} = this.options, - {dom} = this.ctx, - {labelledby} = binding.value ?? {}, - attr = 'aria-labelledby'; - - let - isAttrSet = false; + {binding} = this.params; for (const mod in binding.modifiers) { if (!mod.startsWith('#')) { continue; } - const - title = mod.slice(1), - id = dom.getId(title); - - el.setAttribute(attr, id); - isAttrSet = true; + this.setAttr('aria-labelledby', this.ctx.dom.getId(mod.slice(1))); + break; } - if (labelledby != null) { - el.setAttribute(attr, Object.isArray(labelledby) ? labelledby.join(' ') : labelledby); - isAttrSet = true; - } + if (Object.isDictionary(binding.value)) { + Object.forEach(binding.value, (param, key) => { + const + roleParams = this.role?.Params; - if (isAttrSet) { - this.async.worker(() => el.removeAttribute(attr)); + if (param == null || roleParams?.hasOwnProperty(key)) { + return; + } + + this.setAttr(`aria-${key}`.dasherize(), param); + }); } } /** - * Sets aria attributes from passed params except `aria-labelledby` + * Sets the value of the specified attribute to the node on which the ARIA directive is initialized + * + * @param name - the attribute name + * @param value - the attribute value or a list of values */ - protected setAriaAttributes(): void { + protected setAttr(name: string, value: CanUndef>): void { const - {el, binding} = this.options, - params: Dictionary = binding.value; + {el} = this.params; - for (const key in params) { - if (!params.hasOwnProperty(key)) { - continue; - } - - const - roleParams = this.role?.Params, - param = params[key]; - - if (!roleParams?.hasOwnProperty(key) && key !== 'labelledby' && param != null) { - const - attr = `aria-${key}`; - - el.setAttribute(attr, param); - this.async.worker(() => el.removeAttribute(attr)); - } + if (value == null) { + return; } + + el.setAttribute(name, Object.isArray(value) ? value.join(' ') : String(value)); + this.async.worker(() => el.removeAttribute(name)); } /** - * Sets handlers for the base role events: open, close, change. - * Expects the passed into directive specified event properties to be Function, Promise or String. + * Attaches the handlers specified in the parameters of the associated directive to the created ARIA role. + * Any handler can be specified as a function, a promise, or a string. + * + * To specify a handler, use the `@` character at the beginning of the parameter key. + * For example, `@open` will be converted to the role `onOpen` handler. */ - protected addEventHandlers(): void { - if (this.role == null) { + protected attachRoleHandlers(): void { + const + {role, params: {binding}} = this; + + if (role == null || !Object.isDictionary(binding.value)) { return; } - const - params = this.options.binding.value; + Object.forEach(binding.value, (val, key) => { + if (val == null || !key.startsWith('@')) { + return; + } - for (const key in params) { - if (key.startsWith('@')) { - const - callbackName = `on-${key.slice(1)}`.camelize(false); + const + handlerName = `on-${key.slice(1)}`.camelize(false); - if (!Object.isFunction(this.role[callbackName])) { - Object.throw('Aria role engine does not contains event handler for passed event name or the type of engine\'s property is not a function'); - } + if (!Object.isFunction(role[handlerName])) { + throw new ReferenceError(`The associated ARIA role does not have a handler named "${handlerName}"`); + } - const - callback = this.role[callbackName].bind(this.role), - property = params[key]; + const + handler = role[handlerName].bind(this.role); - if (Object.isFunction(property)) { - property(callback); + if (Object.isFunction(val)) { + val(handler); - } else if (Object.isPromiseLike(property)) { - void property.then(callback); + } else if (Object.isPromiseLike(val)) { + val.then(handler, stderr); - } else if (Object.isString(property)) { - this.ctx.on(property, callback); - } + } else if (Object.isString(val)) { + this.async.on(this.ctx, val, handler); } - } + }); } } From 8fe6b8b4841acd0bc6a980ee002b5a264066159a Mon Sep 17 00:00:00 2001 From: kobezzza Date: Wed, 14 Sep 2022 14:59:02 +0300 Subject: [PATCH 154/185] chore: simple stylish fixes --- src/core/component/directives/aria/index.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/core/component/directives/aria/index.ts b/src/core/component/directives/aria/index.ts index e50f94f653..84a524b473 100644 --- a/src/core/component/directives/aria/index.ts +++ b/src/core/component/directives/aria/index.ts @@ -12,12 +12,12 @@ */ import { ComponentEngine, VNode, VNodeDirective } from 'core/component/engines'; -import AriaAdapter from 'core/component/directives/aria/adapter'; +import ARIAAdapter from 'core/component/directives/aria/adapter'; export * from 'core/component/directives/aria/interface'; const - ariaInstances = new WeakMap(); + ariaInstances = new WeakMap(); ComponentEngine.directive('aria', { inserted(el: HTMLElement, binding: VNodeDirective, vnode: VNode): void { @@ -28,7 +28,7 @@ ComponentEngine.directive('aria', { return; } - ariaInstances.set(el, new AriaAdapter({el, binding, vnode})); + ariaInstances.set(el, new ARIAAdapter({el, binding, vnode})); }, unbind(el: HTMLElement) { From 55e423c8618b8eb5a41c9d232cfbd88c2186177c Mon Sep 17 00:00:00 2001 From: kobezzza Date: Wed, 14 Sep 2022 15:07:32 +0300 Subject: [PATCH 155/185] refactor: renamed `setAttr` to `setAttribute` --- src/core/component/directives/aria/adapter.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/core/component/directives/aria/adapter.ts b/src/core/component/directives/aria/adapter.ts index 7303303b2c..bebe5f71a7 100644 --- a/src/core/component/directives/aria/adapter.ts +++ b/src/core/component/directives/aria/adapter.ts @@ -99,7 +99,7 @@ export default class ARIAAdapter { continue; } - this.setAttr('aria-labelledby', this.ctx.dom.getId(mod.slice(1))); + this.setAttribute('aria-labelledby', this.ctx.dom.getId(mod.slice(1))); break; } @@ -112,7 +112,7 @@ export default class ARIAAdapter { return; } - this.setAttr(`aria-${key}`.dasherize(), param); + this.setAttribute(`aria-${key}`.dasherize(), param); }); } } @@ -123,7 +123,7 @@ export default class ARIAAdapter { * @param name - the attribute name * @param value - the attribute value or a list of values */ - protected setAttr(name: string, value: CanUndef>): void { + protected setAttribute(name: string, value: CanUndef>): void { const {el} = this.params; From 88ebae0c6c04499aba04fa98ad3393f5d1aed763 Mon Sep 17 00:00:00 2001 From: kobezzza Date: Wed, 14 Sep 2022 15:09:10 +0300 Subject: [PATCH 156/185] chore: simple stylish fixes --- src/core/component/directives/aria/adapter.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/component/directives/aria/adapter.ts b/src/core/component/directives/aria/adapter.ts index bebe5f71a7..26d7e20506 100644 --- a/src/core/component/directives/aria/adapter.ts +++ b/src/core/component/directives/aria/adapter.ts @@ -118,7 +118,7 @@ export default class ARIAAdapter { } /** - * Sets the value of the specified attribute to the node on which the ARIA directive is initialized + * Sets a new value of the specified attribute to the node on which the ARIA directive is initialized * * @param name - the attribute name * @param value - the attribute value or a list of values From bf89ab46d35f9420289723c1c87ddea15e2ad3fc Mon Sep 17 00:00:00 2001 From: kobezzza Date: Wed, 14 Sep 2022 16:16:55 +0300 Subject: [PATCH 157/185] refactor: renamed `EngineOptions` to `RoleOptions`; stylish refactoring --- src/core/component/directives/aria/adapter.ts | 4 +- .../directives/aria/roles/interface.ts | 48 +++++++++++-------- 2 files changed, 30 insertions(+), 22 deletions(-) diff --git a/src/core/component/directives/aria/adapter.ts b/src/core/component/directives/aria/adapter.ts index 26d7e20506..014af1812f 100644 --- a/src/core/component/directives/aria/adapter.ts +++ b/src/core/component/directives/aria/adapter.ts @@ -11,7 +11,7 @@ import type iBlock from 'super/i-block/i-block'; import * as roles from 'core/component/directives/aria/roles'; import type { DirectiveOptions } from 'core/component/directives/aria/interface'; -import type { ARIARole, EngineOptions } from 'core/component/directives/aria/roles'; +import type { ARIARole, RoleOptions } from 'core/component/directives/aria/roles'; /** * An adapter to create an ARIA role instance based on the passed directive params and to add common attributes @@ -76,7 +76,7 @@ export default class ARIAAdapter { return null; } - const params: EngineOptions = { + const params: RoleOptions = { el, modifiers, params: value, diff --git a/src/core/component/directives/aria/roles/interface.ts b/src/core/component/directives/aria/roles/interface.ts index 8c9eda8e2e..5b3f1eb883 100644 --- a/src/core/component/directives/aria/roles/interface.ts +++ b/src/core/component/directives/aria/roles/interface.ts @@ -7,43 +7,43 @@ */ import type Async from 'core/async'; -import type { ComponentInterface } from 'super/i-block/i-block'; +import type { ComponentInterface } from 'core/component'; export abstract class ARIARole { /** - * Type: directive passed params + * Type: parameters passed from the associated directive */ readonly Params!: AbstractParams; /** - * Type: component on which the directive is set + * Type: a component context within which the associated directive is used */ readonly Ctx!: ComponentInterface; /** - * Element on which the directive is set + * An element to which the associated directive is applied */ - readonly el: HTMLElement; + readonly el: Element; /** - * Component on which the directive is set + * A component context within which the associated directive is used */ readonly ctx?: this['Ctx']; /** - * Directive passed modifiers + * Parameters passed from the associated directive */ - readonly modifiers?: Dictionary; + readonly params: this['Params']; /** - * Directive passed params + * Modifiers passed from the associated directive */ - readonly params: this['Params']; + readonly modifiers?: Dictionary; /** @see [[Async]] */ - async: Async; + protected async: Async; - constructor({el, ctx, modifiers, params, async}: EngineOptions) { + constructor({el, ctx, modifiers, params, async}: RoleOptions) { this.el = el; this.ctx = ctx; this.modifiers = modifiers; @@ -52,26 +52,34 @@ export abstract class ARIARole { } /** - * Sets base aria attributes for current role + * Initializes the role */ abstract init(): void; /** - * Sets aria attributes and the `Async` destructor + * Sets a new value of the specified attribute to the passed element + * + * @param name - the attribute name + * @param value - the attribute value or a list of values + * @param [el] - the element to set an attribute */ - setAttribute(attr: string, value: string, el: Element = this.el): void { - el.setAttribute(attr, value); - this.async.worker(() => el.removeAttribute(attr)); + protected setAttribute(name: string, value: CanUndef>, el: Element = this.el): void { + if (value == null) { + return; + } + + el.setAttribute(name, Object.isArray(value) ? value.join(' ') : String(value)); + this.async.worker(() => el.removeAttribute(name)); } } interface AbstractParams {} -export interface EngineOptions

      { - el: HTMLElement; +export interface RoleOptions

      { + el: Element; ctx?: C; - modifiers?: Dictionary; params: P; + modifiers?: Dictionary; async: Async; } From 5faa85c8f0d84804e59aef1f109912c8611c4686 Mon Sep 17 00:00:00 2001 From: kobezzza Date: Wed, 14 Sep 2022 16:17:32 +0300 Subject: [PATCH 158/185] doc: added info about role handlers --- src/core/component/directives/aria/README.md | 43 ++++++++++++ .../component/directives/aria/roles/README.md | 66 ++++++++++++++----- 2 files changed, 91 insertions(+), 18 deletions(-) diff --git a/src/core/component/directives/aria/README.md b/src/core/component/directives/aria/README.md index 78d0ed91cc..1c4a1424c5 100644 --- a/src/core/component/directives/aria/README.md +++ b/src/core/component/directives/aria/README.md @@ -101,6 +101,49 @@ See [this](https://www.w3.org/TR/wai-aria/#aria-describedby) for more informatio Items in the trash will be permanently removed after 30 days. ``` +### Adding handlers for a role + +Some roles need to handle component state changes or respond to certain events (add, remove, or change certain attributes). +Such handlers are specified as regular directive parameters, but with the special character `@` added to the beginning of the parameter names. +The value of such parameters can be different types of data. + +#### Listening a component event + +If you pass a string as the value of a handler parameter, then that string will be treated as the name of the component event. +When such an event fires, the corresponding role method will be called, where the `@` symbol is replaced with `on` and +everything is translated into camelCase. For instance, the `@change` key means calling the `onChange` method on the role. +The called method will receive the event parameters as arguments. + +``` +< div v-aria:somerole = {'@change': 'myComponentEvent'} +``` + +#### Handling a promise + +If you pass a promise as the value of a handler parameter, then when this promise is resolved, the corresponding role method +will be called, where the `@` character is replaced with `on`, and everything is converted to camelCase. For instance, +the `@change` key means calling the `onChange` method on the role. The called method will receive the unwrapped promise value +as an argument. + +``` +< div v-aria:somerole = {'@change': myComponentPromise} +``` + +#### Providing a callback + +If you pass a function, it will be called immediately and will receive as a parameter a reference to the corresponding role method, +where the `@` character is replaced with `on` and everything is converted to camelCase. For instance, +the `@change` key means calling the `onChange` method on the role. This approach is the most flexible, because allows you +to register any handlers in this way. + +``` +< div v-aria:somerole = {'@change': (cb) => on('event', cb)} + +// Similar to + +< div v-aria:somerole = {'@change': 'event'} +``` + ## Modifiers ### `#` diff --git a/src/core/component/directives/aria/roles/README.md b/src/core/component/directives/aria/roles/README.md index 3dd4966a8c..ce9f63bec9 100644 --- a/src/core/component/directives/aria/roles/README.md +++ b/src/core/component/directives/aria/roles/README.md @@ -1,33 +1,63 @@ -# core/component/directives/aria/roles/combobox +# core/component/directives/aria/roles -This module provides engines for `v-aria` directive. +This module re-exports implementations of various ARIA roles for the `v-aria` directive, +and also provides a basic set of types and interfaces for adding new roles. -## API +## List of supported roles -Some roles need to handle components state changes or react to some events (add, delete or change certain attributes). -The fields in directive passed options which name starts with `@` respond for this (ex. `@change`, `@open`). -The certain contract should be followed: -the name of the callback, which should be 'connected' with such field should start with `on` and be named in camelCase style (ex. `onChange`, `onOpen`). +Each role is named after the appropriate name from the ARIA specification. +Each role can accept its own set of options, which are described in its documentation. -Directive supports this field type to be function, promise or string (type [`HandlerAttachment`](`core/component/directives/aria/roles/interface.ts`)). -- Function: -expects a callback to be passed. -In this function callback could be added as a listener to certain component events or provide to the callback some component's state. +* [Combobox](https://www.w3.org/TR/wai-aria/#combobox) +* [Dialog](https://www.w3.org/TR/wai-aria/#dialog) +* [Listbox](https://www.w3.org/TR/wai-aria/#listbox) +* [Option](https://www.w3.org/TR/wai-aria/#option) +* [Tab](https://www.w3.org/TR/wai-aria/#tab) +* [Tablist](https://www.w3.org/TR/wai-aria/#tablist) +* [Tabpanel](https://www.w3.org/TR/wai-aria/#tabpanel) +* [Tree](https://www.w3.org/TR/wai-aria/#tree) +* [Treeitem](https://www.w3.org/TR/wai-aria/#treeitem) +* [Controls](https://www.w3.org/TR/wai-aria/#aria-controls) + +## Adding handlers for a role + +Some roles need to handle component state changes or respond to certain events (add, remove, or change certain attributes). +Such handlers are specified as regular directive parameters, but with the special character `@` added to the beginning of the parameter names. +The value of such parameters can be different types of data. + +### Listening a component event + +If you pass a string as the value of a handler parameter, then that string will be treated as the name of the component event. +When such an event fires, the corresponding role method will be called, where the `@` symbol is replaced with `on` and +everything is translated into camelCase. For instance, the `@change` key means calling the `onChange` method on the role. +The called method will receive the event parameters as arguments. ``` -< div v-aria:somerole = {'@change': (cb) => on('event', cb)} +< div v-aria:somerole = {'@change': 'myComponentEvent'} ``` -- Promise: -If the field is a `Promise` or a `PromiseLike` object the callback would be passed to `then`. +### Handling a promise -- String: -If the field is a `string`, the callback would be added as a listener to component's event similar to the string. +If you pass a promise as the value of a handler parameter, then when this promise is resolved, the corresponding role method +will be called, where the `@` character is replaced with `on`, and everything is converted to camelCase. For instance, +the `@change` key means calling the `onChange` method on the role. The called method will receive the unwrapped promise value +as an argument. ``` -< div v-aria:somerole = {'@change': 'event'} +< div v-aria:somerole = {'@change': myComponentPromise} +``` + +### Providing a callback -// the same as +If you pass a function, it will be called immediately and will receive as a parameter a reference to the corresponding role method, +where the `@` character is replaced with `on` and everything is converted to camelCase. For instance, +the `@change` key means calling the `onChange` method on the role. This approach is the most flexible, because allows you +to register any handlers in this way. +``` < div v-aria:somerole = {'@change': (cb) => on('event', cb)} + +// Similar to + +< div v-aria:somerole = {'@change': 'event'} ``` From da47fc6693666cd78e5803362c89c282d1ee69d3 Mon Sep 17 00:00:00 2001 From: kobezzza Date: Wed, 14 Sep 2022 16:17:52 +0300 Subject: [PATCH 159/185] chore: added doc header --- src/core/component/directives/aria/roles/index.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/core/component/directives/aria/roles/index.ts b/src/core/component/directives/aria/roles/index.ts index c25539a631..09323dfc6a 100644 --- a/src/core/component/directives/aria/roles/index.ts +++ b/src/core/component/directives/aria/roles/index.ts @@ -6,6 +6,11 @@ * https://github.com/V4Fire/Client/blob/master/LICENSE */ +/** + * [[include:core/component/directives/aria/roles/README.md]] + * @packageDocumentation + */ + export * from 'core/component/directives/aria/roles/dialog'; export * from 'core/component/directives/aria/roles/tablist'; export * from 'core/component/directives/aria/roles/tab'; @@ -17,4 +22,4 @@ export * from 'core/component/directives/aria/roles/option'; export * from 'core/component/directives/aria/roles/tree'; export * from 'core/component/directives/aria/roles/treeitem'; -export { ARIARole, EngineOptions } from 'core/component/directives/aria/roles/interface'; +export { ARIARole, RoleOptions } from 'core/component/directives/aria/roles/interface'; From 3250bdfc5b5a5e052edb2ad8c864e72744963dac Mon Sep 17 00:00:00 2001 From: kobezzza Date: Wed, 14 Sep 2022 16:18:02 +0300 Subject: [PATCH 160/185] refactor: fixed import --- src/core/component/directives/aria/roles/combobox/index.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/core/component/directives/aria/roles/combobox/index.ts b/src/core/component/directives/aria/roles/combobox/index.ts index c2a2f31605..4a1d3ca4b9 100644 --- a/src/core/component/directives/aria/roles/combobox/index.ts +++ b/src/core/component/directives/aria/roles/combobox/index.ts @@ -10,14 +10,14 @@ import type iAccess from 'traits/i-access/i-access'; import type { ComponentInterface } from 'super/i-block/i-block'; import { ComboboxParams } from 'core/component/directives/aria/roles/combobox/interface'; -import { ARIARole, EngineOptions } from 'core/component/directives/aria/roles/interface'; +import { ARIARole, RoleOptions } from 'core/component/directives/aria/roles/interface'; export class Combobox extends ARIARole { override Params: ComboboxParams = new ComboboxParams(); override Ctx!: ComponentInterface & iAccess; override el: HTMLElement; - constructor(options: EngineOptions) { + constructor(options: RoleOptions) { super(options); const From 48986c68488275b3c91b7aca7aa0822b7498ca24 Mon Sep 17 00:00:00 2001 From: kobezzza Date: Wed, 14 Sep 2022 16:27:47 +0300 Subject: [PATCH 161/185] refactor: replaced `Element` to `AccessibleElement` --- src/core/component/directives/aria/roles/interface.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/core/component/directives/aria/roles/interface.ts b/src/core/component/directives/aria/roles/interface.ts index 5b3f1eb883..df6bf9b13b 100644 --- a/src/core/component/directives/aria/roles/interface.ts +++ b/src/core/component/directives/aria/roles/interface.ts @@ -23,7 +23,7 @@ export abstract class ARIARole { /** * An element to which the associated directive is applied */ - readonly el: Element; + readonly el: AccessibleElement; /** * A component context within which the associated directive is used @@ -76,7 +76,7 @@ export abstract class ARIARole { interface AbstractParams {} export interface RoleOptions

      { - el: Element; + el: AccessibleElement; ctx?: C; params: P; modifiers?: Dictionary; From 957828a2f4216f8998f467cd81b41ca311818439 Mon Sep 17 00:00:00 2001 From: kobezzza Date: Wed, 14 Sep 2022 17:52:11 +0300 Subject: [PATCH 162/185] doc: improved doc --- .../directives/aria/roles/tab/README.md | 88 +++++++++---------- .../directives/aria/roles/tab/index.ts | 39 ++++---- 2 files changed, 62 insertions(+), 65 deletions(-) diff --git a/src/core/component/directives/aria/roles/tab/README.md b/src/core/component/directives/aria/roles/tab/README.md index 2f393e551e..8bc21fbac9 100644 --- a/src/core/component/directives/aria/roles/tab/README.md +++ b/src/core/component/directives/aria/roles/tab/README.md @@ -1,63 +1,55 @@ # core/component/directives/aria/roles/tab -This module provides an engine for `v-aria` directive. +This module provides an implementation of the ARIA [tab](https://www.w3.org/TR/wai-aria/#tab) role. +An element with this role should be used in conjunction with elements with the roles [tablist](https://www.w3.org/TR/wai-aria/#tablist) and [tabpanel](https://www.w3 .org/TR /wai-aria/#tabpanel) +The role expects the component within which the directive is used to implement the [[iAccess]] characteristic. -The engine to set `tab` role attribute. -The ARIA tab role indicates an interactive element inside a `tablist` that, when activated, displays its associated `tabpanel`. +For more information see [this](`https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/tab_role`). -For more information go to [tab](`https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/tab_role`). -For recommendations how to make accessible widget go to [tab](`https://www.w3.org/WAI/ARIA/apg/patterns/tabpanel/`). +``` +< div v-aria:tablist + < template v-for = (tab, i) of tabs + < div :id = 'tab-' + i | v-aria:tab = { & + controls: 'content-' + i, + + isFirst: i === 0, + isSelected: tab.active, + hasDefaultSelectedTabs: tab.some((tab) => Boolean(tab.active)), -## API + '@change': (cb) => cb(tab.active) + } . + +< template v-for = (content, i) of tabsContent + < div :id = 'content-' + i | v-aria:tabpanel = {labelledby: 'tab-' + i} + {{ content }} +``` -The engine expects specific parameters to be passed. -- `isFirst`: `boolean`. -If true current tab is the first one in the list of tabs. -- `isSelected`: `boolean`. -If true current tab is active. -- `hasDefaultSelectedTabs`: `boolean`. -If true there are active tabs in the tablist widget by default. -- `orientation`: `string`. -The tablist widget view orientation. -- `@change`:`HandlerAttachment`, see `core/component/directives/aria/roles/README.md`. -Internal callback `onChange` expects an `Element` or `NodeListOf` to be passed. +## Available options -In addition, tabs expect the `controls` role engine to be added. An id passed to `controls` engine should be the id of the element with role `tabpanel`. +Any ARIA attributes could be added in options through the short syntax. -Example: ``` -< button v-aria:tab | v-aria:controls = {for: 'id1'} +< div v-aria = {label: 'foo', desribedby: 'id1', details: 'id2'} -< v-aria:tabpanel = {labelledby: 'id2'} | :id = 'id1' - < span :id = 'id2' - // content +/// The same as + +< div :aria-label = 'foo' | :aria-desribedby = 'id1' | :aria-details = 'id2' ``` -The engine expects the component to realize`iAccess` trait. +Also, the role introduces several additional settings. -## Usage +### [isFirst = `false`] -Example of passing parameters: -``` -< div v-aria:tab = { & - isFirst: i === 0, - isSelected: el.active, - hasDefaultSelectedTabs: items.some((el) => !!el.active), - orientation: orientation, - '@change': (cb) => cb(el.active) - } -. -``` +Whether the tab is the first in the tablist. -Example of external usage: -``` -< div & - id = 'tab-1' | - v-aria:tab = {...config} | - v-aria:controls = {for: 'tabpanel-1'} - Tab - -< div id = 'tabpanel-1' | v-aria:tabpanel = {labelledby: 'tab-1'} - < p - Content for the panel -``` +### [isSelected = `false`] + +Whether the tab is selected. + +### [hasDefaultSelectedTabs = `false`] + +Whether there is at least one selected tab by default. + +### [@change] + +A handler for changing the active tab. diff --git a/src/core/component/directives/aria/roles/tab/index.ts b/src/core/component/directives/aria/roles/tab/index.ts index 457a16a9e5..6afa68406e 100644 --- a/src/core/component/directives/aria/roles/tab/index.ts +++ b/src/core/component/directives/aria/roles/tab/index.ts @@ -9,6 +9,11 @@ * Copyright © [2022] W3C® (MIT, ERCIM, Keio, Beihang). */ +/** + * [[include:core/component/directives/aria/roles/tab/README.md]] + * @packageDocumentation + */ + import type iBlock from 'super/i-block/i-block'; import type iAccess from 'traits/i-access/i-access'; @@ -21,9 +26,14 @@ export class Tab extends ARIARole { /** @inheritDoc */ init(): void { - const - {el} = this, - {isFirst, isSelected, hasDefaultSelectedTabs} = this.params; + const { + el, + params: { + isFirst, + isSelected, + hasDefaultSelectedTabs + } + } = this; this.setAttribute('role', 'tab'); this.setAttribute('aria-selected', String(isSelected)); @@ -46,17 +56,15 @@ export class Tab extends ARIARole { } /** - * Moves focus to the first tab in tablist + * Moves focus to the first tab in the tablist */ protected moveFocusToFirstTab(): void { - const - firstTab = this.ctx?.findFocusableElement(); - + const firstTab = this.ctx?.findFocusableElement(); firstTab?.focus(); } /** - * Moves focus to the last tab in tablist + * Moves focus to the last tab in the tablist */ protected moveFocusToLastTab(): void { const @@ -77,18 +85,16 @@ export class Tab extends ARIARole { } /** - * Moves focus to the next or previous focusable element via the step parameter + * Moves focus to the next or previous focusable element, according to the step parameter * @param step */ protected moveFocus(step: 1 | -1): void { - const - focusable = this.ctx?.getNextFocusableElement(step); - + const focusable = this.ctx?.getNextFocusableElement(step); focusable?.focus(); } /** - * Handler: active tab changes + * Handler: the active tab has been changed * @param active */ protected onChange(active: Element | NodeListOf): void { @@ -109,14 +115,13 @@ export class Tab extends ARIARole { } /** - * Handler: keyboard event + * Handler: a keyboard event has occurred */ - protected onKeydown(e: Event): void { + protected onKeydown(e: KeyboardEvent): void { const - evt = (e), isVertical = this.params.orientation === 'vertical'; - switch (evt.key) { + switch (e.key) { case KeyCodes.LEFT: if (isVertical) { return; From bc37cfcfb455ec5b09d919de352ffbe02a36bd53 Mon Sep 17 00:00:00 2001 From: kobezzza Date: Wed, 14 Sep 2022 17:53:45 +0300 Subject: [PATCH 163/185] fix: parameters should be optional --- src/core/component/directives/aria/roles/interface.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/core/component/directives/aria/roles/interface.ts b/src/core/component/directives/aria/roles/interface.ts index df6bf9b13b..d3aaae78b0 100644 --- a/src/core/component/directives/aria/roles/interface.ts +++ b/src/core/component/directives/aria/roles/interface.ts @@ -43,7 +43,7 @@ export abstract class ARIARole { /** @see [[Async]] */ protected async: Async; - constructor({el, ctx, modifiers, params, async}: RoleOptions) { + constructor({el, ctx, modifiers, params = {}, async}: RoleOptions) { this.el = el; this.ctx = ctx; this.modifiers = modifiers; @@ -78,7 +78,7 @@ interface AbstractParams {} export interface RoleOptions

      { el: AccessibleElement; ctx?: C; - params: P; + params?: P; modifiers?: Dictionary; async: Async; } From 46a8b9a0f713e6f2a404230b83268f8a2b2d292f Mon Sep 17 00:00:00 2001 From: kobezzza Date: Wed, 14 Sep 2022 17:54:27 +0300 Subject: [PATCH 164/185] chore: small stylish fixes --- src/core/component/directives/aria/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/component/directives/aria/README.md b/src/core/component/directives/aria/README.md index 1c4a1424c5..65eac0d48a 100644 --- a/src/core/component/directives/aria/README.md +++ b/src/core/component/directives/aria/README.md @@ -57,7 +57,7 @@ Any ARIA attributes could be added in options through the short syntax. < div :aria-label = 'foo' | :aria-desribedby = 'id1' | :aria-details = 'id2' ``` -The most common are described below: +The most common are described below. ### [label] From 95fd8d6e7d12f2462e8c35f4e1c46abdfd95e20f Mon Sep 17 00:00:00 2001 From: kobezzza Date: Wed, 14 Sep 2022 23:52:34 +0300 Subject: [PATCH 165/185] refactor: renamed `setAttributes` to `setRoleAttributes` --- src/core/component/directives/aria/adapter.ts | 40 +++++++++---------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/src/core/component/directives/aria/adapter.ts b/src/core/component/directives/aria/adapter.ts index 014af1812f..c76a85ca47 100644 --- a/src/core/component/directives/aria/adapter.ts +++ b/src/core/component/directives/aria/adapter.ts @@ -50,7 +50,7 @@ export default class ARIAAdapter { * Initializes the adapter according to the associated directive parameters */ init(): void { - this.setAttributes(); + this.setRoleAttributes(); this.attachRoleHandlers(); this.role?.init(); } @@ -90,7 +90,7 @@ export default class ARIAAdapter { /** * Sets the ARIA attributes passed in the associated directive parameters */ - protected setAttributes(): void { + protected setRoleAttributes(): void { const {binding} = this.params; @@ -117,24 +117,6 @@ export default class ARIAAdapter { } } - /** - * Sets a new value of the specified attribute to the node on which the ARIA directive is initialized - * - * @param name - the attribute name - * @param value - the attribute value or a list of values - */ - protected setAttribute(name: string, value: CanUndef>): void { - const - {el} = this.params; - - if (value == null) { - return; - } - - el.setAttribute(name, Object.isArray(value) ? value.join(' ') : String(value)); - this.async.worker(() => el.removeAttribute(name)); - } - /** * Attaches the handlers specified in the parameters of the associated directive to the created ARIA role. * Any handler can be specified as a function, a promise, or a string. @@ -176,4 +158,22 @@ export default class ARIAAdapter { } }); } + + /** + * Sets a new value of the specified attribute to the node on which the ARIA directive is initialized + * + * @param name - the attribute name + * @param value - the attribute value or a list of values + */ + protected setAttribute(name: string, value: CanUndef>): void { + const + {el} = this.params; + + if (value == null) { + return; + } + + el.setAttribute(name, Object.isArray(value) ? value.join(' ') : String(value)); + this.async.worker(() => el.removeAttribute(name)); + } } From 04b5138370463508a97fa3e2af746af39174b7c5 Mon Sep 17 00:00:00 2001 From: kobezzza Date: Wed, 14 Sep 2022 23:55:54 +0300 Subject: [PATCH 166/185] fix: should support the `role` attribute --- src/core/component/directives/aria/adapter.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/component/directives/aria/adapter.ts b/src/core/component/directives/aria/adapter.ts index c76a85ca47..1f38c9c79a 100644 --- a/src/core/component/directives/aria/adapter.ts +++ b/src/core/component/directives/aria/adapter.ts @@ -112,7 +112,7 @@ export default class ARIAAdapter { return; } - this.setAttribute(`aria-${key}`.dasherize(), param); + this.setAttribute(key === 'role' ? key : `aria-${key}`.dasherize(), param); }); } } From 2c3413772cf375568e202d90482c002caa4da26d Mon Sep 17 00:00:00 2001 From: kobezzza Date: Wed, 14 Sep 2022 23:56:13 +0300 Subject: [PATCH 167/185] fix: should wrap promises and handlers with async --- src/core/component/directives/aria/adapter.ts | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/core/component/directives/aria/adapter.ts b/src/core/component/directives/aria/adapter.ts index 1f38c9c79a..d300a9d8c4 100644 --- a/src/core/component/directives/aria/adapter.ts +++ b/src/core/component/directives/aria/adapter.ts @@ -125,8 +125,11 @@ export default class ARIAAdapter { * For example, `@open` will be converted to the role `onOpen` handler. */ protected attachRoleHandlers(): void { - const - {role, params: {binding}} = this; + const { + role, + async: $a, + params: {binding} + } = this; if (role == null || !Object.isDictionary(binding.value)) { return; @@ -148,13 +151,13 @@ export default class ARIAAdapter { handler = role[handlerName].bind(this.role); if (Object.isFunction(val)) { - val(handler); + val($a.proxy(handler)); } else if (Object.isPromiseLike(val)) { - val.then(handler, stderr); + $a.promise(val).then(handler, stderr); } else if (Object.isString(val)) { - this.async.on(this.ctx, val, handler); + $a.on(this.ctx, val, handler); } }); } From 50ce8f91f4087656a5ac7d89f41e626eaafeb7de Mon Sep 17 00:00:00 2001 From: Aleksandr Bunin Date: Sun, 18 Sep 2022 14:57:30 +0300 Subject: [PATCH 168/185] refactoring params --- src/base/b-list/b-list.ts | 14 ++----- src/base/b-tree/b-tree.ts | 8 ++-- src/core/component/directives/aria/adapter.ts | 22 +++++------ .../directives/aria/roles/combobox/README.md | 25 ++++++++---- .../directives/aria/roles/combobox/index.ts | 10 ++--- .../aria/roles/combobox/interface.ts | 8 +++- .../aria/roles/combobox/test/unit/combobox.ts | 10 ++++- .../directives/aria/roles/dialog/README.md | 4 -- .../directives/aria/roles/dialog/index.ts | 5 --- .../directives/aria/roles/option/README.md | 16 +++++--- .../directives/aria/roles/option/index.ts | 1 - .../directives/aria/roles/option/interface.ts | 6 ++- .../directives/aria/roles/tab/README.md | 12 ++++-- .../directives/aria/roles/tab/index.ts | 26 ++++++++----- .../directives/aria/roles/tab/interface.ts | 13 +++++-- .../directives/aria/roles/tablist/README.md | 16 +++++--- .../directives/aria/roles/tablist/index.ts | 15 +------- .../aria/roles/tablist/interface.ts | 6 +-- .../directives/aria/roles/tabpanel/index.ts | 4 +- .../aria/roles/tabpanel/test/unit/tabpanel.ts | 10 +---- .../directives/aria/roles/tree/README.md | 24 +++++++----- .../directives/aria/roles/tree/index.ts | 6 +-- .../directives/aria/roles/tree/interface.ts | 4 +- .../directives/aria/roles/treeitem/README.md | 36 ++++++++++++------ .../directives/aria/roles/treeitem/index.ts | 38 +++++++++---------- .../aria/roles/treeitem/interface.ts | 7 ++-- src/form/b-select/b-select.ts | 4 +- 27 files changed, 188 insertions(+), 162 deletions(-) diff --git a/src/base/b-list/b-list.ts b/src/base/b-list/b-list.ts index cb19dbde9f..5da5e10983 100644 --- a/src/base/b-list/b-list.ts +++ b/src/base/b-list/b-list.ts @@ -715,23 +715,15 @@ class bList extends iData implements iVisible, iWidth, iItems, iAccess { protected getAriaConfig(role: 'tab', item: this['Item'], i: number): Dictionary; protected getAriaConfig(role: 'tab' | 'tablist', item?: this['Item'], i?: number): Dictionary { - const - isActive = this.isActive.bind(this, item?.value); - const tablistConfig = { - isMultiple: this.multiple, + multiselectable: this.multiple, orientation: this.orientation }; const tabConfig = { - orientation: this.orientation, - - isFirst: i === 0, + first: i === 0, hasDefaultSelectedTabs: this.active != null, - - get isSelected() { - return isActive(); - }, + selected: this.isActive(item?.value), '@change': bindChangeEvent.bind(this) }; diff --git a/src/base/b-tree/b-tree.ts b/src/base/b-tree/b-tree.ts index bec9ae954d..551479cf39 100644 --- a/src/base/b-tree/b-tree.ts +++ b/src/base/b-tree/b-tree.ts @@ -277,7 +277,7 @@ class bTree extends iData implements iItems, iAccess { root = () => this.top?.$el ?? this.$el; const treeConfig = { - isRoot: this.top == null, + root: this.top == null, orientation: this.orientation, '@change': (cb: Function) => { this.on('fold', (ctx, el: Element, item, value: boolean) => cb(el, !value)); @@ -286,13 +286,13 @@ class bTree extends iData implements iItems, iAccess { const treeitemConfig = { orientation: this.orientation, - isFirstRootItem: this.top == null && i === 0, + firstRootItem: this.top == null && i === 0, - get isExpanded() { + get expanded() { return getFoldedMod() === 'false'; }, - get isExpandable() { + get expandable() { return item?.children != null; }, diff --git a/src/core/component/directives/aria/adapter.ts b/src/core/component/directives/aria/adapter.ts index 014af1812f..2ab348e53b 100644 --- a/src/core/component/directives/aria/adapter.ts +++ b/src/core/component/directives/aria/adapter.ts @@ -50,9 +50,9 @@ export default class ARIAAdapter { * Initializes the adapter according to the associated directive parameters */ init(): void { - this.setAttributes(); - this.attachRoleHandlers(); this.role?.init(); + this.attachRoleHandlers(); + this.setAttributes(); } /** @@ -99,7 +99,7 @@ export default class ARIAAdapter { continue; } - this.setAttribute('aria-labelledby', this.ctx.dom.getId(mod.slice(1))); + this.setAttribute('aria-labelledby', this.ctx.dom.getId(mod.slice(1)), this.role?.el); break; } @@ -112,7 +112,7 @@ export default class ARIAAdapter { return; } - this.setAttribute(`aria-${key}`.dasherize(), param); + this.setAttribute(`aria-${key}`.dasherize(), param, this.role?.el); }); } } @@ -122,11 +122,9 @@ export default class ARIAAdapter { * * @param name - the attribute name * @param value - the attribute value or a list of values + * @param [el] - the element to set an attribute */ - protected setAttribute(name: string, value: CanUndef>): void { - const - {el} = this.params; - + protected setAttribute(name: string, value: CanUndef>, el: Element = this.params.el): void { if (value == null) { return; } @@ -144,7 +142,7 @@ export default class ARIAAdapter { */ protected attachRoleHandlers(): void { const - {role, params: {binding}} = this; + {async, role, params: {binding}} = this; if (role == null || !Object.isDictionary(binding.value)) { return; @@ -166,13 +164,13 @@ export default class ARIAAdapter { handler = role[handlerName].bind(this.role); if (Object.isFunction(val)) { - val(handler); + val(async.proxy(handler, {single: false})); } else if (Object.isPromiseLike(val)) { - val.then(handler, stderr); + async.promise(val).then(handler, stderr); } else if (Object.isString(val)) { - this.async.on(this.ctx, val, handler); + async.on(this.ctx, val, handler); } }); } diff --git a/src/core/component/directives/aria/roles/combobox/README.md b/src/core/component/directives/aria/roles/combobox/README.md index c3b03ad2c7..5868058a06 100644 --- a/src/core/component/directives/aria/roles/combobox/README.md +++ b/src/core/component/directives/aria/roles/combobox/README.md @@ -10,14 +10,23 @@ For recommendations how to make accessible widget go to [combobox](`https://www. ## API -The engine expects specific parameters to be passed. -- `isMultiple`:`boolean`. -If true widget supports multiple selected options. -- `@change`:`HandlerAttachment`, see `core/component/directives/aria/roles/README.md`. -Internal callback `onChange` expects an `Element` to be passed. -- `@open`:`HandlerAttachment`. -Internal callback `onChange` expects an `Element` to be passed. -- `@close`:`HandlerAttachment`. +The role introduces several additional settings. + +### [multiselectable = `false`] + +Whether the widget supports a feature of multiple active options + +### [@change] + +A handler for changing the active option. + +### [@open] + +A handler for opening the option list. + +### [@close] + +A handler for closing the option list. ## Usage diff --git a/src/core/component/directives/aria/roles/combobox/index.ts b/src/core/component/directives/aria/roles/combobox/index.ts index 4a1d3ca4b9..4385b5ec51 100644 --- a/src/core/component/directives/aria/roles/combobox/index.ts +++ b/src/core/component/directives/aria/roles/combobox/index.ts @@ -6,7 +6,7 @@ * https://github.com/V4Fire/Client/blob/master/LICENSE */ -import type iAccess from 'traits/i-access/i-access'; +import iAccess from 'traits/i-access/i-access'; import type { ComponentInterface } from 'super/i-block/i-block'; import { ComboboxParams } from 'core/component/directives/aria/roles/combobox/interface'; @@ -20,6 +20,10 @@ export class Combobox extends ARIARole { constructor(options: RoleOptions) { super(options); + if (!iAccess.is(this.ctx)) { + Object.throw('Combobox aria directive expects the component to realize iAccess interface'); + } + const {el} = this; @@ -31,10 +35,6 @@ export class Combobox extends ARIARole { this.setAttribute('role', 'combobox'); this.setAttribute('aria-expanded', 'false'); - if (this.params.isMultiple) { - this.setAttribute('aria-multiselectable', 'true'); - } - if (this.el.tabIndex < 0) { this.setAttribute('tabindex', '0'); } diff --git a/src/core/component/directives/aria/roles/combobox/interface.ts b/src/core/component/directives/aria/roles/combobox/interface.ts index b83a6ce5c0..7b683c7307 100644 --- a/src/core/component/directives/aria/roles/combobox/interface.ts +++ b/src/core/component/directives/aria/roles/combobox/interface.ts @@ -10,8 +10,14 @@ import type { HandlerAttachment } from 'core/component/directives/aria/roles/int const defaultFn = (): void => undefined; +export interface ComboboxParams { + multiselectable: boolean; + '@change': HandlerAttachment; + '@open': HandlerAttachment; + '@close': HandlerAttachment; +} + export class ComboboxParams { - isMultiple: boolean = false; '@change': HandlerAttachment = defaultFn; '@open': HandlerAttachment = defaultFn; '@close': HandlerAttachment = defaultFn; diff --git a/src/core/component/directives/aria/roles/combobox/test/unit/combobox.ts b/src/core/component/directives/aria/roles/combobox/test/unit/combobox.ts index 1e92ffca7c..3a12f4a5c3 100644 --- a/src/core/component/directives/aria/roles/combobox/test/unit/combobox.ts +++ b/src/core/component/directives/aria/roles/combobox/test/unit/combobox.ts @@ -39,7 +39,15 @@ test.describe('v-aria:combobox', () => { ).toBe('false'); }); - test('aria-multiselectable is set', async ({page}) => { + test('aria-multiselectable is set to false', async ({page}) => { + const target = await init(page); + + test.expect( + await target.evaluate((ctx) => ctx.unsafe.block?.element('input')?.getAttribute('aria-multiselectable')) + ).toBe('false'); + }); + + test('aria-multiselectable is set to true', async ({page}) => { const target = await init(page, {multiple: true}); test.expect( diff --git a/src/core/component/directives/aria/roles/dialog/README.md b/src/core/component/directives/aria/roles/dialog/README.md index b97f8463a5..1582842862 100644 --- a/src/core/component/directives/aria/roles/dialog/README.md +++ b/src/core/component/directives/aria/roles/dialog/README.md @@ -7,10 +7,6 @@ The `dialog` role is used to mark up an HTML based application dialog or window For more information go to [dialog](`https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/dialog_role`). -## API - -The engine expects the component to realize the`iOpen` trait. - ## Usage ``` diff --git a/src/core/component/directives/aria/roles/dialog/index.ts b/src/core/component/directives/aria/roles/dialog/index.ts index adac9eda02..93646220e2 100644 --- a/src/core/component/directives/aria/roles/dialog/index.ts +++ b/src/core/component/directives/aria/roles/dialog/index.ts @@ -7,16 +7,11 @@ */ import { ARIARole } from 'core/component/directives/aria/roles/interface'; -import iOpen from 'traits/i-open/i-open'; export class Dialog extends ARIARole { /** @inheritDoc */ init(): void { this.setAttribute('role', 'dialog'); this.setAttribute('aria-modal', 'true'); - - if (!iOpen.is(this.ctx)) { - Object.throw('Dialog aria directive expects the component to realize iOpen interface'); - } } } diff --git a/src/core/component/directives/aria/roles/option/README.md b/src/core/component/directives/aria/roles/option/README.md index 5717ff3f82..66d3f7a53f 100644 --- a/src/core/component/directives/aria/roles/option/README.md +++ b/src/core/component/directives/aria/roles/option/README.md @@ -9,17 +9,21 @@ For more information go to [option](`https://developer.mozilla.org/en-US/docs/We ## API -The engine expects specific parameters to be passed. -- `isSelected`: `boolean`. -If true current option is selected by default. -- `@change`:`HandlerAttachment`, see `core/component/directives/aria/roles/README.md`. -Internal callback `onChange` expects an `boolean` value if current option is selected. +The role introduces several additional settings. + +### [selected = `false`] + +Whether the option is selected. + +### [@change] + +A handler for changing the active option. ## Usage ``` < div v-aria:option = { & - isSelected: el.active + selected: el.active '@change': (cb) => on('actionChange', () => cb(el.active)) } ``` diff --git a/src/core/component/directives/aria/roles/option/index.ts b/src/core/component/directives/aria/roles/option/index.ts index f7dd79ec29..f8d7b025ae 100644 --- a/src/core/component/directives/aria/roles/option/index.ts +++ b/src/core/component/directives/aria/roles/option/index.ts @@ -15,7 +15,6 @@ export class Option extends ARIARole { /** @inheritDoc */ init(): void { this.setAttribute('role', 'option'); - this.setAttribute('aria-selected', String(this.params.isSelected)); } /** diff --git a/src/core/component/directives/aria/roles/option/interface.ts b/src/core/component/directives/aria/roles/option/interface.ts index c825c44888..7cf06c3cba 100644 --- a/src/core/component/directives/aria/roles/option/interface.ts +++ b/src/core/component/directives/aria/roles/option/interface.ts @@ -8,7 +8,11 @@ import type { HandlerAttachment } from 'core/component/directives/aria/roles/interface'; +export interface OptionParams { + selected: boolean; + '@change': HandlerAttachment; +} + export class OptionParams { - isSelected: boolean = false; '@change': HandlerAttachment = () => undefined; } diff --git a/src/core/component/directives/aria/roles/tab/README.md b/src/core/component/directives/aria/roles/tab/README.md index 8bc21fbac9..c23c46ee37 100644 --- a/src/core/component/directives/aria/roles/tab/README.md +++ b/src/core/component/directives/aria/roles/tab/README.md @@ -12,8 +12,8 @@ For more information see [this](`https://developer.mozilla.org/en-US/docs/Web/Ac < div :id = 'tab-' + i | v-aria:tab = { & controls: 'content-' + i, - isFirst: i === 0, - isSelected: tab.active, + first: i === 0, + selected: tab.active, hasDefaultSelectedTabs: tab.some((tab) => Boolean(tab.active)), '@change': (cb) => cb(tab.active) @@ -38,14 +38,18 @@ Any ARIA attributes could be added in options through the short syntax. Also, the role introduces several additional settings. -### [isFirst = `false`] +### [first = `false`] Whether the tab is the first in the tablist. -### [isSelected = `false`] +### [selected = `false`] Whether the tab is selected. +### [orientation] + +Whether the widget orientation is `horizontal` or `vertical`. + ### [hasDefaultSelectedTabs = `false`] Whether there is at least one selected tab by default. diff --git a/src/core/component/directives/aria/roles/tab/index.ts b/src/core/component/directives/aria/roles/tab/index.ts index 6afa68406e..032f852c65 100644 --- a/src/core/component/directives/aria/roles/tab/index.ts +++ b/src/core/component/directives/aria/roles/tab/index.ts @@ -15,7 +15,7 @@ */ import type iBlock from 'super/i-block/i-block'; -import type iAccess from 'traits/i-access/i-access'; +import iAccess from 'traits/i-access/i-access'; import { TabParams } from 'core/component/directives/aria/roles/tab/interface'; import { ARIARole, KeyCodes } from 'core/component/directives/aria/roles/interface'; @@ -26,24 +26,26 @@ export class Tab extends ARIARole { /** @inheritDoc */ init(): void { + if (!iAccess.is(this.ctx)) { + Object.throw('Tab aria directive expects the component to realize iAccess interface'); + } + const { el, params: { - isFirst, - isSelected, + first, hasDefaultSelectedTabs } } = this; this.setAttribute('role', 'tab'); - this.setAttribute('aria-selected', String(isSelected)); - if (isFirst && !hasDefaultSelectedTabs) { + if (first && !hasDefaultSelectedTabs) { if (el.tabIndex < 0) { this.setAttribute('tabindex', '0'); } - } else if (hasDefaultSelectedTabs && isSelected) { + } else if (hasDefaultSelectedTabs && this.params.selected) { if (el.tabIndex < 0) { this.setAttribute('tabindex', '0'); } @@ -98,9 +100,9 @@ export class Tab extends ARIARole { * @param active */ protected onChange(active: Element | NodeListOf): void { - const setAttributes = (isSelected: boolean) => { - this.setAttribute('aria-selected', String(isSelected)); - this.setAttribute('tabindex', isSelected ? '0' : '-1'); + const setAttributes = (selected: boolean) => { + this.setAttribute('aria-selected', String(selected)); + this.setAttribute('tabindex', selected ? '0' : '-1'); }; if (Object.isArrayLike(active)) { @@ -114,12 +116,16 @@ export class Tab extends ARIARole { setAttributes(this.el === active); } + protected get tablist(): Element | null { + return this.el.closest('[role="tablist"]'); + } + /** * Handler: a keyboard event has occurred */ protected onKeydown(e: KeyboardEvent): void { const - isVertical = this.params.orientation === 'vertical'; + isVertical = this.tablist?.getAttribute('aria-orientation') === 'vertical'; switch (e.key) { case KeyCodes.LEFT: diff --git a/src/core/component/directives/aria/roles/tab/interface.ts b/src/core/component/directives/aria/roles/tab/interface.ts index ec70ef6d2e..897a14571f 100644 --- a/src/core/component/directives/aria/roles/tab/interface.ts +++ b/src/core/component/directives/aria/roles/tab/interface.ts @@ -6,12 +6,19 @@ * https://github.com/V4Fire/Client/blob/master/LICENSE */ +import type { Orientation } from 'base/b-list/interface'; import type { HandlerAttachment } from 'core/component/directives/aria/roles/interface'; +export interface TabParams { + first: boolean; + selected: boolean; + hasDefaultSelectedTabs: boolean; + orientation: Orientation; + '@change': HandlerAttachment; +} + export class TabParams { - isFirst: boolean = false; - isSelected: boolean = false; + first: boolean = false; hasDefaultSelectedTabs: boolean = false; - orientation: string = 'false'; '@change': HandlerAttachment = () => undefined; } diff --git a/src/core/component/directives/aria/roles/tablist/README.md b/src/core/component/directives/aria/roles/tablist/README.md index 91954684ea..840d6ca1b2 100644 --- a/src/core/component/directives/aria/roles/tablist/README.md +++ b/src/core/component/directives/aria/roles/tablist/README.md @@ -10,13 +10,17 @@ For recommendations how to make accessible widget go to [tablist](`https://www.w ## API -The engine expects specific parameters to be passed. -- `isMultiple`:`boolean`. -If true widget supports multiple selected options. -- `orientation`: `string`. -The tablist widget view orientation. +The role introduces several additional settings. -The engine expects the component to realize`iAccess` trait. +### [multiselectable = `false`] + +Whether the widget supports a feature of multiple active items + +### [orientation] + +Whether the widget orientation is `horizontal` or `vertical`. + +The role expects the component to realize`iAccess` trait. ## Usage diff --git a/src/core/component/directives/aria/roles/tablist/index.ts b/src/core/component/directives/aria/roles/tablist/index.ts index 3e0449b84c..6823a99fce 100644 --- a/src/core/component/directives/aria/roles/tablist/index.ts +++ b/src/core/component/directives/aria/roles/tablist/index.ts @@ -6,25 +6,14 @@ * https://github.com/V4Fire/Client/blob/master/LICENSE */ -import { TablistParams } from 'core/component/directives/aria/roles/tablist/interface'; import { ARIARole } from 'core/component/directives/aria/roles/interface'; +import type { TablistParams } from 'core/component/directives/aria/roles/tablist/interface'; export class Tablist extends ARIARole { - override Params: TablistParams = new TablistParams(); + override Params!: TablistParams ; /** @inheritDoc */ init(): void { - const - {params} = this; - this.setAttribute('role', 'tablist'); - - if (params.isMultiple) { - this.setAttribute('aria-multiselectable', 'true'); - } - - if (params.orientation === 'vertical') { - this.setAttribute('aria-orientation', params.orientation); - } } } diff --git a/src/core/component/directives/aria/roles/tablist/interface.ts b/src/core/component/directives/aria/roles/tablist/interface.ts index 6144a59fba..7259d16cd9 100644 --- a/src/core/component/directives/aria/roles/tablist/interface.ts +++ b/src/core/component/directives/aria/roles/tablist/interface.ts @@ -6,7 +6,7 @@ * https://github.com/V4Fire/Client/blob/master/LICENSE */ -export class TablistParams { - isMultiple: boolean = false; - orientation: string = 'false'; +export interface TablistParams { + multiselectable: boolean; + orientation: string; } diff --git a/src/core/component/directives/aria/roles/tabpanel/index.ts b/src/core/component/directives/aria/roles/tabpanel/index.ts index 306b1202e6..28a424f4e6 100644 --- a/src/core/component/directives/aria/roles/tabpanel/index.ts +++ b/src/core/component/directives/aria/roles/tabpanel/index.ts @@ -14,10 +14,10 @@ export class Tabpanel extends ARIARole { const {el} = this; + this.setAttribute('role', 'tabpanel'); + if (!el.hasAttribute('aria-label') && !el.hasAttribute('aria-labelledby')) { Object.throw('Tabpanel aria directive expects "label" or "labelledby" value to be passed'); } - - this.setAttribute('role', 'tabpanel'); } } diff --git a/src/core/component/directives/aria/roles/tabpanel/test/unit/tabpanel.ts b/src/core/component/directives/aria/roles/tabpanel/test/unit/tabpanel.ts index 72e2422067..161e8fbfb9 100644 --- a/src/core/component/directives/aria/roles/tabpanel/test/unit/tabpanel.ts +++ b/src/core/component/directives/aria/roles/tabpanel/test/unit/tabpanel.ts @@ -18,21 +18,13 @@ test.describe('v-aria:tabpanel', () => { }); test('role is set', async ({page}) => { - const target = await init(page, {}); + const target = await init(page); test.expect( await target.evaluate((ctx) => ctx.$el?.getAttribute('role')) ).toBe('tabpanel'); }); - test('no label passed', async ({page}) => { - const target = await init(page, {'v-aria:tabpanel': {}}); - - test.expect( - await target.evaluate((ctx) => ctx.$el?.hasAttribute('role')) - ).toBe(false); - }); - /** * @param page * @param attrs diff --git a/src/core/component/directives/aria/roles/tree/README.md b/src/core/component/directives/aria/roles/tree/README.md index 0c0cdc1305..a6efa75bc9 100644 --- a/src/core/component/directives/aria/roles/tree/README.md +++ b/src/core/component/directives/aria/roles/tree/README.md @@ -10,15 +10,21 @@ For recommendations how to make accessible widget go to [tree](`https://www.w3.o ## API -The engine expects specific parameters to be passed. -- `isRoot`: `boolean`. -If true current tree is the root tree in the component. -- `orientation`: `string`. -The tablist widget view orientation. -- `@change`:`HandlerAttachment`, see `core/component/directives/aria/roles/README.md`. -Internal callback `onChange` expects an `Element` and `boolean` value if current tree is expanded. - -The engine expects the component to realize`iAccess` trait. +The role introduces several additional settings. + +### [root = `false`] + +Whether the current tree is the root tree + +### [orientation] + +Whether the widget orientation is `horizontal` or `vertical`. + +### [@change] + +A handler for expanding the tree item. + +The role expects the component to realize`iAccess` trait. ## Usage diff --git a/src/core/component/directives/aria/roles/tree/index.ts b/src/core/component/directives/aria/roles/tree/index.ts index 7eb1d63d60..c43f440008 100644 --- a/src/core/component/directives/aria/roles/tree/index.ts +++ b/src/core/component/directives/aria/roles/tree/index.ts @@ -15,11 +15,11 @@ export class Tree extends ARIARole { /** @inheritDoc */ init(): void { const - {orientation, isRoot} = this.params; + {orientation, root} = this.params; this.setRootRole(); - if (orientation === 'horizontal' && isRoot) { + if (root) { this.setAttribute('aria-orientation', orientation); } } @@ -28,7 +28,7 @@ export class Tree extends ARIARole { * Sets the role to the element depending on whether the tree is root or nested */ protected setRootRole(): void { - this.setAttribute('role', this.params.isRoot ? 'tree' : 'group'); + this.setAttribute('role', this.params.root ? 'tree' : 'group'); } /** diff --git a/src/core/component/directives/aria/roles/tree/interface.ts b/src/core/component/directives/aria/roles/tree/interface.ts index 88500cf918..82cf683e5a 100644 --- a/src/core/component/directives/aria/roles/tree/interface.ts +++ b/src/core/component/directives/aria/roles/tree/interface.ts @@ -9,7 +9,7 @@ import type { HandlerAttachment } from 'core/component/directives/aria/roles/interface'; export class TreeParams { - isRoot: boolean = false; - orientation: string = 'false'; + root: boolean = false; + orientation: string = 'vertical'; '@change': HandlerAttachment = () => undefined; } diff --git a/src/core/component/directives/aria/roles/treeitem/README.md b/src/core/component/directives/aria/roles/treeitem/README.md index 3621b43255..9b248467cc 100644 --- a/src/core/component/directives/aria/roles/treeitem/README.md +++ b/src/core/component/directives/aria/roles/treeitem/README.md @@ -10,18 +10,30 @@ For recommendations how to make accessible widget go to [treeitem](`https://www. ## API -The engine expects specific parameters to be passed. -- `isFirstRootItem`: `boolean`. -If true the item is first one in the root tree. -- `isExpandable`: `boolean`. -If true the item has children and can be expanded. -- `isExpanded`: `boolean`. -If true the item is expanded in the current moment. -- `orientation`: `string`. -The tablist widget view orientation. -- `rootElement`: `Element`. -The link to the root tree element. -- `toggleFold`: `function`. +The role introduces several additional settings. + +### [firstRootItem = `false`] + +Whether the tree item is the first one in the root tree. + +### [expandable = `false`] + +Whether the tree item has children. + +### [expanded = `false`] + +Whether the tree item is expanded. + +### [orientation] + +Whether the widget orientation is `horizontal` or `vertical`. + +### [rootElement] + +the link to the root tree element. + +### [toggleFold] + The function to toggle the expandable item. The engine expects the component to realize`iAccess` trait. diff --git a/src/core/component/directives/aria/roles/treeitem/index.ts b/src/core/component/directives/aria/roles/treeitem/index.ts index d5c672deb1..50cda09489 100644 --- a/src/core/component/directives/aria/roles/treeitem/index.ts +++ b/src/core/component/directives/aria/roles/treeitem/index.ts @@ -28,10 +28,11 @@ export class Treeitem extends ARIARole { this.async.on(this.el, 'keydown', this.onKeyDown.bind(this)); const - isMuted = this.ctx?.removeAllFromTabSequence(this.el); + muted = this.ctx?.removeAllFromTabSequence(this.el), + {firstRootItem, expandable, expanded} = this.params; - if (this.params.isFirstRootItem) { - if (isMuted) { + if (firstRootItem) { + if (muted) { this.ctx?.restoreAllToTabSequence(this.el); } else { @@ -42,8 +43,8 @@ export class Treeitem extends ARIARole { this.setAttribute('role', 'treeitem'); this.ctx?.$nextTick(() => { - if (this.params.isExpandable) { - this.setAttribute('aria-expanded', String(this.params.isExpanded)); + if (expandable) { + this.setAttribute('aria-expanded', String(expanded)); } }); } @@ -90,16 +91,8 @@ export class Treeitem extends ARIARole { * Moves focus to the parent treeitem */ protected focusParent(): void { - let - parent = this.el.parentElement; - - while (parent != null) { - if (parent.getAttribute('role') === 'treeitem') { - break; - } - - parent = parent.parentElement; - } + const + parent = this.el.parentElement?.closest('[role="treeitem"]'); if (parent == null) { return; @@ -157,11 +150,14 @@ export class Treeitem extends ARIARole { } const - isHorizontal = this.params.orientation === 'horizontal'; + {rootElement, expandable, expanded, toggleFold} = this.params; + + const + isHorizontal = rootElement?.getAttribute('aria-orientation') === 'horizontal'; const open = () => { - if (this.params.isExpandable) { - if (this.params.isExpanded) { + if (expandable) { + if (expanded) { this.moveFocus(1); } else { @@ -171,7 +167,7 @@ export class Treeitem extends ARIARole { }; const close = () => { - if (this.params.isExpandable && this.params.isExpanded) { + if (expandable && expanded) { this.closeFold(); } else { @@ -217,8 +213,8 @@ export class Treeitem extends ARIARole { break; case KeyCodes.ENTER: - if (this.params.isExpandable) { - this.params.toggleFold(this.el); + if (expandable) { + toggleFold(this.el); } break; diff --git a/src/core/component/directives/aria/roles/treeitem/interface.ts b/src/core/component/directives/aria/roles/treeitem/interface.ts index c9ec8aedd1..59349b6421 100644 --- a/src/core/component/directives/aria/roles/treeitem/interface.ts +++ b/src/core/component/directives/aria/roles/treeitem/interface.ts @@ -9,10 +9,9 @@ type FoldToggle = (el: Element, value?: boolean) => void; export class TreeitemParams { - isFirstRootItem: boolean = false; - isExpandable: boolean = false; - isExpanded: boolean = false; - orientation: string = 'false'; + firstRootItem: boolean = false; + expandable: boolean = false; + expanded: boolean = false; rootElement?: Element = undefined; toggleFold: FoldToggle = () => undefined; } diff --git a/src/form/b-select/b-select.ts b/src/form/b-select/b-select.ts index 821a7d76a2..e8582fc062 100644 --- a/src/form/b-select/b-select.ts +++ b/src/form/b-select/b-select.ts @@ -894,14 +894,14 @@ class bSelect extends iInputText implements iOpenToggle, iItems { () => undefined; const comboboxConfig = { - isMultiple: this.multiple, + multiselectable: this.multiple, '@change': (cb) => this.localEmitter.on('el.mod.set.*.marked.*', ({link}) => cb(link)), '@close': (cb) => this.on('close', cb), '@open': (cb) => this.on('open', () => this.$nextTick(() => cb(this.selectedElement))) }; const optionConfig = { - get isSelected() { + get selected() { return isSelected(); }, From f3799961e2e0554a561674fb7f9fe539e275974f Mon Sep 17 00:00:00 2001 From: Aleksandr Bunin Date: Sun, 18 Sep 2022 15:10:25 +0300 Subject: [PATCH 169/185] refactoring interfaces --- src/core/component/directives/aria/roles/combobox/interface.ts | 3 --- src/core/component/directives/aria/roles/option/interface.ts | 1 - src/core/component/directives/aria/roles/tab/interface.ts | 3 --- 3 files changed, 7 deletions(-) diff --git a/src/core/component/directives/aria/roles/combobox/interface.ts b/src/core/component/directives/aria/roles/combobox/interface.ts index 7b683c7307..0bd1395729 100644 --- a/src/core/component/directives/aria/roles/combobox/interface.ts +++ b/src/core/component/directives/aria/roles/combobox/interface.ts @@ -12,9 +12,6 @@ const defaultFn = (): void => undefined; export interface ComboboxParams { multiselectable: boolean; - '@change': HandlerAttachment; - '@open': HandlerAttachment; - '@close': HandlerAttachment; } export class ComboboxParams { diff --git a/src/core/component/directives/aria/roles/option/interface.ts b/src/core/component/directives/aria/roles/option/interface.ts index 7cf06c3cba..8ba454895c 100644 --- a/src/core/component/directives/aria/roles/option/interface.ts +++ b/src/core/component/directives/aria/roles/option/interface.ts @@ -10,7 +10,6 @@ import type { HandlerAttachment } from 'core/component/directives/aria/roles/int export interface OptionParams { selected: boolean; - '@change': HandlerAttachment; } export class OptionParams { diff --git a/src/core/component/directives/aria/roles/tab/interface.ts b/src/core/component/directives/aria/roles/tab/interface.ts index 897a14571f..07089cdf37 100644 --- a/src/core/component/directives/aria/roles/tab/interface.ts +++ b/src/core/component/directives/aria/roles/tab/interface.ts @@ -10,11 +10,8 @@ import type { Orientation } from 'base/b-list/interface'; import type { HandlerAttachment } from 'core/component/directives/aria/roles/interface'; export interface TabParams { - first: boolean; selected: boolean; - hasDefaultSelectedTabs: boolean; orientation: Orientation; - '@change': HandlerAttachment; } export class TabParams { From 42615b4ea767ece180e9e230b9d4b0ad30a959f4 Mon Sep 17 00:00:00 2001 From: Aleksandr Bunin Date: Sun, 18 Sep 2022 16:13:16 +0300 Subject: [PATCH 170/185] fixes --- src/core/component/directives/aria/roles/tab/index.ts | 11 +++++++---- .../component/directives/aria/roles/treeitem/index.ts | 11 +++++++++-- 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/src/core/component/directives/aria/roles/tab/index.ts b/src/core/component/directives/aria/roles/tab/index.ts index 032f852c65..be0fbd1a48 100644 --- a/src/core/component/directives/aria/roles/tab/index.ts +++ b/src/core/component/directives/aria/roles/tab/index.ts @@ -57,6 +57,13 @@ export class Tab extends ARIARole { this.async.on(el, 'keydown', this.onKeydown.bind(this)); } + /** + * The tab list element + */ + protected get tablist(): Element | null { + return this.el.closest('[role="tablist"]'); + } + /** * Moves focus to the first tab in the tablist */ @@ -116,10 +123,6 @@ export class Tab extends ARIARole { setAttributes(this.el === active); } - protected get tablist(): Element | null { - return this.el.closest('[role="tablist"]'); - } - /** * Handler: a keyboard event has occurred */ diff --git a/src/core/component/directives/aria/roles/treeitem/index.ts b/src/core/component/directives/aria/roles/treeitem/index.ts index 50cda09489..c3b4dfa88b 100644 --- a/src/core/component/directives/aria/roles/treeitem/index.ts +++ b/src/core/component/directives/aria/roles/treeitem/index.ts @@ -49,6 +49,13 @@ export class Treeitem extends ARIARole { }); } + /** + * The root tree element + */ + protected get root(): Nullable { + return this.params.rootElement?.querySelector('[role="tree"]'); + } + /** * Changes focus from the current focused element to the passed one * @param el @@ -150,10 +157,10 @@ export class Treeitem extends ARIARole { } const - {rootElement, expandable, expanded, toggleFold} = this.params; + {expandable, expanded, toggleFold} = this.params; const - isHorizontal = rootElement?.getAttribute('aria-orientation') === 'horizontal'; + isHorizontal = this.root?.getAttribute('aria-orientation') === 'horizontal'; const open = () => { if (expandable) { From 238896e233e3a152df47b250aa175d78d423f327 Mon Sep 17 00:00:00 2001 From: kobezzza Date: Fri, 23 Sep 2022 17:42:19 +0300 Subject: [PATCH 171/185] chore: simple stylish fixed --- .../aria/roles/tab/test/unit/tab.ts | 24 ++++++++++--------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/src/core/component/directives/aria/roles/tab/test/unit/tab.ts b/src/core/component/directives/aria/roles/tab/test/unit/tab.ts index 25a587a89b..3a3d0698cf 100644 --- a/src/core/component/directives/aria/roles/tab/test/unit/tab.ts +++ b/src/core/component/directives/aria/roles/tab/test/unit/tab.ts @@ -20,7 +20,7 @@ test.describe('v-aria:tab', () => { const selector = '[data-id="target"]'; - test('role is set', async ({page}) => { + test('list items must have the `role` attributes', async ({page}) => { const target = await init(page); test.expect( @@ -40,7 +40,7 @@ test.describe('v-aria:tab', () => { ).toEqual(['tab', 'tab', 'tab']); }); - test('aria-controls is set', async ({page}) => { + test('list items must have the `aria-controls` attributes', async ({page}) => { const target = await init(page); test.expect( @@ -60,7 +60,7 @@ test.describe('v-aria:tab', () => { ).toEqual(['id4', 'id5', 'id6']); }); - test('has active value', async ({page}) => { + test('the active element must have the `aria-selected` attribute', async ({page}) => { const target = await init(page, {active: 1}); await page.focus(selector); @@ -82,7 +82,7 @@ test.describe('v-aria:tab', () => { ).toEqual(['false', 'true', 'false']); }); - test('tabindexes are set without active item', async ({page}) => { + test('if there is no active element, then all elements except the first must have the `tabindex` attribute equal to `-1`', async ({page}) => { const target = await init(page); test.expect( @@ -102,7 +102,7 @@ test.describe('v-aria:tab', () => { ).toEqual([0, -1, -1]); }); - test('tabindexes are set with active item', async ({page}) => { + test('all elements except the active must have the `tabindex` attribute equal to `-1`', async ({page}) => { const target = await init(page, {active: 1}); test.expect( @@ -122,7 +122,7 @@ test.describe('v-aria:tab', () => { ).toEqual([-1, 0, -1]); }); - test('active item changed', async ({page}) => { + test('changing the active element', async ({page}) => { const target = await init(page); test.expect( @@ -150,7 +150,7 @@ test.describe('v-aria:tab', () => { ]); }); - test('keyboard keys handle on horizontal orientation', async ({page}) => { + test('keyboard support with the horizontal orientation', async ({page}) => { const target = await init(page); test.expect( @@ -187,7 +187,7 @@ test.describe('v-aria:tab', () => { ).toEqual(['id2', 'id1', 'id1', 'id1', 'id3', 'id1']); }); - test('keyboard keys handle on vertical orientation', async ({page}) => { + test('keyboard support with the vertical orientation', async ({page}) => { const target = await init(page, {orientation: 'vertical'}); test.expect( @@ -232,11 +232,13 @@ test.describe('v-aria:tab', () => { return Component.createComponent(page, 'b-list', { attrs: { 'data-id': 'target', + items: [ - {label: 'Male', value: 0, id: 'id1', controls: 'id4'}, - {label: 'Female', value: 1, id: 'id2', controls: 'id5'}, - {label: 'Other', value: 2, id: 'id3', controls: 'id6'} + {id: 'id1', label: 'Male', value: 0, controls: 'id4'}, + {id: 'id2', label: 'Female', value: 1, controls: 'id5'}, + {id: 'id3', label: 'Other', value: 2, controls: 'id6'} ], + ...attrs } }); From 1221236e87ca7aaf20f5552945661f9bc363c840 Mon Sep 17 00:00:00 2001 From: kobezzza Date: Fri, 23 Sep 2022 17:46:14 +0300 Subject: [PATCH 172/185] chore: better exception --- src/core/component/directives/aria/roles/tab/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/component/directives/aria/roles/tab/index.ts b/src/core/component/directives/aria/roles/tab/index.ts index be0fbd1a48..c8f95e7645 100644 --- a/src/core/component/directives/aria/roles/tab/index.ts +++ b/src/core/component/directives/aria/roles/tab/index.ts @@ -27,7 +27,7 @@ export class Tab extends ARIARole { /** @inheritDoc */ init(): void { if (!iAccess.is(this.ctx)) { - Object.throw('Tab aria directive expects the component to realize iAccess interface'); + throw new TypeError('The `tab` role requires that a component in whose context the directive is used implement the `iAccess` interface'); } const { From 7a4e0b9ed603580eebdee2a0b076a4edf8ad223a Mon Sep 17 00:00:00 2001 From: kobezzza Date: Fri, 23 Sep 2022 17:50:08 +0300 Subject: [PATCH 173/185] refactor: added re-export for interfaces --- src/core/component/directives/aria/roles/tab/index.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/core/component/directives/aria/roles/tab/index.ts b/src/core/component/directives/aria/roles/tab/index.ts index c8f95e7645..603d452888 100644 --- a/src/core/component/directives/aria/roles/tab/index.ts +++ b/src/core/component/directives/aria/roles/tab/index.ts @@ -20,6 +20,8 @@ import iAccess from 'traits/i-access/i-access'; import { TabParams } from 'core/component/directives/aria/roles/tab/interface'; import { ARIARole, KeyCodes } from 'core/component/directives/aria/roles/interface'; +export * from 'core/component/directives/aria/roles/tab/interface'; + export class Tab extends ARIARole { override Params: TabParams = new TabParams(); override Ctx!: iBlock & iAccess; From c7d99aae0d6bad608428f230024647770a190fb2 Mon Sep 17 00:00:00 2001 From: kobezzza Date: Fri, 23 Sep 2022 18:00:20 +0300 Subject: [PATCH 174/185] chore: simple stylish fixes --- src/core/component/directives/aria/roles/tab/README.md | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/core/component/directives/aria/roles/tab/README.md b/src/core/component/directives/aria/roles/tab/README.md index c23c46ee37..6cd9e8c72f 100644 --- a/src/core/component/directives/aria/roles/tab/README.md +++ b/src/core/component/directives/aria/roles/tab/README.md @@ -1,7 +1,7 @@ # core/component/directives/aria/roles/tab This module provides an implementation of the ARIA [tab](https://www.w3.org/TR/wai-aria/#tab) role. -An element with this role should be used in conjunction with elements with the roles [tablist](https://www.w3.org/TR/wai-aria/#tablist) and [tabpanel](https://www.w3 .org/TR /wai-aria/#tabpanel) +An element with this role should be used in conjunction with elements with the roles [tablist](https://www.w3.org/TR/wai-aria/#tablist) and [tabpanel](https://www.w3 .org/TR /wai-aria/#tabpanel). The role expects the component within which the directive is used to implement the [[iAccess]] characteristic. For more information see [this](`https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/tab_role`). @@ -46,10 +46,6 @@ Whether the tab is the first in the tablist. Whether the tab is selected. -### [orientation] - -Whether the widget orientation is `horizontal` or `vertical`. - ### [hasDefaultSelectedTabs = `false`] Whether there is at least one selected tab by default. From 9e0803f5ecbeb61386511ee36f24caa272cdde41 Mon Sep 17 00:00:00 2001 From: kobezzza Date: Fri, 23 Sep 2022 18:03:43 +0300 Subject: [PATCH 175/185] chore: simple stylish fixes --- .../component/directives/aria/roles/tabpanel/index.ts | 11 +++++++---- .../aria/roles/tabpanel/test/unit/tabpanel.ts | 2 +- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/src/core/component/directives/aria/roles/tabpanel/index.ts b/src/core/component/directives/aria/roles/tabpanel/index.ts index 28a424f4e6..87f4c3883a 100644 --- a/src/core/component/directives/aria/roles/tabpanel/index.ts +++ b/src/core/component/directives/aria/roles/tabpanel/index.ts @@ -6,18 +6,21 @@ * https://github.com/V4Fire/Client/blob/master/LICENSE */ +/** + * [[include:core/component/directives/aria/roles/tabpanel/README.md]] + * @packageDocumentation + */ + import { ARIARole } from 'core/component/directives/aria/roles/interface'; export class Tabpanel extends ARIARole { /** @inheritDoc */ init(): void { - const - {el} = this; - + const {el} = this; this.setAttribute('role', 'tabpanel'); if (!el.hasAttribute('aria-label') && !el.hasAttribute('aria-labelledby')) { - Object.throw('Tabpanel aria directive expects "label" or "labelledby" value to be passed'); + throw new TypeError('The tabpanel role expects a `label` or `labelledby` value to be passed'); } } } diff --git a/src/core/component/directives/aria/roles/tabpanel/test/unit/tabpanel.ts b/src/core/component/directives/aria/roles/tabpanel/test/unit/tabpanel.ts index 161e8fbfb9..be026da536 100644 --- a/src/core/component/directives/aria/roles/tabpanel/test/unit/tabpanel.ts +++ b/src/core/component/directives/aria/roles/tabpanel/test/unit/tabpanel.ts @@ -17,7 +17,7 @@ test.describe('v-aria:tabpanel', () => { await demoPage.goto(); }); - test('role is set', async ({page}) => { + test('tabpanel must have the `role` attribute', async ({page}) => { const target = await init(page); test.expect( From 10c4ecacbc38de7aab96d8273516ee2fd4ec98d8 Mon Sep 17 00:00:00 2001 From: kobezzza Date: Fri, 23 Sep 2022 18:03:52 +0300 Subject: [PATCH 176/185] doc: improved doc --- .../directives/aria/roles/tabpanel/README.md | 53 ++++++++++++------- 1 file changed, 35 insertions(+), 18 deletions(-) diff --git a/src/core/component/directives/aria/roles/tabpanel/README.md b/src/core/component/directives/aria/roles/tabpanel/README.md index a3b7180666..cf39f626e1 100644 --- a/src/core/component/directives/aria/roles/tabpanel/README.md +++ b/src/core/component/directives/aria/roles/tabpanel/README.md @@ -1,29 +1,46 @@ # core/component/directives/aria/roles/tabpanel -This module provides an engine for `v-aria` directive. +This module provides an implementation of the ARIA [tabpanel](https://www.w3.org/TR/wai-aria/#tabpanel) role. +An element with this role should be used in conjunction with elements with the roles [tablist](https://www.w3.org/TR/wai-aria/#tablist) and [tab](https://www.w3.org/TR/wai-aria/#tab). -The engine to set `tabpanel` role attribute. -The ARIA `tabpanel` is a container for the resources of layered content associated with a `tab`. +For more information see [this](`https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/tab_role`). -For more information go to [tabpanel](`https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/tabpanel_role`). -For recommendations how to make accessible widget go to [tabpanel](`https://www.w3.org/WAI/ARIA/apg/patterns/tabpanel/`). +``` +< div v-aria:tablist + < template v-for = (tab, i) of tabs + < div :id = 'tab-' + i | v-aria:tab = { & + controls: 'content-' + i, + + first: i === 0, + selected: tab.active, + hasDefaultSelectedTabs: tab.some((tab) => Boolean(tab.active)), -## API + '@change': (cb) => cb(tab.active) + } . -The engine expects `label` or `labelledby` params to be passed and the element `id` +< template v-for = (content, i) of tabsContent + < div :id = 'content-' + i | v-aria:tabpanel = {labelledby: 'tab-' + i} + {{ content }} +``` +## Available options -## Usage +Any ARIA attributes could be added in options through the short syntax. -Example: ``` -< div & - id = 'tab-1' | - v-aria:tab = {...config} | - v-aria:controls = {for: 'tabpanel-1'} - Tab - -< div id = 'tabpanel-1' | v-aria:tabpanel = {labelledby: 'tab-1'} - < p - Content for the panel +< div v-aria = {label: 'foo', desribedby: 'id1', details: 'id2'} + +/// The same as + +< div :aria-label = 'foo' | :aria-desribedby = 'id1' | :aria-details = 'id2' ``` + +Also, the role introduces several additional settings. + +### [label] + +A string value that labels the element it is applied to. + +### [labelledby] + +An element (or elements) that labels the element it is applied to. From ad5409c7ee61e163488b333adab0d32b860e63c4 Mon Sep 17 00:00:00 2001 From: kobezzza Date: Fri, 23 Sep 2022 18:12:16 +0300 Subject: [PATCH 177/185] doc: improved doc --- .../directives/aria/roles/tablist/README.md | 53 ++++++++++++------- 1 file changed, 33 insertions(+), 20 deletions(-) diff --git a/src/core/component/directives/aria/roles/tablist/README.md b/src/core/component/directives/aria/roles/tablist/README.md index 840d6ca1b2..d5ff567281 100644 --- a/src/core/component/directives/aria/roles/tablist/README.md +++ b/src/core/component/directives/aria/roles/tablist/README.md @@ -1,33 +1,46 @@ # core/component/directives/aria/roles/tablist -This module provides an engine for `v-aria` directive. +This module provides an implementation of the ARIA [tablist](https://www.w3.org/TR/wai-aria/#tablist) role. +An element with this role should be used in conjunction with elements with the roles [tabpanel](https://www.w3.org/TR/wai-aria/#tabpanel) and [tab](https://www.w3.org/TR/wai-aria/#tab). -The engine to set `tablist` role attribute. -The `tablist` role identifies the element that serves as the container for a set of `tabs`. The `tab` content are referred to as `tabpanel` elements. +For more information see [this](`https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/tab_role`). -For more information go to [tablist](`https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/tablist_role`). -For recommendations how to make accessible widget go to [tablist](`https://www.w3.org/WAI/ARIA/apg/patterns/tabpanel/`). - -## API +``` +< div v-aria:tablist + < template v-for = (tab, i) of tabs + < div :id = 'tab-' + i | v-aria:tab = { & + controls: 'content-' + i, -The role introduces several additional settings. + first: i === 0, + selected: tab.active, + hasDefaultSelectedTabs: tab.some((tab) => Boolean(tab.active)), -### [multiselectable = `false`] + '@change': (cb) => cb(tab.active) + } . -Whether the widget supports a feature of multiple active items +< template v-for = (content, i) of tabsContent + < div :id = 'content-' + i | v-aria:tabpanel = {labelledby: 'tab-' + i} + {{ content }} +``` -### [orientation] +## Available options -Whether the widget orientation is `horizontal` or `vertical`. +Any ARIA attributes could be added in options through the short syntax. -The role expects the component to realize`iAccess` trait. +``` +< div v-aria = {label: 'foo', desribedby: 'id1', details: 'id2'} -## Usage +/// The same as +< div :aria-label = 'foo' | :aria-desribedby = 'id1' | :aria-details = 'id2' ``` -< div v-aria:tablist = { & - isMultiple: multiple; - orientation: orientation; - } -. -``` + +Also, the role introduces several additional settings. + +### [multiselectable = `false`] + +Indicates that the user may select more than one item from the current selectable descendants. + +### [orientation = `'horizontal'`] + +Indicates whether the element orientation is `horizontal`, `vertical`, or unknown/ambiguous. From 24415fed3e148c96b8d052f55b12c19df5900b5f Mon Sep 17 00:00:00 2001 From: kobezzza Date: Fri, 23 Sep 2022 18:12:28 +0300 Subject: [PATCH 178/185] chore: simple stylish fixes --- src/core/component/directives/aria/roles/tablist/index.ts | 2 ++ .../directives/aria/roles/tablist/test/unit/tablist.ts | 8 +++----- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/core/component/directives/aria/roles/tablist/index.ts b/src/core/component/directives/aria/roles/tablist/index.ts index 6823a99fce..4595a07de7 100644 --- a/src/core/component/directives/aria/roles/tablist/index.ts +++ b/src/core/component/directives/aria/roles/tablist/index.ts @@ -9,6 +9,8 @@ import { ARIARole } from 'core/component/directives/aria/roles/interface'; import type { TablistParams } from 'core/component/directives/aria/roles/tablist/interface'; +export * from 'core/component/directives/aria/roles/tablist/interface'; + export class Tablist extends ARIARole { override Params!: TablistParams ; diff --git a/src/core/component/directives/aria/roles/tablist/test/unit/tablist.ts b/src/core/component/directives/aria/roles/tablist/test/unit/tablist.ts index fd4872facb..0de5ef8459 100644 --- a/src/core/component/directives/aria/roles/tablist/test/unit/tablist.ts +++ b/src/core/component/directives/aria/roles/tablist/test/unit/tablist.ts @@ -17,7 +17,7 @@ test.describe('v-aria:tablist', () => { await demoPage.goto(); }); - test('role is set', async ({page}) => { + test('tablist must have the `role` attribute', async ({page}) => { const target = await init(page); test.expect( @@ -29,25 +29,23 @@ test.describe('v-aria:tablist', () => { ).toBe('tablist'); }); - test('multiselectable is set', async ({page}) => { + test('when passing the `multiselectable` parameter, a special attribute must be set', async ({page}) => { const target = await init(page, {multiple: true}); test.expect( await target.evaluate((ctx) => { const el = ctx.unsafe.block?.element('wrapper'); - return el?.getAttribute('aria-multiselectable'); }) ).toBe('true'); }); - test('orientation is set', async ({page}) => { + test('when passing the `orientation` parameter, a special attribute must be set', async ({page}) => { const target = await init(page, {orientation: 'vertical'}); test.expect( await target.evaluate((ctx) => { const el = ctx.unsafe.block?.element('wrapper'); - return el?.getAttribute('aria-orientation'); }) ).toBe('vertical'); From 3006294d51fdd393c8c7f963a408253b8a78a8b6 Mon Sep 17 00:00:00 2001 From: kobezzza Date: Fri, 23 Sep 2022 18:16:34 +0300 Subject: [PATCH 179/185] chore: added a lin to the doc --- src/core/component/directives/aria/roles/tablist/index.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/core/component/directives/aria/roles/tablist/index.ts b/src/core/component/directives/aria/roles/tablist/index.ts index 4595a07de7..89c28955e8 100644 --- a/src/core/component/directives/aria/roles/tablist/index.ts +++ b/src/core/component/directives/aria/roles/tablist/index.ts @@ -6,6 +6,11 @@ * https://github.com/V4Fire/Client/blob/master/LICENSE */ +/** + * [[include:core/component/directives/aria/roles/tablist/README.md]] + * @packageDocumentation + */ + import { ARIARole } from 'core/component/directives/aria/roles/interface'; import type { TablistParams } from 'core/component/directives/aria/roles/tablist/interface'; From 042f297cf7ab7f20d59a42f2f6481fb1bef496b1 Mon Sep 17 00:00:00 2001 From: Aleksandr Bunin Date: Sat, 1 Oct 2022 13:03:42 +0300 Subject: [PATCH 180/185] add listbox --- components-lock.json | 40 +++- src/base/b-list/b-list.ts | 3 +- src/base/b-list/interface.ts | 2 - src/base/b-tree/b-tree.ts | 3 +- src/base/b-tree/interface.ts | 2 - src/core/component/directives/aria/adapter.ts | 2 +- .../component/directives/aria/interface.ts | 2 + .../directives/aria/roles/listbox/index.ts | 135 ++++++++++++- .../aria/roles/listbox/interface.ts | 22 ++ .../aria/roles/listbox/test/unit/listbox.ts | 191 +++++++++++++++++- .../directives/aria/roles/option/index.ts | 6 +- .../directives/aria/roles/tab/interface.ts | 2 +- .../directives/aria/roles/tabpanel/index.ts | 13 +- .../aria/roles/tabpanel/interface.ts | 12 ++ .../aria/roles/tabpanel/test/unit/tabpanel.ts | 15 +- .../directives/aria/roles/tree/interface.ts | 3 +- src/dummies/b-dummy-listbox/CHANGELOG.md | 16 ++ src/dummies/b-dummy-listbox/README.md | 3 + .../b-dummy-listbox/b-dummy-listbox.ss | 30 +++ .../b-dummy-listbox/b-dummy-listbox.styl | 15 ++ .../b-dummy-listbox/b-dummy-listbox.ts | 135 +++++++++++++ src/dummies/b-dummy-listbox/index.js | 10 + src/pages/p-v4-components-demo/index.js | 3 +- 23 files changed, 635 insertions(+), 30 deletions(-) create mode 100644 src/core/component/directives/aria/roles/listbox/interface.ts create mode 100644 src/core/component/directives/aria/roles/tabpanel/interface.ts create mode 100644 src/dummies/b-dummy-listbox/CHANGELOG.md create mode 100644 src/dummies/b-dummy-listbox/README.md create mode 100644 src/dummies/b-dummy-listbox/b-dummy-listbox.ss create mode 100644 src/dummies/b-dummy-listbox/b-dummy-listbox.styl create mode 100644 src/dummies/b-dummy-listbox/b-dummy-listbox.ts create mode 100644 src/dummies/b-dummy-listbox/index.js diff --git a/components-lock.json b/components-lock.json index ea90cc6c97..8078ec502f 100644 --- a/components-lock.json +++ b/components-lock.json @@ -1,5 +1,5 @@ { - "hash": "18506bae5f4874b01c4ad66a342b83571739c7f8713e0762997f1b75b4e9b26f", + "hash": "642520426f817d93ef77303abd6014f9507e63f6c0da7de6dad758d8a2139632", "data": { "%data": "%data:Map", "%data:Map": [ @@ -265,6 +265,38 @@ "etpl": null } ], + [ + "b-dummy-listbox", + { + "index": "src/dummies/b-dummy-listbox/index.js", + "declaration": { + "name": "b-dummy-listbox", + "parent": "i-block", + "dependencies": [], + "libs": [] + }, + "name": "b-dummy-listbox", + "parent": "i-block", + "dependencies": [], + "libs": [], + "resolvedLibs": { + "%data": "%data:Set", + "%data:Set": [] + }, + "resolvedOwnLibs": { + "%data": "%data:Set", + "%data:Set": [] + }, + "type": "block", + "mixin": false, + "logic": "src/dummies/b-dummy-listbox/b-dummy-listbox.ts", + "styles": [ + "src/dummies/b-dummy-listbox/b-dummy-listbox.styl" + ], + "tpl": "src/dummies/b-dummy-listbox/b-dummy-listbox.ss", + "etpl": null + } + ], [ "b-dummy-module-loader", { @@ -1997,7 +2029,8 @@ "b-dummy-sync", "b-dummy-state", "b-dummy-control-list", - "b-dummy-decorators" + "b-dummy-decorators", + "b-dummy-listbox" ], "libs": [ "core/cookies", @@ -2041,7 +2074,8 @@ "b-dummy-sync", "b-dummy-state", "b-dummy-control-list", - "b-dummy-decorators" + "b-dummy-decorators", + "b-dummy-listbox" ], "libs": [ "core/cookies", diff --git a/src/base/b-list/b-list.ts b/src/base/b-list/b-list.ts index 5da5e10983..65822e4c9d 100644 --- a/src/base/b-list/b-list.ts +++ b/src/base/b-list/b-list.ts @@ -30,7 +30,8 @@ import iItems, { IterationKey } from 'traits/i-items/i-items'; import iAccess from 'traits/i-access/i-access'; import iData, { component, prop, field, system, computed, hook, watch, ModsDecl } from 'super/i-data/i-data'; -import type { Active, Item, Items, Orientation } from 'base/b-list/interface'; +import type { Active, Item, Items } from 'base/b-list/interface'; +import type { Orientation } from 'core/component/directives/aria'; export * from 'super/i-data/i-data'; export * from 'base/b-list/interface'; diff --git a/src/base/b-list/interface.ts b/src/base/b-list/interface.ts index 0e573dc07c..50cbc1ab1c 100644 --- a/src/base/b-list/interface.ts +++ b/src/base/b-list/interface.ts @@ -112,5 +112,3 @@ export interface Item extends Dictionary { export type Items = Item[]; export type Active = unknown | Set; - -export type Orientation = 'vertical' | 'horizontal'; diff --git a/src/base/b-tree/b-tree.ts b/src/base/b-tree/b-tree.ts index 551479cf39..2ecfc4b363 100644 --- a/src/base/b-tree/b-tree.ts +++ b/src/base/b-tree/b-tree.ts @@ -25,7 +25,8 @@ import iItems, { IterationKey } from 'traits/i-items/i-items'; import iData, { component, prop, field, TaskParams, TaskI } from 'super/i-data/i-data'; import iAccess from 'traits/i-access/i-access'; -import type { Item, Orientation, RenderFilter } from 'base/b-tree/interface'; +import type { Item, RenderFilter } from 'base/b-tree/interface'; +import type { Orientation } from 'core/component/directives/aria'; export * from 'super/i-data/i-data'; export * from 'base/b-tree/interface'; diff --git a/src/base/b-tree/interface.ts b/src/base/b-tree/interface.ts index 33471ae590..1f513b2974 100644 --- a/src/base/b-tree/interface.ts +++ b/src/base/b-tree/interface.ts @@ -38,5 +38,3 @@ export interface Item extends Dictionary { export interface RenderFilter { (ctx: bTree, el: Item, i: number, task: TaskI): CanPromise; } - -export type Orientation = 'vertical' | 'horizontal'; diff --git a/src/core/component/directives/aria/adapter.ts b/src/core/component/directives/aria/adapter.ts index ba56335c18..cc1b3873cd 100644 --- a/src/core/component/directives/aria/adapter.ts +++ b/src/core/component/directives/aria/adapter.ts @@ -157,7 +157,7 @@ export default class ARIAAdapter { $a.promise(val).then(handler, stderr); } else if (Object.isString(val)) { - $a.on(this.ctx, val, handler); + $a.on(this.ctx, val, (ctx, ...args) => handler(...args)); } }); } diff --git a/src/core/component/directives/aria/interface.ts b/src/core/component/directives/aria/interface.ts index 8fd1a114f2..0b65b21e32 100644 --- a/src/core/component/directives/aria/interface.ts +++ b/src/core/component/directives/aria/interface.ts @@ -13,3 +13,5 @@ export interface DirectiveOptions { binding: VNodeDirective; vnode: VNode; } + +export type Orientation = 'vertical' | 'horizontal'; diff --git a/src/core/component/directives/aria/roles/listbox/index.ts b/src/core/component/directives/aria/roles/listbox/index.ts index 0742c095c6..5239509acd 100644 --- a/src/core/component/directives/aria/roles/listbox/index.ts +++ b/src/core/component/directives/aria/roles/listbox/index.ts @@ -6,12 +6,143 @@ * https://github.com/V4Fire/Client/blob/master/LICENSE */ -import { ARIARole } from 'core/component/directives/aria/roles/interface'; +import iAccess from 'traits/i-access/i-access'; +import type iBlock from 'super/i-block/i-block'; + +import { ARIARole, KeyCodes } from 'core/component/directives/aria/roles/interface'; +import { ListboxParams } from 'core/component/directives/aria/roles/listbox/interface'; export class Listbox extends ARIARole { + override Params: ListboxParams = new ListboxParams(); + override Ctx!: iBlock['unsafe'] & iAccess; + + options: CanUndef; + /** @inheritDoc */ init(): void { + const + {standAlone, label, labelledby} = this.params; + + if (standAlone) { + if (this.ctx == null) { + return; + } + + if (!iAccess.is(this.ctx)) { + throw new TypeError('The `listbox` role requires that a component in whose context the directive is used implement the `iAccess` interface'); + } + + if (label == null && labelledby == null) { + throw new TypeError('The `listbox` role expects a `label` or `labelledby` value to be passed'); + } + + void this.ctx.lfc.execCbAfterBlockReady(() => { + const + options = this.ctx?.block?.elements('item'); + + if (options != null) { + this.options = Array.from(>options); + } + + this.options?.forEach((el) => { + el.setAttribute('tabindex', '0'); + this.ctx?.removeAllFromTabSequence(el); + }); + }); + } + this.setAttribute('role', 'listbox'); - this.setAttribute('tabindex', '-1'); + this.setAttribute('tabindex', standAlone ? '0' : '-1'); + + this.async.on(this.el, 'keydown', this.onKeydown.bind(this)); + } + + /** + * Sets or deletes the id of active descendant element + */ + protected setARIAActive(el: Element | null): void { + this.setAttribute('aria-activedescendant', el?.id ?? ''); + } + + /** + * Changes focus from the current focused element to the passed one + * @param el + */ + protected focusNext(el: AccessibleElement): void { + this.ctx?.removeAllFromTabSequence(this.el); + this.ctx?.restoreAllToTabSequence(el); + + el.focus(); + } + + /** + * Moves focus to the next or previous focusable element via the step parameter + * @param step + */ + protected moveFocus(step: 1 | -1): void { + const + nextEl = this.ctx?.getNextFocusableElement(step); + + if (nextEl?.getAttribute('role') !== 'option') { + return; + } + + this.focusNext(nextEl); + this.setARIAActive(nextEl); + } + + /** + * Handler: a keyboard event has occurred + */ + protected onKeydown(e: KeyboardEvent): void { + const + isHorizontal = this.params.orientation === 'horizontal'; + + switch (e.key) { + case KeyCodes.LEFT: + if (isHorizontal) { + this.moveFocus(-1); + } + + return; + + case KeyCodes.UP: + if (isHorizontal) { + return; + } + + this.moveFocus(-1); + break; + + case KeyCodes.RIGHT: + if (isHorizontal) { + this.moveFocus(1); + break; + } + + return; + + case KeyCodes.DOWN: + if (isHorizontal) { + return; + } + + this.moveFocus(1); + break; + + case KeyCodes.HOME: + this.options?.[0]?.focus(); + break; + + case KeyCodes.END: + this.options?.at(-1)?.focus(); + break; + + default: + return; + } + + e.stopPropagation(); + e.preventDefault(); } } diff --git a/src/core/component/directives/aria/roles/listbox/interface.ts b/src/core/component/directives/aria/roles/listbox/interface.ts new file mode 100644 index 0000000000..ae93cea58d --- /dev/null +++ b/src/core/component/directives/aria/roles/listbox/interface.ts @@ -0,0 +1,22 @@ +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ + +import type { HandlerAttachment } from 'core/component/directives/aria/roles/interface'; +import type { Orientation } from 'core/component/directives/aria'; + +export interface ListboxParams { + multiselectable?: boolean; + orientation?: Orientation; + label?: string; + labelledby?: string; +} + +export class ListboxParams { + standAlone: boolean = true; + '@change'?: HandlerAttachment = () => undefined; +} diff --git a/src/core/component/directives/aria/roles/listbox/test/unit/listbox.ts b/src/core/component/directives/aria/roles/listbox/test/unit/listbox.ts index d4fd92d3cb..d5096166b1 100644 --- a/src/core/component/directives/aria/roles/listbox/test/unit/listbox.ts +++ b/src/core/component/directives/aria/roles/listbox/test/unit/listbox.ts @@ -27,7 +27,7 @@ test.describe('v-aria:listbox', () => { test.expect( await target.evaluate((ctx) => { - const el = ctx.unsafe.block?.element('dropdown'); + const el = ctx.unsafe.block?.element('wrapper'); return el?.getAttribute('role'); }) @@ -35,7 +35,7 @@ test.describe('v-aria:listbox', () => { }); test('tabindex is -1', async ({page}) => { - const target = await init(page); + const target = await init(page, {}, 'b-select'); await page.click(selector); @@ -48,17 +48,194 @@ test.describe('v-aria:listbox', () => { ).toBe('-1'); }); + test.describe('stand alone listbox', () => { + + test('tabindex is 0', async ({page}) => { + const target = await init(page, {standAlone: true, label: 'foo'}); + + await page.click(selector); + + test.expect( + await target.evaluate((ctx) => { + const el = ctx.unsafe.block?.element('wrapper'); + + return el?.getAttribute('tabindex'); + }) + ).toBe('0'); + }); + + test('item is selected and unselected', async ({page}) => { + const target = await init(page, {standAlone: true, label: 'foo'}); + + test.expect( + await target.evaluate((ctx) => { + const list = ctx.unsafe.block?.element('wrapper'); + + list.focus(); + list.dispatchEvent(new KeyboardEvent('keydown', {key: 'ArrowDown'})); + + const el = ctx.unsafe.block?.element('wrapper'); + + return el?.getAttribute('aria-activedescendant'); + }) + ).toBe('id1'); + + test.expect( + await target.evaluate((ctx) => { + const el = ctx.unsafe.block?.element('item'); + + document.activeElement?.dispatchEvent(new KeyboardEvent('keydown', {key: ' '})); + + return el?.getAttribute('aria-selected'); + }) + ).toBe('true'); + + test.expect( + await target.evaluate((ctx) => { + const el = ctx.unsafe.block?.element('item'); + + document.activeElement?.dispatchEvent(new KeyboardEvent('keydown', {key: ' '})); + + return el?.getAttribute('aria-selected'); + }) + ).toBe('false'); + }); + + test('item is selected and unselected with multiple mod', async ({page}) => { + const target = await init(page, {standAlone: true, label: 'foo', multiple: true}); + + test.expect( + await target.evaluate((ctx) => { + const list = ctx.unsafe.block?.element('wrapper'); + + list.focus(); + list.dispatchEvent(new KeyboardEvent('keydown', {key: 'ArrowDown'})); + document.activeElement?.dispatchEvent(new KeyboardEvent('keydown', {key: ' '})); + + return document.activeElement?.getAttribute('aria-selected'); + }) + ).toBe('true'); + + test.expect( + await target.evaluate((ctx) => { + const list = ctx.unsafe.block?.element('wrapper'); + + return list.getAttribute('aria-activedescendant'); + }) + ).toBe('id1'); + + test.expect( + await target.evaluate((ctx) => { + const list = ctx.unsafe.block?.element('wrapper'); + list.dispatchEvent(new KeyboardEvent('keydown', {key: 'ArrowDown'})); + + document.activeElement?.dispatchEvent(new KeyboardEvent('keydown', {key: ' '})); + + return document.activeElement?.getAttribute('aria-selected'); + }) + ).toBe('true'); + + test.expect( + await target.evaluate((ctx) => { + const list = ctx.unsafe.block?.element('wrapper'); + + return list.getAttribute('aria-activedescendant'); + }) + ).toBe('id2'); + + test.expect( + await target.evaluate(() => { + document.activeElement?.dispatchEvent(new KeyboardEvent('keydown', {key: ' '})); + + return document.activeElement?.getAttribute('aria-selected'); + }) + ).toBe('false'); + }); + + test('keyboard support with the vertical orientation', async ({page}) => { + const target = await init(page, {standAlone: true, label: 'foo'}); + + test.expect( + await target.evaluate((ctx) => { + const + list = ctx.unsafe.block?.element('wrapper'), + res: Array> = []; + + const dis = (key) => { + document.activeElement?.dispatchEvent(new KeyboardEvent('keydown', {key, bubbles: true})); + res.push(document.activeElement?.id); + }; + + list.focus(); + list.dispatchEvent(new KeyboardEvent('keydown', {key: 'End'})); + + res.push(document.activeElement?.id); + + dis('Home'); + + dis('ArrowDown'); + + dis('ArrowUp'); + + dis('ArrowLeft'); + + dis('ArrowRight'); + + return res; + }) + ).toEqual(['id3', 'id1', 'id2', 'id1', 'id1', 'id1']); + }); + + test('keyboard support with the horizontal orientation', async ({page}) => { + const target = await init(page, {standAlone: true, label: 'foo', orientation: 'horizontal'}); + + test.expect( + await target.evaluate((ctx) => { + const + list = ctx.unsafe.block?.element('wrapper'), + res: Array> = []; + + const dis = (key) => { + document.activeElement?.dispatchEvent(new KeyboardEvent('keydown', {key, bubbles: true})); + res.push(document.activeElement?.id); + }; + + list.focus(); + list.dispatchEvent(new KeyboardEvent('keydown', {key: 'End'})); + + res.push(document.activeElement?.id); + + dis('Home'); + + dis('ArrowRight'); + + dis('ArrowLeft'); + + dis('ArrowDown'); + + dis('ArrowUp'); + + return res; + }) + ).toEqual(['id3', 'id1', 'id2', 'id1', 'id1', 'id1']); + }); + }); + /** * @param page + * @param attrs + * @param component */ - async function init(page: Page): Promise> { - return Component.createComponent(page, 'b-select', { + async function init(page: Page, attrs: Dictionary = {}, component: string = 'b-dummy-listbox'): Promise> { + return Component.createComponent(page, component, { attrs: { 'data-id': 'target', items: [ - {label: 'foo', value: 0}, - {label: 'bar', value: 1} - ] + {value: 1, label: 'item1', id: 'id1'}, + {value: 2, label: 'item2', id: 'id2'}, + {value: 3, label: 'item3', id: 'id3'} + ], + ...attrs } }); } diff --git a/src/core/component/directives/aria/roles/option/index.ts b/src/core/component/directives/aria/roles/option/index.ts index f8d7b025ae..8efe6ccb96 100644 --- a/src/core/component/directives/aria/roles/option/index.ts +++ b/src/core/component/directives/aria/roles/option/index.ts @@ -15,6 +15,10 @@ export class Option extends ARIARole { /** @inheritDoc */ init(): void { this.setAttribute('role', 'option'); + + if (this.el.getAttribute('id') == null) { + throw new TypeError('The `option` role requires the element to have an `id` attribute'); + } } /** @@ -22,6 +26,6 @@ export class Option extends ARIARole { * @param isSelected */ protected onChange(isSelected: boolean): void { - this.el.setAttribute('aria-selected', String(isSelected)); + this.setAttribute('aria-selected', String(isSelected)); } } diff --git a/src/core/component/directives/aria/roles/tab/interface.ts b/src/core/component/directives/aria/roles/tab/interface.ts index 07089cdf37..d494f50f65 100644 --- a/src/core/component/directives/aria/roles/tab/interface.ts +++ b/src/core/component/directives/aria/roles/tab/interface.ts @@ -6,8 +6,8 @@ * https://github.com/V4Fire/Client/blob/master/LICENSE */ -import type { Orientation } from 'base/b-list/interface'; import type { HandlerAttachment } from 'core/component/directives/aria/roles/interface'; +import type { Orientation } from 'core/component/directives/aria'; export interface TabParams { selected: boolean; diff --git a/src/core/component/directives/aria/roles/tabpanel/index.ts b/src/core/component/directives/aria/roles/tabpanel/index.ts index 87f4c3883a..61c1f9551f 100644 --- a/src/core/component/directives/aria/roles/tabpanel/index.ts +++ b/src/core/component/directives/aria/roles/tabpanel/index.ts @@ -12,15 +12,20 @@ */ import { ARIARole } from 'core/component/directives/aria/roles/interface'; +import type { TabpanelParams } from 'core/component/directives/aria/roles/tabpanel/interface'; export class Tabpanel extends ARIARole { + override Params!: TabpanelParams; + /** @inheritDoc */ init(): void { - const {el} = this; - this.setAttribute('role', 'tabpanel'); + const + {label, labelledby} = this.params; - if (!el.hasAttribute('aria-label') && !el.hasAttribute('aria-labelledby')) { - throw new TypeError('The tabpanel role expects a `label` or `labelledby` value to be passed'); + if (label == null && labelledby == null) { + throw new TypeError('The `tabpanel` role expects a `label` or `labelledby` value to be passed'); } + + this.setAttribute('role', 'tabpanel'); } } diff --git a/src/core/component/directives/aria/roles/tabpanel/interface.ts b/src/core/component/directives/aria/roles/tabpanel/interface.ts new file mode 100644 index 0000000000..c8b51237d8 --- /dev/null +++ b/src/core/component/directives/aria/roles/tabpanel/interface.ts @@ -0,0 +1,12 @@ +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ + +export interface TabpanelParams { + label?: string; + labelledby?: string; +} diff --git a/src/core/component/directives/aria/roles/tabpanel/test/unit/tabpanel.ts b/src/core/component/directives/aria/roles/tabpanel/test/unit/tabpanel.ts index be026da536..134b5ae3fb 100644 --- a/src/core/component/directives/aria/roles/tabpanel/test/unit/tabpanel.ts +++ b/src/core/component/directives/aria/roles/tabpanel/test/unit/tabpanel.ts @@ -18,13 +18,21 @@ test.describe('v-aria:tabpanel', () => { }); test('tabpanel must have the `role` attribute', async ({page}) => { - const target = await init(page); + const target = await init(page, {label: 'foo'}); test.expect( await target.evaluate((ctx) => ctx.$el?.getAttribute('role')) ).toBe('tabpanel'); }); + test('tabpanel must have the `label` or `labelledby` params to be passed', async ({page}) => { + const target = await init(page); + + test.expect( + await target.evaluate((ctx) => ctx.$el?.getAttribute('role')) + ).toBe(null); + }); + /** * @param page * @param attrs @@ -32,8 +40,9 @@ test.describe('v-aria:tabpanel', () => { async function init(page: Page, attrs: Dictionary = {}): Promise> { return Component.createComponent(page, 'b-dummy', { attrs: { - 'v-aria:tabpanel': {label: 'foo'}, - ...attrs + 'v-aria:tabpanel': { + ...attrs + } } }); } diff --git a/src/core/component/directives/aria/roles/tree/interface.ts b/src/core/component/directives/aria/roles/tree/interface.ts index 82cf683e5a..c2a7958c23 100644 --- a/src/core/component/directives/aria/roles/tree/interface.ts +++ b/src/core/component/directives/aria/roles/tree/interface.ts @@ -7,9 +7,10 @@ */ import type { HandlerAttachment } from 'core/component/directives/aria/roles/interface'; +import type { Orientation } from 'core/component/directives/aria'; export class TreeParams { root: boolean = false; - orientation: string = 'vertical'; + orientation: Orientation = 'vertical'; '@change': HandlerAttachment = () => undefined; } diff --git a/src/dummies/b-dummy-listbox/CHANGELOG.md b/src/dummies/b-dummy-listbox/CHANGELOG.md new file mode 100644 index 0000000000..19fb3e32d3 --- /dev/null +++ b/src/dummies/b-dummy-listbox/CHANGELOG.md @@ -0,0 +1,16 @@ +Changelog +========= + +> **Tags:** +> - :boom: [Breaking Change] +> - :rocket: [New Feature] +> - :bug: [Bug Fix] +> - :memo: [Documentation] +> - :house: [Internal] +> - :nail_care: [Polish] + +## v3.?.? (2022-0?-??) + +#### :rocket: New Feature + +* Initial release diff --git a/src/dummies/b-dummy-listbox/README.md b/src/dummies/b-dummy-listbox/README.md new file mode 100644 index 0000000000..dd54c4352f --- /dev/null +++ b/src/dummies/b-dummy-listbox/README.md @@ -0,0 +1,3 @@ +# dummies/b-dummy-listbox + +Dummy component to test `aria/roles/listbox`. diff --git a/src/dummies/b-dummy-listbox/b-dummy-listbox.ss b/src/dummies/b-dummy-listbox/b-dummy-listbox.ss new file mode 100644 index 0000000000..46acf2cd8d --- /dev/null +++ b/src/dummies/b-dummy-listbox/b-dummy-listbox.ss @@ -0,0 +1,30 @@ +- namespace [%fileName%] + +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ + +- include 'super/i-block'|b as placeholder + +- template index() extends ['i-block'].index + + - block body + - super + + < ul.&__wrapper v-aria:listbox = {...getAriaConfig('listbox'), label: 'test'} + + < template v-for = (el, i) in items | :key = getItemKey(el, i) + < li.&__item & + :id = el.id | + :value = el.value | + + v-aria:option = getAriaConfig('option', el) | + @click = onItemClick | + @keydown = onItemKeydown + . + < span.&__cell.&__link-value + {{ el.label }} diff --git a/src/dummies/b-dummy-listbox/b-dummy-listbox.styl b/src/dummies/b-dummy-listbox/b-dummy-listbox.styl new file mode 100644 index 0000000000..63b00dd127 --- /dev/null +++ b/src/dummies/b-dummy-listbox/b-dummy-listbox.styl @@ -0,0 +1,15 @@ +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ + +@import "super/i-block/i-block.styl" + +$p = { + +} + +b-dummy-listbox extends i-block diff --git a/src/dummies/b-dummy-listbox/b-dummy-listbox.ts b/src/dummies/b-dummy-listbox/b-dummy-listbox.ts new file mode 100644 index 0000000000..8ecbd8f390 --- /dev/null +++ b/src/dummies/b-dummy-listbox/b-dummy-listbox.ts @@ -0,0 +1,135 @@ +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ + +/** + * [[include:dummies/b-dummy-control-list/README.md]] + * @packageDocumentation + */ + +import { derive } from 'core/functools/trait'; + +import iBlock, { component, prop } from 'super/i-block/i-block'; +import iAccess from 'traits/i-access/i-access'; +import type iItems from 'traits/i-items/i-items'; +import type { Item } from 'base/b-list/interface'; +import type { Orientation } from 'core/component/directives/aria'; + +export * from 'super/i-block/i-block'; + +interface bDummyListbox extends Trait { +} + +@component({ + functional: { + functional: true, + dataProvider: undefined + } +}) + +@derive(iAccess) +class bDummyListbox extends iBlock implements iAccess, iItems { + readonly Item!: Item; + + readonly Items!: Array; + + @prop(Boolean) + readonly multiple: boolean = false; + + @prop(String) + readonly orientation: Orientation = 'vertical'; + + @prop(Array) + readonly items: this['Items'] = []; + + protected activeStore: CanUndef | string>; + + setActive(value: string): void { + if (this.multiple) { + if (Object.isSet(this.activeStore)) { + if (this.activeStore.has(value)) { + this.activeStore.delete(value); + + } else { + this.activeStore.add(value); + } + + } else { + this.activeStore = new Set(); + this.activeStore.add(value); + } + + } else if (this.activeStore !== value) { + this.activeStore = value; + + } else { + this.activeStore = undefined; + } + + const + items = this.block?.elements('item'); + + items?.forEach((el) => { + if (el.getAttribute('value') === value) { + const + mod = this.block?.getElMod(el, 'item', 'active') ?? 'false'; + + this.block?.setElMod(el, 'item', 'active', mod === 'false'); + + } else if (!Object.isSet(this.activeStore)) { + this.block?.setElMod(el, 'item', 'active', false); + } + }); + } + + isActive(value: string): boolean { + if (Object.isSet(this.activeStore)) { + return this.activeStore.has(value); + } + + return this.activeStore === value; + } + + protected getAriaConfig(role: 'listbox' | 'option', item?: this['Item']): Dictionary { + const onChange = (cb) => { + this.on('change', () => cb(this.isActive(String(item?.value)))); + }; + + const listboxConfig = { + standAlone: true, + multiselectable: this.multiple, + orientation: this.orientation + }; + + const optionConfig = { + '@change': onChange + }; + + switch (role) { + case 'listbox': return listboxConfig; + case 'option': return optionConfig; + default: return {}; + } + } + + protected onItemClick(e: Event): void { + const + target = e.target, + value = String(target.getAttribute('value')); + + this.setActive(value); + this.emit('change', this.activeStore); + } + + protected onItemKeydown(e: KeyboardEvent): void { + if (e.key === ' ') { + this.onItemClick(e); + } + } +} + +export default bDummyListbox; diff --git a/src/dummies/b-dummy-listbox/index.js b/src/dummies/b-dummy-listbox/index.js new file mode 100644 index 0000000000..cd6eb97e9f --- /dev/null +++ b/src/dummies/b-dummy-listbox/index.js @@ -0,0 +1,10 @@ +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ + +package('b-dummy-listbox') + .extends('i-block'); diff --git a/src/pages/p-v4-components-demo/index.js b/src/pages/p-v4-components-demo/index.js index 846862b7ea..74ee6bf8d4 100644 --- a/src/pages/p-v4-components-demo/index.js +++ b/src/pages/p-v4-components-demo/index.js @@ -54,10 +54,11 @@ package('p-v4-components-demo') 'b-dummy-state', 'b-dummy-control-list', 'b-dummy-decorators', + 'b-dummy-listbox', components ) .libs([ 'core/cookies', 'models/demo/form' - ]) \ No newline at end of file + ]) From 574bd0738d3f0ddea2a356dc8035dfc1a63fea37 Mon Sep 17 00:00:00 2001 From: Aleksandr Bunin Date: Mon, 3 Oct 2022 21:02:30 +0300 Subject: [PATCH 181/185] replace new methods from iAccess to dom --- .../directives/aria/roles/combobox/index.ts | 13 +- .../directives/aria/roles/listbox/index.ts | 15 +- .../aria/roles/listbox/test/unit/listbox.ts | 6 +- .../directives/aria/roles/tab/index.ts | 8 +- .../directives/aria/roles/treeitem/index.ts | 23 +- src/super/i-block/modules/dom/index.ts | 192 ++++++++++++++++ src/traits/i-access/i-access.ts | 215 +----------------- 7 files changed, 220 insertions(+), 252 deletions(-) diff --git a/src/core/component/directives/aria/roles/combobox/index.ts b/src/core/component/directives/aria/roles/combobox/index.ts index 4385b5ec51..faa36b18ad 100644 --- a/src/core/component/directives/aria/roles/combobox/index.ts +++ b/src/core/component/directives/aria/roles/combobox/index.ts @@ -6,28 +6,23 @@ * https://github.com/V4Fire/Client/blob/master/LICENSE */ -import iAccess from 'traits/i-access/i-access'; -import type { ComponentInterface } from 'super/i-block/i-block'; +import type iBlock from 'super/i-block/i-block'; import { ComboboxParams } from 'core/component/directives/aria/roles/combobox/interface'; import { ARIARole, RoleOptions } from 'core/component/directives/aria/roles/interface'; export class Combobox extends ARIARole { override Params: ComboboxParams = new ComboboxParams(); - override Ctx!: ComponentInterface & iAccess; + override Ctx!: iBlock['unsafe']; override el: HTMLElement; - constructor(options: RoleOptions) { + constructor(options: RoleOptions) { super(options); - if (!iAccess.is(this.ctx)) { - Object.throw('Combobox aria directive expects the component to realize iAccess interface'); - } - const {el} = this; - this.el = this.ctx?.findFocusableElement() ?? el; + this.el = this.ctx?.dom.findFocusableElement() ?? el; } /** @inheritDoc */ diff --git a/src/core/component/directives/aria/roles/listbox/index.ts b/src/core/component/directives/aria/roles/listbox/index.ts index 5239509acd..620774aca8 100644 --- a/src/core/component/directives/aria/roles/listbox/index.ts +++ b/src/core/component/directives/aria/roles/listbox/index.ts @@ -6,7 +6,6 @@ * https://github.com/V4Fire/Client/blob/master/LICENSE */ -import iAccess from 'traits/i-access/i-access'; import type iBlock from 'super/i-block/i-block'; import { ARIARole, KeyCodes } from 'core/component/directives/aria/roles/interface'; @@ -14,7 +13,7 @@ import { ListboxParams } from 'core/component/directives/aria/roles/listbox/inte export class Listbox extends ARIARole { override Params: ListboxParams = new ListboxParams(); - override Ctx!: iBlock['unsafe'] & iAccess; + override Ctx!: iBlock['unsafe']; options: CanUndef; @@ -28,10 +27,6 @@ export class Listbox extends ARIARole { return; } - if (!iAccess.is(this.ctx)) { - throw new TypeError('The `listbox` role requires that a component in whose context the directive is used implement the `iAccess` interface'); - } - if (label == null && labelledby == null) { throw new TypeError('The `listbox` role expects a `label` or `labelledby` value to be passed'); } @@ -46,7 +41,7 @@ export class Listbox extends ARIARole { this.options?.forEach((el) => { el.setAttribute('tabindex', '0'); - this.ctx?.removeAllFromTabSequence(el); + this.ctx?.dom.removeAllFromTabSequence(el); }); }); } @@ -69,8 +64,8 @@ export class Listbox extends ARIARole { * @param el */ protected focusNext(el: AccessibleElement): void { - this.ctx?.removeAllFromTabSequence(this.el); - this.ctx?.restoreAllToTabSequence(el); + this.ctx?.dom.removeAllFromTabSequence(this.el); + this.ctx?.dom.restoreAllToTabSequence(el); el.focus(); } @@ -81,7 +76,7 @@ export class Listbox extends ARIARole { */ protected moveFocus(step: 1 | -1): void { const - nextEl = this.ctx?.getNextFocusableElement(step); + nextEl = this.ctx?.dom.getNextFocusableElement(step); if (nextEl?.getAttribute('role') !== 'option') { return; diff --git a/src/core/component/directives/aria/roles/listbox/test/unit/listbox.ts b/src/core/component/directives/aria/roles/listbox/test/unit/listbox.ts index d5096166b1..0b17523edb 100644 --- a/src/core/component/directives/aria/roles/listbox/test/unit/listbox.ts +++ b/src/core/component/directives/aria/roles/listbox/test/unit/listbox.ts @@ -226,7 +226,11 @@ test.describe('v-aria:listbox', () => { * @param attrs * @param component */ - async function init(page: Page, attrs: Dictionary = {}, component: string = 'b-dummy-listbox'): Promise> { + async function init( + page: Page, + attrs: Dictionary = {}, + component: string = 'b-dummy-listbox' + ): Promise> { return Component.createComponent(page, component, { attrs: { 'data-id': 'target', diff --git a/src/core/component/directives/aria/roles/tab/index.ts b/src/core/component/directives/aria/roles/tab/index.ts index 603d452888..8af9e3ae99 100644 --- a/src/core/component/directives/aria/roles/tab/index.ts +++ b/src/core/component/directives/aria/roles/tab/index.ts @@ -24,7 +24,7 @@ export * from 'core/component/directives/aria/roles/tab/interface'; export class Tab extends ARIARole { override Params: TabParams = new TabParams(); - override Ctx!: iBlock & iAccess; + override Ctx!: iBlock['unsafe']; /** @inheritDoc */ init(): void { @@ -70,7 +70,7 @@ export class Tab extends ARIARole { * Moves focus to the first tab in the tablist */ protected moveFocusToFirstTab(): void { - const firstTab = this.ctx?.findFocusableElement(); + const firstTab = this.ctx?.dom.findFocusableElement(); firstTab?.focus(); } @@ -79,7 +79,7 @@ export class Tab extends ARIARole { */ protected moveFocusToLastTab(): void { const - tabs = this.ctx?.findFocusableElements(); + tabs = this.ctx?.dom.findFocusableElements(); if (tabs == null) { return; @@ -100,7 +100,7 @@ export class Tab extends ARIARole { * @param step */ protected moveFocus(step: 1 | -1): void { - const focusable = this.ctx?.getNextFocusableElement(step); + const focusable = this.ctx?.dom.getNextFocusableElement(step); focusable?.focus(); } diff --git a/src/core/component/directives/aria/roles/treeitem/index.ts b/src/core/component/directives/aria/roles/treeitem/index.ts index c3b4dfa88b..e6cbfb0bdd 100644 --- a/src/core/component/directives/aria/roles/treeitem/index.ts +++ b/src/core/component/directives/aria/roles/treeitem/index.ts @@ -9,7 +9,6 @@ * Copyright © [2022] W3C® (MIT, ERCIM, Keio, Beihang). */ -import iAccess from 'traits/i-access/i-access'; import type iBlock from 'super/i-block/i-block'; import { TreeitemParams } from 'core/component/directives/aria/roles/treeitem/interface'; @@ -17,23 +16,19 @@ import { ARIARole, KeyCodes } from 'core/component/directives/aria/roles/interfa export class Treeitem extends ARIARole { override Params: TreeitemParams = new TreeitemParams(); - override Ctx!: iBlock & iAccess; + override Ctx!: iBlock['unsafe']; /** @inheritDoc */ init(): void { - if (!iAccess.is(this.ctx)) { - Object.throw('Treeitem aria directive expects the component to realize iAccess interface'); - } - this.async.on(this.el, 'keydown', this.onKeyDown.bind(this)); const - muted = this.ctx?.removeAllFromTabSequence(this.el), + muted = this.ctx?.dom.removeAllFromTabSequence(this.el), {firstRootItem, expandable, expanded} = this.params; if (firstRootItem) { if (muted) { - this.ctx?.restoreAllToTabSequence(this.el); + this.ctx?.dom.restoreAllToTabSequence(this.el); } else { this.setAttribute('tabindex', '0'); @@ -61,8 +56,8 @@ export class Treeitem extends ARIARole { * @param el */ protected focusNext(el: AccessibleElement): void { - this.ctx?.removeAllFromTabSequence(this.el); - this.ctx?.restoreAllToTabSequence(el); + this.ctx?.dom.removeAllFromTabSequence(this.el); + this.ctx?.dom.restoreAllToTabSequence(el); el.focus(); } @@ -73,7 +68,7 @@ export class Treeitem extends ARIARole { */ protected moveFocus(step: 1 | -1): void { const - nextEl = this.ctx?.getNextFocusableElement(step); + nextEl = this.ctx?.dom.getNextFocusableElement(step); if (nextEl != null) { this.focusNext(nextEl); @@ -106,7 +101,7 @@ export class Treeitem extends ARIARole { } const - focusableParent = this.ctx?.findFocusableElement(parent); + focusableParent = this.ctx?.dom.findFocusableElement(parent); if (focusableParent != null) { this.focusNext(focusableParent); @@ -118,7 +113,7 @@ export class Treeitem extends ARIARole { */ protected setFocusToFirstItem(): void { const - firstItem = this.ctx?.findFocusableElement(this.params.rootElement); + firstItem = this.ctx?.dom.findFocusableElement(this.params.rootElement); if (firstItem != null) { this.focusNext(firstItem); @@ -130,7 +125,7 @@ export class Treeitem extends ARIARole { */ protected setFocusToLastItem(): void { const - items = this.ctx?.findFocusableElements(this.params.rootElement); + items = this.ctx?.dom.findFocusableElements(this.params.rootElement); if (items == null) { return; diff --git a/src/super/i-block/modules/dom/index.ts b/src/super/i-block/modules/dom/index.ts index 3fd5dc7c43..f1ab9d3f48 100644 --- a/src/super/i-block/modules/dom/index.ts +++ b/src/super/i-block/modules/dom/index.ts @@ -13,6 +13,8 @@ import { memoize } from 'core/promise/sync'; import { deprecated } from 'core/functools/deprecation'; +import { intoIter } from 'core/iter'; +import { sequence } from 'core/iter/combinators'; import { wrapAsDelegateHandler } from 'core/dom'; import type { InViewInitOptions, InViewAdapter } from 'core/dom/in-view'; @@ -27,6 +29,7 @@ import Friend from 'super/i-block/modules/friend'; import { componentRgxp } from 'super/i-block/modules/dom/const'; import { ElCb, inViewInstanceStore, DOMManipulationOptions } from 'super/i-block/modules/dom/interface'; +import { FOCUSABLE_SELECTOR } from 'traits/i-access/const'; export * from 'super/i-block/modules/dom/const'; export * from 'super/i-block/modules/dom/interface'; @@ -504,4 +507,193 @@ export default class DOM extends Friend { component: resolvedCtx }); } + + /** + * Removes all children of the specified element that can be focused from the Tab toggle sequence. + * In effect, these elements are set to -1 for the tabindex attribute. + * + * @param [searchCtx] - a context to search, if not set, the component root element will be used + * @param [opts] - dictionary with options of including the search context, {includeCtx: true} by default + */ + removeAllFromTabSequence( + searchCtx: CanUndef = this.ctx.$el, + opts: {includeCtx: boolean} = {includeCtx: true} + ): boolean { + let + areElementsRemoved = false; + + if (searchCtx == null) { + return areElementsRemoved; + } + + const + ctx = opts.includeCtx ? searchCtx : (searchCtx.nextElementSibling ?? searchCtx); + + const + focusableEls = this.findFocusableElements(ctx); + + for (const el of focusableEls) { + if (!el.hasAttribute('data-tabindex')) { + el.setAttribute('data-tabindex', String(el.tabIndex)); + } + + el.tabIndex = -1; + areElementsRemoved = true; + } + + return areElementsRemoved; + } + + /** + * Restores all children of the specified element that can be focused to the Tab toggle sequence. + * This method is used to restore the state of elements to the state they had before `removeAllFromTabSequence` was + * applied. + * + * @param [searchCtx] - a context to search, if not set, the component root element will be used + * @param [opts] - dictionary with options of including the search context, {includeCtx: true} by default + */ + restoreAllToTabSequence( + searchCtx: CanUndef = this.ctx.$el, + opts: {includeCtx: boolean} = {includeCtx: true} + ): boolean { + let + areElementsRestored = false; + + if (searchCtx == null) { + return areElementsRestored; + } + + let + removedEls = intoIter(searchCtx.querySelectorAll('[data-tabindex]')); + + if (opts.includeCtx && searchCtx.hasAttribute('data-tabindex')) { + removedEls = sequence(removedEls, intoIter([searchCtx])); + } + + for (const elem of removedEls) { + const + originalTabIndex = elem.getAttribute('data-tabindex'); + + if (originalTabIndex != null) { + elem.tabIndex = Number(originalTabIndex); + elem.removeAttribute('data-tabindex'); + areElementsRestored = true; + } + } + + return areElementsRestored; + } + + /** + * Returns the next (or previous) element to which focus will be switched by pressing Tab. + * The method takes a "step" parameter, i.e. you can control the Tab sequence direction. For example, + * by setting the step to `-1` you will get an element that will be switched to focus by pressing Shift+Tab. + * + * @param step + * @param [searchCtx] - a context to search, if not set, document will be used + */ + getNextFocusableElement( + step: 1 | -1, + searchCtx: Element = document.documentElement + ): T | null { + const + {activeElement} = document; + + if (activeElement == null) { + return null; + } + + const + focusableEls = [...this.findFocusableElements(searchCtx, {native: false})], + index = focusableEls.indexOf(activeElement); + + if (index < 0) { + return null; + } + + focusableEls.forEach((el) => { + if (el.tabIndex > 0) { + Object.throw('The tab sequence has an element with tabindex more than 0. The sequence would be different in different browsers. It is strongly recommended not to use tabindexes more than 0.'); + } + }); + + return focusableEls[index + step] ?? null; + } + + /** + * Finds the first non-disabled visible focusable element from the passed context to search and returns it. + * The element that is the search context is also taken into account in the search. + * + * @param [searchCtx] - a context to search, if not set, the component root element will be used + */ + findFocusableElement(searchCtx?: Element): T | null { + const + search = this.findFocusableElements(searchCtx).next(); + + if (search.done) { + return null; + } + + return search.value; + } + + /** + * Finds all non-disabled visible focusable elements and returns an iterator with the found ones. + * The element that is the search context is also taken into account in the search. + * Also expects a dictionary with option of filtration invisible elements. + * If native property is set to true, the method filters invisible elements by css properties + * `disabled`, `visible` and `display`. + * Native in false also adds the filtration by element's current visibility on the screen. + * + * @param [searchCtx] - a context to search, if not set, the component root element will be used + * @param [opts] - dictionary with options of elements' visibility filtration, {native: true} by default + */ + findFocusableElements< + T extends AccessibleElement = AccessibleElement + >(searchCtx: CanUndef = this.ctx.$el, opts: {native: boolean} = {native: true}): IterableIterator { + const + accessibleEls = searchCtx?.querySelectorAll(FOCUSABLE_SELECTOR); + + let + searchIter = intoIter(accessibleEls ?? []); + + if (searchCtx?.matches(FOCUSABLE_SELECTOR)) { + searchIter = sequence(searchIter, intoIter([searchCtx])); + } + + const + focusableWithoutDisabled = filterDisabledElements(searchIter); + + return { + [Symbol.iterator]() { + return this; + }, + + next: focusableWithoutDisabled.next.bind(focusableWithoutDisabled) + }; + + function* filterDisabledElements( + iter: IterableIterator + ): IterableIterator { + for (const el of iter) { + const + rect = el.getBoundingClientRect(); + + if ( + !el.hasAttribute('disabled') && + el.getAttribute('visibility') !== 'hidden' && + el.getAttribute('display') !== 'none' + ) { + if (!opts.native) { + if (rect.height > 0 || rect.width > 0) { + yield el; + } + + } else { + yield el; + } + } + } + } + } } diff --git a/src/traits/i-access/i-access.ts b/src/traits/i-access/i-access.ts index fe8593e930..165051a7ee 100644 --- a/src/traits/i-access/i-access.ts +++ b/src/traits/i-access/i-access.ts @@ -15,14 +15,9 @@ import SyncPromise from 'core/promise/sync'; -import { intoIter } from 'core/iter'; -import { sequence } from 'core/iter/combinators'; - import type iBlock from 'super/i-block/i-block'; import type { ModsDecl, ModEvent } from 'super/i-block/i-block'; -import { FOCUSABLE_SELECTOR } from 'traits/i-access/const'; - export default abstract class iAccess { /** * Trait modifiers @@ -65,7 +60,7 @@ export default abstract class iAccess { } const dict = Object.cast(obj); - return Object.isFunction(dict.removeAllFromTabSequence) && Object.isFunction(dict.getNextFocusableElement); + return Object.isFunction(dict.focus) && Object.isFunction(dict.initModEvents); } /** @@ -180,151 +175,6 @@ export default abstract class iAccess { }); } - /** @see [[iAccess.removeAllFromTabSequence]] */ - static removeAllFromTabSequence: AddSelf = - (component, searchCtx = component.$el): boolean => { - let - areElementsRemoved = false; - - if (searchCtx == null) { - return areElementsRemoved; - } - - const - focusableEls = this.findFocusableElements(component, searchCtx); - - for (const el of focusableEls) { - if (!el.hasAttribute('data-tabindex')) { - el.setAttribute('data-tabindex', String(el.tabIndex)); - } - - el.tabIndex = -1; - areElementsRemoved = true; - } - - return areElementsRemoved; - }; - - /** @see [[iAccess.restoreAllToTabSequence]] */ - static restoreAllToTabSequence: AddSelf = - (component, searchCtx = component.$el): boolean => { - let - areElementsRestored = false; - - if (searchCtx == null) { - return areElementsRestored; - } - - let - removedEls = intoIter(searchCtx.querySelectorAll('[data-tabindex]')); - - if (searchCtx.hasAttribute('data-tabindex')) { - removedEls = sequence(removedEls, intoIter([searchCtx])); - } - - for (const elem of removedEls) { - const - originalTabIndex = elem.getAttribute('data-tabindex'); - - if (originalTabIndex != null) { - elem.tabIndex = Number(originalTabIndex); - elem.removeAttribute('data-tabindex'); - areElementsRestored = true; - } - } - - return areElementsRestored; - }; - - /** @see [[iAccess.getNextFocusableElement]] */ - static getNextFocusableElement: AddSelf = - (component, step, searchCtx = document.documentElement): AccessibleElement | null => { - const - {activeElement} = document; - - if (activeElement == null) { - return null; - } - - const - focusableEls = [...this.findFocusableElements(component, searchCtx, {native: false})], - index = focusableEls.indexOf(activeElement); - - if (index < 0) { - return null; - } - - focusableEls.forEach((el) => { - if (el.tabIndex > 0) { - Object.throw('The tab sequence has an element with tabindex more than 0. The sequence would be different in different browsers. It is strongly recommended not to use tabindexes more than 0.'); - } - }); - - return focusableEls[index + step] ?? null; - }; - - /** @see [[iAccess.findFocusableElement]] */ - static findFocusableElement: AddSelf = - (component, searchCtx?): AccessibleElement | null => { - const - search = this.findFocusableElements(component, searchCtx).next(); - - if (search.done) { - return null; - } - - return search.value; - }; - - /** @see [[iAccess.findFocusableElements]] */ - static findFocusableElements: AddSelf = - (component, searchCtx = component.$el, opts = {native: true}): IterableIterator => { - const - accessibleEls = searchCtx?.querySelectorAll(FOCUSABLE_SELECTOR); - - let - searchIter = intoIter(accessibleEls ?? []); - - if (searchCtx?.matches(FOCUSABLE_SELECTOR)) { - searchIter = sequence(searchIter, intoIter([searchCtx])); - } - - const - focusableWithoutDisabled = filterDisabledElements(searchIter); - - return { - [Symbol.iterator]() { - return this; - }, - - next: focusableWithoutDisabled.next.bind(focusableWithoutDisabled) - }; - - function* filterDisabledElements( - iter: IterableIterator - ): IterableIterator { - for (const el of iter) { - const - rect = el.getBoundingClientRect(); - - if ( - !el.hasAttribute('disabled') && - el.getAttribute('visibility') !== 'hidden' && - el.getAttribute('display') !== 'none' - ) { - if (!opts.native) { - if (rect.height > 0 || rect.width > 0) { - yield el; - } - - } else { - yield el; - } - } - } - } - }; - /** * A Boolean attribute which, if present, indicates that the component should automatically * have focus when the page has finished loading (or when the `

      ` containing the element has been displayed) @@ -381,67 +231,4 @@ export default abstract class iAccess { blur(...args: unknown[]): Promise { return Object.throw(); } - - /** - * Removes all children of the specified element that can be focused from the Tab toggle sequence. - * In effect, these elements are set to -1 for the tabindex attribute. - * - * @param [searchCtx] - a context to search, if not set, the component root element will be used - */ - removeAllFromTabSequence(searchCtx?: Element): boolean { - return Object.throw(); - } - - /** - * Restores all children of the specified element that can be focused to the Tab toggle sequence. - * This method is used to restore the state of elements to the state they had before `removeAllFromTabSequence` was - * applied. - * - * @param [searchCtx] - a context to search, if not set, the component root element will be used - */ - restoreAllToTabSequence(searchCtx?: Element): boolean { - return Object.throw(); - } - - /** - * Returns the next (or previous) element to which focus will be switched by pressing Tab. - * The method takes a "step" parameter, i.e. you can control the Tab sequence direction. For example, - * by setting the step to `-1` you will get an element that will be switched to focus by pressing Shift+Tab. - * - * @param step - * @param [searchCtx] - a context to search, if not set, document will be used - */ - getNextFocusableElement( - step: 1 | -1, - searchCtx?: Element - ): T | null { - return Object.throw(); - } - - /** - * Finds the first non-disabled visible focusable element from the passed context to search and returns it. - * The element that is the search context is also taken into account in the search. - * - * @param [searchCtx] - a context to search, if not set, the component root element will be used - */ - findFocusableElement(searchCtx?: Element): T | null { - return Object.throw(); - } - - /** - * Finds all non-disabled visible focusable elements and returns an iterator with the found ones. - * The element that is the search context is also taken into account in the search. - * Also expects a dictionary with option of filtration invisible elements. - * If native property is set to true, the method filters invisible elements by css properties - * `disabled`, `visible` and `display`. - * Native in false also adds the filtration by element's current visibility on the screen. - * - * @param [searchCtx] - a context to search, if not set, the component root element will be used - * @param [opts] - dictionary with options of elements' visibility filtration, {native: true} by default - */ - findFocusableElements< - T extends AccessibleElement = AccessibleElement - >(searchCtx?: Element, opts?: {native: boolean}): IterableIterator { - return Object.throw(); - } } From 85baf073192e1d5af4b48399f652142ba46ed09f Mon Sep 17 00:00:00 2001 From: Aleksandr Bunin Date: Tue, 4 Oct 2022 19:35:04 +0300 Subject: [PATCH 182/185] add dialog logic --- components-lock.json | 40 +++++++++- src/base/b-window/b-window.ss | 8 +- src/base/b-window/b-window.ts | 10 +++ .../directives/aria/roles/dialog/index.ts | 71 +++++++++++++++++- .../directives/aria/roles/dialog/interface.ts | 19 +++++ .../aria/roles/dialog/test/unit/dialog.ts | 75 +++++++++++++++++-- .../directives/aria/roles/interface.ts | 3 +- src/dummies/b-dummy-dialog/CHANGELOG.md | 16 ++++ src/dummies/b-dummy-dialog/README.md | 3 + src/dummies/b-dummy-dialog/b-dummy-dialog.ss | 31 ++++++++ .../b-dummy-dialog/b-dummy-dialog.styl | 15 ++++ src/dummies/b-dummy-dialog/b-dummy-dialog.ts | 26 +++++++ src/dummies/b-dummy-dialog/index.js | 9 +++ .../b-dummy-listbox/b-dummy-listbox.ss | 24 +++--- src/pages/p-v4-components-demo/index.js | 1 + 15 files changed, 324 insertions(+), 27 deletions(-) create mode 100644 src/core/component/directives/aria/roles/dialog/interface.ts create mode 100644 src/dummies/b-dummy-dialog/CHANGELOG.md create mode 100644 src/dummies/b-dummy-dialog/README.md create mode 100644 src/dummies/b-dummy-dialog/b-dummy-dialog.ss create mode 100644 src/dummies/b-dummy-dialog/b-dummy-dialog.styl create mode 100644 src/dummies/b-dummy-dialog/b-dummy-dialog.ts create mode 100644 src/dummies/b-dummy-dialog/index.js diff --git a/components-lock.json b/components-lock.json index 8078ec502f..f486ee7064 100644 --- a/components-lock.json +++ b/components-lock.json @@ -1,5 +1,5 @@ { - "hash": "642520426f817d93ef77303abd6014f9507e63f6c0da7de6dad758d8a2139632", + "hash": "02638d9c8a09972771dc7664065e29dfbb9cb4fbc36405d85f85e5230ec4cba6", "data": { "%data": "%data:Map", "%data:Map": [ @@ -233,6 +233,38 @@ "etpl": null } ], + [ + "b-dummy-dialog", + { + "index": "src/dummies/b-dummy-dialog/index.js", + "declaration": { + "name": "b-dummy-dialog", + "parent": null, + "dependencies": [], + "libs": [] + }, + "name": "b-dummy-dialog", + "parent": null, + "dependencies": [], + "libs": [], + "resolvedLibs": { + "%data": "%data:Set", + "%data:Set": [] + }, + "resolvedOwnLibs": { + "%data": "%data:Set", + "%data:Set": [] + }, + "type": "block", + "mixin": false, + "logic": "src/dummies/b-dummy-dialog/b-dummy-dialog.ts", + "styles": [ + "src/dummies/b-dummy-dialog/b-dummy-dialog.styl" + ], + "tpl": "src/dummies/b-dummy-dialog/b-dummy-dialog.ss", + "etpl": null + } + ], [ "b-dummy-lfc", { @@ -2030,7 +2062,8 @@ "b-dummy-state", "b-dummy-control-list", "b-dummy-decorators", - "b-dummy-listbox" + "b-dummy-listbox", + "b-dummy-dialog" ], "libs": [ "core/cookies", @@ -2075,7 +2108,8 @@ "b-dummy-state", "b-dummy-control-list", "b-dummy-decorators", - "b-dummy-listbox" + "b-dummy-listbox", + "b-dummy-dialog" ], "libs": [ "core/cookies", diff --git a/src/base/b-window/b-window.ss b/src/base/b-window/b-window.ss index 38ecae2b96..2a788b24bc 100644 --- a/src/base/b-window/b-window.ss +++ b/src/base/b-window/b-window.ss @@ -26,7 +26,13 @@ < :section.&__window & ref = window | - v-aria:dialog.#title + :v-attrs = title ? + { + 'v-aria:dialog': {...getAriaConfig(), label: title} + } : + { + 'v-aria:dialog.#title': getAriaConfig() + } . - if thirdPartySlots < template v-if = slotName diff --git a/src/base/b-window/b-window.ts b/src/base/b-window/b-window.ts index f960ac5cd6..a3624c5d86 100644 --- a/src/base/b-window/b-window.ts +++ b/src/base/b-window/b-window.ts @@ -314,6 +314,16 @@ class bWindow extends iData implements iVisible, iWidth, iOpenToggle, iLockPageS super.beforeDestroy(); this.removeRootMod('hidden'); } + + /** + * Returns a dictionary with configurations for the `v-aria` directive + */ + protected getAriaConfig(): Dictionary { + return { + '@open': (cb) => this.on('open', cb), + '@close': (cb) => this.on('close', cb) + }; + } } export default bWindow; diff --git a/src/core/component/directives/aria/roles/dialog/index.ts b/src/core/component/directives/aria/roles/dialog/index.ts index 93646220e2..87fd660024 100644 --- a/src/core/component/directives/aria/roles/dialog/index.ts +++ b/src/core/component/directives/aria/roles/dialog/index.ts @@ -6,12 +6,79 @@ * https://github.com/V4Fire/Client/blob/master/LICENSE */ -import { ARIARole } from 'core/component/directives/aria/roles/interface'; +import { ARIARole, KeyCodes } from 'core/component/directives/aria/roles/interface'; +import { DialogParams } from 'core/component/directives/aria/roles/dialog/interface'; +import type iBlock from 'super/i-block/i-block'; export class Dialog extends ARIARole { + override Params: DialogParams = new DialogParams(); + override Ctx!: iBlock['unsafe']; + + protected previousFocusedElement: Nullable; + protected focusableElements: AccessibleElement[] = []; + /** @inheritDoc */ - init(): void { + async init(): Promise { this.setAttribute('role', 'dialog'); this.setAttribute('aria-modal', 'true'); + + if (this.ctx == null) { + return; + } + + this.focusableElements = [...this.ctx.dom.findFocusableElements(this.el)]; + + this.async.on(this.el, 'keydown', this.onKeydown.bind(this)); + + await this.ctx.nextTick(); + + const + {label, labelledby} = this.params; + + if (label == null && labelledby == null && !this.el.hasAttribute('aria-labelledby')) { + throw new TypeError('The `dialog` role expects a `label` or `labelledby` value to be passed'); + } + } + + /** + * Handler: the dialog has been opened + */ + protected async onOpen(): Promise { + await this.ctx?.nextTick(); + + if (this.previousFocusedElement == null) { + this.previousFocusedElement = document.activeElement; + } + + this.ctx?.dom.removeAllFromTabSequence(document.body); + this.ctx?.dom.restoreAllToTabSequence(this.el); + this.ctx?.dom.findFocusableElement(this.el)?.focus(); + } + + /** + * Handler: the dialog has been closed + */ + protected async onClose(): Promise { + await this.ctx?.nextTick(); + + this.ctx?.dom.restoreAllToTabSequence(document.body); + + this.previousFocusedElement?.focus(); + this.previousFocusedElement = null; + } + + /** + * Handler: a keyboard event has occurred + */ + protected onKeydown(e: KeyboardEvent): void { + if (e.key === KeyCodes.TAB) { + + if (document.activeElement === this.focusableElements.at(-1)) { + this.focusableElements[0]?.focus(); + + e.stopPropagation(); + e.preventDefault(); + } + } } } diff --git a/src/core/component/directives/aria/roles/dialog/interface.ts b/src/core/component/directives/aria/roles/dialog/interface.ts new file mode 100644 index 0000000000..fd7bdb5bf7 --- /dev/null +++ b/src/core/component/directives/aria/roles/dialog/interface.ts @@ -0,0 +1,19 @@ +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ + +import type { HandlerAttachment } from 'core/component/directives/aria/roles/interface'; + +export interface DialogParams { + label?: string; + labelledby?: string; +} + +export class DialogParams { + '@open': HandlerAttachment = () => undefined; + '@close': HandlerAttachment = () => undefined; +} diff --git a/src/core/component/directives/aria/roles/dialog/test/unit/dialog.ts b/src/core/component/directives/aria/roles/dialog/test/unit/dialog.ts index 5031e0debe..a54ae8241f 100644 --- a/src/core/component/directives/aria/roles/dialog/test/unit/dialog.ts +++ b/src/core/component/directives/aria/roles/dialog/test/unit/dialog.ts @@ -8,11 +8,12 @@ import type { JSHandle, Page } from 'playwright'; import type iBlock from 'super/i-block/i-block'; +import type { ComponentElement } from 'super/i-block/i-block'; import test from 'tests/config/unit/test'; import Component from 'tests/helpers/component'; -test.describe('v-aria:dialog', () => { +test.describe.only('v-aria:dialog', () => { test.beforeEach(async ({demoPage}) => { await demoPage.goto(); }); @@ -21,10 +22,11 @@ test.describe('v-aria:dialog', () => { const target = await init(page); test.expect( - await target.evaluate((ctx) => { - const el = ctx.unsafe.block?.element('window'); + await target.evaluate(() => { + const + elem = >document.querySelector('#window'); - return el?.getAttribute('role'); + return elem.component?.unsafe.block?.element('window')?.getAttribute('role'); }) ).toBe('dialog'); }); @@ -33,18 +35,75 @@ test.describe('v-aria:dialog', () => { const target = await init(page); test.expect( - await target.evaluate((ctx) => { - const el = ctx.unsafe.block?.element('window'); + await target.evaluate(() => { + const + elem = >document.querySelector('#window'); - return el?.getAttribute('aria-modal'); + return elem.component?.unsafe.block?.element('window')?.getAttribute('aria-modal'); }) ).toBe('true'); }); + test('aria-label is set', async ({page}) => { + const target = await init(page); + + test.expect( + await target.evaluate(() => { + const + elem = >document.querySelector('#window'); + + return elem.component?.unsafe.block?.element('window')?.getAttribute('aria-label'); + }) + ).toBe('Title'); + }); + + test('tab indexes are correct', async ({page}) => { + const target = await init(page); + + test.expect( + await target.evaluate(async (ctx) => { + const + openBtn = document.querySelector('#openBtn'), + closeBtn = document.querySelector('#closeBtn'), + res: Array> = []; + + openBtn.click(); + await ctx.nextTick(); + + res.push(openBtn.getAttribute('tabindex')); + res.push(closeBtn.getAttribute('tabindex')); + + return res; + }) + ).toEqual(['-1', '0']); + }); + + test.only('previous focused element get focus', async ({page}) => { + const target = await init(page); + + test.expect( + await target.evaluate(async (ctx) => { + const + openBtn = document.querySelector('#openBtn'), + closeBtn = document.querySelector('#closeBtn'); + + openBtn.focus(); + openBtn.dispatchEvent(new KeyboardEvent('keydown', {key: 'Enter', bubbles: true})); + await ctx.nextTick(); + + closeBtn.focus(); + closeBtn.dispatchEvent(new KeyboardEvent('keydown', {key: 'Enter', bubbles: true})); + await ctx.nextTick(); + + return document.activeElement?.id; + }) + ).toEqual('openBtn'); + }); + /** * @param page */ async function init(page: Page): Promise> { - return Component.createComponent(page, 'b-window'); + return Component.createComponent(page, 'b-dummy-dialog'); } }); diff --git a/src/core/component/directives/aria/roles/interface.ts b/src/core/component/directives/aria/roles/interface.ts index d3aaae78b0..e42449f22f 100644 --- a/src/core/component/directives/aria/roles/interface.ts +++ b/src/core/component/directives/aria/roles/interface.ts @@ -92,5 +92,6 @@ export const enum KeyCodes { LEFT = 'ArrowLeft', UP = 'ArrowUp', RIGHT = 'ArrowRight', - DOWN = 'ArrowDown' + DOWN = 'ArrowDown', + TAB = 'Tab' } diff --git a/src/dummies/b-dummy-dialog/CHANGELOG.md b/src/dummies/b-dummy-dialog/CHANGELOG.md new file mode 100644 index 0000000000..19fb3e32d3 --- /dev/null +++ b/src/dummies/b-dummy-dialog/CHANGELOG.md @@ -0,0 +1,16 @@ +Changelog +========= + +> **Tags:** +> - :boom: [Breaking Change] +> - :rocket: [New Feature] +> - :bug: [Bug Fix] +> - :memo: [Documentation] +> - :house: [Internal] +> - :nail_care: [Polish] + +## v3.?.? (2022-0?-??) + +#### :rocket: New Feature + +* Initial release diff --git a/src/dummies/b-dummy-dialog/README.md b/src/dummies/b-dummy-dialog/README.md new file mode 100644 index 0000000000..9e61d67d58 --- /dev/null +++ b/src/dummies/b-dummy-dialog/README.md @@ -0,0 +1,3 @@ +# dummies/b-dummy-dialog + +Dummy component to test `aria/roles/dialog`. diff --git a/src/dummies/b-dummy-dialog/b-dummy-dialog.ss b/src/dummies/b-dummy-dialog/b-dummy-dialog.ss new file mode 100644 index 0000000000..8a351f47c0 --- /dev/null +++ b/src/dummies/b-dummy-dialog/b-dummy-dialog.ss @@ -0,0 +1,31 @@ +- namespace [%fileName%] + +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ + +- include 'super/i-block'|b as placeholder + +- template index() extends ['i-block'].index + + - block body + - super + + < b-window ref = window | :id = 'window' | :title = 'Title' | @keydown = onKeyDown + Window content + + < div + < button @click = $refs.window.close() | :id = 'closeBtn' + Close the window + + < button :id = 'btn1' + Do something + < button :id = 'btn2' + Do something#2 + + < button @click = $refs.window.open() | :id = 'openBtn' + Open the window diff --git a/src/dummies/b-dummy-dialog/b-dummy-dialog.styl b/src/dummies/b-dummy-dialog/b-dummy-dialog.styl new file mode 100644 index 0000000000..3bca376090 --- /dev/null +++ b/src/dummies/b-dummy-dialog/b-dummy-dialog.styl @@ -0,0 +1,15 @@ +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ + +@import "super/i-block/i-block.styl" + +$p = { + +} + +b-dummy-dialog extends i-block diff --git a/src/dummies/b-dummy-dialog/b-dummy-dialog.ts b/src/dummies/b-dummy-dialog/b-dummy-dialog.ts new file mode 100644 index 0000000000..41ece1eba6 --- /dev/null +++ b/src/dummies/b-dummy-dialog/b-dummy-dialog.ts @@ -0,0 +1,26 @@ +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ + +/** + * [[include:dummies/b-dummy-text/README.md]] + * @packageDocumentation + */ + +import { component } from 'super/i-input-text/i-input-text'; +import iBlock from 'super/i-block/i-block'; + +@component({ + functional: { + functional: true, + dataProvider: undefined + } +}) + +export default class bDummyDialog extends iBlock { + +} diff --git a/src/dummies/b-dummy-dialog/index.js b/src/dummies/b-dummy-dialog/index.js new file mode 100644 index 0000000000..d568c46519 --- /dev/null +++ b/src/dummies/b-dummy-dialog/index.js @@ -0,0 +1,9 @@ +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ + +package('b-dummy-dialog'); diff --git a/src/dummies/b-dummy-listbox/b-dummy-listbox.ss b/src/dummies/b-dummy-listbox/b-dummy-listbox.ss index 46acf2cd8d..f9058fb476 100644 --- a/src/dummies/b-dummy-listbox/b-dummy-listbox.ss +++ b/src/dummies/b-dummy-listbox/b-dummy-listbox.ss @@ -13,18 +13,18 @@ - template index() extends ['i-block'].index - block body - - super + - super - < ul.&__wrapper v-aria:listbox = {...getAriaConfig('listbox'), label: 'test'} + < ul.&__wrapper v-aria:listbox = {...getAriaConfig('listbox'), label: 'test'} - < template v-for = (el, i) in items | :key = getItemKey(el, i) - < li.&__item & - :id = el.id | - :value = el.value | + < template v-for = (el, i) in items | :key = getItemKey(el, i) + < li.&__item & + :id = el.id | + :value = el.value | - v-aria:option = getAriaConfig('option', el) | - @click = onItemClick | - @keydown = onItemKeydown - . - < span.&__cell.&__link-value - {{ el.label }} + v-aria:option = getAriaConfig('option', el) | + @click = onItemClick | + @keydown = onItemKeydown + . + < span.&__cell.&__link-value + {{ el.label }} diff --git a/src/pages/p-v4-components-demo/index.js b/src/pages/p-v4-components-demo/index.js index 74ee6bf8d4..dd8f17048b 100644 --- a/src/pages/p-v4-components-demo/index.js +++ b/src/pages/p-v4-components-demo/index.js @@ -55,6 +55,7 @@ package('p-v4-components-demo') 'b-dummy-control-list', 'b-dummy-decorators', 'b-dummy-listbox', + 'b-dummy-dialog', components ) From f5ec456b10649234e33af6f1fe2b843a1c9a5973 Mon Sep 17 00:00:00 2001 From: Aleksandr Bunin Date: Wed, 5 Oct 2022 12:55:48 +0300 Subject: [PATCH 183/185] add accessibility methods to widgets --- src/base/b-list/b-list.ts | 70 +++++++++++++++++++ src/base/b-tree/b-tree.ts | 53 ++++++++++++++ .../aria/roles/dialog/test/unit/dialog.ts | 4 +- src/form/b-select/b-select.ts | 17 ++++- 4 files changed, 140 insertions(+), 4 deletions(-) diff --git a/src/base/b-list/b-list.ts b/src/base/b-list/b-list.ts index 65822e4c9d..adf07eabd5 100644 --- a/src/base/b-list/b-list.ts +++ b/src/base/b-list/b-list.ts @@ -271,6 +271,76 @@ class bList extends iData implements iVisible, iWidth, iItems, iAccess { protected activeStore!: this['Active']; + /** @see [[iAccess.focus]] */ + focus(): Promise { + const + element = this.block?.element('link'); + + if (element != null) { + element.focus(); + return iAccess.focus(this); + } + + return SyncPromise.resolve(false); + } + + /** @see [[iAccess.blur]] */ + blur(): Promise { + const + active = document.activeElement; + + if (active != null && this.$el?.contains(active)) { + active.blur(); + return iAccess.blur(this); + } + + return SyncPromise.resolve(false); + } + + /** @see [[iAccess.disable]] */ + disable(): Promise { + let + areDisabled = false; + + const + elements = this.dom.findFocusableElements(this.$el); + + for (const element of elements) { + element.setAttribute('disabled', 'true'); + areDisabled = true; + } + + if (areDisabled) { + return iAccess.disable(this); + } + + return SyncPromise.resolve(areDisabled); + } + + /** @see [[iAccess.enable]] */ + enable(): Promise { + let + areEnabled = false; + + if (this.$el == null) { + return SyncPromise.resolve(areEnabled); + } + + const + elements = this.$el.querySelectorAll('[disabled="true"]'); + + for (const element of Array.from(elements)) { + element.removeAttribute('disabled'); + areEnabled = true; + } + + if (areEnabled) { + return iAccess.enable(this); + } + + return SyncPromise.resolve(areEnabled); + } + /** * A link to the active item element. * If the component is switched to the `multiple` mode, the getter will return an array of elements. diff --git a/src/base/b-tree/b-tree.ts b/src/base/b-tree/b-tree.ts index 2ecfc4b363..3d0c4e0612 100644 --- a/src/base/b-tree/b-tree.ts +++ b/src/base/b-tree/b-tree.ts @@ -21,6 +21,7 @@ import symbolGenerator from 'core/symbol'; import { derive } from 'core/functools/trait'; +import SyncPromise from 'core/promise/sync'; import iItems, { IterationKey } from 'traits/i-items/i-items'; import iData, { component, prop, field, TaskParams, TaskI } from 'super/i-data/i-data'; import iAccess from 'traits/i-access/i-access'; @@ -129,6 +130,58 @@ class bTree extends iData implements iItems, iAccess { @field((o) => o.sync.link()) items!: this['Items']; + /** @see [[iAccess.focus]] */ + focus(): Promise { + const + element = this.dom.findFocusableElement(this.top != null ? this.top.$el : this.$el); + + if (element != null) { + element.focus(); + return iAccess.focus(this); + } + + return SyncPromise.resolve(false); + } + + /** @see [[iAccess.blur]] */ + blur(): Promise { + const + active = document.activeElement; + + if (active != null && this.$el?.contains(active)) { + active.blur(); + return iAccess.blur(this); + } + + return SyncPromise.resolve(false); + } + + /** @see [[iAccess.disable]] */ + disable(): Promise { + const + element = this.dom.findFocusableElement(this.top != null ? this.top.$el : this.$el); + + if (element != null) { + element.setAttribute('disabled', 'true'); + return iAccess.disable(this); + } + + return SyncPromise.resolve(false); + } + + /** @see [[iAccess.enable]] */ + enable(): Promise { + const + element = this.$el?.querySelector('[disabled="true"]'); + + if (element != null) { + element.removeAttribute('disabled'); + return iAccess.enable(this); + } + + return SyncPromise.resolve(false); + } + /** * Parameters for async render tasks */ diff --git a/src/core/component/directives/aria/roles/dialog/test/unit/dialog.ts b/src/core/component/directives/aria/roles/dialog/test/unit/dialog.ts index a54ae8241f..982dc235cb 100644 --- a/src/core/component/directives/aria/roles/dialog/test/unit/dialog.ts +++ b/src/core/component/directives/aria/roles/dialog/test/unit/dialog.ts @@ -13,7 +13,7 @@ import type { ComponentElement } from 'super/i-block/i-block'; import test from 'tests/config/unit/test'; import Component from 'tests/helpers/component'; -test.describe.only('v-aria:dialog', () => { +test.describe('v-aria:dialog', () => { test.beforeEach(async ({demoPage}) => { await demoPage.goto(); }); @@ -78,7 +78,7 @@ test.describe.only('v-aria:dialog', () => { ).toEqual(['-1', '0']); }); - test.only('previous focused element get focus', async ({page}) => { + test('previous focused element get focus', async ({page}) => { const target = await init(page); test.expect( diff --git a/src/form/b-select/b-select.ts b/src/form/b-select/b-select.ts index e8582fc062..e14b56a086 100644 --- a/src/form/b-select/b-select.ts +++ b/src/form/b-select/b-select.ts @@ -64,6 +64,7 @@ import type { UnsafeBSelect } from 'form/b-select/interface'; +import iAccess from 'traits/i-access/i-access'; export * from 'form/b-input/b-input'; export * from 'traits/i-open-toggle/i-open-toggle'; @@ -84,8 +85,8 @@ interface bSelect extends Trait {} } }) -@derive(iOpenToggle) -class bSelect extends iInputText implements iOpenToggle, iItems { +@derive(iOpenToggle, iAccess) +class bSelect extends iInputText implements iOpenToggle, iItems, iAccess { override readonly Value!: Value; override readonly FormValue!: FormValue; @@ -777,6 +778,18 @@ class bSelect extends iInputText implements iOpenToggle, iItems { await on.openedChange(this, e); } + /** @see [[iAccess.focus]] */ + override focus(): Promise { + this.onFocus(); + return iAccess.focus(this); + } + + /** @see [[iAccess.blur]] */ + override blur(): Promise { + void this.close(); + return iAccess.blur(this); + } + /** * Sets the scroll position to the first marked or selected item */ From 2fc6de725b9372c23f70fdd558d7669ab1fc1443 Mon Sep 17 00:00:00 2001 From: Aleksandr Bunin Date: Fri, 7 Oct 2022 01:30:28 +0300 Subject: [PATCH 184/185] fix methods --- src/base/b-list/b-list.ts | 74 ++++++++++++++++++++++++--------------- src/base/b-tree/b-tree.ts | 57 +++++++++++++++++++++++------- 2 files changed, 90 insertions(+), 41 deletions(-) diff --git a/src/base/b-list/b-list.ts b/src/base/b-list/b-list.ts index adf07eabd5..b6fa42e29d 100644 --- a/src/base/b-list/b-list.ts +++ b/src/base/b-list/b-list.ts @@ -28,7 +28,21 @@ import iVisible from 'traits/i-visible/i-visible'; import iWidth from 'traits/i-width/i-width'; import iItems, { IterationKey } from 'traits/i-items/i-items'; import iAccess from 'traits/i-access/i-access'; -import iData, { component, prop, field, system, computed, hook, watch, ModsDecl } from 'super/i-data/i-data'; +import type iBlock from 'super/i-block/i-block'; + +import iData, { + + component, + prop, + field, + system, + computed, + hook, + watch, + ModsDecl, + ComponentElement + +} from 'super/i-data/i-data'; import type { Active, Item, Items } from 'base/b-list/interface'; import type { Orientation } from 'core/component/directives/aria'; @@ -299,46 +313,50 @@ class bList extends iData implements iVisible, iWidth, iItems, iAccess { /** @see [[iAccess.disable]] */ disable(): Promise { - let - areDisabled = false; - const - elements = this.dom.findFocusableElements(this.$el); + items = this.block?.elements>('link'); - for (const element of elements) { - element.setAttribute('disabled', 'true'); - areDisabled = true; - } + items?.forEach((item) => { + const + {component} = item; - if (areDisabled) { - return iAccess.disable(this); - } + if (component == null) { + item.setAttribute('disabled', 'true'); + return; + } - return SyncPromise.resolve(areDisabled); + if (iAccess.is(component)) { + return component.disable(); + } + + void iAccess.disable(component); + }); + + return iAccess.disable(this); } /** @see [[iAccess.enable]] */ enable(): Promise { - let - areEnabled = false; + const + items = this.block?.elements>('link'); - if (this.$el == null) { - return SyncPromise.resolve(areEnabled); - } + items?.forEach((item) => { + const + {component} = item; - const - elements = this.$el.querySelectorAll('[disabled="true"]'); + if (component == null) { + item.removeAttribute('disabled'); + return; + } - for (const element of Array.from(elements)) { - element.removeAttribute('disabled'); - areEnabled = true; - } + if (iAccess.is(component)) { + return component.enable(); + } - if (areEnabled) { - return iAccess.enable(this); - } + void iAccess.enable(component); + }); - return SyncPromise.resolve(areEnabled); + return iAccess.enable(this); } /** diff --git a/src/base/b-tree/b-tree.ts b/src/base/b-tree/b-tree.ts index 3d0c4e0612..869f33a71f 100644 --- a/src/base/b-tree/b-tree.ts +++ b/src/base/b-tree/b-tree.ts @@ -23,8 +23,9 @@ import { derive } from 'core/functools/trait'; import SyncPromise from 'core/promise/sync'; import iItems, { IterationKey } from 'traits/i-items/i-items'; -import iData, { component, prop, field, TaskParams, TaskI } from 'super/i-data/i-data'; +import iData, { component, prop, field, TaskParams, TaskI, ComponentElement } from 'super/i-data/i-data'; import iAccess from 'traits/i-access/i-access'; +import type iBlock from 'super/i-block/i-block'; import type { Item, RenderFilter } from 'base/b-tree/interface'; import type { Orientation } from 'core/component/directives/aria'; @@ -159,27 +160,57 @@ class bTree extends iData implements iItems, iAccess { /** @see [[iAccess.disable]] */ disable(): Promise { const - element = this.dom.findFocusableElement(this.top != null ? this.top.$el : this.$el); + items = this.block?.elements>('item'), + children = this.block?.elements>('child'); - if (element != null) { - element.setAttribute('disabled', 'true'); - return iAccess.disable(this); - } + items?.forEach(disableElement); + children?.forEach(disableElement); - return SyncPromise.resolve(false); + return iAccess.disable(this); + + function disableElement(element: ComponentElement) { + const + {component} = element; + + if (component == null) { + element.setAttribute('disabled', 'true'); + return; + } + + if (iAccess.is(component)) { + return component.disable(); + } + + void iAccess.disable(component); + } } /** @see [[iAccess.enable]] */ enable(): Promise { const - element = this.$el?.querySelector('[disabled="true"]'); + items = this.block?.elements>('item'), + children = this.block?.elements>('child'); - if (element != null) { - element.removeAttribute('disabled'); - return iAccess.enable(this); - } + items?.forEach(enableElement); + children?.forEach(enableElement); - return SyncPromise.resolve(false); + return iAccess.enable(this); + + function enableElement(element: ComponentElement) { + const + {component} = element; + + if (component == null) { + element.removeAttribute('disabled'); + return; + } + + if (iAccess.is(component)) { + return component.enable(); + } + + void iAccess.enable(component); + } } /** From f9009d38e97b94ebea89105e56c1ba811406fe3d Mon Sep 17 00:00:00 2001 From: Aleksandr Bunin Date: Sat, 8 Oct 2022 10:12:45 +0300 Subject: [PATCH 185/185] refactor iInput --- src/super/i-input/CHANGELOG.md | 4 ++++ src/super/i-input/i-input.ts | 12 ------------ 2 files changed, 4 insertions(+), 12 deletions(-) diff --git a/src/super/i-input/CHANGELOG.md b/src/super/i-input/CHANGELOG.md index c97e0a31ae..dfe015cf6d 100644 --- a/src/super/i-input/CHANGELOG.md +++ b/src/super/i-input/CHANGELOG.md @@ -15,6 +15,10 @@ Changelog * Now the component derives `iAccess` +#### :house: Internal + +* Removed `enable` and `disable` methods + ## v3.0.0-rc.199 (2021-06-16) #### :boom: Breaking Change diff --git a/src/super/i-input/i-input.ts b/src/super/i-input/i-input.ts index 7ac86b8d17..6561e0df24 100644 --- a/src/super/i-input/i-input.ts +++ b/src/super/i-input/i-input.ts @@ -668,18 +668,6 @@ abstract class iInput extends iData implements iVisible, iAccess { @system() private validationMsg?: string; - /** @see [[iAccess.enable]] */ - @p({replace: false}) - enable(): Promise { - return iAccess.enable(this); - } - - /** @see [[iAccess.disable]] */ - @p({replace: false}) - disable(): Promise { - return iAccess.disable(this); - } - /** @see [[iAccess.focus]] */ @p({replace: false}) @wait('ready', {label: $$.focus})