From a9b5ede2cee575ec6aa30e4492868d8d1d3c88fa Mon Sep 17 00:00:00 2001 From: maks Date: Sat, 17 Aug 2024 13:11:06 +0100 Subject: [PATCH 1/4] feat: #506 - accessibility added --- docs | 2 +- src/components/overlay/keyboard.service.ts | 3 +- src/components/revoGrid/revo-grid.tsx | 73 +++++++++------- src/plugins/wcag/index.ts | 99 ++++++++++++++++++++++ 4 files changed, 145 insertions(+), 32 deletions(-) create mode 100644 src/plugins/wcag/index.ts diff --git a/docs b/docs index 87985133..b5e4ec43 160000 --- a/docs +++ b/docs @@ -1 +1 @@ -Subproject commit 879851338eb312b9b92e487343af6ef5cb9fe9b6 +Subproject commit b5e4ec4390be0a84eb007854c2956a6849b76e91 diff --git a/src/components/overlay/keyboard.service.ts b/src/components/overlay/keyboard.service.ts index 73c76cb3..7ff623e2 100644 --- a/src/components/overlay/keyboard.service.ts +++ b/src/components/overlay/keyboard.service.ts @@ -8,6 +8,7 @@ import { isEnterKeyValue, isLetterKey, isPaste, + isTab, } from '../../utils/key.utils'; import { timeout } from '../../utils'; import { @@ -80,7 +81,7 @@ export class KeyboardService { } // tab key means same as arrow right - if (codesLetter.TAB === e.code) { + if (isTab(e.code)) { this.keyChangeSelection(e, canRange); return; } diff --git a/src/components/revoGrid/revo-grid.tsx b/src/components/revoGrid/revo-grid.tsx index fc5aae5b..2378021f 100644 --- a/src/components/revoGrid/revo-grid.tsx +++ b/src/components/revoGrid/revo-grid.tsx @@ -12,6 +12,37 @@ import { Host, } from '@stencil/core'; +import type { + MultiDimensionType, + DimensionRows, + DimensionCols, + DimensionType, + DimensionTypeCol, + RowHeaders, + ColumnRegular, + ColumnGrouping, + DataType, + RowDefinition, + ColumnType, + FocusTemplateFunc, + PositionItem, + ColumnProp, + ViewPortScrollEvent, + InitialHeaderClick, + AllDimensionType, + Editors, + BeforeSaveDataDetails, + BeforeRangeSaveDataDetails, + Cell, + ChangedRange, + RangeArea, + AfterEditEvent, + Theme, + PluginBaseComponent, + HeaderProperties, + PluginProviders, +} from '@type'; + import ColumnDataProvider from '../../services/column.data.provider'; import { DataProvider } from '../../services/data.provider'; import { DSourceState, getVisibleSourceItem } from '@store'; @@ -45,39 +76,10 @@ import { rowDefinitionByType, rowDefinitionRemoveByType } from './grid.helpers'; import ColumnPlugin from '../../plugins/moveColumn/column.drag.plugin'; import { getPropertyFromEvent } from '../../utils/events'; import { isMobileDevice } from '../../utils/mobile'; -import { - MultiDimensionType, - DimensionRows, - DimensionCols, - DimensionType, - DimensionTypeCol, - RowHeaders, - ColumnRegular, - ColumnGrouping, - DataType, - RowDefinition, - ColumnType, - FocusTemplateFunc, - PositionItem, - ColumnProp, - ViewPortScrollEvent, - InitialHeaderClick, - AllDimensionType, - Editors, - BeforeSaveDataDetails, - BeforeRangeSaveDataDetails, - Cell, - ChangedRange, - RangeArea, - AfterEditEvent, - Theme, - PluginBaseComponent, - HeaderProperties, - PluginProviders, -} from '@type'; import type { Observable } from '../../utils/store.utils'; import type { GridPlugin } from '../../plugins/base.plugin'; import { ColumnCollection, getColumnByProp, getColumns } from '../../utils/column.utils'; +import { WCAGPlugin } from '../../plugins/wcag'; /** @@ -285,6 +287,13 @@ export class RevoGridComponent { */ @Prop() registerVNode: VNode[] = []; + + /** + * Enable accessibility. If disabled, the grid will not be accessible. + * @default true + */ + @Prop() accessible = true; + // #endregion // #region Events @@ -1215,6 +1224,10 @@ export class RevoGridComponent { selection: this.selectionStoreConnector, }; + if (this.accessible) { + this.internalPlugins.push(new WCAGPlugin(this.element, pluginData)); + } + // register auto size plugin if (this.autoSizeColumn) { this.internalPlugins.push( diff --git a/src/plugins/wcag/index.ts b/src/plugins/wcag/index.ts new file mode 100644 index 00000000..132fd13e --- /dev/null +++ b/src/plugins/wcag/index.ts @@ -0,0 +1,99 @@ +import { PluginProviders } from '@type'; +import { BasePlugin } from '../base.plugin'; +import { ColumnCollection } from 'src/utils'; + +/** + * WCAG Plugin is responsible for enhancing the accessibility features of the RevoGrid component. + * It ensures that the grid is fully compliant with Web Content Accessibility Guidelines (WCAG) 2.1. + * This plugin should be the last plugin you add, as it modifies the grid's default behavior. + * + * The WCAG Plugin performs the following tasks: + * - Sets the 'dir' attribute to 'ltr' for left-to-right text direction. + * - Sets the 'role' attribute to 'treegrid' for treelike hierarchical structure. + * - Sets the 'aria-keyshortcuts' attribute to 'Enter' and 'Esc' for keyboard shortcuts. + * - Adds event listeners for keyboard navigation and editing. + * + * By default, the plugin adds ARIA roles and properties to the grid elements, providing semantic information + * for assistive technologies. These roles include 'grid', 'row', and 'gridcell'. The plugin also sets + * ARIA attributes such as 'aria-rowindex', 'aria-colindex', and 'aria-selected'. + * + * The WCAG Plugin ensures that the grid is fully functional and usable for users with various disabilities, + * including visual impairments, deaf-blindness, and cognitive disabilities. + * + * Note: The WCAG Plugin should be added as the last plugin in the list of plugins, as it modifies the grid's + * default behavior and may conflict with other plugins if added earlier. + */ +export class WCAGPlugin extends BasePlugin { + constructor(revogrid: HTMLRevoGridElement, providers: PluginProviders) { + super(revogrid, providers); + + revogrid.setAttribute('dir', 'ltr'); + revogrid.setAttribute('role', 'treegrid'); + revogrid.setAttribute('aria-keyshortcuts', 'Enter'); + revogrid.setAttribute('aria-keyshortcuts', 'Esc'); + + /** + * Before Columns Set Event + */ + this.addEventListener( + 'beforecolumnsset', + ({ detail }: CustomEvent) => { + const columns = [ + ...detail.columns.colPinStart, + ...detail.columns.rgCol, + ...detail.columns.colPinEnd, + ]; + + revogrid.setAttribute('aria-colcount', `${columns.length}`); + + columns.forEach((column, index) => { + const { columnProperties, cellProperties } = column; + + column.columnProperties = (...args) => { + const result = columnProperties?.(...args) || {}; + + result.role = 'columnheader'; + result['aria-colindex'] = index; + + return result; + }; + + column.cellProperties = (...args) => { + const columnProps = cellProperties?.(...args) || {}; + + return { + ...columnProps, + role: 'gridcell', + ['aria-colindex']: index, + ['aria-rowindex']: args[0].rowIndex, + }; + }; + }); + }, + ); + + /** + * Before Row Set Event + */ + this.addEventListener( + 'beforesourceset', + ({ + detail, + }: CustomEvent) => { + revogrid.setAttribute('aria-rowcount', `${detail.source.length}`); + }, + ); + this.addEventListener( + 'beforerowrender', + ({ + detail, + }: CustomEvent) => { + detail.node.$attrs$ = { + ...detail.node.$attrs$, + role: 'row', + ['aria-rowindex']: detail.item.itemIndex, + }; + }, + ); + } +} From d06ce364f996afdaf93a29a00afb261a9c4f529d Mon Sep 17 00:00:00 2001 From: maks Date: Sat, 17 Aug 2024 14:26:18 +0100 Subject: [PATCH 2/4] feat: #506 - accessibility added --- src/components.d.ts | 26 +++++++------ src/components/data/column.service.ts | 2 +- src/components/data/revogr-data-style.scss | 1 + .../overlay/revogr-overlay-selection.tsx | 1 + src/components/overlay/selection.utils.ts | 2 +- src/components/revoGrid/revo-grid.tsx | 3 +- .../scroll/revogr-viewport-scroll-style.scss | 1 + .../selectionFocus/revogr-focus.tsx | 39 +++++++++---------- src/plugins/column.auto-size.plugin.ts | 2 +- .../groupingRow/grouping.row.plugin.ts | 10 ++--- src/plugins/groupingRow/grouping.service.ts | 28 ++++++++----- src/plugins/wcag/index.ts | 24 +++++++++++- src/serve/index.html | 2 +- src/store/dataSource/data.store.ts | 15 ++++--- src/types/interfaces.ts | 25 ++++++++---- src/types/selection.ts | 4 +- 16 files changed, 117 insertions(+), 68 deletions(-) diff --git a/src/components.d.ts b/src/components.d.ts index b230ac8f..a09d9b58 100644 --- a/src/components.d.ts +++ b/src/components.d.ts @@ -5,7 +5,7 @@ * It contains typing information for all components that exist in this project. */ import { HTMLStencilElement, JSXBase } from "@stencil/core/internal"; -import { AfterEditEvent, AllDimensionType, ApplyFocusEvent, BeforeCellRenderEvent, BeforeEdit, BeforeRangeSaveDataDetails, BeforeRowRenderEvent, BeforeSaveDataDetails, Cell, ChangedRange, ColumnDataSchemaModel, ColumnGrouping, ColumnProp, ColumnRegular, ColumnType, DataFormat, DataType, DimensionCols, DimensionRows, DimensionSettingsState, DimensionType, DimensionTypeCol, DragStartEvent, EditCell, EditorCtr, Editors, ElementScroll, FocusRenderEvent, FocusTemplateFunc, InitialHeaderClick, MultiDimensionType, Nullable, PluginBaseComponent, PositionItem, RangeArea, RangeClipboardCopyEventProps, RangeClipboardPasteEvent, RowDefinition, RowHeaders, SaveDataDetails, SelectionStoreState, TempRange, Theme, ViewportData, ViewPortResizeEvent, ViewPortScrollEvent, ViewportState, ViewSettingSizeProp } from "./types/index"; +import { AfterEditEvent, AllDimensionType, ApplyFocusEvent, BeforeCellRenderEvent, BeforeEdit, BeforeRangeSaveDataDetails, BeforeRowRenderEvent, BeforeSaveDataDetails, Cell, ChangedRange, ColumnDataSchemaModel, ColumnGrouping, ColumnProp, ColumnRegular, ColumnType, DataFormat, DataType, DimensionCols, DimensionRows, DimensionSettingsState, DimensionType, DimensionTypeCol, DragStartEvent, EditCell, EditorCtr, Editors, ElementScroll, FocusAfterRenderEvent, FocusRenderEvent, FocusTemplateFunc, InitialHeaderClick, MultiDimensionType, Nullable, PluginBaseComponent, PositionItem, RangeArea, RangeClipboardCopyEventProps, RangeClipboardPasteEvent, RowDefinition, RowHeaders, SaveDataDetails, SelectionStoreState, TempRange, Theme, ViewportData, ViewPortResizeEvent, ViewPortScrollEvent, ViewportState, ViewSettingSizeProp } from "./types/index"; import { GridPlugin } from "./plugins/base.plugin"; import { AutoSizeColumnConfig } from "./plugins/column.auto-size.plugin"; import { ColumnFilterConfig, FilterCaptions, FilterCollection } from "./plugins/filter/filter.plugin"; @@ -21,7 +21,7 @@ import { LogicFunction } from "./plugins/filter/filter.types"; import { ResizeProps } from "./components/header/resizable.directive"; import { Cell as Cell1, ColumnRegular as ColumnRegular1, DataType as DataType1, DimensionCols as DimensionCols1, DimensionRows as DimensionRows1, DimensionSettingsState as DimensionSettingsState1, Observable as Observable1, SelectionStoreState as SelectionStoreState1 } from "./components"; import { EventData } from "./components/overlay/selection.utils"; -export { AfterEditEvent, AllDimensionType, ApplyFocusEvent, BeforeCellRenderEvent, BeforeEdit, BeforeRangeSaveDataDetails, BeforeRowRenderEvent, BeforeSaveDataDetails, Cell, ChangedRange, ColumnDataSchemaModel, ColumnGrouping, ColumnProp, ColumnRegular, ColumnType, DataFormat, DataType, DimensionCols, DimensionRows, DimensionSettingsState, DimensionType, DimensionTypeCol, DragStartEvent, EditCell, EditorCtr, Editors, ElementScroll, FocusRenderEvent, FocusTemplateFunc, InitialHeaderClick, MultiDimensionType, Nullable, PluginBaseComponent, PositionItem, RangeArea, RangeClipboardCopyEventProps, RangeClipboardPasteEvent, RowDefinition, RowHeaders, SaveDataDetails, SelectionStoreState, TempRange, Theme, ViewportData, ViewPortResizeEvent, ViewPortScrollEvent, ViewportState, ViewSettingSizeProp } from "./types/index"; +export { AfterEditEvent, AllDimensionType, ApplyFocusEvent, BeforeCellRenderEvent, BeforeEdit, BeforeRangeSaveDataDetails, BeforeRowRenderEvent, BeforeSaveDataDetails, Cell, ChangedRange, ColumnDataSchemaModel, ColumnGrouping, ColumnProp, ColumnRegular, ColumnType, DataFormat, DataType, DimensionCols, DimensionRows, DimensionSettingsState, DimensionType, DimensionTypeCol, DragStartEvent, EditCell, EditorCtr, Editors, ElementScroll, FocusAfterRenderEvent, FocusRenderEvent, FocusTemplateFunc, InitialHeaderClick, MultiDimensionType, Nullable, PluginBaseComponent, PositionItem, RangeArea, RangeClipboardCopyEventProps, RangeClipboardPasteEvent, RowDefinition, RowHeaders, SaveDataDetails, SelectionStoreState, TempRange, Theme, ViewportData, ViewPortResizeEvent, ViewPortScrollEvent, ViewportState, ViewSettingSizeProp } from "./types/index"; export { GridPlugin } from "./plugins/base.plugin"; export { AutoSizeColumnConfig } from "./plugins/column.auto-size.plugin"; export { ColumnFilterConfig, FilterCaptions, FilterCollection } from "./plugins/filter/filter.plugin"; @@ -52,6 +52,11 @@ export namespace Components { * @example focus-rgCol-rgRow - focus layer for main data. Applies extra elements in . */ interface RevoGrid { + /** + * Enable accessibility. If disabled, the grid will not be accessible. + * @default true + */ + "accessible": boolean; /** * Add trimmed by type */ @@ -962,10 +967,7 @@ declare global { interface HTMLRevogrFocusElementEventMap { "beforefocusrender": FocusRenderEvent; "beforescrollintoview": { el: HTMLElement }; - "afterfocus": { - model: any; - column: ColumnRegular; - }; + "afterfocus": FocusAfterRenderEvent; } /** * Focus component. Shows focus layer around the cell that is currently in focus. @@ -1207,6 +1209,11 @@ declare namespace LocalJSX { * @example focus-rgCol-rgRow - focus layer for main data. Applies extra elements in . */ interface RevoGrid { + /** + * Enable accessibility. If disabled, the grid will not be accessible. + * @default true + */ + "accessible"?: boolean; /** * Additional data to be passed to plugins, renders or editors. For example if you need to pass Vue component instance. */ @@ -1308,7 +1315,7 @@ declare namespace LocalJSX { */ "onAfteredit"?: (event: RevoGridCustomEvent) => void; /** - * Triggered after focus render finished. Can be used to access a focus element through `event.target` + * Triggered after focus render finished. Can be used to access a focus element through `event.target`. This is just a duplicate of `afterfocus` from `revogr-focus.tsx`. */ "onAfterfocus"?: (event: RevoGridCustomEvent<{ model: any; @@ -1746,10 +1753,7 @@ declare namespace LocalJSX { /** * Used to setup properties after focus was rendered */ - "onAfterfocus"?: (event: RevogrFocusCustomEvent<{ - model: any; - column: ColumnRegular; - }>) => void; + "onAfterfocus"?: (event: RevogrFocusCustomEvent) => void; /** * Before focus render event. Can be prevented by event.preventDefault(). If preventDefault used slot will be rendered. */ diff --git a/src/components/data/column.service.ts b/src/components/data/column.service.ts index 9b446978..23a07ff0 100644 --- a/src/components/data/column.service.ts +++ b/src/components/data/column.service.ts @@ -322,7 +322,7 @@ export default class ColumnService { prop: ColumnProp; rowIndex: number; colIndex: number; - model: DataType; + model?: DataType; colType: DimensionCols; type: DimensionRows; }[] = []; diff --git a/src/components/data/revogr-data-style.scss b/src/components/data/revogr-data-style.scss index c9a78319..999e4fa0 100644 --- a/src/components/data/revogr-data-style.scss +++ b/src/components/data/revogr-data-style.scss @@ -70,6 +70,7 @@ revogr-data { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; + outline: none; &.align-center { text-align: center; diff --git a/src/components/overlay/revogr-overlay-selection.tsx b/src/components/overlay/revogr-overlay-selection.tsx index ccd49858..68bfd7be 100644 --- a/src/components/overlay/revogr-overlay-selection.tsx +++ b/src/components/overlay/revogr-overlay-selection.tsx @@ -790,6 +790,7 @@ export class OverlaySelection { range, ...this.types, }); + if (canPaste) { return; } diff --git a/src/components/overlay/selection.utils.ts b/src/components/overlay/selection.utils.ts index c9af3837..baf01889 100644 --- a/src/components/overlay/selection.utils.ts +++ b/src/components/overlay/selection.utils.ts @@ -19,7 +19,7 @@ export type EventData = { }; export function collectModelsOfRange(data: DataLookup, store: Observable>) { - const models: DataLookup = {}; + const models: Partial = {}; for (let i in data) { const rowIndex = parseInt(i, 10); models[rowIndex] = getSourceItem( diff --git a/src/components/revoGrid/revo-grid.tsx b/src/components/revoGrid/revo-grid.tsx index 2378021f..2f826208 100644 --- a/src/components/revoGrid/revo-grid.tsx +++ b/src/components/revoGrid/revo-grid.tsx @@ -336,7 +336,8 @@ export class RevoGridComponent { /** * Triggered after focus render finished. - * Can be used to access a focus element through `event.target` + * Can be used to access a focus element through `event.target`. + * This is just a duplicate of `afterfocus` from `revogr-focus.tsx`. */ @Event() afterfocus: EventEmitter<{ model: any; diff --git a/src/components/scroll/revogr-viewport-scroll-style.scss b/src/components/scroll/revogr-viewport-scroll-style.scss index 219e6f74..07fd1c8f 100644 --- a/src/components/scroll/revogr-viewport-scroll-style.scss +++ b/src/components/scroll/revogr-viewport-scroll-style.scss @@ -67,6 +67,7 @@ revogr-viewport-scroll { position: relative; width: 100%; flex-grow: 1; + outline: none; // avoid accessibility focus issue @include noScroll; revogr-data, diff --git a/src/components/selectionFocus/revogr-focus.tsx b/src/components/selectionFocus/revogr-focus.tsx index d56b0e7b..fda6aae0 100644 --- a/src/components/selectionFocus/revogr-focus.tsx +++ b/src/components/selectionFocus/revogr-focus.tsx @@ -20,6 +20,7 @@ import { FocusTemplateFunc, DimensionCols, DimensionRows, + FocusAfterRenderEvent, } from '@type'; import { Observable } from '../../utils/store.utils'; @@ -78,29 +79,11 @@ export class RevogrFocus { /** * Used to setup properties after focus was rendered */ - @Event({ eventName: 'afterfocus' }) afterFocus: EventEmitter<{ - model: any; - column: ColumnRegular; - }>; + @Event({ eventName: 'afterfocus' }) afterFocus: EventEmitter; @Element() el: HTMLElement; private activeFocus: Cell | null = null; - private changed(e: HTMLElement, focus: Cell) { - const beforeScrollIn = this.beforeScrollIntoView.emit({ el: e }); - if (!beforeScrollIn.defaultPrevented) { - e.scrollIntoView({ - block: 'nearest', - inline: 'nearest', - }); - } - const model = getSourceItem(this.dataStore, focus.y); - const column = getSourceItem(this.colData, focus.x); - this.afterFocus.emit({ - model, - column, - }); - } componentDidRender() { const currentFocus = this.selectionStore.get('focus'); @@ -112,7 +95,23 @@ export class RevogrFocus { } this.activeFocus = currentFocus; if (currentFocus && this.el) { - this.changed(this.el, currentFocus); + const beforeScrollIn = this.beforeScrollIntoView.emit({ el: this.el }); + if (!beforeScrollIn.defaultPrevented) { + this.el.scrollIntoView({ + block: 'nearest', + inline: 'nearest', + }); + } + const model = getSourceItem(this.dataStore, currentFocus.y); + const column = getSourceItem(this.colData, currentFocus.x); + this.afterFocus.emit({ + model, + column, + rowType: this.rowType, + colType: this.colType, + rowIndex: currentFocus.y, + colIndex: currentFocus.x, + }); } } diff --git a/src/plugins/column.auto-size.plugin.ts b/src/plugins/column.auto-size.plugin.ts index e4eb6fd6..a2444ea7 100644 --- a/src/plugins/column.auto-size.plugin.ts +++ b/src/plugins/column.auto-size.plugin.ts @@ -250,7 +250,7 @@ export default class AutoSizeColumnPlugin extends BasePlugin { s.store.get('items'), (prev, _row, i) => { const item = getSourceItem(s.store, i); - return Math.max(prev || 0, this.getLength(item[rgCol.prop])); + return Math.max(prev || 0, this.getLength(item?.[rgCol.prop])); }, 0, ); diff --git a/src/plugins/groupingRow/grouping.row.plugin.ts b/src/plugins/groupingRow/grouping.row.plugin.ts index fc3cc0a9..58466de1 100644 --- a/src/plugins/groupingRow/grouping.row.plugin.ts +++ b/src/plugins/groupingRow/grouping.row.plugin.ts @@ -102,14 +102,14 @@ export default class GroupingRowPlugin extends BasePlugin { } // grouping filter - if (!isGrouping(model)) { - result.source.push(model); - result.oldNewIndexes[i] = index; - index++; - } else { + if (isGrouping(model)) { if (model[GROUP_EXPANDED]) { result.prevExpanded[model[PSEUDO_GROUP_ITEM_VALUE]] = true; } + } else { + result.source.push(model); + result.oldNewIndexes[i] = index; + index++; } return result; }, diff --git a/src/plugins/groupingRow/grouping.service.ts b/src/plugins/groupingRow/grouping.service.ts index 57967fad..c4e4a4bb 100644 --- a/src/plugins/groupingRow/grouping.service.ts +++ b/src/plugins/groupingRow/grouping.service.ts @@ -24,7 +24,6 @@ function getGroupValueDefault(item: DataType, prop: string | number) { return item[prop] || null; } - /** * Gather data for grouping * @param array - flat data array @@ -34,11 +33,17 @@ function getGroupValueDefault(item: DataType, prop: string | number) { export function gatherGrouping( array: DataType[], groupIds: ColumnProp[], - { prevExpanded, expandedAll, getGroupValue = getGroupValueDefault }: ExpandedOptions, + { + prevExpanded, + expandedAll, + getGroupValue = getGroupValueDefault, + }: ExpandedOptions, ) { const groupedItems: GroupedData = new Map(); array.forEach((item, originalIndex) => { - const groupLevelValues = groupIds.map(groupId => getGroupValue(item, groupId)); + const groupLevelValues = groupIds.map(groupId => + getGroupValue(item, groupId), + ); const lastLevelValue = groupLevelValues.pop(); let currentGroupLevel = groupedItems; groupLevelValues.forEach(value => { @@ -50,9 +55,7 @@ export function gatherGrouping( if (!currentGroupLevel.has(lastLevelValue)) { currentGroupLevel.set(lastLevelValue, []); } - const lastLevelItems = currentGroupLevel.get( - lastLevelValue, - ) as DataType[]; + const lastLevelItems = currentGroupLevel.get(lastLevelValue) as DataType[]; lastLevelItems.push({ ...item, [GROUP_ORIGINAL_INDEX]: originalIndex, @@ -128,8 +131,15 @@ export function getGroupingName(rgRow?: DataType) { return rgRow && rgRow[PSEUDO_GROUP_ITEM]; } -export function isGrouping(rgRow?: DataType) { - return rgRow && typeof rgRow[PSEUDO_GROUP_ITEM] !== 'undefined'; +type GroupingItem = { + [PSEUDO_GROUP_ITEM]: string; + [GROUP_EXPANDED]: boolean; + [PSEUDO_GROUP_ITEM_VALUE]: string; + [GROUP_DEPTH]: number; +}; + +export function isGrouping(rgRow?: DataType): rgRow is GroupingItem { + return typeof rgRow?.[PSEUDO_GROUP_ITEM] !== 'undefined'; } export function isGroupingColumn(column?: ColumnRegular) { @@ -147,7 +157,7 @@ export function measureEqualDepth(groupA: T[], groupB: T[]) { return i; } -export function getParsedGroup(id: string){ +export function getParsedGroup(id: string) { const parseGroup = JSON.parse(id); // extra precaution and type safeguard if (!Array.isArray(parseGroup)) { diff --git a/src/plugins/wcag/index.ts b/src/plugins/wcag/index.ts index 132fd13e..4d209889 100644 --- a/src/plugins/wcag/index.ts +++ b/src/plugins/wcag/index.ts @@ -30,7 +30,8 @@ export class WCAGPlugin extends BasePlugin { revogrid.setAttribute('dir', 'ltr'); revogrid.setAttribute('role', 'treegrid'); revogrid.setAttribute('aria-keyshortcuts', 'Enter'); - revogrid.setAttribute('aria-keyshortcuts', 'Esc'); + revogrid.setAttribute('aria-multiselectable', 'true'); + revogrid.setAttribute('tabindex', '0'); /** * Before Columns Set Event @@ -62,10 +63,11 @@ export class WCAGPlugin extends BasePlugin { const columnProps = cellProperties?.(...args) || {}; return { - ...columnProps, role: 'gridcell', ['aria-colindex']: index, ['aria-rowindex']: args[0].rowIndex, + ['tabindex']: -1, + ...columnProps, }; }; }); @@ -95,5 +97,23 @@ export class WCAGPlugin extends BasePlugin { }; }, ); + + // focuscell + this.addEventListener( + 'afterfocus', + async ( + e: CustomEvent, + ) => { + if (e.defaultPrevented) { + return; + } + const el = this.revogrid.querySelector( + `revogr-data[type="${e.detail.rowType}"][col-type="${e.detail.colType}"] [data-rgrow="${e.detail.rowIndex}"][data-rgcol="${e.detail.colIndex}"]`, + ); + if (el instanceof HTMLElement) { + el.focus(); + } + }, + ); } } diff --git a/src/serve/index.html b/src/serve/index.html index 25861991..f1211ca3 100644 --- a/src/serve/index.html +++ b/src/serve/index.html @@ -125,7 +125,7 @@
Themes
- +
diff --git a/src/store/dataSource/data.store.ts b/src/store/dataSource/data.store.ts index b33222c7..84fb77b2 100644 --- a/src/store/dataSource/data.store.ts +++ b/src/store/dataSource/data.store.ts @@ -143,10 +143,10 @@ export function getVisibleSourceItem( * @param store - store to process * @param virtualIndex - virtual index to process */ -export const getSourceItem = ( - store: Observable>, +export const getSourceItem = ( + store: Observable>, virtualIndex: number, -): any | undefined => { +) => { const items = store.get('items'); const source = store.get('source'); return source[items[virtualIndex]]; @@ -160,7 +160,7 @@ export const getSourceItem = ( */ export function setSourceByVirtualIndex( store: Observable>, - modelByIndex: Record, + modelByIndex: Record, mutate = true, ) { const items = store.get('items'); @@ -168,7 +168,12 @@ export function setSourceByVirtualIndex( for (let virtualIndex in modelByIndex) { const realIndex = items[virtualIndex]; - source[realIndex] = modelByIndex[virtualIndex]; + const item = modelByIndex[virtualIndex]; + if (!item) { + delete source[realIndex]; + } else { + source[realIndex] = item; + } } if (mutate) { store.set('source', [...source]); diff --git a/src/types/interfaces.ts b/src/types/interfaces.ts index 484708db..03e1be0c 100644 --- a/src/types/interfaces.ts +++ b/src/types/interfaces.ts @@ -186,7 +186,7 @@ export type Order = 'asc' | 'desc' | undefined; /** * Interface for regular column definition. * Regular column can be any column that is not a grouping column. - * + * */ /** * ColumnRegular interface represents regular column definition. @@ -253,7 +253,7 @@ export interface ColumnTemplateProp extends ColumnRegular { * Index of the column, used for mapping value to cell from data source model/row. */ index: number; -}; +} export type ColumnPropProp = ColumnGrouping | ColumnTemplateProp; // Column prop used for mapping value to cell from data source model/row, used for indexing. @@ -385,7 +385,6 @@ export type FocusTemplateFunc = ( detail: FocusRenderEvent, ) => any; - /** * `CellCompareFunc` is a function that takes the column property to compare, * the data of the first cell, and the data of the second cell. It returns a @@ -759,6 +758,19 @@ export interface FocusRenderEvent extends AllDimensionType { */ next?: Partial; } + +export interface FocusAfterRenderEvent extends AllDimensionType { + model?: any; + column?: ColumnRegular; + /** + * Index of the row in the viewport + */ + rowIndex: number; + /** + * Index of the column in the viewport + */ + colIndex: number; +} /** * Represents the event object that is emitted when scrolling occurs. * The `type` property indicates the type of dimension (row or column) being scrolled. @@ -778,13 +790,10 @@ export type ScrollCoordinateEvent = { coordinate: number; }; - /** Range paste. */ export type RangeClipboardPasteEvent = { data: DataLookup; - models: { - [rowIndex: number]: DataType; - }; + models: Partial; range: RangeArea | null; } & AllDimensionType; @@ -793,4 +802,4 @@ export type RangeClipboardCopyEventProps = { data: DataFormat[][]; range: RangeArea; mapping: OldNewRangeMapping; -} & AllDimensionType; \ No newline at end of file +} & AllDimensionType; diff --git a/src/types/selection.ts b/src/types/selection.ts index b21002dc..dcdb3009 100644 --- a/src/types/selection.ts +++ b/src/types/selection.ts @@ -114,9 +114,7 @@ export type BeforeSaveDataDetails = { }; export type BeforeRangeSaveDataDetails = { data: DataLookup; - models: { - [rowIndex: number]: DataType; - }; + models: Partial; type: DimensionRows; }; From b947ccf6c06fc50fdc4698a254c0019799db5976 Mon Sep 17 00:00:00 2001 From: maks Date: Sat, 17 Aug 2024 14:27:59 +0100 Subject: [PATCH 3/4] correction on focus typing --- src/components/revoGrid/revo-grid.tsx | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/components/revoGrid/revo-grid.tsx b/src/components/revoGrid/revo-grid.tsx index 2f826208..2ce1e63b 100644 --- a/src/components/revoGrid/revo-grid.tsx +++ b/src/components/revoGrid/revo-grid.tsx @@ -41,6 +41,7 @@ import type { PluginBaseComponent, HeaderProperties, PluginProviders, + FocusAfterRenderEvent, } from '@type'; import ColumnDataProvider from '../../services/column.data.provider'; @@ -339,10 +340,7 @@ export class RevoGridComponent { * Can be used to access a focus element through `event.target`. * This is just a duplicate of `afterfocus` from `revogr-focus.tsx`. */ - @Event() afterfocus: EventEmitter<{ - model: any; - column: ColumnRegular; - }>; + @Event() afterfocus: EventEmitter; /** * This event is triggered before the order of `rgRow` is applied. From c66cd109d05e720707a5cf58db7189ea78ec8af2 Mon Sep 17 00:00:00 2001 From: maks Date: Sat, 17 Aug 2024 14:31:18 +0100 Subject: [PATCH 4/4] correction on typing --- src/store/dataSource/data.store.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/store/dataSource/data.store.ts b/src/store/dataSource/data.store.ts index 84fb77b2..abd7113c 100644 --- a/src/store/dataSource/data.store.ts +++ b/src/store/dataSource/data.store.ts @@ -169,11 +169,7 @@ export function setSourceByVirtualIndex( for (let virtualIndex in modelByIndex) { const realIndex = items[virtualIndex]; const item = modelByIndex[virtualIndex]; - if (!item) { - delete source[realIndex]; - } else { - source[realIndex] = item; - } + source[realIndex] = item as T; } if (mutate) { store.set('source', [...source]);