-
Notifications
You must be signed in to change notification settings - Fork 4.5k
Restrict formatting controls in write mode (contentOnly) #71058
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Restrict formatting controls in write mode (contentOnly) #71058
Conversation
- Add useBlockEditingMode() hook to detect write mode - In contentOnly mode, only show bold and italic controls - Remove link and unknown formats from write mode toolbar - Hide 'More' dropdown with additional formatting options in write mode - Keep write mode simple and focused on essential content editing
The following accounts have interacted with this PR and/or linked issues. I will continue to update these lists as activity occurs. You can also manually ask me to refresh this list by adding the If you're merging code through a pull request on GitHub, copy and paste the following into the bottom of the merge commit message.
To understand the WordPress project's expectations around crediting contributors, please review the Contributor Attribution page in the Core Handbook. |
Size Change: +175 B (+0.01%) Total Size: 1.91 MB
ℹ️ View Unchanged
|
The content-only editing can be used without the write mode. Do we want this change to affect blocks inside containers when with Hardcoding "essential" formats seems limiting for extenders. What if they provide custom formats that are essential for their editorial teams? Maybe we should consider letting formats declare themself as essential for write mode. Screenshot |
@Mamaduka Thanks for flagging. It's been a while. So is Write Mode equivalent to |
Indeed, it's been a while.
As far as I know, that's the way to derive writing mode state; the feature is still behind the experimental flag. |
Yes. Let me explore that approach. |
I agree with this in principle. I do have concerns over it being abused. If I'm a plugin author trying to promote my tooling, why would I not define it as essential? It will promote my product and also save me questions about why they can't find the tool if they're in Write mode and don't understand Write vs Design. At the same time, I agree we should allow this to be extended. I'm not sure how to approach it, but I'd like to do it in a way that we can explore moving forwards without being locked into a decision if possible :)
Maybe I expanded the conversation too far beyond just formats. I'm trying to think from a high level how to let tools opt-in to showing in write mode. At the moment, it seems like it has to be done within the JS which feels really ad-hoc. This may be a terrible idea - but could we add a flag to some base-level components like |
IMO, all public APIs can be abused; the best we can do here is to tuck them in a dropdown (similar to the current system). P.S. It is still possible for custom format libraries to render items in the write mode toolbar if they know how SlotFills work. |
- Add withWriteModeFilter HOC to filter format edit components in write mode - Add essential property to format registrations (default: false) - Mark core formats (bold, italic, link, strikethrough) as essential - Apply HOC in FormatEdit component to filter non-essential formats - Maintain backward compatibility for existing formats - Enable third-party plugins to opt formats into write mode via essential flag This allows write mode to show only essential formatting controls while maintaining full functionality in default mode.
const isWriteMode = isNavigationMode && isContentOnlyMode; | ||
|
||
// In write mode, only show essential formats | ||
if ( isWriteMode && ! formatSettings?.essential ) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Note that if essential
is not defined then the default behaviour is not to show in Write Mode.
- Add early return for Write Mode experiment flag check - Avoid applying HOC when experiment is not enabled - Maintain existing optimizations for essential formats - Improve performance by reducing unnecessary wrapper components
@Mamaduka @jeryj I went ahead and implemented one route for this. I didn't want to remove formats entirely or stop their registration because that's not the goal of Write Mode. All we want to do is not provide the UI in write mode. I found the best way to do that was to wrap the format's I provided early escape hatches and also ensured it doesn't filter the component if the experiment isn't active. I updated the description and you can follow the testing instructions there to get a feel for how this works. The result is the same as per the original implementation, but just with the benefit that extenders can choose to mark their formats as essential to force them to show in Write Mode (under the dropdown). |
What if |
@getdave, what do you think about handling all related filtering inside the @jeryj, that might be more on a higher level, but it would be hard to control what Example patch, still requires full removal of `withWriteModeFilter`
diff --git a/packages/block-editor/src/components/rich-text/format-edit.js b/packages/block-editor/src/components/rich-text/format-edit.js
index 20d13c1d3f..a70b9f8f77 100644
--- a/packages/block-editor/src/components/rich-text/format-edit.js
+++ b/packages/block-editor/src/components/rich-text/format-edit.js
@@ -8,7 +8,6 @@ import { useContext, useMemo } from '@wordpress/element';
* Internal dependencies
*/
import BlockContext from '../block-context';
-import withWriteModeFilter from './with-write-mode-filter';
const DEFAULT_BLOCK_CONTEXT = {};
@@ -38,9 +37,6 @@ function Edit( { onChange, onFocus, value, forwardedRef, settings } ) {
return null;
}
- // Apply the write mode filter HOC
- const FilteredEditFunction = withWriteModeFilter( EditFunction, settings );
-
const activeFormat = getActiveFormat( value, name );
const isActive = activeFormat !== undefined;
const activeObject = getActiveObject( value );
@@ -48,7 +44,7 @@ function Edit( { onChange, onFocus, value, forwardedRef, settings } ) {
activeObject !== undefined && activeObject.type === name;
return (
- <FilteredEditFunction
+ <EditFunction
key={ name }
isActive={ isActive }
activeAttributes={ isActive ? activeFormat.attributes || {} : {} }
diff --git a/packages/block-editor/src/components/rich-text/index.js b/packages/block-editor/src/components/rich-text/index.js
index 768ffbb0cd..bd831558f7 100644
--- a/packages/block-editor/src/components/rich-text/index.js
+++ b/packages/block-editor/src/components/rich-text/index.js
@@ -132,8 +132,12 @@ export function RichTextWrapper(
return { isSelected: false };
}
- const { getSelectionStart, getSelectionEnd } =
- select( blockEditorStore );
+ const {
+ getSelectionStart,
+ getSelectionEnd,
+ getBlockEditingMode,
+ isNavigationMode,
+ } = select( blockEditorStore );
const selectionStart = getSelectionStart();
const selectionEnd = getSelectionEnd();
@@ -154,15 +158,21 @@ export function RichTextWrapper(
selectionStart: isSelected ? selectionStart.offset : undefined,
selectionEnd: isSelected ? selectionEnd.offset : undefined,
isSelected,
+ isWriteMode:
+ isNavigationMode() &&
+ getBlockEditingMode( clientId ) === 'contentOnly',
};
};
- const { selectionStart, selectionEnd, isSelected } = useSelect( selector, [
- clientId,
- identifier,
- instanceId,
- originalIsSelected,
- isBlockSelected,
- ] );
+ const { selectionStart, selectionEnd, isSelected, isWriteMode } = useSelect(
+ selector,
+ [
+ clientId,
+ identifier,
+ instanceId,
+ originalIsSelected,
+ isBlockSelected,
+ ]
+ );
const { disableBoundBlock, bindingsPlaceholder, bindingsLabel } = useSelect(
( select ) => {
@@ -323,8 +333,9 @@ export function RichTextWrapper(
} = useFormatTypes( {
clientId,
identifier,
- withoutInteractiveFormatting,
allowedFormats: adjustedAllowedFormats,
+ withoutInteractiveFormatting,
+ disableNoneEssentialFormatting: isWriteMode,
} );
function addEditorOnlyFormats( value ) {
diff --git a/packages/block-editor/src/components/rich-text/use-format-types.js b/packages/block-editor/src/components/rich-text/use-format-types.js
index 0bbebbf262..fcb58a852c 100644
--- a/packages/block-editor/src/components/rich-text/use-format-types.js
+++ b/packages/block-editor/src/components/rich-text/use-format-types.js
@@ -56,35 +56,48 @@ function getPrefixedSelectKeys( selected, prefix ) {
* This hook provides RichText with the `formatTypes` and its derived props from
* experimental format type settings.
*
- * @param {Object} $0 Options
- * @param {string} $0.clientId Block client ID.
- * @param {string} $0.identifier Block attribute.
- * @param {boolean} $0.withoutInteractiveFormatting Whether to clean the interactive formatting or not.
- * @param {Array} $0.allowedFormats Allowed formats
+ * @param {Object} options Options
+ * @param {string} options.clientId Block client ID.
+ * @param {string} options.identifier Block attribute.
+ * @param {Array} options.allowedFormats Allowed formats
+ * @param {boolean} options.withoutInteractiveFormatting Whether to clean the interactive formatting or not.
+ * @param {boolean} options.disableNoneEssentialFormatting Whether to disable none-essential formatting or not.
*/
export function useFormatTypes( {
clientId,
identifier,
- withoutInteractiveFormatting,
allowedFormats,
+ withoutInteractiveFormatting,
+ disableNoneEssentialFormatting = false,
} ) {
const allFormatTypes = useSelect( formatTypesSelector, [] );
const formatTypes = useMemo( () => {
- return allFormatTypes.filter( ( { name, interactive, tagName } ) => {
- if ( allowedFormats && ! allowedFormats.includes( name ) ) {
- return false;
- }
+ return allFormatTypes.filter(
+ ( { name, interactive, tagName, __unstableEssential } ) => {
+ if ( allowedFormats && ! allowedFormats.includes( name ) ) {
+ return false;
+ }
- if (
- withoutInteractiveFormatting &&
- ( interactive || interactiveContentTags.has( tagName ) )
- ) {
- return false;
- }
+ if ( disableNoneEssentialFormatting && ! __unstableEssential ) {
+ return false;
+ }
+
+ if (
+ withoutInteractiveFormatting &&
+ ( interactive || interactiveContentTags.has( tagName ) )
+ ) {
+ return false;
+ }
- return true;
- } );
- }, [ allFormatTypes, allowedFormats, withoutInteractiveFormatting ] );
+ return true;
+ }
+ );
+ }, [
+ allFormatTypes,
+ allowedFormats,
+ disableNoneEssentialFormatting,
+ withoutInteractiveFormatting,
+ ] );
const keyedSelected = useSelect(
( select ) =>
formatTypes.reduce( ( accumulator, type ) => {
|
- Remove withWriteModeFilter HOC approach in favor of centralized filtering - Add Write Mode detection to RichText component with experiment flag check - Enhance useFormatTypes hook with disableNoneEssentialFormatting parameter - Centralize all format filtering logic in one place for better performance - Eliminate individual store subscriptions per format component - Maintain same functionality with improved architecture and performance
I looked at this yesterday and dismissed it, but looking again it seems ideal. I'll make some adjustments. |
- isNavigationMode() already checks window.__experimentalEditorWriteMode internally - Removes duplicate experiment flag check for cleaner, more efficient code - Maintains same functionality while following existing codebase patterns
- Add essentialFormatKey to private-keys.js for internal Core usage - Expose essentialFormatKey in private-apis.js for Core packages - Update use-format-types.js to use private Symbol instead of __unstableEssential - Update format library packages (bold, italic, link) to use private Symbol - Follow correct import pattern: unlock from lock-unlock.js + privateApis from block-editor - Remove unstable API prefix as per Core guidelines
- More accurately reflects that we're checking both Write Mode and content-only editing - Makes the dual condition check explicit: Write Mode + content-only block editing - Improves code readability and maintainability - Clarifies the relationship between Write Mode and content editing mode
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the follow-ups, @getdave!
The changes work as advertised.
originalIsSelected, | ||
isBlockSelected, | ||
] ); | ||
const { selectionStart, selectionEnd, isSelected, isContentOnlyWriteMode } = |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I know you didn't introduce it in this PR, just wondering if there's a reason why selector
is passed as a const
here rather than just doing it inline like we usually do?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not sure. Seems odd.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looking great, thank you!
(I had to add privateApis as blockEditorPrivateApis,
to the @wordpress/block-editor
import, to get strikethrough to show up)
WP 6.9 note - Stabilization NeededThe This would enable plugins to provide essential formatting options that integrate seamlessly with the Write Mode experience. Note that this PR implements the feature as private via a Symbol. |
@Mamaduka @scruffian @jeryj @talldan I've been thinking about PRs like this one which alter the UI based on a combo of
I'm thinking that actually we don't need the "is write mode" check at all as blocks marked as To me "write mode" should just dictate the state of blocks (e.g. contentOnly, disabled...etc). The way those blocks actually behave (e.g. restricted UI...etc) in those states is up to them. I'd value your thoughts on:
If I'm correct then I'll raise PRs to update the recent implementations I've made. |
You might need to check whether a block is a |
What
Restricts RichText formatting controls when in write mode (contentOnly) to provide a simpler, more focused editing experience while maintaining essential content functionality. Introduces a private
essentialFormatKey
Symbol for format registration to distinguish between essential and non-essential formats.Why
Write mode should be as simple as possible to help users focus on content creation without distractions from advanced formatting options. Currently, users see all formatting controls including strikethrough, highlight, and other advanced options even in write mode. However, links are essential content functionality and should remain available in Write Mode, especially for Navigation blocks.
How
useFormatTypes
hook to filter formats based on Write Mode and essential statusessentialFormatKey
Symbol for format registration (using WordPress Core's private API pattern)[ essentialFormatKey ]: true
in their registrationwindow.__experimentalEditorWriteMode
is false ensures minimal overheadTesting
Testing Format Registration
You can test the essential format registration by applying the following diff to make strikethrough essential:
After applying this diff:
Screenshots
Breaking Changes
None - this is a UI enhancement that maintains all existing functionality in regular editing mode.
Performance Considerations
useFormatTypes
hookAPI Considerations
essentialFormatKey
is a private Symbol following WordPress Core's private API patternRelated
This change supports the Navigation Block Write Mode improvements (see #70977) where navigation blocks need to be editable in Write Mode with link functionality available.