@wordpress/editor 13.28.2 → 13.29.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (81) hide show
  1. package/CHANGELOG.md +2 -0
  2. package/build/components/commands/index.js +201 -0
  3. package/build/components/commands/index.js.map +1 -0
  4. package/build/components/commands/index.native.js +9 -0
  5. package/build/components/commands/index.native.js.map +1 -0
  6. package/build/components/document-bar/index.js +78 -67
  7. package/build/components/document-bar/index.js.map +1 -1
  8. package/build/components/document-tools/index.js +4 -2
  9. package/build/components/document-tools/index.js.map +1 -1
  10. package/build/components/global-keyboard-shortcuts/index.js +20 -2
  11. package/build/components/global-keyboard-shortcuts/index.js.map +1 -1
  12. package/build/components/global-keyboard-shortcuts/register-shortcuts.js +18 -0
  13. package/build/components/global-keyboard-shortcuts/register-shortcuts.js.map +1 -1
  14. package/build/components/mode-switcher/index.js +86 -0
  15. package/build/components/mode-switcher/index.js.map +1 -0
  16. package/build/components/post-title/index.js +0 -5
  17. package/build/components/post-title/index.js.map +1 -1
  18. package/build/components/provider/index.js +6 -0
  19. package/build/components/provider/index.js.map +1 -1
  20. package/build/components/provider/index.native.js +4 -9
  21. package/build/components/provider/index.native.js.map +1 -1
  22. package/build/components/provider/use-block-editor-settings.js +4 -8
  23. package/build/components/provider/use-block-editor-settings.js.map +1 -1
  24. package/build/components/provider/use-hide-blocks-from-inserter.js +53 -0
  25. package/build/components/provider/use-hide-blocks-from-inserter.js.map +1 -0
  26. package/build/private-apis.js +2 -0
  27. package/build/private-apis.js.map +1 -1
  28. package/build/store/actions.js +69 -2
  29. package/build/store/actions.js.map +1 -1
  30. package/build/store/selectors.js +13 -1
  31. package/build/store/selectors.js.map +1 -1
  32. package/build-module/components/commands/index.js +194 -0
  33. package/build-module/components/commands/index.js.map +1 -0
  34. package/build-module/components/commands/index.native.js +3 -0
  35. package/build-module/components/commands/index.native.js.map +1 -0
  36. package/build-module/components/document-bar/index.js +81 -70
  37. package/build-module/components/document-bar/index.js.map +1 -1
  38. package/build-module/components/document-tools/index.js +4 -2
  39. package/build-module/components/document-tools/index.js.map +1 -1
  40. package/build-module/components/global-keyboard-shortcuts/index.js +20 -2
  41. package/build-module/components/global-keyboard-shortcuts/index.js.map +1 -1
  42. package/build-module/components/global-keyboard-shortcuts/register-shortcuts.js +18 -0
  43. package/build-module/components/global-keyboard-shortcuts/register-shortcuts.js.map +1 -1
  44. package/build-module/components/mode-switcher/index.js +80 -0
  45. package/build-module/components/mode-switcher/index.js.map +1 -0
  46. package/build-module/components/post-title/index.js +0 -5
  47. package/build-module/components/post-title/index.js.map +1 -1
  48. package/build-module/components/provider/index.js +6 -0
  49. package/build-module/components/provider/index.js.map +1 -1
  50. package/build-module/components/provider/index.native.js +4 -10
  51. package/build-module/components/provider/index.native.js.map +1 -1
  52. package/build-module/components/provider/use-block-editor-settings.js +5 -9
  53. package/build-module/components/provider/use-block-editor-settings.js.map +1 -1
  54. package/build-module/components/provider/use-hide-blocks-from-inserter.js +47 -0
  55. package/build-module/components/provider/use-hide-blocks-from-inserter.js.map +1 -0
  56. package/build-module/private-apis.js +2 -0
  57. package/build-module/private-apis.js.map +1 -1
  58. package/build-module/store/actions.js +66 -0
  59. package/build-module/store/actions.js.map +1 -1
  60. package/build-module/store/selectors.js +12 -0
  61. package/build-module/store/selectors.js.map +1 -1
  62. package/build-style/style-rtl.css +4 -47
  63. package/build-style/style.css +4 -47
  64. package/package.json +32 -32
  65. package/src/components/commands/index.js +202 -0
  66. package/src/components/commands/index.native.js +2 -0
  67. package/src/components/document-bar/index.js +115 -94
  68. package/src/components/document-bar/style.scss +4 -37
  69. package/src/components/document-tools/index.js +22 -16
  70. package/src/components/global-keyboard-shortcuts/index.js +35 -4
  71. package/src/components/global-keyboard-shortcuts/register-shortcuts.js +20 -0
  72. package/src/components/mode-switcher/index.js +90 -0
  73. package/src/components/post-title/index.js +0 -3
  74. package/src/components/provider/index.js +7 -0
  75. package/src/components/provider/index.native.js +7 -6
  76. package/src/components/provider/use-block-editor-settings.js +6 -15
  77. package/src/components/provider/use-hide-blocks-from-inserter.js +81 -0
  78. package/src/private-apis.js +2 -0
  79. package/src/store/actions.js +95 -0
  80. package/src/store/selectors.js +12 -0
  81. package/src/store/test/actions.js +82 -0
