Skip to content

Commit

Permalink
Announce Plugin, add features to current plugin (#2119)
Browse files Browse the repository at this point in the history
* init

* remove

* Fix type issues

* add tests

* Fix build

* Move logic from Editor to Plugin

* Add type to param

* DefaultAnnounceString to KnownAnnounceStrings

* merge classes

* Add more details in comments

* Fix build

* const enum

* fix

* init

* Add callback that returns string

* init2

* Fix test after merge

* Refactor

* refactor

* Fix

* Dispose editor

* Move util from dom to plugin pkg & fix

* remove unneeded if

* remove unneeded test
  • Loading branch information
BryanValverdeU authored Oct 5, 2023
1 parent badefc5 commit 1ba8d4a
Show file tree
Hide file tree
Showing 16 changed files with 600 additions and 145 deletions.
8 changes: 6 additions & 2 deletions demo/scripts/controls/getToggleablePlugins.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,11 @@ export default function getToggleablePlugins(initState: BuildInPluginState) {

function getDefaultStringsMap(): Map<KnownAnnounceStrings, string> {
return new Map<KnownAnnounceStrings, string>([
[KnownAnnounceStrings.AnnounceListItemBulletIndentation, 'Autocorrected Bullet'],
[KnownAnnounceStrings.AnnounceListItemNumberingIndentation, 'Autocorrected {0}'],
[KnownAnnounceStrings.AnnounceListItemBullet, 'Autocorrected Bullet'],
[KnownAnnounceStrings.AnnounceListItemNumbering, 'Autocorrected {0}'],
[
KnownAnnounceStrings.AnnounceOnFocusLastCell,
'Warning, pressing tab here adds an extra row.',
],
]);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { KnownAnnounceStrings } from 'roosterjs-editor-types';
import {
convertDecimalsToAlpha,
convertDecimalsToRoman,
safeInstanceOf,
VList,
} from 'roosterjs-editor-dom';
import type { AnnounceData } from 'roosterjs-editor-types';

/**
* @internal
* Get the announce data for the current List
* @returns announce data for list or undefined.
*/
export default function getAnnounceDataForList(
list: HTMLElement | null,
li: HTMLElement | null
): AnnounceData | undefined {
if (!safeInstanceOf(li, 'HTMLLIElement')) {
return undefined;
}

if (li && safeInstanceOf(list, 'HTMLOListElement')) {
const vList = new VList(list);
const listItemIndex = vList.getListItemIndex(li);
let stringToAnnounce = listItemIndex == -1 ? '' : listItemIndex.toString();
switch (list.style.listStyleType) {
case 'lower-alpha':
case 'lower-latin':
case 'upper-alpha':
case 'upper-latin':
stringToAnnounce = convertDecimalsToAlpha(listItemIndex - 1);
break;
case 'lower-roman':
case 'upper-roman':
stringToAnnounce = convertDecimalsToRoman(listItemIndex);
break;
}

return {
defaultStrings: KnownAnnounceStrings.AnnounceListItemNumbering,
formatStrings: [stringToAnnounce],
};
} else if (safeInstanceOf(list, 'HTMLUListElement')) {
return {
defaultStrings: KnownAnnounceStrings.AnnounceListItemBullet,
};
}
return undefined;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import type { IEditor, AnnounceData } from 'roosterjs-editor-types';

/**
* Represents a Announce feature used in Announce Plugin.
* If the Should Handle Callback returns announce data, it will be announced by using a aria-live region.
*/
export interface AnnounceFeature {
/**
* Whether to handle this feature, if returns Announce Data, will be announced, otherwise will do nothing.
* @returns
*/
shouldHandle: (editor: IEditor, lastFocusedElement: HTMLElement | null) => AnnounceData | false;
/**
* Keys handled in the current event
*/
keys: number[];
}
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
import { createElement } from 'roosterjs-editor-dom';
import { AnnounceFeatures } from './features/AnnounceFeatures';
import { createElement, getObjectKeys } from 'roosterjs-editor-dom';
import { PluginEventType } from 'roosterjs-editor-types';
import type { AnnounceFeatureKey } from './features/AnnounceFeatures';
import type { AnnounceFeature } from './AnnounceFeature';
import type { CompatibleKnownAnnounceStrings } from 'roosterjs-editor-types/lib/compatibleTypes';
import type {
EditorPlugin,
IEditor,
PluginEvent,
AnnounceData,
PluginKeyDownEvent,
KnownAnnounceStrings,
} from 'roosterjs-editor-types';

Expand Down Expand Up @@ -36,13 +40,28 @@ const createAriaLiveElement = (document: Document): HTMLDivElement => {
export default class Announce implements EditorPlugin {
private ariaLiveElement: HTMLDivElement | undefined;
private editor: IEditor | undefined;
private features: AnnounceFeature[];
private lastFocusedElement: HTMLElement | null = null;

constructor(
private stringsMapOrGetter?:
| Map<CompatibleKnownAnnounceStrings | KnownAnnounceStrings, string>
| ((key: CompatibleKnownAnnounceStrings | KnownAnnounceStrings) => string)
| undefined
) {}
| Map<KnownAnnounceStrings | CompatibleKnownAnnounceStrings, string>
| ((key: KnownAnnounceStrings | CompatibleKnownAnnounceStrings) => string)
| undefined,
skipAnnounceFeatures: AnnounceFeatureKey[] = [],
additionalFeatures?: AnnounceFeature[]
) {
this.features = getObjectKeys(AnnounceFeatures)
.map(key => {
if (skipAnnounceFeatures.indexOf(key) == -1) {
return AnnounceFeatures[key];
}

return undefined;
})
.filter(feature => !!feature)
.concat(additionalFeatures || []) as AnnounceFeature[];
}

/**
* Get a friendly name of this plugin
Expand All @@ -65,23 +84,49 @@ export default class Announce implements EditorPlugin {
dispose() {
this.ariaLiveElement?.parentElement?.removeChild(this.ariaLiveElement);
this.ariaLiveElement = undefined;
this.stringsMapOrGetter = undefined;
this.lastFocusedElement = null;
while (this.features.length > 0) {
this.features.pop();
}
this.editor = undefined;
}

/**
* Handle events triggered from editor
* @param event PluginEvent object
*/
onPluginEvent(event: PluginEvent) {
onPluginEvent(ev: PluginEvent) {
if (
this.editor &&
event.eventType == PluginEventType.ContentChanged &&
event.additionalData?.getAnnounceData
ev.eventType == PluginEventType.ContentChanged &&
ev.additionalData?.getAnnounceData
) {
const data = event.additionalData.getAnnounceData();
const data = ev.additionalData.getAnnounceData();
if (data) {
this.announce(data, this.editor);
}
}

if (ev.eventType == PluginEventType.KeyDown && this.editor) {
this.handleFeatures(ev, this.editor);
}
}

private handleFeatures(event: PluginKeyDownEvent, editorInput: IEditor) {
editorInput.runAsync(editor => {
this.features
.filter(feature => feature.keys.indexOf(event.rawEvent.which) > -1)
.some(feature => {
const announceData = feature.shouldHandle(editor, this.lastFocusedElement);
if (announceData) {
this.announce(announceData, editor);
}
return !!announceData;
});

this.lastFocusedElement = editor.getElementAtCursor();
});
}

protected announce(announceData: AnnounceData, editor: IEditor) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import announceNewListItemNumber from './announceNewListItem';
import announceWarningOnLastCell from './announceWarningOnLastTableCell';
import type { AnnounceFeature } from '../AnnounceFeature';

/**
* Announce feature keys
*/
export type AnnounceFeatureKey = 'announceNewListItem' | 'announceWarningOnLastTableCell';
/**
* @internal
*/
export const AnnounceFeatures: Record<AnnounceFeatureKey, AnnounceFeature> = {
announceNewListItem: announceNewListItemNumber,
announceWarningOnLastTableCell: announceWarningOnLastCell,
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import getAnnounceDataForList from '../../../pluginUtils/announceData/getAnnounceDataForList';
import { Keys } from 'roosterjs-editor-types';
import type { AnnounceFeature } from '../AnnounceFeature';

const LIST_SELECTOR = 'OL,UL';
const LIST_ITEM_SELECTOR = 'LI';

const announceNewListItemNumber: AnnounceFeature = {
keys: [Keys.ENTER],
shouldHandle: editor => {
const li = editor.getElementAtCursor(LIST_ITEM_SELECTOR);
const list = editor.getElementAtCursor(LIST_SELECTOR);
return (!!(list && li) && getAnnounceDataForList(list, li)) || false;
},
};

export default announceNewListItemNumber;
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { contains, safeInstanceOf } from 'roosterjs-editor-dom';
import { Keys, KnownAnnounceStrings, SelectionRangeTypes } from 'roosterjs-editor-types';
import type { AnnounceFeature } from '../AnnounceFeature';

const TABLE_CELL_SELECTOR = 'td,th';
const TABLE_SELECTOR = 'table';

const announceWarningOnLastCell: AnnounceFeature = {
shouldHandle: (editor, lastFocusedElement) => {
const selection = editor.getSelectionRangeEx();

return (
selection?.type == SelectionRangeTypes.Normal &&
selection.areAllCollapsed &&
selection.ranges.length === 1 &&
!contains(
lastFocusedElement,
selection.ranges[0].startContainer,
true /*treatSameNodeAsContain*/
) &&
isLastCell() && {
defaultStrings: KnownAnnounceStrings.AnnounceOnFocusLastCell,
}
);
function isLastCell(): boolean {
const table = editor.getElementAtCursor(TABLE_SELECTOR);

if (safeInstanceOf(table, 'HTMLTableElement')) {
const allCells = table.querySelectorAll(TABLE_CELL_SELECTOR);
const focusedCell = editor.getElementAtCursor(TABLE_CELL_SELECTOR);

return focusedCell == allCells.item(allCells.length - 1);
}
return false;
}
},
keys: [Keys.TAB, Keys.UP, Keys.DOWN, Keys.LEFT, Keys.RIGHT],
};

export default announceWarningOnLastCell;
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
export { AnnounceFeatureKey } from './features/AnnounceFeatures';
export { AnnounceFeature } from './AnnounceFeature';
export { default as Announce } from './AnnouncePlugin';
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import getAnnounceDataForList from '../../../pluginUtils/announceData/getAnnounceDataForList';
import getAutoBulletListStyle from '../utils/getAutoBulletListStyle';
import getAutoNumberingListStyle from '../utils/getAutoNumberingListStyle';
import {
Browser,
cacheGetEventData,
convertDecimalsToAlpha,
convertDecimalsToRoman,
createNumberDefinition,
createObjectDefinition,
createVListFromRegion,
Expand Down Expand Up @@ -43,7 +42,6 @@ import {
PositionType,
NumberingListType,
BulletListType,
KnownAnnounceStrings,
ChangeSource,
} from 'roosterjs-editor-types';

Expand Down Expand Up @@ -74,45 +72,6 @@ const ListStyleDefinitionMetadata = createObjectDefinition<ListStyleMetadata>(
true /** allowNull */
);

/**
* @internal Exported for unit testing
* @returns
*/
export const getAnnounceDataForList = (editor: IEditor) => {
const li = editor.getElementAtCursor('li') as HTMLLIElement;
const list = editor.getElementAtCursor('OL,UL', li) as
| undefined
| HTMLOListElement
| HTMLUListElement;
if (li && safeInstanceOf(list, 'HTMLOListElement')) {
const vList = new VList(list);
const listItemIndex = vList.getListItemIndex(li);
let stringToAnnounce = listItemIndex.toString();
switch (list.style.listStyleType) {
case 'lower-alpha':
case 'lower-latin':
case 'upper-alpha':
case 'upper-latin':
stringToAnnounce = convertDecimalsToAlpha(listItemIndex - 1);
break;
case 'lower-roman':
case 'upper-roman':
stringToAnnounce = convertDecimalsToRoman(listItemIndex);
break;
}

return {
defaultStrings: KnownAnnounceStrings.AnnounceListItemNumberingIndentation,
formatStrings: [stringToAnnounce],
};
} else if (safeInstanceOf(list, 'HTMLUListElement')) {
return {
defaultStrings: KnownAnnounceStrings.AnnounceListItemBulletIndentation,
};
}
return undefined;
};

const shouldHandleIndentationEvent = (indenting: boolean) => (
event: PluginKeyboardEvent,
editor: IEditor
Expand Down Expand Up @@ -148,7 +107,11 @@ const handleIndentationEvent = (indenting: boolean) => (
ChangeSource.Format,
false /* canUndoByBackspace */,
{
getAnnounceData: () => getAnnounceDataForList(editor),
getAnnounceData: () =>
getAnnounceDataForList(
editor.getElementAtCursor('OL,UL'),
editor.getElementAtCursor('LI')
),
}
);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -613,4 +613,3 @@ export default class PickerPlugin<T extends PickerDataProvider = PickerDataProvi
);
}
}

Loading

0 comments on commit 1ba8d4a

Please sign in to comment.