diff --git a/demo/scripts/controlsV2/mainPane/MainPane.tsx b/demo/scripts/controlsV2/mainPane/MainPane.tsx index d25ad59ed55..9f3ec7f2316 100644 --- a/demo/scripts/controlsV2/mainPane/MainPane.tsx +++ b/demo/scripts/controlsV2/mainPane/MainPane.tsx @@ -2,8 +2,6 @@ import * as React from 'react'; import * as ReactDOM from 'react-dom'; import SampleEntityPlugin from '../plugins/SampleEntityPlugin'; import { ApiPlaygroundPlugin } from '../sidePane/apiPlayground/ApiPlaygroundPlugin'; -import { Border, ContentModelDocument, EditorOptions } from 'roosterjs-content-model-types'; -import { Colors, EditorPlugin, IEditor, Snapshots } from 'roosterjs-content-model-types'; import { ContentModelPanePlugin } from '../sidePane/contentModel/ContentModelPanePlugin'; import { createEmojiPlugin } from '../roosterjsReact/emoji'; import { createImageEditMenuProvider } from '../roosterjsReact/contextMenu/menus/createImageEditMenuProvider'; @@ -41,6 +39,16 @@ import { undoButton } from '../roosterjsReact/ribbon/buttons/undoButton'; import { UpdateContentPlugin } from '../plugins/UpdateContentPlugin'; import { WindowProvider } from '@fluentui/react/lib/WindowProvider'; import { zoomButton } from '../demoButtons/zoomButton'; +import { + Border, + Colors, + ContentModelDocument, + EditorOptions, + EditorPlugin, + IEditor, + KnownAnnounceStrings, + Snapshots, +} from 'roosterjs-content-model-types'; import { AutoFormatPlugin, CustomReplacePlugin, @@ -361,6 +369,7 @@ export class MainPane extends React.Component<{}, MainPaneState> { dir={this.state.isRtl ? 'rtl' : 'ltr'} knownColors={this.knownColors} disableCache={this.state.initState.disableCache} + announcerStringGetter={getAnnouncingString} /> )} @@ -511,6 +520,16 @@ export class MainPane extends React.Component<{}, MainPaneState> { } } +const AnnounceStringMap: Record = { + announceListItemBullet: 'Auto corrected Bullet', + announceListItemNumbering: 'Auto corrected {0}', + announceOnFocusLastCell: 'Warning, pressing tab here adds an extra row.', +}; + +function getAnnouncingString(key: KnownAnnounceStrings) { + return AnnounceStringMap[key]; +} + export function mount(parent: HTMLElement) { ReactDOM.render(, parent); } diff --git a/demo/scripts/controlsV2/plugins/createLegacyPlugins.ts b/demo/scripts/controlsV2/plugins/createLegacyPlugins.ts index 89a8911d9f6..a4fdd37cc88 100644 --- a/demo/scripts/controlsV2/plugins/createLegacyPlugins.ts +++ b/demo/scripts/controlsV2/plugins/createLegacyPlugins.ts @@ -1,5 +1,5 @@ -import { Announce, ImageEdit } from 'roosterjs-editor-plugins'; -import { EditorPlugin as LegacyEditorPlugin, KnownAnnounceStrings } from 'roosterjs-editor-types'; +import { EditorPlugin as LegacyEditorPlugin } from 'roosterjs-editor-types'; +import { ImageEdit } from 'roosterjs-editor-plugins'; import { LegacyPluginList, OptionState } from '../sidePane/editorOptions/OptionState'; export function createLegacyPlugins(initState: OptionState): LegacyEditorPlugin[] { @@ -12,19 +12,7 @@ export function createLegacyPlugins(initState: OptionState): LegacyEditorPlugin[ applyChangesOnMouseUp: initState.applyChangesOnMouseUp, }) : null, - announce: pluginList.announce ? new Announce(getDefaultStringsMap()) : null, }; return Object.values(plugins).filter(x => !!x); } - -function getDefaultStringsMap(): Map { - return new Map([ - [KnownAnnounceStrings.AnnounceListItemBullet, 'Autocorrected Bullet'], - [KnownAnnounceStrings.AnnounceListItemNumbering, 'Autocorrected {0}'], - [ - KnownAnnounceStrings.AnnounceOnFocusLastCell, - 'Warning, pressing tab here adds an extra row.', - ], - ]); -} diff --git a/demo/scripts/controlsV2/roosterjsReact/rooster/component/Rooster.tsx b/demo/scripts/controlsV2/roosterjsReact/rooster/component/Rooster.tsx index 1efdc590789..134a49a2c04 100644 --- a/demo/scripts/controlsV2/roosterjsReact/rooster/component/Rooster.tsx +++ b/demo/scripts/controlsV2/roosterjsReact/rooster/component/Rooster.tsx @@ -53,7 +53,14 @@ export function Rooster(props: RoosterProps) { }, [inDarkMode]); const divProps = getNativeProps>(props, divProperties); - return
; + return ( +
+ ); } function defaultEditorCreator(div: HTMLDivElement, options: EditorOptions) { diff --git a/demo/scripts/controlsV2/sidePane/editorOptions/EditorOptionsPlugin.ts b/demo/scripts/controlsV2/sidePane/editorOptions/EditorOptionsPlugin.ts index fe09f861a55..4213946b7b1 100644 --- a/demo/scripts/controlsV2/sidePane/editorOptions/EditorOptionsPlugin.ts +++ b/demo/scripts/controlsV2/sidePane/editorOptions/EditorOptionsPlugin.ts @@ -22,7 +22,6 @@ const initialState: OptionState = { // Legacy plugins imageEdit: false, - announce: false, }, defaultFormat: { fontFamily: 'Calibri', diff --git a/demo/scripts/controlsV2/sidePane/editorOptions/OptionState.ts b/demo/scripts/controlsV2/sidePane/editorOptions/OptionState.ts index 37c1267040f..57bbd973492 100644 --- a/demo/scripts/controlsV2/sidePane/editorOptions/OptionState.ts +++ b/demo/scripts/controlsV2/sidePane/editorOptions/OptionState.ts @@ -4,7 +4,6 @@ import type { ContentModelSegmentFormat } from 'roosterjs-content-model-types'; export interface LegacyPluginList { imageEdit: boolean; - announce: boolean; } export interface NewPluginList { diff --git a/demo/scripts/controlsV2/sidePane/editorOptions/Plugins.tsx b/demo/scripts/controlsV2/sidePane/editorOptions/Plugins.tsx index 103662a386d..6e5ca1dae52 100644 --- a/demo/scripts/controlsV2/sidePane/editorOptions/Plugins.tsx +++ b/demo/scripts/controlsV2/sidePane/editorOptions/Plugins.tsx @@ -118,7 +118,6 @@ export class LegacyPlugins extends PluginsBase { (state, value) => (state.forcePreserveRatio = value) ) )} - {this.renderPluginItem('announce', 'Announce')} ); diff --git a/packages/roosterjs-content-model-api/lib/index.ts b/packages/roosterjs-content-model-api/lib/index.ts index 7259a8b3f3d..7fc2bdf07d2 100644 --- a/packages/roosterjs-content-model-api/lib/index.ts +++ b/packages/roosterjs-content-model-api/lib/index.ts @@ -56,3 +56,4 @@ export { setModelListStartNumber } from './modelApi/list/setModelListStartNumber export { findListItemsInSameThread } from './modelApi/list/findListItemsInSameThread'; export { setModelIndentation } from './modelApi/block/setModelIndentation'; export { matchLink } from './modelApi/link/matchLink'; +export { getListAnnounceData } from './modelApi/list/getListAnnounceData'; diff --git a/packages/roosterjs-content-model-api/lib/modelApi/block/setModelIndentation.ts b/packages/roosterjs-content-model-api/lib/modelApi/block/setModelIndentation.ts index 65358c5bfe6..828635044a0 100644 --- a/packages/roosterjs-content-model-api/lib/modelApi/block/setModelIndentation.ts +++ b/packages/roosterjs-content-model-api/lib/modelApi/block/setModelIndentation.ts @@ -1,4 +1,5 @@ import { findListItemsInSameThread } from '../list/findListItemsInSameThread'; +import { getListAnnounceData } from '../list/getListAnnounceData'; import { createListLevel, getOperationalBlocks, @@ -13,6 +14,7 @@ import type { ContentModelDocument, ContentModelListItem, ContentModelListLevel, + FormatContentModelContext, } from 'roosterjs-content-model-types'; const IndentStepInPixel = 40; @@ -26,7 +28,8 @@ const IndentStepInPixel = 40; export function setModelIndentation( model: ContentModelDocument, indentation: 'indent' | 'outdent', - length: number = IndentStepInPixel + length: number = IndentStepInPixel, + context?: FormatContentModelContext ) { const paragraphOrListItem = getOperationalBlocks( model, @@ -80,6 +83,10 @@ export function setModelIndentation( } else { block.levels.pop(); } + + if (block.levels.length > 0 && context) { + context.announceData = getListAnnounceData([block, ...path]); + } } } else if (block) { let currentBlock: ContentModelBlock = block; diff --git a/packages/roosterjs-content-model-api/lib/modelApi/list/findListItemsInSameThread.ts b/packages/roosterjs-content-model-api/lib/modelApi/list/findListItemsInSameThread.ts index 55511429be8..3ad7824f584 100644 --- a/packages/roosterjs-content-model-api/lib/modelApi/list/findListItemsInSameThread.ts +++ b/packages/roosterjs-content-model-api/lib/modelApi/list/findListItemsInSameThread.ts @@ -1,8 +1,4 @@ -import type { - ContentModelBlockGroup, - ContentModelDocument, - ContentModelListItem, -} from 'roosterjs-content-model-types'; +import type { ContentModelBlockGroup, ContentModelListItem } from 'roosterjs-content-model-types'; /** * @param model The content model @@ -10,12 +6,12 @@ import type { * Search for all list items in the same thread as the current list item */ export function findListItemsInSameThread( - model: ContentModelDocument, + group: ContentModelBlockGroup, currentItem: ContentModelListItem ): ContentModelListItem[] { const items: (ContentModelListItem | null)[] = []; - findListItems(model, items); + findListItems(group, items); return filterListItems(items, currentItem); } @@ -97,7 +93,11 @@ function filterListItems( if (isOrderedList && startNumberOverride) { break; } - } else if (!isOrderedList || startNumberOverride) { + } else if ( + !isOrderedList || + startNumberOverride || + item.levels.length < currentItem.levels.length + ) { break; } } @@ -117,7 +117,11 @@ function filterListItems( if (areListTypesCompatible(items, currentIndex, i) && !startNumberOverride) { result.push(item); - } else if (!isOrderedList || startNumberOverride) { + } else if ( + !isOrderedList || + startNumberOverride || + item.levels.length < currentItem.levels.length + ) { break; } } diff --git a/packages/roosterjs-content-model-api/lib/modelApi/list/getListAnnounceData.ts b/packages/roosterjs-content-model-api/lib/modelApi/list/getListAnnounceData.ts new file mode 100644 index 00000000000..20fc55e3ee1 --- /dev/null +++ b/packages/roosterjs-content-model-api/lib/modelApi/list/getListAnnounceData.ts @@ -0,0 +1,80 @@ +import { findListItemsInSameThread } from './findListItemsInSameThread'; +import { + getAutoListStyleType, + getClosestAncestorBlockGroupIndex, + getOrderedListNumberStr, + updateListMetadata, +} from 'roosterjs-content-model-dom'; +import type { + AnnounceData, + ContentModelBlockGroup, + ContentModelListItem, +} from 'roosterjs-content-model-types'; + +/** + * Get announce data for list item + * @param path Content model path that include the list item + * @returns Announce data of current list item if any, or null + */ +export function getListAnnounceData(path: ContentModelBlockGroup[]): AnnounceData | null { + const index = getClosestAncestorBlockGroupIndex(path, ['ListItem'], ['TableCell']); + + if (index >= 0) { + const listItem = path[index] as ContentModelListItem; + const level = listItem.levels[listItem.levels.length - 1]; + + if (level.format.displayForDummyItem) { + return null; + } else if (level.listType == 'OL') { + const listNumber = getListNumber(path, listItem); + const metadata = updateListMetadata(level); + const listStyle = getAutoListStyleType( + 'OL', + metadata ?? {}, + listItem.levels.length - 1, + level.format.listStyleType + ); + + return listStyle === undefined + ? null + : { + defaultStrings: 'announceListItemNumbering', + formatStrings: [getOrderedListNumberStr(listStyle, listNumber)], + }; + } else { + return { + defaultStrings: 'announceListItemBullet', + }; + } + } else { + return null; + } +} + +function getListNumber(path: ContentModelBlockGroup[], listItem: ContentModelListItem) { + const items = findListItemsInSameThread(path[path.length - 1], listItem); + let listNumber = 0; + + for (let i = 0; i < items.length; i++) { + const item = items[i]; + + if (listNumber == 0 && item.levels.length == listItem.levels.length) { + listNumber = item.levels[item.levels.length - 1]?.format.startNumberOverride ?? 1; + } + + if (item == listItem) { + // Found current item, so break and return + break; + } else if (item.levels.length < listItem.levels.length) { + // Found upper level item, reset list number + listNumber = 0; + } else if (item.levels.length > listItem.levels.length) { + // Found deeper level item, skip + continue; + } else if (!item.levels[item.levels.length - 1].format.displayForDummyItem) { + // Save level, and is not dummy, number plus one + listNumber++; + } + } + return listNumber; +} diff --git a/packages/roosterjs-content-model-api/test/modelApi/block/setModelIndentationTest.ts b/packages/roosterjs-content-model-api/test/modelApi/block/setModelIndentationTest.ts index 9cad3b4dc88..765ca3526e6 100644 --- a/packages/roosterjs-content-model-api/test/modelApi/block/setModelIndentationTest.ts +++ b/packages/roosterjs-content-model-api/test/modelApi/block/setModelIndentationTest.ts @@ -1,3 +1,5 @@ +import * as getListAnnounceData from '../../../lib/modelApi/list/getListAnnounceData'; +import { FormatContentModelContext } from 'roosterjs-content-model-types'; import { setModelIndentation } from '../../../lib/modelApi/block/setModelIndentation'; import { createContentModelDocument, @@ -9,30 +11,61 @@ import { } from 'roosterjs-content-model-dom'; describe('indent', () => { + let getListAnnounceDataSpy: jasmine.Spy; + const mockedAnnounceData = 'ANNOUNCE' as any; + + beforeEach(() => { + getListAnnounceDataSpy = spyOn(getListAnnounceData, 'getListAnnounceData').and.returnValue( + mockedAnnounceData + ); + }); + it('Empty group', () => { const group = createContentModelDocument(); + const context: FormatContentModelContext = { + deletedEntities: [], + newEntities: [], + newImages: [], + }; - const result = setModelIndentation(group, 'indent'); + const result = setModelIndentation(group, 'indent', undefined, context); expect(group).toEqual({ blockGroupType: 'Document', blocks: [], }); expect(result).toBeFalse(); + expect(context).toEqual({ + deletedEntities: [], + newEntities: [], + newImages: [], + }); + expect(getListAnnounceDataSpy).not.toHaveBeenCalled(); }); it('Group without selection', () => { const group = createContentModelDocument(); const para = createParagraph(); + const context: FormatContentModelContext = { + deletedEntities: [], + newEntities: [], + newImages: [], + }; group.blocks.push(para); - const result = setModelIndentation(group, 'indent'); + const result = setModelIndentation(group, 'indent', undefined, context); expect(group).toEqual({ blockGroupType: 'Document', blocks: [para], }); expect(result).toBeFalse(); + expect(context).toEqual({ + deletedEntities: [], + newEntities: [], + newImages: [], + }); + expect(getListAnnounceDataSpy).not.toHaveBeenCalled(); }); it('Group with selected paragraph', () => { @@ -43,6 +76,11 @@ describe('indent', () => { const text1 = createText('test1'); const text2 = createText('test2'); const text3 = createText('test3'); + const context: FormatContentModelContext = { + deletedEntities: [], + newEntities: [], + newImages: [], + }; para1.segments.push(text1); para2.segments.push(text2); @@ -53,7 +91,7 @@ describe('indent', () => { text2.isSelected = true; - const result = setModelIndentation(group, 'indent'); + const result = setModelIndentation(group, 'indent', undefined, context); expect(group).toEqual({ blockGroupType: 'Document', blocks: [ @@ -77,6 +115,12 @@ describe('indent', () => { ], }); expect(result).toBeTrue(); + expect(context).toEqual({ + deletedEntities: [], + newEntities: [], + newImages: [], + }); + expect(getListAnnounceDataSpy).not.toHaveBeenCalled(); }); it('Group with selected indented paragraph', () => { @@ -87,6 +131,11 @@ describe('indent', () => { const text1 = createText('test1'); const text2 = createText('test2'); const text3 = createText('test3'); + const context: FormatContentModelContext = { + deletedEntities: [], + newEntities: [], + newImages: [], + }; para1.segments.push(text1); para2.segments.push(text2); @@ -99,7 +148,7 @@ describe('indent', () => { text2.isSelected = true; text3.isSelected = true; - const result = setModelIndentation(group, 'indent'); + const result = setModelIndentation(group, 'indent', undefined, context); expect(group).toEqual({ blockGroupType: 'Document', blocks: [ @@ -127,6 +176,12 @@ describe('indent', () => { ], }); expect(result).toBeTrue(); + expect(context).toEqual({ + deletedEntities: [], + newEntities: [], + newImages: [], + }); + expect(getListAnnounceDataSpy).not.toHaveBeenCalled(); }); it('Group with selected indented paragraph in RTL', () => { @@ -137,13 +192,18 @@ describe('indent', () => { direction: 'rtl', }); const text1 = createText('test1'); + const context: FormatContentModelContext = { + deletedEntities: [], + newEntities: [], + newImages: [], + }; para1.segments.push(text1); group.blocks.push(para1); text1.isSelected = true; - const result = setModelIndentation(group, 'indent'); + const result = setModelIndentation(group, 'indent', undefined, context); expect(group).toEqual({ blockGroupType: 'Document', blocks: [ @@ -159,6 +219,12 @@ describe('indent', () => { ], }); expect(result).toBeTrue(); + expect(context).toEqual({ + deletedEntities: [], + newEntities: [], + newImages: [], + }); + expect(getListAnnounceDataSpy).not.toHaveBeenCalled(); }); it('Group with multiple selected paragraph - 1', () => { @@ -169,6 +235,11 @@ describe('indent', () => { const text1 = createText('test1'); const text2 = createText('test2'); const text3 = createText('test3'); + const context: FormatContentModelContext = { + deletedEntities: [], + newEntities: [], + newImages: [], + }; para1.segments.push(text1); para2.segments.push(text2); @@ -180,7 +251,7 @@ describe('indent', () => { text2.isSelected = true; text3.isSelected = true; - const result = setModelIndentation(group, 'indent'); + const result = setModelIndentation(group, 'indent', undefined, context); expect(group).toEqual({ blockGroupType: 'Document', blocks: [ @@ -206,6 +277,12 @@ describe('indent', () => { ], }); expect(result).toBeTrue(); + expect(context).toEqual({ + deletedEntities: [], + newEntities: [], + newImages: [], + }); + expect(getListAnnounceDataSpy).not.toHaveBeenCalled(); }); it('Group with multiple selected paragraph - 2', () => { @@ -216,6 +293,11 @@ describe('indent', () => { const text1 = createText('test1'); const text2 = createText('test2'); const text3 = createText('test3'); + const context: FormatContentModelContext = { + deletedEntities: [], + newEntities: [], + newImages: [], + }; para1.segments.push(text1); para2.segments.push(text2); @@ -227,7 +309,7 @@ describe('indent', () => { text1.isSelected = true; text3.isSelected = true; - const result = setModelIndentation(group, 'indent'); + const result = setModelIndentation(group, 'indent', undefined, context); expect(group).toEqual({ blockGroupType: 'Document', blocks: [ @@ -253,6 +335,12 @@ describe('indent', () => { ], }); expect(result).toBeTrue(); + expect(context).toEqual({ + deletedEntities: [], + newEntities: [], + newImages: [], + }); + expect(getListAnnounceDataSpy).not.toHaveBeenCalled(); }); it('Group with paragraph under OL', () => { @@ -264,6 +352,11 @@ describe('indent', () => { const text2 = createText('test2'); const text3 = createText('test3'); const listItem = createListItem([createListLevel('OL')]); + const context: FormatContentModelContext = { + deletedEntities: [], + newEntities: [], + newImages: [], + }; para1.segments.push(text1); para2.segments.push(text2); @@ -275,7 +368,7 @@ describe('indent', () => { text2.isSelected = true; - const result = setModelIndentation(group, 'indent'); + const result = setModelIndentation(group, 'indent', undefined, context); expect(group).toEqual({ blockGroupType: 'Document', @@ -299,6 +392,12 @@ describe('indent', () => { ], }); expect(result).toBeTrue(); + expect(context).toEqual({ + deletedEntities: [], + newEntities: [], + newImages: [], + }); + expect(getListAnnounceDataSpy).not.toHaveBeenCalled(); }); it('Group with paragraph under OL with formats', () => { @@ -320,6 +419,11 @@ describe('indent', () => { } ), ]); + const context: FormatContentModelContext = { + deletedEntities: [], + newEntities: [], + newImages: [], + }; para1.segments.push(text1); para2.segments.push(text2); @@ -331,7 +435,7 @@ describe('indent', () => { text2.isSelected = true; - const result = setModelIndentation(group, 'indent'); + const result = setModelIndentation(group, 'indent', undefined, context); expect(group).toEqual({ blockGroupType: 'Document', @@ -361,6 +465,12 @@ describe('indent', () => { ], }); expect(result).toBeTrue(); + expect(context).toEqual({ + deletedEntities: [], + newEntities: [], + newImages: [], + }); + expect(getListAnnounceDataSpy).not.toHaveBeenCalled(); }); it('Group with paragraph and multiple OL', () => { @@ -370,6 +480,11 @@ describe('indent', () => { const listItem1 = createListItem([createListLevel('OL')]); const listItem2 = createListItem([createListLevel('OL')]); const listItem3 = createListItem([createListLevel('OL')]); + const context: FormatContentModelContext = { + deletedEntities: [], + newEntities: [], + newImages: [], + }; para2.segments.push(text2); listItem2.blocks.push(para2); @@ -379,7 +494,7 @@ describe('indent', () => { text2.isSelected = true; - const result = setModelIndentation(group, 'indent'); + const result = setModelIndentation(group, 'indent', undefined, context); expect(group).toEqual({ blockGroupType: 'Document', @@ -404,6 +519,14 @@ describe('indent', () => { ], }); expect(result).toBeTrue(); + expect(context).toEqual({ + deletedEntities: [], + newEntities: [], + newImages: [], + announceData: mockedAnnounceData, + }); + expect(getListAnnounceDataSpy).toHaveBeenCalledTimes(1); + expect(getListAnnounceDataSpy).toHaveBeenCalledWith([listItem2, group]); }); it('Group with multiple selected paragraph and multiple OL', () => { @@ -417,6 +540,11 @@ describe('indent', () => { const listItem1 = createListItem([createListLevel('OL')]); const listItem2 = createListItem([createListLevel('OL')]); const listItem3 = createListItem([createListLevel('OL')]); + const context: FormatContentModelContext = { + deletedEntities: [], + newEntities: [], + newImages: [], + }; para1.segments.push(text1); para2.segments.push(text2); @@ -432,7 +560,7 @@ describe('indent', () => { text2.isSelected = true; text3.isSelected = true; - const result = setModelIndentation(group, 'indent'); + const result = setModelIndentation(group, 'indent', undefined, context); expect(group).toEqual({ blockGroupType: 'Document', @@ -471,6 +599,15 @@ describe('indent', () => { ], }); expect(result).toBeTrue(); + expect(context).toEqual({ + deletedEntities: [], + newEntities: [], + newImages: [], + announceData: mockedAnnounceData, + }); + expect(getListAnnounceDataSpy).toHaveBeenCalledTimes(2); + expect(getListAnnounceDataSpy).toHaveBeenCalledWith([listItem2, group]); + expect(getListAnnounceDataSpy).toHaveBeenCalledWith([listItem3, group]); }); it('Group with multiple selected paragraph and UL and OL', () => { @@ -484,6 +621,11 @@ describe('indent', () => { const listItem1 = createListItem([createListLevel('OL')]); const listItem2 = createListItem([createListLevel('OL')]); const listItem3 = createListItem([createListLevel('UL')]); + const context: FormatContentModelContext = { + deletedEntities: [], + newEntities: [], + newImages: [], + }; para1.segments.push(text1); para2.segments.push(text2); @@ -499,7 +641,7 @@ describe('indent', () => { text2.isSelected = true; text3.isSelected = true; - const result = setModelIndentation(group, 'indent'); + const result = setModelIndentation(group, 'indent', undefined, context); expect(group).toEqual({ blockGroupType: 'Document', @@ -539,6 +681,14 @@ describe('indent', () => { ], }); expect(result).toBeTrue(); + expect(context).toEqual({ + deletedEntities: [], + newEntities: [], + newImages: [], + announceData: mockedAnnounceData, + }); + expect(getListAnnounceDataSpy).toHaveBeenCalledTimes(1); + expect(getListAnnounceDataSpy).toHaveBeenCalledWith([listItem2, group]); }); it('Mixed with paragraph, list item and quote', () => { @@ -552,6 +702,11 @@ describe('indent', () => { const text3 = createText('test3'); const listItem1 = createListItem([createListLevel('OL')]); const listItem2 = createListItem([createListLevel('UL')]); + const context: FormatContentModelContext = { + deletedEntities: [], + newEntities: [], + newImages: [], + }; para1.segments.push(text1); para2.segments.push(text2); @@ -568,7 +723,7 @@ describe('indent', () => { text2.isSelected = true; text3.isSelected = true; - const result = setModelIndentation(group, 'indent'); + const result = setModelIndentation(group, 'indent', undefined, context); expect(group).toEqual({ blockGroupType: 'Document', @@ -615,6 +770,12 @@ describe('indent', () => { ], }); expect(result).toBeTrue(); + expect(context).toEqual({ + deletedEntities: [], + newEntities: [], + newImages: [], + }); + expect(getListAnnounceDataSpy).not.toHaveBeenCalled(); }); it('Group with selected indented paragraph, outdent with different length', () => { @@ -623,13 +784,18 @@ describe('indent', () => { marginLeft: '60px', }); const text1 = createText('test1'); + const context: FormatContentModelContext = { + deletedEntities: [], + newEntities: [], + newImages: [], + }; para1.segments.push(text1); group.blocks.push(para1); text1.isSelected = true; - const result = setModelIndentation(group, 'indent', 15); + const result = setModelIndentation(group, 'indent', 15, context); expect(group).toEqual({ blockGroupType: 'Document', blocks: [ @@ -643,6 +809,12 @@ describe('indent', () => { ], }); expect(result).toBeTrue(); + expect(context).toEqual({ + deletedEntities: [], + newEntities: [], + newImages: [], + }); + expect(getListAnnounceDataSpy).not.toHaveBeenCalled(); }); it('Group with list with first item selected', () => { @@ -656,6 +828,12 @@ describe('indent', () => { const text1 = createText('test1'); const text2 = createText('test2'); const text3 = createText('test3'); + const context: FormatContentModelContext = { + deletedEntities: [], + newEntities: [], + newImages: [], + }; + text1.isSelected = true; text2.isSelected = true; text3.isSelected = true; @@ -669,7 +847,7 @@ describe('indent', () => { group.blocks.push(listItem2); group.blocks.push(listItem3); - const result = setModelIndentation(group, 'indent'); + const result = setModelIndentation(group, 'indent', undefined, context); expect(group).toEqual({ blockGroupType: 'Document', @@ -714,34 +892,71 @@ describe('indent', () => { }); expect(result).toBeTrue(); + expect(context).toEqual({ + deletedEntities: [], + newEntities: [], + newImages: [], + }); + expect(getListAnnounceDataSpy).not.toHaveBeenCalled(); }); }); describe('outdent', () => { + let getListAnnounceDataSpy: jasmine.Spy; + const mockedAnnounceData = 'ANNOUNCE' as any; + + beforeEach(() => { + getListAnnounceDataSpy = spyOn(getListAnnounceData, 'getListAnnounceData').and.returnValue( + mockedAnnounceData + ); + }); + it('Empty group', () => { const group = createContentModelDocument(); + const context: FormatContentModelContext = { + deletedEntities: [], + newEntities: [], + newImages: [], + }; - const result = setModelIndentation(group, 'outdent'); + const result = setModelIndentation(group, 'outdent', undefined, context); expect(group).toEqual({ blockGroupType: 'Document', blocks: [], }); expect(result).toBeFalse(); + expect(context).toEqual({ + deletedEntities: [], + newEntities: [], + newImages: [], + }); + expect(getListAnnounceDataSpy).not.toHaveBeenCalled(); }); it('Group without selection', () => { const group = createContentModelDocument(); const para = createParagraph(); + const context: FormatContentModelContext = { + deletedEntities: [], + newEntities: [], + newImages: [], + }; group.blocks.push(para); - const result = setModelIndentation(group, 'outdent'); + const result = setModelIndentation(group, 'outdent', undefined, context); expect(group).toEqual({ blockGroupType: 'Document', blocks: [para], }); expect(result).toBeFalse(); + expect(context).toEqual({ + deletedEntities: [], + newEntities: [], + newImages: [], + }); + expect(getListAnnounceDataSpy).not.toHaveBeenCalled(); }); it('Group with selected paragraph that cannot outdent', () => { @@ -752,6 +967,11 @@ describe('outdent', () => { const text1 = createText('test1'); const text2 = createText('test2'); const text3 = createText('test3'); + const context: FormatContentModelContext = { + deletedEntities: [], + newEntities: [], + newImages: [], + }; para1.segments.push(text1); para2.segments.push(text2); @@ -762,12 +982,18 @@ describe('outdent', () => { text2.isSelected = true; - const result = setModelIndentation(group, 'outdent'); + const result = setModelIndentation(group, 'outdent', undefined, context); expect(group).toEqual({ blockGroupType: 'Document', blocks: [para1, para2, para3], }); expect(result).toBeTrue(); + expect(context).toEqual({ + deletedEntities: [], + newEntities: [], + newImages: [], + }); + expect(getListAnnounceDataSpy).not.toHaveBeenCalled(); }); it('Group with selected single indented paragraph', () => { @@ -784,6 +1010,11 @@ describe('outdent', () => { const text1 = createText('test1'); const text2 = createText('test2'); const text3 = createText('test3'); + const context: FormatContentModelContext = { + deletedEntities: [], + newEntities: [], + newImages: [], + }; para1.segments.push(text1); para2.segments.push(text2); @@ -794,7 +1025,7 @@ describe('outdent', () => { text2.isSelected = true; - const result = setModelIndentation(group, 'outdent'); + const result = setModelIndentation(group, 'outdent', undefined, context); expect(group).toEqual({ blockGroupType: 'Document', blocks: [ @@ -822,6 +1053,12 @@ describe('outdent', () => { ], }); expect(result).toBeTrue(); + expect(context).toEqual({ + deletedEntities: [], + newEntities: [], + newImages: [], + }); + expect(getListAnnounceDataSpy).not.toHaveBeenCalled(); }); it('Group with selected 2 indented paragraph', () => { @@ -834,6 +1071,11 @@ describe('outdent', () => { const text2 = createText('test2'); const text3 = createText('test3'); const text4 = createText('test4'); + const context: FormatContentModelContext = { + deletedEntities: [], + newEntities: [], + newImages: [], + }; para1.segments.push(text1); para2.segments.push(text2); @@ -847,7 +1089,7 @@ describe('outdent', () => { text2.isSelected = true; text3.isSelected = true; - const result = setModelIndentation(group, 'outdent'); + const result = setModelIndentation(group, 'outdent', undefined, context); expect(group).toEqual({ blockGroupType: 'Document', blocks: [ @@ -882,6 +1124,12 @@ describe('outdent', () => { ], }); expect(result).toBeTrue(); + expect(context).toEqual({ + deletedEntities: [], + newEntities: [], + newImages: [], + }); + expect(getListAnnounceDataSpy).not.toHaveBeenCalled(); }); it('Group with selected multiple indented paragraph', () => { @@ -892,6 +1140,11 @@ describe('outdent', () => { const text1 = createText('test1'); const text2 = createText('test2'); const text3 = createText('test3'); + const context: FormatContentModelContext = { + deletedEntities: [], + newEntities: [], + newImages: [], + }; para1.segments.push(text1); para2.segments.push(text2); @@ -903,7 +1156,7 @@ describe('outdent', () => { text1.isSelected = true; text3.isSelected = true; - const result = setModelIndentation(group, 'outdent'); + const result = setModelIndentation(group, 'outdent', undefined, context); expect(group).toEqual({ blockGroupType: 'Document', blocks: [ @@ -931,6 +1184,12 @@ describe('outdent', () => { ], }); expect(result).toBeTrue(); + expect(context).toEqual({ + deletedEntities: [], + newEntities: [], + newImages: [], + }); + expect(getListAnnounceDataSpy).not.toHaveBeenCalled(); }); it('Group with selected list item', () => { @@ -938,6 +1197,11 @@ describe('outdent', () => { const para1 = createParagraph(); const text1 = createText('test1'); const listItem = createListItem([createListLevel('OL')]); + const context: FormatContentModelContext = { + deletedEntities: [], + newEntities: [], + newImages: [], + }; para1.segments.push(text1); listItem.blocks.push(para1); @@ -945,7 +1209,7 @@ describe('outdent', () => { text1.isSelected = true; - const result = setModelIndentation(group, 'outdent'); + const result = setModelIndentation(group, 'outdent', undefined, context); expect(group).toEqual({ blockGroupType: 'Document', blocks: [ @@ -956,6 +1220,12 @@ describe('outdent', () => { ], }); expect(result).toBeTrue(); + expect(context).toEqual({ + deletedEntities: [], + newEntities: [], + newImages: [], + }); + expect(getListAnnounceDataSpy).not.toHaveBeenCalled(); }); it('Group with selected multiple level list item', () => { @@ -972,6 +1242,11 @@ describe('outdent', () => { ), createListLevel('UL'), ]); + const context: FormatContentModelContext = { + deletedEntities: [], + newEntities: [], + newImages: [], + }; para1.segments.push(text1); listItem.blocks.push(para1); @@ -979,7 +1254,7 @@ describe('outdent', () => { text1.isSelected = true; - const result = setModelIndentation(group, 'outdent'); + const result = setModelIndentation(group, 'outdent', undefined, context); expect(group).toEqual({ blockGroupType: 'Document', blocks: [ @@ -1003,6 +1278,14 @@ describe('outdent', () => { ], }); expect(result).toBeTrue(); + expect(context).toEqual({ + deletedEntities: [], + newEntities: [], + newImages: [], + announceData: mockedAnnounceData, + }); + expect(getListAnnounceDataSpy).toHaveBeenCalledTimes(1); + expect(getListAnnounceDataSpy).toHaveBeenCalledWith([listItem, group]); }); it('Group with mixed list item, quote and paragraph', () => { @@ -1014,6 +1297,11 @@ describe('outdent', () => { const text2 = createText('test2'); const text3 = createText('test3'); const listItem = createListItem([createListLevel('UL')]); + const context: FormatContentModelContext = { + deletedEntities: [], + newEntities: [], + newImages: [], + }; para1.segments.push(text1); para2.segments.push(text2); @@ -1029,7 +1317,7 @@ describe('outdent', () => { text2.isSelected = true; text3.isSelected = true; - const result = setModelIndentation(group, 'outdent'); + const result = setModelIndentation(group, 'outdent', undefined, context); expect(group).toEqual({ blockGroupType: 'Document', blocks: [ @@ -1054,6 +1342,12 @@ describe('outdent', () => { ], }); expect(result).toBeTrue(); + expect(context).toEqual({ + deletedEntities: [], + newEntities: [], + newImages: [], + }); + expect(getListAnnounceDataSpy).not.toHaveBeenCalled(); }); it('Group with selected indented paragraph in RTL', () => { @@ -1064,13 +1358,18 @@ describe('outdent', () => { direction: 'rtl', }); const text1 = createText('test1'); + const context: FormatContentModelContext = { + deletedEntities: [], + newEntities: [], + newImages: [], + }; para1.segments.push(text1); group.blocks.push(para1); text1.isSelected = true; - const result = setModelIndentation(group, 'outdent'); + const result = setModelIndentation(group, 'outdent', undefined, context); expect(group).toEqual({ blockGroupType: 'Document', blocks: [ @@ -1086,6 +1385,12 @@ describe('outdent', () => { ], }); expect(result).toBeTrue(); + expect(context).toEqual({ + deletedEntities: [], + newEntities: [], + newImages: [], + }); + expect(getListAnnounceDataSpy).not.toHaveBeenCalled(); }); it('Group with selected indented paragraph, outdent with different length', () => { @@ -1094,13 +1399,18 @@ describe('outdent', () => { marginLeft: '60px', }); const text1 = createText('test1'); + const context: FormatContentModelContext = { + deletedEntities: [], + newEntities: [], + newImages: [], + }; para1.segments.push(text1); group.blocks.push(para1); text1.isSelected = true; - const result = setModelIndentation(group, 'outdent', 15); + const result = setModelIndentation(group, 'outdent', 15, context); expect(group).toEqual({ blockGroupType: 'Document', blocks: [ @@ -1114,6 +1424,12 @@ describe('outdent', () => { ], }); expect(result).toBeTrue(); + expect(context).toEqual({ + deletedEntities: [], + newEntities: [], + newImages: [], + }); + expect(getListAnnounceDataSpy).not.toHaveBeenCalled(); }); it('Group with list with no indention selected', () => { @@ -1127,6 +1443,12 @@ describe('outdent', () => { const text1 = createText('test1'); const text2 = createText('test2'); const text3 = createText('test3'); + const context: FormatContentModelContext = { + deletedEntities: [], + newEntities: [], + newImages: [], + }; + text1.isSelected = true; text2.isSelected = true; text3.isSelected = true; @@ -1140,7 +1462,7 @@ describe('outdent', () => { group.blocks.push(listItem2); group.blocks.push(listItem3); - const result = setModelIndentation(group, 'outdent'); + const result = setModelIndentation(group, 'outdent', undefined, context); expect(group).toEqual({ blockGroupType: 'Document', @@ -1161,6 +1483,12 @@ describe('outdent', () => { }); expect(result).toBeTrue(); + expect(context).toEqual({ + deletedEntities: [], + newEntities: [], + newImages: [], + }); + expect(getListAnnounceDataSpy).not.toHaveBeenCalled(); }); it('Outdent parent format container, ltr', () => { @@ -1172,6 +1500,11 @@ describe('outdent', () => { const text1 = createText('test1'); const text2 = createText('test2'); const text3 = createText('test3'); + const context: FormatContentModelContext = { + deletedEntities: [], + newEntities: [], + newImages: [], + }; text2.isSelected = true; formatContainer.format.marginLeft = '100px'; @@ -1184,7 +1517,7 @@ describe('outdent', () => { formatContainer.blocks.push(para1, para2); group.blocks.push(formatContainer, para3); - const result = setModelIndentation(group, 'outdent'); + const result = setModelIndentation(group, 'outdent', undefined, context); expect(group).toEqual({ blockGroupType: 'Document', @@ -1223,6 +1556,12 @@ describe('outdent', () => { }); expect(result).toBeTrue(); + expect(context).toEqual({ + deletedEntities: [], + newEntities: [], + newImages: [], + }); + expect(getListAnnounceDataSpy).not.toHaveBeenCalled(); }); it('Outdent parent format container, rtl', () => { @@ -1234,6 +1573,11 @@ describe('outdent', () => { const text1 = createText('test1'); const text2 = createText('test2'); const text3 = createText('test3'); + const context: FormatContentModelContext = { + deletedEntities: [], + newEntities: [], + newImages: [], + }; text1.isSelected = true; text2.isSelected = true; @@ -1248,7 +1592,7 @@ describe('outdent', () => { formatContainer.blocks.push(para1, para2); group.blocks.push(formatContainer, para3); - const result = setModelIndentation(group, 'outdent'); + const result = setModelIndentation(group, 'outdent', undefined, context); expect(group).toEqual({ blockGroupType: 'Document', @@ -1294,5 +1638,11 @@ describe('outdent', () => { }); expect(result).toBeTrue(); + expect(context).toEqual({ + deletedEntities: [], + newEntities: [], + newImages: [], + }); + expect(getListAnnounceDataSpy).not.toHaveBeenCalled(); }); }); diff --git a/packages/roosterjs-content-model-api/test/modelApi/list/findListItemsInSameThreadTest.ts b/packages/roosterjs-content-model-api/test/modelApi/list/findListItemsInSameThreadTest.ts index d1599daadf6..6974519f717 100644 --- a/packages/roosterjs-content-model-api/test/modelApi/list/findListItemsInSameThreadTest.ts +++ b/packages/roosterjs-content-model-api/test/modelApi/list/findListItemsInSameThreadTest.ts @@ -277,4 +277,27 @@ describe('findListItemsInSameThread', () => { expect(result).toEqual([item1, item2]); }); + + it('Multiple deeper OL under different groups', () => { + const group = createContentModelDocument(); + const item1 = createListItem([createListLevel('OL')]); + const item2 = createListItem([createListLevel('OL'), createListLevel('OL')]); + const item3 = createListItem([createListLevel('OL'), createListLevel('OL')]); + const item4 = createListItem([createListLevel('OL')]); + const item5 = createListItem([createListLevel('OL'), createListLevel('OL')]); + const item6 = createListItem([createListLevel('OL'), createListLevel('OL')]); + + item1.format.lineHeight = '1'; + item1.format.lineHeight = '2'; + item1.format.lineHeight = '3'; + item1.format.lineHeight = '4'; + item1.format.lineHeight = '5'; + item1.format.lineHeight = '6'; + + group.blocks.push(item1, item2, item3, item4, item5, item6); + + const result = findListItemsInSameThread(group, item6); + + expect(result).toEqual([item5, item6]); + }); }); diff --git a/packages/roosterjs-content-model-api/test/modelApi/list/getListAnnounceDataTest.ts b/packages/roosterjs-content-model-api/test/modelApi/list/getListAnnounceDataTest.ts new file mode 100644 index 00000000000..03fd9c715a1 --- /dev/null +++ b/packages/roosterjs-content-model-api/test/modelApi/list/getListAnnounceDataTest.ts @@ -0,0 +1,125 @@ +import * as getAutoListStyleType from 'roosterjs-content-model-dom/lib/modelApi/list/getAutoListStyleType'; +import { getListAnnounceData } from '../../../lib/modelApi/list/getListAnnounceData'; +import { + NumberingListType, + createContentModelDocument, + createListItem, + createListLevel, +} from 'roosterjs-content-model-dom'; + +describe('getListAnnounceData', () => { + let getAutoListStyleTypeSpy: jasmine.Spy; + + beforeEach(() => { + getAutoListStyleTypeSpy = spyOn(getAutoListStyleType, 'getAutoListStyleType'); + }); + it('empty path', () => { + const result = getListAnnounceData([]); + + expect(result).toEqual(null); + expect(getAutoListStyleTypeSpy).not.toHaveBeenCalled(); + }); + + it('no list item', () => { + const doc = createContentModelDocument(); + const result = getListAnnounceData([doc]); + + expect(result).toEqual(null); + expect(getAutoListStyleTypeSpy).not.toHaveBeenCalled(); + }); + + it('path has single list item, no list style', () => { + const doc = createContentModelDocument(); + const listItem = createListItem([createListLevel('OL')]); + + doc.blocks.push(listItem); + + getAutoListStyleTypeSpy.and.returnValue(undefined); + + const result = getListAnnounceData([listItem, doc]); + + expect(result).toEqual(null); + expect(getAutoListStyleTypeSpy).toHaveBeenCalledWith('OL', {}, 0, undefined); + }); + + it('path has single list item, has list style in format', () => { + const doc = createContentModelDocument(); + const listItem = createListItem([createListLevel('OL', { listStyleType: 'decimal' })]); + + doc.blocks.push(listItem); + + getAutoListStyleTypeSpy.and.returnValue(NumberingListType.Decimal); + + const result = getListAnnounceData([listItem, doc]); + + expect(result).toEqual({ + defaultStrings: 'announceListItemNumbering', + formatStrings: ['1'], + }); + expect(getAutoListStyleTypeSpy).toHaveBeenCalledWith('OL', {}, 0, 'decimal'); + }); + + it('path has dummy list item', () => { + const doc = createContentModelDocument(); + const listItem = createListItem([createListLevel('OL', { displayForDummyItem: 'block' })]); + + doc.blocks.push(listItem); + + const result = getListAnnounceData([listItem, doc]); + + expect(result).toEqual(null); + expect(getAutoListStyleTypeSpy).not.toHaveBeenCalled(); + }); + + it('path with bullet list', () => { + const doc = createContentModelDocument(); + const listItem = createListItem([createListLevel('UL')]); + + doc.blocks.push(listItem); + + const result = getListAnnounceData([listItem, doc]); + + expect(result).toEqual({ + defaultStrings: 'announceListItemBullet', + }); + expect(getAutoListStyleTypeSpy).not.toHaveBeenCalled(); + }); + + it('path has deeper list', () => { + const doc = createContentModelDocument(); + const listItem = createListItem([ + createListLevel('UL'), + createListLevel('OL', { listStyleType: 'decimal' }), + ]); + + doc.blocks.push(listItem); + + getAutoListStyleTypeSpy.and.returnValue(NumberingListType.Decimal); + + const result = getListAnnounceData([listItem, doc]); + + expect(result).toEqual({ + defaultStrings: 'announceListItemNumbering', + formatStrings: ['1'], + }); + expect(getAutoListStyleTypeSpy).toHaveBeenCalledWith('OL', {}, 1, 'decimal'); + }); + + it('path has multiple list items', () => { + const doc = createContentModelDocument(); + const listItem1 = createListItem([createListLevel('OL')]); + const listItem2 = createListItem([createListLevel('OL')]); + + doc.blocks.push(listItem1, listItem2); + + getAutoListStyleTypeSpy.and.returnValue(NumberingListType.Decimal); + + const result = getListAnnounceData([listItem2, doc]); + + expect(result).toEqual({ + defaultStrings: 'announceListItemNumbering', + formatStrings: ['2'], + }); + expect(getAutoListStyleTypeSpy).toHaveBeenCalledWith('OL', {}, 0, undefined); + }); +}); diff --git a/packages/roosterjs-content-model-core/lib/corePlugin/selection/SelectionPlugin.ts b/packages/roosterjs-content-model-core/lib/corePlugin/selection/SelectionPlugin.ts index 9c75100e40b..9dfef270c5c 100644 --- a/packages/roosterjs-content-model-core/lib/corePlugin/selection/SelectionPlugin.ts +++ b/packages/roosterjs-content-model-core/lib/corePlugin/selection/SelectionPlugin.ts @@ -422,14 +422,27 @@ class SelectionPlugin implements PluginWithState { col = reverse ? parsedTable[row].length - 1 : 0; } const cell = parsedTable[row][col]; + if (typeof cell != 'string') { this.setRangeSelectionInTable(cell, 0, this.editor); + lastCo.row = row; + lastCo.col = col; break; } } } else { this.state.tableSelection = null; } + + if ( + collapsed && + (lastCo.col != oldCo.col || lastCo.row != oldCo.row) && + lastCo.row >= 0 && + lastCo.row == parsedTable.length - 1 && + lastCo.col == parsedTable[lastCo.row]?.length - 1 + ) { + this.editor?.announce({ defaultStrings: 'announceOnFocusLastCell' }); + } } if (!collapsed && lastCo) { diff --git a/packages/roosterjs-content-model-core/test/corePlugin/selection/SelectionPluginTest.ts b/packages/roosterjs-content-model-core/test/corePlugin/selection/SelectionPluginTest.ts index 4b3501d1093..cc1adf1e727 100644 --- a/packages/roosterjs-content-model-core/test/corePlugin/selection/SelectionPluginTest.ts +++ b/packages/roosterjs-content-model-core/test/corePlugin/selection/SelectionPluginTest.ts @@ -698,6 +698,7 @@ describe('SelectionPlugin handle table selection', () => { let requestAnimationFrameSpy: jasmine.Spy; let getComputedStyleSpy: jasmine.Spy; let addEventListenerSpy: jasmine.Spy; + let announceSpy: jasmine.Spy; beforeEach(() => { contentDiv = document.createElement('div'); @@ -707,6 +708,7 @@ describe('SelectionPlugin handle table selection', () => { requestAnimationFrameSpy = jasmine.createSpy('requestAnimationFrame'); getComputedStyleSpy = jasmine.createSpy('getComputedStyle'); addEventListenerSpy = jasmine.createSpy('addEventListener'); + announceSpy = jasmine.createSpy('announce'); getDocumentSpy = jasmine.createSpy('getDocument').and.returnValue({ createRange: createRangeSpy, defaultView: { @@ -735,6 +737,7 @@ describe('SelectionPlugin handle table selection', () => { return focusDisposer; } }, + announce: announceSpy, } as any; plugin = createSelectionPlugin({}); plugin.initialize(editor); @@ -1205,6 +1208,7 @@ describe('SelectionPlugin handle table selection', () => { imageSelectionBorderColor: undefined, }); expect(setDOMSelectionSpy).not.toHaveBeenCalled(); + expect(announceSpy).not.toHaveBeenCalled(); }); it('From Range, Press Right', () => { @@ -1245,6 +1249,7 @@ describe('SelectionPlugin handle table selection', () => { imageSelectionBorderColor: undefined, }); expect(setDOMSelectionSpy).toHaveBeenCalledTimes(0); + expect(announceSpy).not.toHaveBeenCalled(); }); it('From Range, Press Tab', () => { @@ -1306,6 +1311,7 @@ describe('SelectionPlugin handle table selection', () => { isReverted: false, }); expect(setStartSpy).toHaveBeenCalledWith(td2, 0); + expect(announceSpy).not.toHaveBeenCalled(); }); it('From Range, Press Shift+Tab', () => { @@ -1368,6 +1374,7 @@ describe('SelectionPlugin handle table selection', () => { isReverted: false, }); expect(setStartSpy).toHaveBeenCalledWith(td1, 0); + expect(announceSpy).not.toHaveBeenCalled(); }); it('From Range, Press Tab - Next Row', () => { @@ -1429,6 +1436,7 @@ describe('SelectionPlugin handle table selection', () => { isReverted: false, }); expect(setStartSpy).toHaveBeenCalledWith(td3, 0); + expect(announceSpy).not.toHaveBeenCalled(); }); it('From Range, First cell - Press Shift+Tab', () => { @@ -1491,6 +1499,7 @@ describe('SelectionPlugin handle table selection', () => { isReverted: false, }); expect(setStartSpy).toHaveBeenCalledWith(table.parentNode, 0); + expect(announceSpy).not.toHaveBeenCalled(); }); it('From Range, Last cell - Press Tab', () => { @@ -1552,6 +1561,7 @@ describe('SelectionPlugin handle table selection', () => { isReverted: false, }); expect(setStartSpy).toHaveBeenCalledWith(table.parentNode, 1); + expect(announceSpy).not.toHaveBeenCalled(); }); it('From Range, Press Down', () => { @@ -1613,6 +1623,9 @@ describe('SelectionPlugin handle table selection', () => { isReverted: false, }); expect(setStartSpy).toHaveBeenCalledWith(td4, 0); + expect(announceSpy).toHaveBeenCalledWith({ + defaultStrings: 'announceOnFocusLastCell', + }); }); it('From Range, Press Shift+Up', () => { @@ -1677,6 +1690,7 @@ describe('SelectionPlugin handle table selection', () => { lastRow: 0, lastColumn: 1, }); + expect(announceSpy).not.toHaveBeenCalled(); }); it('From Range, Press Shift+Down', () => { @@ -1741,6 +1755,7 @@ describe('SelectionPlugin handle table selection', () => { lastRow: 1, lastColumn: 1, }); + expect(announceSpy).not.toHaveBeenCalled(); }); it('From Range, Press Shift+Down to ouside of table', () => { @@ -1796,6 +1811,7 @@ describe('SelectionPlugin handle table selection', () => { imageSelectionBorderColor: undefined, }); expect(setDOMSelectionSpy).toHaveBeenCalledTimes(0); + expect(announceSpy).not.toHaveBeenCalled(); }); it('From Table, Press A', () => { @@ -1840,6 +1856,7 @@ describe('SelectionPlugin handle table selection', () => { }); expect(setDOMSelectionSpy).not.toHaveBeenCalled(); expect(preventDefaultSpy).not.toHaveBeenCalled(); + expect(announceSpy).not.toHaveBeenCalled(); }); it('From Table, Press Left', () => { @@ -1893,6 +1910,7 @@ describe('SelectionPlugin handle table selection', () => { expect(setDOMSelectionSpy).toHaveBeenCalledTimes(1); expect(setDOMSelectionSpy).toHaveBeenCalledWith(null); expect(preventDefaultSpy).not.toHaveBeenCalled(); + expect(announceSpy).not.toHaveBeenCalled(); }); it('From Table, Press Shift+Left', () => { @@ -1948,6 +1966,7 @@ describe('SelectionPlugin handle table selection', () => { lastColumn: 0, }); expect(preventDefaultSpy).toHaveBeenCalled(); + expect(announceSpy).not.toHaveBeenCalled(); }); it('From Table, Press Shift+Up', () => { @@ -2003,6 +2022,7 @@ describe('SelectionPlugin handle table selection', () => { lastColumn: 1, }); expect(preventDefaultSpy).toHaveBeenCalled(); + expect(announceSpy).not.toHaveBeenCalled(); }); }); }); diff --git a/packages/roosterjs-content-model-dom/lib/modelApi/selection/collectSelections.ts b/packages/roosterjs-content-model-dom/lib/modelApi/selection/collectSelections.ts index 91b4d2b307a..110c8f9a086 100644 --- a/packages/roosterjs-content-model-dom/lib/modelApi/selection/collectSelections.ts +++ b/packages/roosterjs-content-model-dom/lib/modelApi/selection/collectSelections.ts @@ -76,20 +76,20 @@ export function getSelectedParagraphs(model: ContentModelDocument): ContentModel /** * Get an array of block group - block pair that is of the expected block group type from selection - * @param model The Content Model to get selection from + * @param group The root block group to search * @param blockGroupTypes The expected block group types * @param stopTypes Block group types that will stop searching when hit * @param deepFirst True means search in deep first, otherwise wide first */ export function getOperationalBlocks( - model: ContentModelDocument, + group: ContentModelBlockGroup, blockGroupTypes: TypeOfBlockGroup[], stopTypes: ContentModelBlockGroupType[], deepFirst?: boolean ): OperationalBlocks[] { const result: OperationalBlocks[] = []; const findSequence = deepFirst ? blockGroupTypes.map(type => [type]) : [blockGroupTypes]; - const selections = collectSelections(model, { + const selections = collectSelections(group, { includeListFormatHolder: 'never', contentUnderSelectedTableCell: 'ignoreForTable', // When whole table is selected, we treat the table as a single block }); @@ -182,13 +182,13 @@ interface SelectionInfo { } function collectSelections( - model: ContentModelDocument, + group: ContentModelBlockGroup, option?: IterateSelectionsOption ): SelectionInfo[] { const selections: SelectionInfo[] = []; iterateSelections( - model, + group, (path, tableContext, block, segments) => { selections.push({ path, diff --git a/packages/roosterjs-content-model-plugins/lib/edit/inputSteps/handleEnterOnList.ts b/packages/roosterjs-content-model-plugins/lib/edit/inputSteps/handleEnterOnList.ts index 46d08fc71da..869d20ac8f5 100644 --- a/packages/roosterjs-content-model-plugins/lib/edit/inputSteps/handleEnterOnList.ts +++ b/packages/roosterjs-content-model-plugins/lib/edit/inputSteps/handleEnterOnList.ts @@ -1,3 +1,4 @@ +import { getListAnnounceData } from 'roosterjs-content-model-api'; import { createBr, createListItem, @@ -39,9 +40,12 @@ export const handleEnterOnList: DeleteSelectionStep = context => { if (listItem && listItem.blockGroupType === 'ListItem' && listParent) { const listIndex = listParent.blocks.indexOf(listItem); const nextBlock = listParent.blocks[listIndex + 1]; + if (deleteResult == 'range' && nextBlock) { normalizeContentModel(listParent); + const nextListItem = listParent.blocks[listIndex + 1]; + if ( isBlockGroupOfType(nextListItem, 'ListItem') && nextListItem.levels[0] @@ -52,8 +56,10 @@ export const handleEnterOnList: DeleteSelectionStep = context => { ? listItem.levels[index].dataset : {}; }); + const lastParagraph = listItem.blocks[listItem.blocks.length - 1]; const nextParagraph = nextListItem.blocks[0]; + if ( nextParagraph.blockType === 'Paragraph' && lastParagraph.blockType === 'Paragraph' && @@ -66,15 +72,24 @@ export const handleEnterOnList: DeleteSelectionStep = context => { createSelectionMarker(insertPoint.marker.format) ); } + context.lastParagraph = undefined; } } else if (deleteResult !== 'range') { if (isEmptyListItem(listItem)) { listItem.levels.pop(); } else { - createNewListItem(context, listItem, listParent); + const newListItem = createNewListItem(context, listItem, listParent); + + if (context.formatContext) { + context.formatContext.announceData = getListAnnounceData([ + newListItem, + ...path.slice(index + 1), + ]); + } } } + rawEvent?.preventDefault(); context.deleteResult = 'range'; } @@ -106,6 +121,8 @@ const createNewListItem = ( insertPoint.paragraph = newParagraph; context.lastParagraph = newParagraph; listParent.blocks.splice(listIndex + 1, 0, newListItem); + + return newListItem; }; const createNewListLevel = (listItem: ContentModelListItem) => { diff --git a/packages/roosterjs-content-model-plugins/lib/edit/keyboardTab.ts b/packages/roosterjs-content-model-plugins/lib/edit/keyboardTab.ts index 0fb3dd7e327..52b1b8ff5df 100644 --- a/packages/roosterjs-content-model-plugins/lib/edit/keyboardTab.ts +++ b/packages/roosterjs-content-model-plugins/lib/edit/keyboardTab.ts @@ -8,6 +8,7 @@ import type { ContentModelDocument, ContentModelListItem, ContentModelTableCell, + FormatContentModelContext, IEditor, } from 'roosterjs-content-model-types'; @@ -20,8 +21,8 @@ export function keyboardTab(editor: IEditor, rawEvent: KeyboardEvent) { switch (selection?.type) { case 'range': editor.formatContentModel( - model => { - return handleTab(model, rawEvent); + (model, context) => { + return handleTab(model, rawEvent, context); }, { apiName: 'handleTabKey', @@ -49,13 +50,18 @@ export function keyboardTab(editor: IEditor, rawEvent: KeyboardEvent) { * - If it is a paragraph, call handleTabOnParagraph to handle the tab key. * - If it is a list item, call handleTabOnList to handle the tab key. */ -function handleTab(model: ContentModelDocument, rawEvent: KeyboardEvent) { +function handleTab( + model: ContentModelDocument, + rawEvent: KeyboardEvent, + context: FormatContentModelContext +) { const blocks = getOperationalBlocks( model, ['ListItem', 'TableCell'], [] ); const block = blocks.length > 0 ? blocks[0].block : undefined; + if (blocks.length > 1) { setModelIndentation(model, rawEvent.shiftKey ? 'outdent' : 'indent'); rawEvent.preventDefault(); @@ -63,9 +69,9 @@ function handleTab(model: ContentModelDocument, rawEvent: KeyboardEvent) { } else if (isBlockGroupOfType(block, 'TableCell')) { return handleTabOnTableCell(model, block, rawEvent); } else if (block?.blockType === 'Paragraph') { - return handleTabOnParagraph(model, block, rawEvent); + return handleTabOnParagraph(model, block, rawEvent, context); } else if (isBlockGroupOfType(block, 'ListItem')) { - return handleTabOnList(model, block, rawEvent); + return handleTabOnList(model, block, rawEvent, context); } return false; } diff --git a/packages/roosterjs-content-model-plugins/lib/edit/tabUtils/handleTabOnList.ts b/packages/roosterjs-content-model-plugins/lib/edit/tabUtils/handleTabOnList.ts index 1f2cbf130ff..2cec2fefc77 100644 --- a/packages/roosterjs-content-model-plugins/lib/edit/tabUtils/handleTabOnList.ts +++ b/packages/roosterjs-content-model-plugins/lib/edit/tabUtils/handleTabOnList.ts @@ -1,6 +1,10 @@ import { handleTabOnParagraph } from './handleTabOnParagraph'; import { setModelIndentation } from 'roosterjs-content-model-api'; -import type { ContentModelDocument, ContentModelListItem } from 'roosterjs-content-model-types'; +import type { + ContentModelDocument, + ContentModelListItem, + FormatContentModelContext, +} from 'roosterjs-content-model-types'; /** * 1. When the selection is collapsed and the cursor is at start of a list item, call setModelIndentation. @@ -10,7 +14,8 @@ import type { ContentModelDocument, ContentModelListItem } from 'roosterjs-conte export function handleTabOnList( model: ContentModelDocument, listItem: ContentModelListItem, - rawEvent: KeyboardEvent + rawEvent: KeyboardEvent, + context?: FormatContentModelContext ) { const selectedParagraph = findSelectedParagraph(listItem); if ( @@ -18,9 +23,14 @@ export function handleTabOnList( selectedParagraph.length == 1 && selectedParagraph[0].blockType === 'Paragraph' ) { - return handleTabOnParagraph(model, selectedParagraph[0], rawEvent); + return handleTabOnParagraph(model, selectedParagraph[0], rawEvent, context); } else { - setModelIndentation(model, rawEvent.shiftKey ? 'outdent' : 'indent'); + setModelIndentation( + model, + rawEvent.shiftKey ? 'outdent' : 'indent', + undefined /*length*/, + context + ); rawEvent.preventDefault(); return true; } diff --git a/packages/roosterjs-content-model-plugins/lib/edit/tabUtils/handleTabOnParagraph.ts b/packages/roosterjs-content-model-plugins/lib/edit/tabUtils/handleTabOnParagraph.ts index 91a8bfc359d..0a3c89bbcaa 100644 --- a/packages/roosterjs-content-model-plugins/lib/edit/tabUtils/handleTabOnParagraph.ts +++ b/packages/roosterjs-content-model-plugins/lib/edit/tabUtils/handleTabOnParagraph.ts @@ -1,6 +1,10 @@ import { createSelectionMarker, createText } from 'roosterjs-content-model-dom'; import { setModelIndentation } from 'roosterjs-content-model-api'; -import type { ContentModelDocument, ContentModelParagraph } from 'roosterjs-content-model-types'; +import type { + ContentModelDocument, + ContentModelParagraph, + FormatContentModelContext, +} from 'roosterjs-content-model-types'; const tabSpaces = '    '; const space = ' '; @@ -23,7 +27,8 @@ const space = ' '; export function handleTabOnParagraph( model: ContentModelDocument, paragraph: ContentModelParagraph, - rawEvent: KeyboardEvent + rawEvent: KeyboardEvent, + context?: FormatContentModelContext ) { const selectedSegments = paragraph.segments.filter(segment => segment.isSelected); const isCollapsed = @@ -39,7 +44,12 @@ export function handleTabOnParagraph( ) { return false; } - setModelIndentation(model, rawEvent.shiftKey ? 'outdent' : 'indent'); + setModelIndentation( + model, + rawEvent.shiftKey ? 'outdent' : 'indent', + undefined /*length*/, + context + ); } else { if (!isCollapsed) { let firstSelectedSegmentIndex: number | undefined = undefined; diff --git a/packages/roosterjs-content-model-plugins/lib/shortcut/utils/setShortcutIndentationCommand.ts b/packages/roosterjs-content-model-plugins/lib/shortcut/utils/setShortcutIndentationCommand.ts index 7c6934f06a4..5b38b9393ea 100644 --- a/packages/roosterjs-content-model-plugins/lib/shortcut/utils/setShortcutIndentationCommand.ts +++ b/packages/roosterjs-content-model-plugins/lib/shortcut/utils/setShortcutIndentationCommand.ts @@ -6,7 +6,7 @@ import type { IEditor } from 'roosterjs-content-model-types'; * @internal */ export function setShortcutIndentationCommand(editor: IEditor, operation: 'indent' | 'outdent') { - editor.formatContentModel(model => { + editor.formatContentModel((model, context) => { const listItem = getFirstSelectedListItem(model); if ( @@ -14,7 +14,7 @@ export function setShortcutIndentationCommand(editor: IEditor, operation: 'inden listItem.blocks[0].blockType == 'Paragraph' && listItem.blocks[0].segments[0].segmentType == 'SelectionMarker' ) { - setModelIndentation(model, operation); + setModelIndentation(model, operation, undefined /*length*/, context); return true; } return false; diff --git a/packages/roosterjs-content-model-plugins/test/edit/inputSteps/handleEnterOnListTest.ts b/packages/roosterjs-content-model-plugins/test/edit/inputSteps/handleEnterOnListTest.ts index 3edb4b8fff5..f1d394a1c40 100644 --- a/packages/roosterjs-content-model-plugins/test/edit/inputSteps/handleEnterOnListTest.ts +++ b/packages/roosterjs-content-model-plugins/test/edit/inputSteps/handleEnterOnListTest.ts @@ -1,20 +1,58 @@ -import { ContentModelDocument } from 'roosterjs-content-model-types'; +import * as getListAnnounceData from 'roosterjs-content-model-api/lib/modelApi/list/getListAnnounceData'; import { deleteSelection, normalizeContentModel } from 'roosterjs-content-model-dom'; import { editingTestCommon } from '../editingTestCommon'; import { handleEnterOnList } from '../../../lib/edit/inputSteps/handleEnterOnList'; import { keyboardInput } from '../../../lib/edit/keyboardInput'; +import { + ContentModelDocument, + ContentModelListItem, + FormatContentModelContext, +} from 'roosterjs-content-model-types'; describe('handleEnterOnList', () => { + let getListAnnounceDataSpy: jasmine.Spy; + const mockedAnnounceData = 'ANNOUNCE' as any; + + beforeEach(() => { + getListAnnounceDataSpy = spyOn(getListAnnounceData, 'getListAnnounceData').and.returnValue( + mockedAnnounceData + ); + }); + function runTest( model: ContentModelDocument, expectedModel: ContentModelDocument, - expectedResult: 'notDeleted' | 'range' + expectedResult: 'notDeleted' | 'range', + expectedListItem: ContentModelListItem | null ) { - const result = deleteSelection(model, [handleEnterOnList]); + const context: FormatContentModelContext = { + deletedEntities: [], + newEntities: [], + newImages: [], + }; + const result = deleteSelection(model, [handleEnterOnList], context); normalizeContentModel(model); expect(model).toEqual(expectedModel); expect(result.deleteResult).toBe(expectedResult); + + if (expectedListItem) { + expect(getListAnnounceDataSpy).toHaveBeenCalledTimes(1); + expect(getListAnnounceDataSpy).toHaveBeenCalledWith([expectedListItem, model]); + expect(context).toEqual({ + deletedEntities: [], + newEntities: [], + newImages: [], + announceData: mockedAnnounceData, + }); + } else { + expect(getListAnnounceDataSpy).not.toHaveBeenCalled(); + expect(context).toEqual({ + deletedEntities: [], + newEntities: [], + newImages: [], + }); + } } it('no list item', () => { @@ -34,7 +72,7 @@ describe('handleEnterOnList', () => { }, ], }; - runTest(model, model, 'notDeleted'); + runTest(model, model, 'notDeleted', null); }); it('empty list item', () => { @@ -177,7 +215,7 @@ describe('handleEnterOnList', () => { ], format: {}, }; - runTest(model, expectedModel, 'range'); + runTest(model, expectedModel, 'range', null); }); it('enter on middle list item', () => { @@ -233,45 +271,52 @@ describe('handleEnterOnList', () => { ], format: {}, }; - const expectedModel: ContentModelDocument = { - blockGroupType: 'Document', + const expectedListItem: ContentModelListItem = { + blockType: 'BlockGroup', + blockGroupType: 'ListItem', blocks: [ { - blockType: 'BlockGroup', - blockGroupType: 'ListItem', - blocks: [ + blockType: 'Paragraph', + segments: [ { - blockType: 'Paragraph', - segments: [ - { - segmentType: 'Text', - text: 'te', - format: {}, - }, - ], + segmentType: 'SelectionMarker', + isSelected: true, format: {}, }, - ], - levels: [ { - listType: 'OL', - format: { - marginTop: '0px', - marginBottom: '0px', - listStyleType: 'decimal', - }, - dataset: { - editingInfo: '{"orderedStyleType":1}', - }, + segmentType: 'Text', + text: 'st', + format: {}, }, ], - formatHolder: { - segmentType: 'SelectionMarker', - isSelected: false, - format: {}, - }, format: {}, }, + ], + levels: [ + { + listType: 'OL', + format: { + marginTop: '0px', + marginBottom: '0px', + listStyleType: 'decimal', + startNumberOverride: undefined, + displayForDummyItem: undefined, + }, + dataset: { + editingInfo: '{"orderedStyleType":1}', + }, + }, + ], + formatHolder: { + segmentType: 'SelectionMarker', + isSelected: false, + format: {}, + }, + format: {}, + }; + const expectedModel: ContentModelDocument = { + blockGroupType: 'Document', + blocks: [ { blockType: 'BlockGroup', blockGroupType: 'ListItem', @@ -279,14 +324,9 @@ describe('handleEnterOnList', () => { { blockType: 'Paragraph', segments: [ - { - segmentType: 'SelectionMarker', - isSelected: true, - format: {}, - }, { segmentType: 'Text', - text: 'st', + text: 'te', format: {}, }, ], @@ -300,8 +340,6 @@ describe('handleEnterOnList', () => { marginTop: '0px', marginBottom: '0px', listStyleType: 'decimal', - startNumberOverride: undefined, - displayForDummyItem: undefined, }, dataset: { editingInfo: '{"orderedStyleType":1}', @@ -315,10 +353,11 @@ describe('handleEnterOnList', () => { }, format: {}, }, + expectedListItem, ], format: {}, }; - runTest(model, expectedModel, 'range'); + runTest(model, expectedModel, 'range', expectedListItem); }); it('enter on last list item', () => { @@ -369,46 +408,52 @@ describe('handleEnterOnList', () => { ], format: {}, }; - - const expectedModel: ContentModelDocument = { - blockGroupType: 'Document', + const expectedListItem: ContentModelListItem = { + blockType: 'BlockGroup', + blockGroupType: 'ListItem', blocks: [ { - blockType: 'BlockGroup', - blockGroupType: 'ListItem', - blocks: [ + blockType: 'Paragraph', + segments: [ { - blockType: 'Paragraph', - segments: [ - { - segmentType: 'Text', - text: 'test', - format: {}, - }, - ], + segmentType: 'SelectionMarker', + isSelected: true, format: {}, }, - ], - levels: [ { - listType: 'OL', - format: { - marginTop: '0px', - marginBottom: '0px', - listStyleType: 'decimal', - }, - dataset: { - editingInfo: '{"orderedStyleType":1}', - }, + segmentType: 'Br', + format: {}, }, ], - formatHolder: { - segmentType: 'SelectionMarker', - isSelected: false, - format: {}, - }, format: {}, }, + ], + levels: [ + { + listType: 'OL', + format: { + marginTop: '0px', + marginBottom: '0px', + listStyleType: 'decimal', + startNumberOverride: undefined, + displayForDummyItem: undefined, + }, + dataset: { + editingInfo: '{"orderedStyleType":1}', + }, + }, + ], + formatHolder: { + segmentType: 'SelectionMarker', + isSelected: false, + format: {}, + }, + format: {}, + }; + + const expectedModel: ContentModelDocument = { + blockGroupType: 'Document', + blocks: [ { blockType: 'BlockGroup', blockGroupType: 'ListItem', @@ -417,12 +462,8 @@ describe('handleEnterOnList', () => { blockType: 'Paragraph', segments: [ { - segmentType: 'SelectionMarker', - isSelected: true, - format: {}, - }, - { - segmentType: 'Br', + segmentType: 'Text', + text: 'test', format: {}, }, ], @@ -436,8 +477,6 @@ describe('handleEnterOnList', () => { marginTop: '0px', marginBottom: '0px', listStyleType: 'decimal', - startNumberOverride: undefined, - displayForDummyItem: undefined, }, dataset: { editingInfo: '{"orderedStyleType":1}', @@ -451,11 +490,12 @@ describe('handleEnterOnList', () => { }, format: {}, }, + expectedListItem, ], format: {}, }; - runTest(model, expectedModel, 'range'); + runTest(model, expectedModel, 'range', expectedListItem); }); it('enter on last list item of second list', () => { @@ -629,6 +669,47 @@ describe('handleEnterOnList', () => { ], format: {}, }; + const expectedListItem: ContentModelListItem = { + blockType: 'BlockGroup', + blockGroupType: 'ListItem', + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'SelectionMarker', + isSelected: true, + format: {}, + }, + { + segmentType: 'Br', + format: {}, + }, + ], + format: {}, + }, + ], + levels: [ + { + listType: 'OL', + format: { + marginTop: '0px', + marginBottom: '0px', + startNumberOverride: undefined, + displayForDummyItem: undefined, + }, + dataset: { + editingInfo: '{"orderedStyleType":10}', + }, + }, + ], + formatHolder: { + segmentType: 'SelectionMarker', + isSelected: false, + format: {}, + }, + format: {}, + }; const expectedModel: ContentModelDocument = { blockGroupType: 'Document', @@ -792,52 +873,12 @@ describe('handleEnterOnList', () => { listStyleType: '"B) "', }, }, - { - blockType: 'BlockGroup', - blockGroupType: 'ListItem', - blocks: [ - { - blockType: 'Paragraph', - segments: [ - { - segmentType: 'SelectionMarker', - isSelected: true, - format: {}, - }, - { - segmentType: 'Br', - format: {}, - }, - ], - format: {}, - }, - ], - levels: [ - { - listType: 'OL', - format: { - marginTop: '0px', - marginBottom: '0px', - startNumberOverride: undefined, - displayForDummyItem: undefined, - }, - dataset: { - editingInfo: '{"orderedStyleType":10}', - }, - }, - ], - formatHolder: { - segmentType: 'SelectionMarker', - isSelected: false, - format: {}, - }, - format: {}, - }, + expectedListItem, ], format: {}, }; - runTest(model, expectedModel, 'range'); + runTest(model, expectedModel, 'range', expectedListItem); }); it('enter on list item with selected text', () => { @@ -1003,7 +1044,7 @@ describe('handleEnterOnList', () => { ], format: {}, }; - runTest(model, expectedModel, 'range'); + runTest(model, expectedModel, 'range', null); }); it('enter on multiple list items with selected text', () => { @@ -1288,7 +1329,7 @@ describe('handleEnterOnList', () => { ], format: {}, }; - runTest(model, expectedModel, 'range'); + runTest(model, expectedModel, 'range', null); }); it('expanded range mixed list with paragraph', () => { @@ -1479,7 +1520,7 @@ describe('handleEnterOnList', () => { ], format: {}, }; - runTest(model, expectedModel, 'range'); + runTest(model, expectedModel, 'range', null); }); it('expanded range with mixed list with paragraph | different styles', () => { @@ -1672,7 +1713,7 @@ describe('handleEnterOnList', () => { ], format: {}, }; - runTest(model, expectedModel, 'range'); + runTest(model, expectedModel, 'range', null); }); }); diff --git a/packages/roosterjs-content-model-plugins/test/edit/keyboardTabTest.ts b/packages/roosterjs-content-model-plugins/test/edit/keyboardTabTest.ts index 9fb4f67f241..18eef96162e 100644 --- a/packages/roosterjs-content-model-plugins/test/edit/keyboardTabTest.ts +++ b/packages/roosterjs-content-model-plugins/test/edit/keyboardTabTest.ts @@ -1,5 +1,5 @@ import * as setModelIndentation from '../../../roosterjs-content-model-api/lib/modelApi/block/setModelIndentation'; -import { ContentModelDocument } from 'roosterjs-content-model-types'; +import { ContentModelDocument, FormatContentModelContext } from 'roosterjs-content-model-types'; import { editingTestCommon } from './editingTestCommon'; import { keyboardTab } from '../../lib/edit/keyboardTab'; @@ -18,14 +18,15 @@ describe('keyboardTab', () => { shiftKey: boolean, expectedResult: boolean ) { + const context: FormatContentModelContext = { + newEntities: [], + deletedEntities: [], + newImages: [], + }; const formatWithContentModelSpy = jasmine .createSpy('formatWithContentModel') .and.callFake((callback, options) => { - const result = callback(input, { - newEntities: [], - deletedEntities: [], - newImages: [], - }); + const result = callback(input, context); expect(result).toBe(expectedResult); }); @@ -54,7 +55,12 @@ describe('keyboardTab', () => { expect(formatWithContentModelSpy).toHaveBeenCalled(); if (indent) { - expect(setModelIndentationSpy).toHaveBeenCalledWith(input as any, indent); + expect(setModelIndentationSpy).toHaveBeenCalledWith( + input as any, + indent, + undefined, + context + ); } else { expect(setModelIndentationSpy).not.toHaveBeenCalled(); } diff --git a/packages/roosterjs-content-model-plugins/test/shortcut/utils/setShortcutIndentationCommandTest.ts b/packages/roosterjs-content-model-plugins/test/shortcut/utils/setShortcutIndentationCommandTest.ts index f9562243de8..c9f412eb65b 100644 --- a/packages/roosterjs-content-model-plugins/test/shortcut/utils/setShortcutIndentationCommandTest.ts +++ b/packages/roosterjs-content-model-plugins/test/shortcut/utils/setShortcutIndentationCommandTest.ts @@ -49,7 +49,12 @@ describe('setShortcutIndentationCommand', () => { expect(formatContentModelSpy).toHaveBeenCalledTimes(1); if (shouldIndent) { expect(setModelIndentationSpy).toHaveBeenCalledTimes(1); - expect(setModelIndentationSpy).toHaveBeenCalledWith(model, operation); + expect(setModelIndentationSpy).toHaveBeenCalledWith( + model, + operation, + undefined, + context + ); } else { expect(setModelIndentationSpy).not.toHaveBeenCalled(); }