Skip to content

Commit

Permalink
Add announce Plugin (#2109)
Browse files Browse the repository at this point in the history
Add announce plugin
Add a AnnounceHandler, that will be in charge of announcing the messages by using a aria-live element, this handler will require a string map with the localized strings to announce messages from built-in RoosterJS features.
Add an additional callback property to ContentEditEventData, getAnnounceData, used in the Announce Plugin
Add first announcing logic when indenting/outdenting list
  • Loading branch information
BryanValverdeU authored Oct 2, 2023
1 parent 43a947b commit a0d3e94
Show file tree
Hide file tree
Showing 22 changed files with 465 additions and 25 deletions.
1 change: 1 addition & 0 deletions demo/scripts/controls/BuildInPluginState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export interface BuildInPluginList {
contextMenu: boolean;
autoFormat: boolean;
contentModelPaste: boolean;
announce: boolean;
}

export default interface BuildInPluginState {
Expand Down
2 changes: 1 addition & 1 deletion demo/scripts/controls/MainPaneBase.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,12 @@ import * as ReactDOM from 'react-dom';
import BuildInPluginState from './BuildInPluginState';
import SidePane from './sidePane/SidePane';
import SnapshotPlugin from './sidePane/snapshot/SnapshotPlugin';
import { EditorOptions, EditorPlugin, IEditor } from 'roosterjs-editor-types';
import { getDarkColor } from 'roosterjs-color-utils';
import { PartialTheme, ThemeProvider } from '@fluentui/react/lib/Theme';
import { registerWindowForCss, unregisterWindowForCss } from '../utils/cssMonitor';
import { trustedHTMLHandler } from '../utils/trustedHTMLHandler';
import { WindowProvider } from '@fluentui/react/lib/WindowProvider';
import { EditorOptions, EditorPlugin, IEditor } from 'roosterjs-editor-types';
import {
createUpdateContentPlugin,
Rooster,
Expand Down
11 changes: 10 additions & 1 deletion demo/scripts/controls/getToggleablePlugins.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import Announce from 'roosterjs-editor-plugins/lib/plugins/Announce/AnnouncePlugin';
import BuildInPluginState, { BuildInPluginList, UrlPlaceholder } from './BuildInPluginState';
import { AutoFormat } from 'roosterjs-editor-plugins/lib/AutoFormat';
import { ContentEdit } from 'roosterjs-editor-plugins/lib/ContentEdit';
import { ContentModelPastePlugin } from 'roosterjs-content-model-editor';
import { CustomReplace as CustomReplacePlugin } from 'roosterjs-editor-plugins/lib/CustomReplace';
import { CutPasteListChain } from 'roosterjs-editor-plugins/lib/CutPasteListChain';
import { EditorPlugin } from 'roosterjs-editor-types';
import { EditorPlugin, KnownAnnounceStrings } from 'roosterjs-editor-types';
import { HyperLink } from 'roosterjs-editor-plugins/lib/HyperLink';
import { ImageEdit } from 'roosterjs-editor-plugins/lib/ImageEdit';
import { Paste } from 'roosterjs-editor-plugins/lib/Paste';
Expand Down Expand Up @@ -59,7 +60,15 @@ export default function getToggleablePlugins(initState: BuildInPluginState) {
: null,
contextMenu: pluginList.contextMenu ? createContextMenuPlugin() : null,
contentModelPaste: pluginList.contentModelPaste ? new ContentModelPastePlugin() : null,
announce: pluginList.announce ? new Announce(getDefaultStringsMap()) : null,
};

return Object.values(plugins);
}

function getDefaultStringsMap(): Map<KnownAnnounceStrings, string> {
return new Map<KnownAnnounceStrings, string>([
[KnownAnnounceStrings.AnnounceListItemBulletIndentation, 'Autocorrected Bullet'],
[KnownAnnounceStrings.AnnounceListItemNumberingIndentation, 'Autocorrected {0}'],
]);
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ const initialState: BuildInPluginState = {
contextMenu: true,
autoFormat: true,
contentModelPaste: true,
announce: true,
},
contentEditFeatures: getDefaultContentEditFeatureSettings(),
defaultFormat: {},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ const initialState: BuildInPluginState = {
contextMenu: true,
autoFormat: true,
contentModelPaste: false,
announce: true,
},
contentEditFeatures: getDefaultContentEditFeatureSettings(),
defaultFormat: {},
Expand Down
1 change: 1 addition & 0 deletions demo/scripts/controls/sidePane/editorOptions/Plugins.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ export default class Plugins extends React.Component<PluginsProps, {}> {
'Show customized context menu for special cases'
)}
{this.renderPluginItem('tableCellSelection', 'Table Cell Selection')}
{this.renderPluginItem('announce', 'Announce')}
</tbody>
</table>
);
Expand Down
2 changes: 2 additions & 0 deletions packages/roosterjs-editor-dom/lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,8 @@ export { default as VList } from './list/VList';
export { default as VListItem } from './list/VListItem';
export { default as createVListFromRegion } from './list/createVListFromRegion';
export { default as VListChain } from './list/VListChain';
export { default as convertDecimalsToAlpha } from './list/convertDecimalsToAlpha';
export { default as convertDecimalsToRoman } from './list/convertDecimalsToRomans';
export { default as setListItemStyle } from './list/setListItemStyle';
export { getTableFormatInfo } from './table/tableFormatInfo';
export { saveTableCellMetadata } from './table/tableCellInfo';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@ const ALPHABET: Record<number, string> = {
};

/**
* @internal
* Convert decimal numbers into english alphabet letters
* @param decimal The decimal number that needs to be converted
* @param isLowerCase if true the roman value will appear in lower case
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ const RomanValues: Record<string, number> = {
};

/**
* @internal
* Convert decimal numbers into roman numbers
* @param decimal The decimal number that needs to be converted
* @param isLowerCase if true the roman value will appear in lower case
Expand Down
1 change: 1 addition & 0 deletions packages/roosterjs-editor-plugins/lib/Announce.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './plugins/Announce/index';
1 change: 1 addition & 0 deletions packages/roosterjs-editor-plugins/lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,4 @@ export * from './TableResize';
export * from './Watermark';
export * from './TableCellSelection';
export * from './AutoFormat';
export * from './Announce';
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
import { createElement } from 'roosterjs-editor-dom';
import { PluginEventType } from 'roosterjs-editor-types';
import type { CompatibleKnownAnnounceStrings } from 'roosterjs-editor-types/lib/compatibleTypes';
import type {
EditorPlugin,
IEditor,
PluginEvent,
AnnounceData,
KnownAnnounceStrings,
} from 'roosterjs-editor-types';

const ARIA_LIVE_STYLE =
'clip: rect(0px, 0px, 0px, 0px); clip-path: inset(100%); height: 1px; overflow: hidden; position: absolute; white-space: nowrap; width: 1px;';
const ARIA_LIVE_ASSERTIVE = 'assertive';
const DIV_TAG = 'div';
const createAriaLiveElement = (document: Document): HTMLDivElement => {
const element = createElement(
{
tag: DIV_TAG,
style: ARIA_LIVE_STYLE,
attributes: {
'aria-live': ARIA_LIVE_ASSERTIVE,
},
},
document
) as HTMLDivElement;

document.body.appendChild(element);

return element;
};

/**
* Announce messages to screen reader by using aria live element.
*/
export default class Announce implements EditorPlugin {
private ariaLiveElement: HTMLDivElement | undefined;
private editor: IEditor | undefined;

constructor(
private stringsMapOrGetter?:
| Map<CompatibleKnownAnnounceStrings | KnownAnnounceStrings, string>
| ((key: CompatibleKnownAnnounceStrings | KnownAnnounceStrings) => string)
| undefined
) {}

/**
* Get a friendly name of this plugin
*/
getName() {
return 'Announce';
}

/**
* Initialize this plugin
* @param editor The editor instance
*/
initialize(editor: IEditor) {
this.editor = editor;
}

/**
* Dispose this plugin
*/
dispose() {
this.ariaLiveElement?.parentElement?.removeChild(this.ariaLiveElement);
this.ariaLiveElement = undefined;
}

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

protected announce(announceData: AnnounceData, editor: IEditor) {
const { text, defaultStrings, formatStrings = [] } = announceData;
let textToAnnounce = formatString(this.getString(defaultStrings) || text, formatStrings);
if (textToAnnounce) {
if (!this.ariaLiveElement || textToAnnounce == this.ariaLiveElement?.textContent) {
this.ariaLiveElement?.parentElement?.removeChild(this.ariaLiveElement);
this.ariaLiveElement = createAriaLiveElement(editor.getDocument());
}
if (this.ariaLiveElement) {
this.ariaLiveElement.textContent = textToAnnounce;
}
}
}

private getString(key: CompatibleKnownAnnounceStrings | KnownAnnounceStrings | undefined) {
if (this.stringsMapOrGetter == undefined || key == undefined) {
return undefined;
}

if (typeof this.stringsMapOrGetter === 'function') {
return this.stringsMapOrGetter(key);
} else {
return this.stringsMapOrGetter.get(key);
}
}

/**
* @internal
* Public only for unit testing.
* @returns
*/
public getAriaLiveElement() {
return this.ariaLiveElement;
}
}

function formatString(text: string | undefined, formatStrings: string[]) {
if (text == undefined) {
return text;
}

formatStrings.forEach((value, index) => {
text = text?.replace(`{${index}}`, value);
});

return text;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default as AnnouncePlugin } from './AnnouncePlugin';
Original file line number Diff line number Diff line change
@@ -1,31 +1,33 @@
import getAutoBulletListStyle from '../utils/getAutoBulletListStyle';
import getAutoNumberingListStyle from '../utils/getAutoNumberingListStyle';
import {
blockFormat,
commitListChains,
setIndentation,
toggleBullet,
toggleNumbering,
toggleListType,
} from 'roosterjs-editor-api';
import {
Browser,
cacheGetEventData,
convertDecimalsToAlpha,
convertDecimalsToRoman,
createNumberDefinition,
createObjectDefinition,
createVListFromRegion,
findClosestElementAncestor,
getComputedStyle,
getMetadata,
getTagOfNode,
isBlockElement,
isNodeEmpty,
isPositionAtBeginningOf,
Position,
VListChain,
createVListFromRegion,
isBlockElement,
cacheGetEventData,
safeInstanceOf,
VList,
createObjectDefinition,
createNumberDefinition,
getMetadata,
findClosestElementAncestor,
getComputedStyle,
VListChain,
} from 'roosterjs-editor-dom';
import {
blockFormat,
commitListChains,
setIndentation,
toggleBullet,
toggleNumbering,
toggleListType,
} from 'roosterjs-editor-api';
import type {
BuildInEditFeature,
IEditor,
Expand All @@ -41,6 +43,8 @@ import {
PositionType,
NumberingListType,
BulletListType,
KnownAnnounceStrings,
ChangeSource,
} from 'roosterjs-editor-types';

const PREVIOUS_BLOCK_CACHE_KEY = 'previousBlock';
Expand Down Expand Up @@ -70,6 +74,45 @@ 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 All @@ -94,7 +137,21 @@ const handleIndentationEvent = (indenting: boolean) => (
event.rawEvent.keyCode !== Keys.TAB &&
(currentElement = editor.getElementAtCursor()) &&
getComputedStyle(currentElement, 'direction') == 'rtl';
setIndentation(editor, isRTL == indenting ? Indentation.Decrease : Indentation.Increase);

editor.addUndoSnapshot(
() => {
setIndentation(
editor,
isRTL == indenting ? Indentation.Decrease : Indentation.Increase
);
},
ChangeSource.Format,
false /* canUndoByBackspace */,
{
getAnnounceData: () => getAnnounceDataForList(editor),
}
);

event.rawEvent.preventDefault();
};

Expand Down
Loading

0 comments on commit a0d3e94

Please sign in to comment.