From 0015110bf1c9c27fdde3f0ec8bba890198f70d67 Mon Sep 17 00:00:00 2001 From: Aleksandr Yakunichev Date: Tue, 10 Jan 2023 07:02:35 +0000 Subject: [PATCH] TINY-9337: Remove focusing after toggling the toolbar (#8272) * TINY-9337: Add skipFocus option to ToggleToolbarDrawer command * TINY-9337: Update CHANGELOG.md * TINY-9337: Fix eslint errors * TINY-9337: Remove Toggling dependance on skipFocus value * TINY-9337: Add skipFocus: false test * TINY-9337: Use Singleton to store skipFocus state * TINY-9337: Remove options for toggleWithoutFocusing api * TINY-9337: Fix retrieving skipFocus singleton value * TINY-9337: Update the ToggleToolbarDrawer command to be more readable * TINY-9337: Simplify SplitFloatingToolbar code * TINY-9337: Update ToolbarDrawerToggleTest --- .../alloy/api/ui/FloatingToolbarButton.ts | 25 +++++++-- .../alloy/api/ui/SplitFloatingToolbar.ts | 6 +++ .../ui/types/FloatingToolbarButtonTypes.ts | 1 + .../alloy/ui/types/SplitToolbarBaseTypes.ts | 1 + modules/tinymce/CHANGELOG.md | 1 + .../src/themes/silver/main/ts/Render.ts | 8 ++- .../main/ts/ui/general/OuterContainer.ts | 12 ++++- .../editor/toolbar/ToolbarDrawerToggleTest.ts | 51 ++++++++++++++++++- 8 files changed, 98 insertions(+), 7 deletions(-) diff --git a/modules/alloy/src/main/ts/ephox/alloy/api/ui/FloatingToolbarButton.ts b/modules/alloy/src/main/ts/ephox/alloy/api/ui/FloatingToolbarButton.ts index e3f23358ee0..699eac2016d 100644 --- a/modules/alloy/src/main/ts/ephox/alloy/api/ui/FloatingToolbarButton.ts +++ b/modules/alloy/src/main/ts/ephox/alloy/api/ui/FloatingToolbarButton.ts @@ -1,4 +1,4 @@ -import { Fun, Optional } from '@ephox/katamari'; +import { Fun, Optional, Singleton } from '@ephox/katamari'; import * as ComponentStructure from '../../alien/ComponentStructure'; import * as AriaControls from '../../aria/AriaControls'; @@ -26,6 +26,14 @@ import * as Sketcher from './Sketcher'; import { Toolbar } from './Toolbar'; import { CompositeSketchFactory } from './UiSketcher'; +const shouldSkipFocus = Singleton.value(); + +const toggleWithoutFocusing = (button: AlloyComponent, externals: Record) => { + shouldSkipFocus.set(true); + toggle(button, externals); + shouldSkipFocus.clear(); +}; + const toggle = (button: AlloyComponent, externals: Record) => { const toolbarSandbox = Coupling.getCoupled(button, 'toolbarSandbox'); if (Sandboxing.isOpen(toolbarSandbox)) { @@ -61,17 +69,22 @@ const makeSandbox = (button: AlloyComponent, spec: FloatingToolbarButtonSpec, de const ariaControls = AriaControls.manager(); const onOpen = (sandbox: AlloyComponent, toolbar: AlloyComponent) => { + const skipFocus = shouldSkipFocus.get().getOr(false); detail.fetch().get((groups) => { setGroups(button, toolbar, detail, spec.layouts, groups); ariaControls.link(button.element); - Keying.focusIn(toolbar); + if (!skipFocus) { + Keying.focusIn(toolbar); + } }); }; const onClose = () => { // Toggle and focus the button Toggling.off(button); - Focusing.focus(button); + if (!shouldSkipFocus.get().getOr(false)) { + Focusing.focus(button); + } ariaControls.unlink(button.element); }; @@ -154,6 +167,9 @@ const factory: CompositeSketchFactory { toggle(button, externals); }, + toggleWithoutFocusing: (button: AlloyComponent) => { + toggleWithoutFocusing(button, externals); + }, getToolbar: (button: AlloyComponent) => { return Sandboxing.getState(Coupling.getCoupled(button, 'toolbarSandbox')); }, @@ -178,6 +194,9 @@ const FloatingToolbarButton: FloatingToolbarButtonSketcher = Sketcher.composite< toggle: (apis, button) => { apis.toggle(button); }, + toggleWithoutFocusing: (apis, button) => { + apis.toggleWithoutFocusing(button); + }, getToolbar: (apis, button) => apis.getToolbar(button), isOpen: (apis, button) => apis.isOpen(button) } diff --git a/modules/alloy/src/main/ts/ephox/alloy/api/ui/SplitFloatingToolbar.ts b/modules/alloy/src/main/ts/ephox/alloy/api/ui/SplitFloatingToolbar.ts index ef60d8cccdc..9a7d3fcb2d7 100644 --- a/modules/alloy/src/main/ts/ephox/alloy/api/ui/SplitFloatingToolbar.ts +++ b/modules/alloy/src/main/ts/ephox/alloy/api/ui/SplitFloatingToolbar.ts @@ -87,6 +87,9 @@ const factory: CompositeSketchFactory { + memFloatingToolbarButton.getOpt(toolbar).each(FloatingToolbarButton.toggleWithoutFocusing); + }, isOpen: (toolbar: AlloyComponent) => memFloatingToolbarButton.getOpt(toolbar).map(FloatingToolbarButton.isOpen).getOr(false), reposition: (toolbar: AlloyComponent) => { @@ -122,6 +125,9 @@ const SplitFloatingToolbar: SplitFloatingToolbarSketcher = Sketcher.composite { apis.toggle(toolbar); }, + toggleWithoutFocusing: (apis, toolbar) => { + apis.toggle(toolbar); + }, isOpen: (apis, toolbar) => apis.isOpen(toolbar), getOverflow: (apis, toolbar) => apis.getOverflow(toolbar) } diff --git a/modules/alloy/src/main/ts/ephox/alloy/ui/types/FloatingToolbarButtonTypes.ts b/modules/alloy/src/main/ts/ephox/alloy/ui/types/FloatingToolbarButtonTypes.ts index cfc2ecae53c..6720f73a9ca 100644 --- a/modules/alloy/src/main/ts/ephox/alloy/ui/types/FloatingToolbarButtonTypes.ts +++ b/modules/alloy/src/main/ts/ephox/alloy/ui/types/FloatingToolbarButtonTypes.ts @@ -27,6 +27,7 @@ export interface FloatingToolbarButtonApis { setGroups: (floatingToolbarButton: AlloyComponent, groups: AlloySpec[]) => Optional; reposition: (floatingToolbarButton: AlloyComponent) => void; toggle: (floatingToolbarButton: AlloyComponent) => void; + toggleWithoutFocusing: (floatingToolbarButton: AlloyComponent) => void; getToolbar: (floatingToolbarButton: AlloyComponent) => Optional; isOpen: (floatingToolbarButton: AlloyComponent) => boolean; } diff --git a/modules/alloy/src/main/ts/ephox/alloy/ui/types/SplitToolbarBaseTypes.ts b/modules/alloy/src/main/ts/ephox/alloy/ui/types/SplitToolbarBaseTypes.ts index 036e91dffb1..f41e6c86c66 100644 --- a/modules/alloy/src/main/ts/ephox/alloy/ui/types/SplitToolbarBaseTypes.ts +++ b/modules/alloy/src/main/ts/ephox/alloy/ui/types/SplitToolbarBaseTypes.ts @@ -19,6 +19,7 @@ export interface SplitToolbarBaseApis { setGroups: (toolbar: AlloyComponent, groups: SketchSpec[]) => void; refresh: (toolbar: AlloyComponent) => void; toggle: (toolbar: AlloyComponent) => void; + toggleWithoutFocusing: (toolbar: AlloyComponent) => void; isOpen: (toolbar: AlloyComponent) => boolean; } diff --git a/modules/tinymce/CHANGELOG.md b/modules/tinymce/CHANGELOG.md index 56b57ce4042..bed0f4c8bf9 100644 --- a/modules/tinymce/CHANGELOG.md +++ b/modules/tinymce/CHANGELOG.md @@ -44,6 +44,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - New `color_default_foreground` and `color_default_background` options to set the initial default color for the `forecolor` and `backcolor` toolbar buttons and menu items. #TINY-9183 - New `getTransparentElements` function added to `tinymce.html.Schema` to return a map object of transparent HTML elements. #TINY-9172 - Added `ToggleToolbarDrawer` event to subscribe to toolbar’s opening and closing. #TINY-9271 +- Added `skipFocus` option to the `ToggleToolbarDrawer` command to preserve focus. #TINY-9337 ### Changed - Transparent elements, like anchors, are now allowed in the root of the editor body if they contain blocks. #TINY-9172 diff --git a/modules/tinymce/src/themes/silver/main/ts/Render.ts b/modules/tinymce/src/themes/silver/main/ts/Render.ts index 106c4041306..9b3ca5babaa 100644 --- a/modules/tinymce/src/themes/silver/main/ts/Render.ts +++ b/modules/tinymce/src/themes/silver/main/ts/Render.ts @@ -425,8 +425,12 @@ const setup = (editor: Editor): RenderInfo => { OuterContainer.focusToolbar(outerContainer); }); - editor.addCommand('ToggleToolbarDrawer', () => { - OuterContainer.toggleToolbarDrawer(outerContainer); + editor.addCommand('ToggleToolbarDrawer', (_ui, options?: { skipFocus: boolean }) => { + if (options?.skipFocus) { + OuterContainer.toggleToolbarDrawerWithoutFocusing(outerContainer); + } else { + OuterContainer.toggleToolbarDrawer(outerContainer); + } }); editor.addQueryStateHandler('ToggleToolbarDrawer', () => OuterContainer.isToolbarDrawerToggled(outerContainer)); diff --git a/modules/tinymce/src/themes/silver/main/ts/ui/general/OuterContainer.ts b/modules/tinymce/src/themes/silver/main/ts/ui/general/OuterContainer.ts index 1f6b6355b49..35ce27920ff 100644 --- a/modules/tinymce/src/themes/silver/main/ts/ui/general/OuterContainer.ts +++ b/modules/tinymce/src/themes/silver/main/ts/ui/general/OuterContainer.ts @@ -66,6 +66,7 @@ interface OuterContainerApis { readonly setToolbars: (comp: AlloyComponent, toolbars: ToolbarGroup[][]) => void; readonly refreshToolbar: (comp: AlloyComponent) => void; readonly toggleToolbarDrawer: (comp: AlloyComponent) => void; + readonly toggleToolbarDrawerWithoutFocusing: (comp: AlloyComponent) => void; readonly isToolbarDrawerToggled: (comp: AlloyComponent) => boolean; readonly getThrobber: (comp: AlloyComponent) => Optional; readonly focusToolbar: (comp: AlloyComponent) => void; @@ -81,7 +82,8 @@ interface OuterContainerApis { interface ToolbarApis { readonly setGroups: (toolbar: AlloyComponent, groups: SketchSpec[]) => void; readonly refresh: (toolbar: AlloyComponent) => void; - readonly toggle?: (toolbar: AlloyComponent) => void; + readonly toggle: (toolbar: AlloyComponent) => void; + readonly toggleWithoutFocusing: (toolbar: AlloyComponent) => void; readonly isOpen?: (toolbar: AlloyComponent) => boolean; } @@ -134,6 +136,11 @@ const factory: UiSketcher.CompositeSketchFactory().toggle, (toggle) => toggle(toolbar)); }); }, + toggleToolbarDrawerWithoutFocusing: (comp) => { + Composite.parts.getPart(comp, detail, 'toolbar').each((toolbar) => { + Optionals.mapFrom(toolbar.getApis().toggleWithoutFocusing, (toggleWithoutFocusing) => toggleWithoutFocusing(toolbar)); + }); + }, isToolbarDrawerToggled: (comp) => { // isOpen may not be defined on all toolbars e.g. 'scrolling' and 'wrap' return Composite.parts.getPart(comp, detail, 'toolbar') @@ -424,6 +431,9 @@ export default Sketcher.composite { apis.toggleToolbarDrawer(comp); }, + toggleToolbarDrawerWithoutFocusing: (apis, comp) => { + apis.toggleToolbarDrawerWithoutFocusing(comp); + }, isToolbarDrawerToggled: (apis, comp) => { return apis.isToolbarDrawerToggled(comp); }, diff --git a/modules/tinymce/src/themes/silver/test/ts/browser/editor/toolbar/ToolbarDrawerToggleTest.ts b/modules/tinymce/src/themes/silver/test/ts/browser/editor/toolbar/ToolbarDrawerToggleTest.ts index 2265cd52739..f02c05fd5d9 100644 --- a/modules/tinymce/src/themes/silver/test/ts/browser/editor/toolbar/ToolbarDrawerToggleTest.ts +++ b/modules/tinymce/src/themes/silver/test/ts/browser/editor/toolbar/ToolbarDrawerToggleTest.ts @@ -1,6 +1,7 @@ -import { TestStore, Waiter } from '@ephox/agar'; +import { TestStore, Waiter, FocusTools } from '@ephox/agar'; import { context, describe, it } from '@ephox/bedrock-client'; import { Arr } from '@ephox/katamari'; +import { SugarDocument } from '@ephox/sugar'; import { McEditor, TinyUiActions } from '@ephox/wrap-mcagar'; import { assert } from 'chai'; @@ -107,4 +108,52 @@ describe('browser.tinymce.themes.silver.editor.toolbar.ToolbarDrawerToggleTest', }); }); }); + + context(`Should preserve focus if skipFocus: true option was passed`, () => { + Arr.each([ 'floating', 'sliding' ], (toolbarMode) => { + it(`TINY-9337: Preserves focus in ${toolbarMode} if skipFocus is true`, async () => { + const editor = await McEditor.pFromSettings({ + menubar: false, + statusbar: false, + width: 200, + toolbar_mode: toolbarMode, + base_url: '/project/tinymce/js/tinymce' + }); + await UiUtils.pWaitForEditorToRender(); + editor.focus(); + + const doc = SugarDocument.getDocument(); + FocusTools.isOnSelector('Root element is focused', doc, 'iframe'); + editor.execCommand('ToggleToolbarDrawer', false, { skipFocus: true }); + await TinyUiActions.pWaitForUi(editor, '.tox-toolbar__overflow'); + await FocusTools.pTryOnSelector('Focus should be preserved', doc, 'iframe'); + editor.execCommand('ToggleToolbarDrawer', false, { skipFocus: true }); + await FocusTools.pTryOnSelector('Focus should be preserved', doc, 'iframe'); + McEditor.remove(editor); + }); + + it(`TINY-9337: Does not preserve focus in ${toolbarMode} if skipFocus is false`, async () => { + const editor = await McEditor.pFromSettings({ + menubar: false, + statusbar: false, + width: 200, + toolbar_mode: toolbarMode, + base_url: '/project/tinymce/js/tinymce' + }); + await UiUtils.pWaitForEditorToRender(); + editor.focus(); + const initialFocusedElement = document.activeElement; + editor.execCommand('ToggleToolbarDrawer', false, { skipFocus: false }); + await TinyUiActions.pWaitForUi(editor, '.tox-toolbar__overflow'); + await Waiter.pTryUntil('Wait for toolbar to be completely open', () => { + assert.notEqual(initialFocusedElement, document.activeElement, 'Focus should not be preserved'); + }); + editor.execCommand('ToggleToolbarDrawer', false, { skipFocus: false }); + await Waiter.pTryUntil('Wait for toolbar to be completely closed', () => { + assert.notEqual(initialFocusedElement, document.activeElement, 'Focus should not be preserved'); + }); + McEditor.remove(editor); + }); + }); + }); });