diff --git a/demo/scripts/controls/BuildInPluginState.ts b/demo/scripts/controls/BuildInPluginState.ts index 3ce32529df7..ca322275bfb 100644 --- a/demo/scripts/controls/BuildInPluginState.ts +++ b/demo/scripts/controls/BuildInPluginState.ts @@ -23,6 +23,7 @@ export interface BuildInPluginList { contextMenu: boolean; autoFormat: boolean; contentModelPaste: boolean; + announce: boolean; } export default interface BuildInPluginState { diff --git a/demo/scripts/controls/MainPaneBase.tsx b/demo/scripts/controls/MainPaneBase.tsx index ccd28c983a7..46a6cee2dbd 100644 --- a/demo/scripts/controls/MainPaneBase.tsx +++ b/demo/scripts/controls/MainPaneBase.tsx @@ -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, diff --git a/demo/scripts/controls/getToggleablePlugins.ts b/demo/scripts/controls/getToggleablePlugins.ts index 24029ca55a7..b19e2b0411b 100644 --- a/demo/scripts/controls/getToggleablePlugins.ts +++ b/demo/scripts/controls/getToggleablePlugins.ts @@ -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'; @@ -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 { + return new Map([ + [KnownAnnounceStrings.AnnounceListItemBulletIndentation, 'Autocorrected Bullet'], + [KnownAnnounceStrings.AnnounceListItemNumberingIndentation, 'Autocorrected {0}'], + ]); +} diff --git a/demo/scripts/controls/sidePane/editorOptions/ContentModelEditorOptionsPlugin.ts b/demo/scripts/controls/sidePane/editorOptions/ContentModelEditorOptionsPlugin.ts index 53f23dba7b7..2ced4619a66 100644 --- a/demo/scripts/controls/sidePane/editorOptions/ContentModelEditorOptionsPlugin.ts +++ b/demo/scripts/controls/sidePane/editorOptions/ContentModelEditorOptionsPlugin.ts @@ -21,6 +21,7 @@ const initialState: BuildInPluginState = { contextMenu: true, autoFormat: true, contentModelPaste: true, + announce: true, }, contentEditFeatures: getDefaultContentEditFeatureSettings(), defaultFormat: {}, diff --git a/demo/scripts/controls/sidePane/editorOptions/EditorOptionsPlugin.ts b/demo/scripts/controls/sidePane/editorOptions/EditorOptionsPlugin.ts index 2bfbe97ca9b..8c111a1cbca 100644 --- a/demo/scripts/controls/sidePane/editorOptions/EditorOptionsPlugin.ts +++ b/demo/scripts/controls/sidePane/editorOptions/EditorOptionsPlugin.ts @@ -21,6 +21,7 @@ const initialState: BuildInPluginState = { contextMenu: true, autoFormat: true, contentModelPaste: false, + announce: true, }, contentEditFeatures: getDefaultContentEditFeatureSettings(), defaultFormat: {}, diff --git a/demo/scripts/controls/sidePane/editorOptions/Plugins.tsx b/demo/scripts/controls/sidePane/editorOptions/Plugins.tsx index 1591a68058f..952f890ce85 100644 --- a/demo/scripts/controls/sidePane/editorOptions/Plugins.tsx +++ b/demo/scripts/controls/sidePane/editorOptions/Plugins.tsx @@ -61,6 +61,7 @@ export default class Plugins extends React.Component { 'Show customized context menu for special cases' )} {this.renderPluginItem('tableCellSelection', 'Table Cell Selection')} + {this.renderPluginItem('announce', 'Announce')} ); diff --git a/packages/roosterjs-editor-dom/lib/index.ts b/packages/roosterjs-editor-dom/lib/index.ts index 759f6a0cc01..cdc5c62f17b 100644 --- a/packages/roosterjs-editor-dom/lib/index.ts +++ b/packages/roosterjs-editor-dom/lib/index.ts @@ -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'; diff --git a/packages/roosterjs-editor-dom/lib/list/convertDecimalsToAlpha.ts b/packages/roosterjs-editor-dom/lib/list/convertDecimalsToAlpha.ts index 567ad55d15c..b23c5c4c2b3 100644 --- a/packages/roosterjs-editor-dom/lib/list/convertDecimalsToAlpha.ts +++ b/packages/roosterjs-editor-dom/lib/list/convertDecimalsToAlpha.ts @@ -28,7 +28,6 @@ const ALPHABET: Record = { }; /** - * @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 diff --git a/packages/roosterjs-editor-dom/lib/list/convertDecimalsToRomans.ts b/packages/roosterjs-editor-dom/lib/list/convertDecimalsToRomans.ts index c2d4ff8f08d..093ddab509a 100644 --- a/packages/roosterjs-editor-dom/lib/list/convertDecimalsToRomans.ts +++ b/packages/roosterjs-editor-dom/lib/list/convertDecimalsToRomans.ts @@ -17,7 +17,6 @@ const RomanValues: Record = { }; /** - * @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 diff --git a/packages/roosterjs-editor-plugins/lib/Announce.ts b/packages/roosterjs-editor-plugins/lib/Announce.ts new file mode 100644 index 00000000000..5561551056e --- /dev/null +++ b/packages/roosterjs-editor-plugins/lib/Announce.ts @@ -0,0 +1 @@ +export * from './plugins/Announce/index'; diff --git a/packages/roosterjs-editor-plugins/lib/index.ts b/packages/roosterjs-editor-plugins/lib/index.ts index 5cd296430b2..d6977ef38e9 100644 --- a/packages/roosterjs-editor-plugins/lib/index.ts +++ b/packages/roosterjs-editor-plugins/lib/index.ts @@ -11,3 +11,4 @@ export * from './TableResize'; export * from './Watermark'; export * from './TableCellSelection'; export * from './AutoFormat'; +export * from './Announce'; diff --git a/packages/roosterjs-editor-plugins/lib/plugins/Announce/AnnouncePlugin.ts b/packages/roosterjs-editor-plugins/lib/plugins/Announce/AnnouncePlugin.ts new file mode 100644 index 00000000000..5d87e5b1645 --- /dev/null +++ b/packages/roosterjs-editor-plugins/lib/plugins/Announce/AnnouncePlugin.ts @@ -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 + | ((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; +} diff --git a/packages/roosterjs-editor-plugins/lib/plugins/Announce/index.ts b/packages/roosterjs-editor-plugins/lib/plugins/Announce/index.ts new file mode 100644 index 00000000000..bd0b7f3c140 --- /dev/null +++ b/packages/roosterjs-editor-plugins/lib/plugins/Announce/index.ts @@ -0,0 +1 @@ +export { default as AnnouncePlugin } from './AnnouncePlugin'; diff --git a/packages/roosterjs-editor-plugins/lib/plugins/ContentEdit/features/listFeatures.ts b/packages/roosterjs-editor-plugins/lib/plugins/ContentEdit/features/listFeatures.ts index 1b429bad76f..4b95e0c1d73 100644 --- a/packages/roosterjs-editor-plugins/lib/plugins/ContentEdit/features/listFeatures.ts +++ b/packages/roosterjs-editor-plugins/lib/plugins/ContentEdit/features/listFeatures.ts @@ -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, @@ -41,6 +43,8 @@ import { PositionType, NumberingListType, BulletListType, + KnownAnnounceStrings, + ChangeSource, } from 'roosterjs-editor-types'; const PREVIOUS_BLOCK_CACHE_KEY = 'previousBlock'; @@ -70,6 +74,45 @@ const ListStyleDefinitionMetadata = createObjectDefinition( 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 @@ -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(); }; diff --git a/packages/roosterjs-editor-plugins/test/Announce/AnnouncePluginTest.ts b/packages/roosterjs-editor-plugins/test/Announce/AnnouncePluginTest.ts new file mode 100644 index 00000000000..1be411444e7 --- /dev/null +++ b/packages/roosterjs-editor-plugins/test/Announce/AnnouncePluginTest.ts @@ -0,0 +1,102 @@ +import AnnouncePlugin from '../../lib/plugins/Announce/AnnouncePlugin'; +import { IEditor, PluginEventType } from 'roosterjs-editor-types'; + +describe('AnnouncePlugin', () => { + const mockEditor: IEditor = { + getDocument: () => document, + } as any; + + it('initialize', () => { + const plugin = new AnnouncePlugin(); + plugin.initialize(mockEditor); + + expect((plugin as any).editor).toEqual(mockEditor); + }); + + it('onPluginEvent & dispose', () => { + const mockStrings = 'MockStrings' as any; + + const plugin = new AnnouncePlugin(mockStrings); + const mockAnnounceData = { + text: 'Announcement text', + defaultStrings: undefined, + formatStrings: [], + } as any; + + plugin.initialize(mockEditor); + plugin.onPluginEvent({ + eventType: PluginEventType.ContentChanged, + source: 'Test', + additionalData: { + getAnnounceData: () => mockAnnounceData, + }, + }); + + expect(plugin.getAriaLiveElement()).toBeDefined(); + expect(plugin.getAriaLiveElement()?.textContent).toEqual(mockAnnounceData.text); + plugin.dispose(); + expect(plugin.getAriaLiveElement()).toBeUndefined(); + }); + + it('onPluginEvent & replace {0}, {1}! with [Hello, World] => Hello World', () => { + const mockStrings = 'MockStrings' as any; + + const plugin = new AnnouncePlugin(mockStrings); + const announceData = { + text: '{0}, {1}!', + defaultStrings: undefined, + formatStrings: ['Hello', 'World'], + } as any; + + plugin.initialize(mockEditor); + plugin.onPluginEvent({ + eventType: PluginEventType.ContentChanged, + source: 'Test', + additionalData: { + getAnnounceData: () => announceData, + }, + }); + + expect(plugin.getAriaLiveElement()).toBeDefined(); + expect(plugin.getAriaLiveElement()?.textContent).toEqual('Hello, World!'); + plugin.dispose(); + expect(plugin.getAriaLiveElement()).toBeUndefined(); + }); + + it('onPluginEvent & dispose, getAnnounceData returns undefined', () => { + const mockStrings = 'MockStrings' as any; + + const plugin = new AnnouncePlugin(mockStrings); + + plugin.initialize(mockEditor); + plugin.onPluginEvent({ + eventType: PluginEventType.ContentChanged, + source: 'Test', + additionalData: { + getAnnounceData: () => { + return undefined; + }, + }, + }); + + expect(plugin.getAriaLiveElement()).toBeUndefined(); + plugin.dispose(); + expect(plugin.getAriaLiveElement()).toBeUndefined(); + }); + + it('onPluginEvent & dispose, getAnnounceData is undefined', () => { + const mockStrings = 'MockStrings' as any; + + const plugin = new AnnouncePlugin(mockStrings); + + plugin.initialize(mockEditor); + plugin.onPluginEvent({ + eventType: PluginEventType.ContentChanged, + source: 'Test', + }); + + expect(plugin.getAriaLiveElement()).toBeUndefined(); + plugin.dispose(); + expect(plugin.getAriaLiveElement()).toBeUndefined(); + }); +}); diff --git a/packages/roosterjs-editor-plugins/test/ContentEdit/features/listFeaturesTest.ts b/packages/roosterjs-editor-plugins/test/ContentEdit/features/listFeaturesTest.ts index 3240773bc73..7aa52a85218 100644 --- a/packages/roosterjs-editor-plugins/test/ContentEdit/features/listFeaturesTest.ts +++ b/packages/roosterjs-editor-plugins/test/ContentEdit/features/listFeaturesTest.ts @@ -3,8 +3,11 @@ import * as setIndentation from 'roosterjs-editor-api/lib/format/setIndentation' import * as TestHelper from 'roosterjs-editor-api/test/TestHelper'; import * as toggleListType from 'roosterjs-editor-api/lib/utils/toggleListType'; import getBlockElementAtNode from 'roosterjs-editor-dom/lib/blockElements/getBlockElementAtNode'; -import { ListFeatures } from '../../../lib/plugins/ContentEdit/features/listFeatures'; -import { Position, PositionContentSearcher } from 'roosterjs-editor-dom'; +import { createElement, Position, PositionContentSearcher } from 'roosterjs-editor-dom'; +import { + getAnnounceDataForList, + ListFeatures, +} from '../../../lib/plugins/ContentEdit/features/listFeatures'; import { IEditor, Indentation, @@ -13,6 +16,7 @@ import { Keys, BlockElement, IContentTraverser, + KnownAnnounceStrings, } from 'roosterjs-editor-types'; describe('listFeatures | AutoBullet', () => { @@ -780,3 +784,81 @@ describe('listFeatures | mergeListOnBackspaceAfterList', () => { ); }); }); + +describe('getAnnounceDataForList', () => { + it('should return announce data for numbered list item | OL', () => { + const el = createElement( + { + tag: 'OL', + children: [ + { + tag: 'LI', + children: ['asd'], + }, + ], + }, + document + ); + + el && document.body.appendChild(el); + + const editorMock = { + getElementAtCursor: (selector: string) => { + if (selector == 'OL,UL') { + return el; + } else { + return el?.firstChild; + } + }, + }; + + const announceData = getAnnounceDataForList(editorMock as any); + expect(announceData).toEqual({ + defaultStrings: KnownAnnounceStrings.AnnounceListItemNumberingIndentation, + formatStrings: ['1'], + }); + }); + + it('should return announce data for numbered list item | UL', () => { + const el = createElement( + { + tag: 'UL', + children: [ + { + tag: 'LI', + children: ['asd'], + }, + ], + }, + document + ); + + el && document.body.appendChild(el); + + const editorMock = { + getElementAtCursor: (selector: string) => { + if (selector == 'OL,UL') { + return el; + } else { + return el?.firstChild; + } + }, + }; + + const announceData = getAnnounceDataForList(editorMock as any); + expect(announceData).toEqual({ + defaultStrings: KnownAnnounceStrings.AnnounceListItemBulletIndentation, + }); + }); + + it('should return announce data for bullet list item | undefined', () => { + const editorMock: any = { + getElementAtCursor: (): any => { + return undefined; + }, + }; + + const announceData = getAnnounceDataForList(editorMock as any); + expect(announceData).toEqual(undefined); + }); +}); diff --git a/packages/roosterjs-editor-types/lib/enum/KnownAnnounceStrings.ts b/packages/roosterjs-editor-types/lib/enum/KnownAnnounceStrings.ts new file mode 100644 index 00000000000..c733140ea83 --- /dev/null +++ b/packages/roosterjs-editor-types/lib/enum/KnownAnnounceStrings.ts @@ -0,0 +1,18 @@ +/** + * Known announce strings + */ +export const enum KnownAnnounceStrings { + /** + * String announced when Indenting or Outdenting a list item in a OL List + * @example + * Auto corrected, {0} + * Where &lcub0&rcub is the new list item bullet + */ + AnnounceListItemNumberingIndentation = 1, + /** + * String announced when Indenting or Outdenting a list item in a UL List + * @example + * Auto corrected bullet + */ + AnnounceListItemBulletIndentation, +} diff --git a/packages/roosterjs-editor-types/lib/enum/index.ts b/packages/roosterjs-editor-types/lib/enum/index.ts index 0db87f996bd..1aa95cdd2f4 100644 --- a/packages/roosterjs-editor-types/lib/enum/index.ts +++ b/packages/roosterjs-editor-types/lib/enum/index.ts @@ -1,3 +1,4 @@ +export { KnownAnnounceStrings } from './KnownAnnounceStrings'; export { DocumentCommand } from './DocumentCommand'; export { DocumentPosition } from './DocumentPosition'; export { Keys } from './Keys'; diff --git a/packages/roosterjs-editor-types/lib/interface/AnnounceData.ts b/packages/roosterjs-editor-types/lib/interface/AnnounceData.ts new file mode 100644 index 00000000000..113c7361f98 --- /dev/null +++ b/packages/roosterjs-editor-types/lib/interface/AnnounceData.ts @@ -0,0 +1,23 @@ +import type { CompatibleKnownAnnounceStrings } from '../compatibleEnum/KnownAnnounceStrings'; +import type { KnownAnnounceStrings } from '../enum/KnownAnnounceStrings'; + +/** + * Represents data, that can be used to announce text to screen reader. + */ +export default interface AnnounceData { + /** + * @optional Default announce strings built in Rooster + */ + defaultStrings?: KnownAnnounceStrings | CompatibleKnownAnnounceStrings; + + /** + * @optional string to announce from this Content Changed event, will be the fallback value if default string + * is not provided or if it is not found in the strings map. + */ + text?: string; + + /** + * @optional if provided, will attempt to replace {n} with each of the values inside of the array. + */ + formatStrings?: string[]; +} diff --git a/packages/roosterjs-editor-types/lib/interface/ContentChangedData.ts b/packages/roosterjs-editor-types/lib/interface/ContentChangedData.ts index 5e06c440391..b74129d496a 100644 --- a/packages/roosterjs-editor-types/lib/interface/ContentChangedData.ts +++ b/packages/roosterjs-editor-types/lib/interface/ContentChangedData.ts @@ -1,3 +1,4 @@ +import type AnnounceData from './AnnounceData'; import type { EntityState } from './Snapshot'; /** @@ -15,4 +16,11 @@ export default interface ContentChangedData { * @returns Related entity state array */ getEntityState?: () => EntityState[]; + + /** + * @optional + * Get Announce data from this content changed event. + * @returns + */ + getAnnounceData?: () => AnnounceData | undefined; } diff --git a/packages/roosterjs-editor-types/lib/interface/IEditor.ts b/packages/roosterjs-editor-types/lib/interface/IEditor.ts index 0e58f4c95e6..64a34bad60f 100644 --- a/packages/roosterjs-editor-types/lib/interface/IEditor.ts +++ b/packages/roosterjs-editor-types/lib/interface/IEditor.ts @@ -576,7 +576,6 @@ export default interface IEditor { * @param keyboardEvent Optional keyboard event object */ ensureTypeInContainer(position: NodePosition, keyboardEvent?: KeyboardEvent): void; - //#endregion //#region Dark mode APIs diff --git a/packages/roosterjs-editor-types/lib/interface/index.ts b/packages/roosterjs-editor-types/lib/interface/index.ts index 93fd12d35ea..334fdff5529 100644 --- a/packages/roosterjs-editor-types/lib/interface/index.ts +++ b/packages/roosterjs-editor-types/lib/interface/index.ts @@ -1,3 +1,4 @@ +export { default as AnnounceData } from './AnnounceData'; export { default as BlockElement } from './BlockElement'; export { default as ClipboardData } from './ClipboardData'; export { default as ContextMenuProvider } from './ContextMenuProvider';