@@ -10,10 +10,41 @@ import { useDispatch, useSelect } from '@wordpress/data';
10
10
  import { store as editorStore } from '../../store';
11
11
 
12
12
  export default function EditorKeyboardShortcuts() {
13
- const { redo, undo, savePost, setIsListViewOpened } =
14
- useDispatch( editorStore );
15
- const { isEditedPostDirty, isPostSavingLocked, isListViewOpened } =
16
- useSelect( editorStore );
13
+ const isModeToggleDisabled = useSelect( ( select ) => {
14
+ const { richEditingEnabled, codeEditingEnabled } =
15
+ select( editorStore ).getEditorSettings();
16
+ return ! richEditingEnabled || ! codeEditingEnabled;
17
+ }, [] );
18
+ const {
19
+ redo,
20
+ undo,
21
+ savePost,
22
+ setIsListViewOpened,
23
+ switchEditorMode,
24
+ toggleDistractionFree,
25
+ } = useDispatch( editorStore );
26
+ const {
27
+ isEditedPostDirty,
28
+ isPostSavingLocked,
29
+ isListViewOpened,
30
+ getEditorMode,
31
+ } = useSelect( editorStore );
32
+
33
+ useShortcut(
34
+ 'core/editor/toggle-mode',
35
+ () => {
36
+ switchEditorMode(
37
+ getEditorMode() === 'visual' ? 'text' : 'visual'
38
+ );
39
+ },
40
+ {
41
+ isDisabled: isModeToggleDisabled,
42
+ }
43
+ );
44
+
45
+ useShortcut( 'core/editor/toggle-distraction-free', () => {
46
+ toggleDistractionFree();
47
+ } );
17
48
 
18
49
  useShortcut( 'core/editor/undo', ( event ) => {
19
50
  undo();
@@ -12,6 +12,16 @@ function EditorKeyboardShortcutsRegister() {
12
12
  // Registering the shortcuts.
13
13
  const { registerShortcut } = useDispatch( keyboardShortcutsStore );
14
14
  useEffect( () => {
15
+ registerShortcut( {
16
+ name: 'core/editor/toggle-mode',
17
+ category: 'global',
18
+ description: __( 'Switch between visual editor and code editor.' ),
19
+ keyCombination: {
20
+ modifier: 'secondary',
21
+ character: 'm',
22
+ },
23
+ } );
24
+
15
25
  registerShortcut( {
16
26
  name: 'core/editor/save',
17
27
  category: 'global',
@@ -63,6 +73,16 @@ function EditorKeyboardShortcutsRegister() {
63
73
  character: 'o',
64
74
  },
65
75
  } );
76
+
77
+ registerShortcut( {
78
+ name: 'core/editor/toggle-distraction-free',
79
+ category: 'global',
80
+ description: __( 'Toggle distraction free mode.' ),
81
+ keyCombination: {
82
+ modifier: 'primaryShift',
83
+ character: '\\',
84
+ },
85
+ } );
66
86
  }, [ registerShortcut ] );
67
87
 
68
88
  return <BlockEditorKeyboardShortcuts.Register />;
@@ -0,0 +1,90 @@
1
+ /**
2
+ * WordPress dependencies
3
+ */
4
+ import { __ } from '@wordpress/i18n';
5
+ import { MenuItemsChoice, MenuGroup } from '@wordpress/components';
6
+ import { useSelect, useDispatch } from '@wordpress/data';
7
+ import { store as keyboardShortcutsStore } from '@wordpress/keyboard-shortcuts';
8
+
9
+ /**
10
+ * Internal dependencies
11
+ */
12
+ import { store as editorStore } from '../../store';
13
+
14
+ /**
15
+ * Set of available mode options.
16
+ *
17
+ * @type {Array}
18
+ */
19
+ const MODES = [
20
+ {
21
+ value: 'visual',
22
+ label: __( 'Visual editor' ),
23
+ },
24
+ {
25
+ value: 'text',
26
+ label: __( 'Code editor' ),
27
+ },
28
+ ];
29
+
30
+ function ModeSwitcher() {
31
+ const { shortcut, isRichEditingEnabled, isCodeEditingEnabled, mode } =
32
+ useSelect(
33
+ ( select ) => ( {
34
+ shortcut: select(
35
+ keyboardShortcutsStore
36
+ ).getShortcutRepresentation( 'core/editor/toggle-mode' ),
37
+ isRichEditingEnabled:
38
+ select( editorStore ).getEditorSettings()
39
+ .richEditingEnabled,
40
+ isCodeEditingEnabled:
41
+ select( editorStore ).getEditorSettings()
42
+ .codeEditingEnabled,
43
+ mode: select( editorStore ).getEditorMode(),
44
+ } ),
45
+ []
46
+ );
47
+ const { switchEditorMode } = useDispatch( editorStore );
48
+
49
+ let selectedMode = mode;
50
+ if ( ! isRichEditingEnabled && mode === 'visual' ) {
51
+ selectedMode = 'text';
52
+ }
53
+ if ( ! isCodeEditingEnabled && mode === 'text' ) {
54
+ selectedMode = 'visual';
55
+ }
56
+
57
+ const choices = MODES.map( ( choice ) => {
58
+ if ( ! isCodeEditingEnabled && choice.value === 'text' ) {
59
+ choice = {
60
+ ...choice,
61
+ disabled: true,
62
+ };
63
+ }
64
+ if ( ! isRichEditingEnabled && choice.value === 'visual' ) {
65
+ choice = {
66
+ ...choice,
67
+ disabled: true,
68
+ info: __(
69
+ 'You can enable the visual editor in your profile settings.'
70
+ ),
71
+ };
72
+ }
73
+ if ( choice.value !== selectedMode && ! choice.disabled ) {
74
+ return { ...choice, shortcut };
75
+ }
76
+ return choice;
77
+ } );
78
+
79
+ return (
80
+ <MenuGroup label={ __( 'Editor' ) }>
81
+ <MenuItemsChoice
82
+ choices={ choices }
83
+ value={ selectedMode }
84
+ onSelect={ switchEditorMode }
85
+ />
86
+ </MenuGroup>
87
+ );
88
+ }
89
+
90
+ export default ModeSwitcher;
@@ -25,7 +25,6 @@ import { __unstableStripHTML as stripHTML } from '@wordpress/dom';
25
25
  /**
26
26
  * Internal dependencies
27
27
  */
28
- import { store as editorStore } from '../../store';
29
28
  import { DEFAULT_CLASSNAMES, REGEXP_NEWLINES } from './constants';
30
29
  import usePostTitleFocus from './use-post-title-focus';
31
30
  import usePostTitle from './use-post-title';
@@ -33,13 +32,11 @@ import PostTypeSupportCheck from '../post-type-support-check';
33
32
 
34
33
  function PostTitle( _, forwardedRef ) {
35
34
  const { placeholder, hasFixedToolbar } = useSelect( ( select ) => {
36
- const { getEditedPostAttribute } = select( editorStore );
37
35
  const { getSettings } = select( blockEditorStore );
38
36
  const { titlePlaceholder, hasFixedToolbar: _hasFixedToolbar } =
39
37
  getSettings();
40
38
 
41
39
  return {
42
- title: getEditedPostAttribute( 'title' ),
43
40
  placeholder: titlePlaceholder,
44
41
  hasFixedToolbar: _hasFixedToolbar,
45
42
  };
@@ -23,6 +23,8 @@ import useBlockEditorSettings from './use-block-editor-settings';
23
23
  import { unlock } from '../../lock-unlock';
24
24
  import DisableNonPageContentBlocks from './disable-non-page-content-blocks';
25
25
  import NavigationBlockEditingMode from './navigation-block-editing-mode';
26
+ import { useHideBlocksFromInserter } from './use-hide-blocks-from-inserter';
27
+ import useCommands from '../commands';
26
28
 
27
29
  const { ExperimentalBlockEditorProvider } = unlock( blockEditorPrivateApis );
28
30
  const { PatternsMenuItems } = unlock( editPatternsPrivateApis );
@@ -229,6 +231,11 @@ export const ExperimentalEditorProvider = withRegistryProvider(
229
231
  setRenderingMode( settings.defaultRenderingMode ?? 'post-only' );
230
232
  }, [ settings.defaultRenderingMode, setRenderingMode ] );
231
233
 
234
+ useHideBlocksFromInserter( post.type );
235
+
236
+ // Register the editor commands.
237
+ useCommands();
238
+
232
239
  if ( ! isReady ) {
233
240
  return null;
234
241
  }
@@ -59,8 +59,6 @@ const postTypeEntities = [
59
59
  import { EditorHelpTopics, store as editorStore } from '@wordpress/editor';
60
60
  import { store as noticesStore } from '@wordpress/notices';
61
61
  import { store as coreStore } from '@wordpress/core-data';
62
- // eslint-disable-next-line no-restricted-imports
63
- import { store as editPostStore } from '@wordpress/edit-post';
64
62
 
65
63
  /**
66
64
  * Internal dependencies
@@ -392,8 +390,8 @@ const ComposedNativeProvider = compose( [
392
390
  getEditedPostAttribute,
393
391
  getEditedPostContent,
394
392
  getEditorSettings,
393
+ getEditorMode,
395
394
  } = select( editorStore );
396
- const { getEditorMode } = select( editPostStore );
397
395
 
398
396
  const { getBlockIndex, getSelectedBlockClientId, getGlobalBlockCount } =
399
397
  select( blockEditorStore );
@@ -417,15 +415,18 @@ const ComposedNativeProvider = compose( [
417
415
  };
418
416
  } ),
419
417
  withDispatch( ( dispatch ) => {
420
- const { editPost, resetEditorBlocks, updateEditorSettings } =
421
- dispatch( editorStore );
418
+ const {
419
+ editPost,
420
+ resetEditorBlocks,
421
+ updateEditorSettings,
422
+ switchEditorMode,
423
+ } = dispatch( editorStore );
422
424
  const {
423
425
  clearSelectedBlock,
424
426
  updateSettings,
425
427
  insertBlock,
426
428
  replaceBlock,
427
429
  } = dispatch( blockEditorStore );
428
- const { switchEditorMode } = dispatch( editPostStore );
429
430
  const { addEntities, receiveEntityRecords } = dispatch( coreStore );
430
431
  const { createSuccessNotice, createErrorNotice } =
431
432
  dispatch( noticesStore );
@@ -7,12 +7,12 @@ import {
7
7
  store as coreStore,
8
8
  __experimentalFetchLinkSuggestions as fetchLinkSuggestions,
9
9
  __experimentalFetchUrlData as fetchUrlData,
10
- fetchBlockPatterns,
11
10
  } from '@wordpress/core-data';
12
11
  import { __ } from '@wordpress/i18n';
13
12
  import { store as preferencesStore } from '@wordpress/preferences';
14
13
  import { useViewportMatch } from '@wordpress/compose';
15
14
  import { store as blocksStore } from '@wordpress/blocks';
15
+ import { privateApis } from '@wordpress/block-editor';
16
16
 
17
17
  /**
18
18
  * Internal dependencies
@@ -20,6 +20,7 @@ import { store as blocksStore } from '@wordpress/blocks';
20
20
  import inserterMediaCategories from '../media-categories';
21
21
  import { mediaUpload } from '../../utils';
22
22
  import { store as editorStore } from '../../store';
23
+ import { unlock } from '../../lock-unlock';
23
24
 
24
25
  const EMPTY_BLOCKS_LIST = [];
25
26
 
@@ -28,7 +29,6 @@ const BLOCK_EDITOR_SETTINGS = [
28
29
  '__experimentalDiscussionSettings',
29
30
  '__experimentalFeatures',
30
31
  '__experimentalGlobalStylesBaseStyles',
31
- '__experimentalPreferredStyleVariations',
32
32
  '__unstableGalleryWithImageBlocks',
33
33
  'alignWide',
34
34
  'blockInspectorTabs',
@@ -52,7 +52,6 @@ const BLOCK_EDITOR_SETTINGS = [
52
52
  'gradients',
53
53
  'generateAnchors',
54
54
  'onNavigateToEntityRecord',
55
- 'hasInlineToolbar',
56
55
  'imageDefaultSize',
57
56
  'imageDimensions',
58
57
  'imageEditing',
@@ -60,7 +59,6 @@ const BLOCK_EDITOR_SETTINGS = [
60
59
  'isRTL',
61
60
  'locale',
62
61
  'maxWidth',
63
- 'onUpdateDefaultBlockStyles',
64
62
  'postContentAttributes',
65
63
  'postsPerPage',
66
64
  'readOnly',
@@ -247,17 +245,10 @@ function useBlockEditorSettings( settings, postType, postId ) {
247
245
  keepCaretInsideBlock,
248
246
  mediaUpload: hasUploadPermissions ? mediaUpload : undefined,
249
247
  __experimentalBlockPatterns: blockPatterns,
250
- __experimentalFetchBlockPatterns: async () => {
251
- return ( await fetchBlockPatterns() ).filter(
252
- ( { postTypes } ) => {
253
- return (
254
- ! postTypes ||
255
- ( Array.isArray( postTypes ) &&
256
- postTypes.includes( postType ) )
257
- );
258
- }
259
- );
260
- },
248
+ [ unlock( privateApis ).selectBlockPatternsKey ]: ( select ) =>
249
+ unlock( select( coreStore ) ).getBlockPatternsForPostType(
250
+ postType
251
+ ),
261
252
  __experimentalReusableBlocks: reusableBlocks,
262
253
  __experimentalBlockPatternCategories: blockPatternCategories,
263
254
  __experimentalUserPatternCategories: userPatternCategories,
@@ -0,0 +1,81 @@
1
+ /**
2
+ * WordPress dependencies
3
+ */
4
+ import { useEffect } from '@wordpress/element';
5
+ import { addFilter, removeFilter } from '@wordpress/hooks';
6
+
7
+ // These post types are "structural" block lists.
8
+ // We should be allowed to use
9
+ // the post content and template parts blocks within them.
10
+ const POST_TYPES_ALLOWING_POST_CONTENT_TEMPLATE_PART = [
11
+ 'wp_block',
12
+ 'wp_template',
13
+ 'wp_template_part',
14
+ ];
15
+
16
+ /**
17
+ * In some specific contexts,
18
+ * the template part and post content blocks need to be hidden.
19
+ *
20
+ * @param {string} postType Post Type
21
+ */
22
+ export function useHideBlocksFromInserter( postType ) {
23
+ useEffect( () => {
24
+ /*
25
+ * Prevent adding template part in the editor.
26
+ */
27
+ addFilter(
28
+ 'blockEditor.__unstableCanInsertBlockType',
29
+ 'removeTemplatePartsFromInserter',
30
+ ( canInsert, blockType ) => {
31
+ if (
32
+ ! POST_TYPES_ALLOWING_POST_CONTENT_TEMPLATE_PART.includes(
33
+ postType
34
+ ) &&
35
+ blockType.name === 'core/template-part'
36
+ ) {
37
+ return false;
38
+ }
39
+ return canInsert;
40
+ }
41
+ );
42
+
43
+ /*
44
+ * Prevent adding post content block (except in query block) in the editor.
45
+ */
46
+ addFilter(
47
+ 'blockEditor.__unstableCanInsertBlockType',
48
+ 'removePostContentFromInserter',
49
+ (
50
+ canInsert,
51
+ blockType,
52
+ rootClientId,
53
+ { getBlockParentsByBlockName }
54
+ ) => {
55
+ if (
56
+ ! POST_TYPES_ALLOWING_POST_CONTENT_TEMPLATE_PART.includes(
57
+ postType
58
+ ) &&
59
+ blockType.name === 'core/post-content'
60
+ ) {
61
+ return (
62
+ getBlockParentsByBlockName( rootClientId, 'core/query' )
63
+ .length > 0
64
+ );
65
+ }
66
+ return canInsert;
67
+ }
68
+ );
69
+
70
+ return () => {
71
+ removeFilter(
72
+ 'blockEditor.__unstableCanInsertBlockType',
73
+ 'removeTemplatePartsFromInserter'
74
+ );
75
+ removeFilter(
76
+ 'blockEditor.__unstableCanInsertBlockType',
77
+ 'removePostContentFromInserter'
78
+ );
79
+ };
80
+ }, [ postType ] );
81
+ }
@@ -10,6 +10,7 @@ import useBlockEditorSettings from './components/provider/use-block-editor-setti
10
10
  import DocumentTools from './components/document-tools';
11
11
  import InserterSidebar from './components/inserter-sidebar';
12
12
  import ListViewSidebar from './components/list-view-sidebar';
13
+ import ModeSwitcher from './components/mode-switcher';
13
14
  import PluginPostExcerpt from './components/post-excerpt/plugin';
14
15
  import PostPanelRow from './components/post-panel-row';
15
16
  import PostViewLink from './components/post-view-link';
@@ -25,6 +26,7 @@ lock( privateApis, {
25
26
  EntitiesSavedStatesExtensible,
26
27
  InserterSidebar,
27
28
  ListViewSidebar,
29
+ ModeSwitcher,
28
30
  PluginPostExcerpt,
29
31
  PostPanelRow,
30
32
  PostViewLink,
@@ -1,6 +1,7 @@
1
1
  /**
2
2
  * WordPress dependencies
3
3
  */
4
+ import { speak } from '@wordpress/a11y';
4
5
  import apiFetch from '@wordpress/api-fetch';
5
6
  import deprecated from '@wordpress/deprecated';
6
7
  import {
@@ -13,6 +14,7 @@ import { store as coreStore } from '@wordpress/core-data';
13
14
  import { store as blockEditorStore } from '@wordpress/block-editor';
14
15
  import { applyFilters } from '@wordpress/hooks';
15
16
  import { store as preferencesStore } from '@wordpress/preferences';
17
+ import { __ } from '@wordpress/i18n';
16
18
 
17
19
  /**
18
20
  * Internal dependencies
@@ -725,6 +727,99 @@ export function setIsListViewOpened( isOpen ) {
725
727
  };
726
728
  }
727
729
 
730
+ /**
731
+ * Action that toggles Distraction free mode.
732
+ * Distraction free mode expects there are no sidebars, as due to the
733
+ * z-index values set, you can't close sidebars.
734
+ */
735
+ export const toggleDistractionFree =
736
+ () =>
737
+ ( { dispatch, registry } ) => {
738
+ const isDistractionFree = registry
739
+ .select( preferencesStore )
740
+ .get( 'core', 'distractionFree' );
741
+ if ( isDistractionFree ) {
742
+ registry
743
+ .dispatch( preferencesStore )
744
+ .set( 'core', 'fixedToolbar', false );
745
+ }
746
+ if ( ! isDistractionFree ) {
747
+ registry.batch( () => {
748
+ registry
749
+ .dispatch( preferencesStore )
750
+ .set( 'core', 'fixedToolbar', true );
751
+ dispatch.setIsInserterOpened( false );
752
+ dispatch.setIsListViewOpened( false );
753
+ } );
754
+ }
755
+ registry.batch( () => {
756
+ registry
757
+ .dispatch( preferencesStore )
758
+ .set( 'core', 'distractionFree', ! isDistractionFree );
759
+ registry
760
+ .dispatch( noticesStore )
761
+ .createInfoNotice(
762
+ isDistractionFree
763
+ ? __( 'Distraction free off.' )
764
+ : __( 'Distraction free on.' ),
765
+ {
766
+ id: 'core/editor/distraction-free-mode/notice',
767
+ type: 'snackbar',
768
+ actions: [
769
+ {
770
+ label: __( 'Undo' ),
771
+ onClick: () => {
772
+ registry.batch( () => {
773
+ registry
774
+ .dispatch( preferencesStore )
775
+ .set(
776
+ 'core',
777
+ 'fixedToolbar',
778
+ isDistractionFree ? true : false
779
+ );
780
+ registry
781
+ .dispatch( preferencesStore )
782
+ .toggle(
783
+ 'core',
784
+ 'distractionFree'
785
+ );
786
+ } );
787
+ },
788
+ },
789
+ ],
790
+ }
791
+ );
792
+ } );
793
+ };
794
+
795
+ /**
796
+ * Triggers an action used to switch editor mode.
797
+ *
798
+ * @param {string} mode The editor mode.
799
+ */
800
+ export const switchEditorMode =
801
+ ( mode ) =>
802
+ ( { dispatch, registry } ) => {
803
+ registry.dispatch( preferencesStore ).set( 'core', 'editorMode', mode );
804
+
805
+ // Unselect blocks when we switch to a non visual mode.
806
+ if ( mode !== 'visual' ) {
807
+ registry.dispatch( blockEditorStore ).clearSelectedBlock();
808
+ }
809
+
810
+ if ( mode === 'visual' ) {
811
+ speak( __( 'Visual editor selected' ), 'assertive' );
812
+ } else if ( mode === 'text' ) {
813
+ const isDistractionFree = registry
814
+ .select( preferencesStore )
815
+ .get( 'core', 'distractionFree' );
816
+ if ( isDistractionFree ) {
817
+ dispatch.toggleDistractionFree();
818
+ }
819
+ speak( __( 'Code editor selected' ), 'assertive' );
820
+ }
821
+ };
822
+
728
823
  /**
729
824
  * Backward compatibility
730
825
  */
@@ -1313,6 +1313,18 @@ export function isInserterOpened( state ) {
1313
1313
  return !! state.blockInserterPanel;
1314
1314
  }
1315
1315
 
1316
+ /**
1317
+ * Returns the current editing mode.
1318
+ *
1319
+ * @param {Object} state Global application state.
1320
+ *
1321
+ * @return {string} Editing mode.
1322
+ */
1323
+ export const getEditorMode = createRegistrySelector(
1324
+ ( select ) => () =>
1325
+ select( preferencesStore ).get( 'core', 'editorMode' ) ?? 'visual'
1326
+ );
1327
+
1316
1328
  /*
1317
1329
  * Backward compatibility
1318
1330
  */
@@ -488,4 +488,86 @@ describe( 'Editor actions', () => {
488
488
  ).toBe( false );
489
489
  } );
490
490
  } );
491
+
492
+ describe( 'switchEditorMode', () => {
493
+ let registry;
494
+
495
+ beforeEach( () => {
496
+ registry = createRegistryWithStores();
497
+ } );
498
+
499
+ it( 'to visual', () => {
500
+ // Switch to text first, since the default is visual.
501
+ registry.dispatch( editorStore ).switchEditorMode( 'text' );
502
+ expect( registry.select( editorStore ).getEditorMode() ).toEqual(
503
+ 'text'
504
+ );
505
+ registry.dispatch( editorStore ).switchEditorMode( 'visual' );
506
+ expect( registry.select( editorStore ).getEditorMode() ).toEqual(
507
+ 'visual'
508
+ );
509
+ } );
510
+
511
+ it( 'to text', () => {
512
+ // It defaults to visual.
513
+ expect( registry.select( editorStore ).getEditorMode() ).toEqual(
514
+ 'visual'
515
+ );
516
+ // Add a selected client id and make sure it's there.
517
+ const clientId = 'clientId_1';
518
+ registry.dispatch( blockEditorStore ).selectionChange( clientId );
519
+ expect(
520
+ registry.select( blockEditorStore ).getSelectedBlockClientId()
521
+ ).toEqual( clientId );
522
+
523
+ registry.dispatch( editorStore ).switchEditorMode( 'text' );
524
+ expect(
525
+ registry.select( blockEditorStore ).getSelectedBlockClientId()
526
+ ).toBeNull();
527
+ expect( registry.select( editorStore ).getEditorMode() ).toEqual(
528
+ 'text'
529
+ );
530
+ } );
531
+ it( 'should turn off distraction free mode when switching to code editor', () => {
532
+ registry
533
+ .dispatch( preferencesStore )
534
+ .set( 'core', 'distractionFree', true );
535
+ registry.dispatch( editorStore ).switchEditorMode( 'text' );
536
+ expect(
537
+ registry
538
+ .select( preferencesStore )
539
+ .get( 'core', 'distractionFree' )
540
+ ).toBe( false );
541
+ } );
542
+ } );
543
+
544
+ describe( 'toggleDistractionFree', () => {
545
+ it( 'should properly update settings to prevent layout corruption when enabling distraction free mode', () => {
546
+ const registry = createRegistryWithStores();
547
+
548
+ // Enable everything that shouldn't be enabled in distraction free mode.
549
+ registry
550
+ .dispatch( preferencesStore )
551
+ .set( 'core', 'fixedToolbar', true );
552
+ registry.dispatch( editorStore ).setIsListViewOpened( true );
553
+ // Initial state is falsy.
554
+ registry.dispatch( editorStore ).toggleDistractionFree();
555
+ expect(
556
+ registry
557
+ .select( preferencesStore )
558
+ .get( 'core', 'fixedToolbar' )
559
+ ).toBe( true );
560
+ expect( registry.select( editorStore ).isListViewOpened() ).toBe(
561
+ false
562
+ );
563
+ expect( registry.select( editorStore ).isInserterOpened() ).toBe(
564
+ false
565
+ );
566
+ expect(
567
+ registry
568
+ .select( preferencesStore )
569
+ .get( 'core', 'distractionFree' )
570
+ ).toBe( true );
571
+ } );
572
+ } );
491
573
  } );