From 556f408026710c880f1eb6749a5a5637357633ae Mon Sep 17 00:00:00 2001 From: Kyle McLean Date: Thu, 1 Dec 2022 04:23:27 -0700 Subject: [PATCH 1/7] Use stylesheet to give Editable components a default style --- .../slate-react/src/components/editable.tsx | 46 ++++++++++++++----- 1 file changed, 34 insertions(+), 12 deletions(-) diff --git a/packages/slate-react/src/components/editable.tsx b/packages/slate-react/src/components/editable.tsx index e4f202fd2d..3ed9f4b8ef 100644 --- a/packages/slate-react/src/components/editable.tsx +++ b/packages/slate-react/src/components/editable.tsx @@ -76,6 +76,9 @@ const Children = (props: Parameters[0]) => ( {useChildren(props)} ) +// The number of Editable components currently mounted. +let mountedCount = 0 + /** * `RenderElementProps` are passed to the `renderElement` handler. */ @@ -802,6 +805,36 @@ export const Editable = (props: EditableProps) => { }) }) + useEffect(() => { + mountedCount++ + + if (mountedCount === 1) { + // Set global default styles for editors. + const defaultStylesElement = document.createElement('style') + defaultStylesElement.setAttribute('data-slate-default-styles', 'true') + defaultStylesElement.innerHTML = + // :where is used to give these rules lower specificity so user stylesheets can override them. + `:where([data-slate-editor]) {` + + // Allow positioning relative to the editable element. + `position: relative;` + + // Prevent the default outline styles. + `outline: none;` + + // Preserve adjacent whitespace and new lines. + `white-space: pre-wrap;` + + // Allow words to break if they are too long. + `word-wrap: break-word;` + + `}` + document.head.appendChild(defaultStylesElement) + } + + return () => { + mountedCount-- + + if (mountedCount <= 0) + document.querySelector('style[data-slate-default-styles]')?.remove() + } + }, []) + return ( @@ -840,18 +873,7 @@ export const Editable = (props: EditableProps) => { zindex={-1} suppressContentEditableWarning ref={ref} - style={{ - // Allow positioning relative to the editable element. - position: 'relative', - // Prevent the default outline styles. - outline: 'none', - // Preserve adjacent whitespace and new lines. - whiteSpace: 'pre-wrap', - // Allow words to break if they are too long. - wordWrap: 'break-word', - // Allow for passed-in styles to override anything. - ...style, - }} + style={style} onBeforeInput={useCallback( (event: React.FormEvent) => { // COMPAT: Certain browsers don't support the `beforeinput` event, so we From 7a3de63327fdef8a2fb64444442b7d448c364623 Mon Sep 17 00:00:00 2001 From: Kyle McLean Date: Thu, 1 Dec 2022 04:24:56 -0700 Subject: [PATCH 2/7] Give Editors a unique id --- packages/slate-react/src/components/editable.tsx | 1 + packages/slate/src/create-editor.ts | 3 +++ packages/slate/src/interfaces/editor.ts | 1 + 3 files changed, 5 insertions(+) diff --git a/packages/slate-react/src/components/editable.tsx b/packages/slate-react/src/components/editable.tsx index 3ed9f4b8ef..eb2347af6c 100644 --- a/packages/slate-react/src/components/editable.tsx +++ b/packages/slate-react/src/components/editable.tsx @@ -864,6 +864,7 @@ export const Editable = (props: EditableProps) => { : 'false' } data-slate-editor + data-slate-editor-id={editor.id} data-slate-node="value" // explicitly set this contentEditable={!readOnly} diff --git a/packages/slate/src/create-editor.ts b/packages/slate/src/create-editor.ts index b2cd9f512b..874e41dcf9 100644 --- a/packages/slate/src/create-editor.ts +++ b/packages/slate/src/create-editor.ts @@ -16,6 +16,8 @@ import { import { DIRTY_PATHS, DIRTY_PATH_KEYS, FLUSHING } from './utils/weak-maps' import { TextUnit } from './interfaces/types' +let nextEditorId = 0 + /** * Create a new Slate `Editor` object. */ @@ -26,6 +28,7 @@ export const createEditor = (): Editor => { operations: [], selection: null, marks: null, + id: nextEditorId++, isInline: () => false, isVoid: () => false, markableVoid: () => false, diff --git a/packages/slate/src/interfaces/editor.ts b/packages/slate/src/interfaces/editor.ts index 9d620b95e2..432cba21c9 100644 --- a/packages/slate/src/interfaces/editor.ts +++ b/packages/slate/src/interfaces/editor.ts @@ -57,6 +57,7 @@ export interface BaseEditor { selection: Selection operations: Operation[] marks: EditorMarks | null + readonly id: number // Schema-specific node behaviors. isInline: (element: Element) => boolean From e0b96dd9778b770e73947e5aca3eb17e9d88787f Mon Sep 17 00:00:00 2001 From: Kyle McLean Date: Thu, 1 Dec 2022 04:35:35 -0700 Subject: [PATCH 3/7] Use per-editor stylesheets to give editors a min-height --- packages/slate-react/src/components/editable.tsx | 11 +++++++++++ packages/slate-react/src/components/leaf.tsx | 10 ++++++++-- packages/slate-react/src/utils/weak-maps.ts | 4 ++++ 3 files changed, 23 insertions(+), 2 deletions(-) diff --git a/packages/slate-react/src/components/editable.tsx b/packages/slate-react/src/components/editable.tsx index eb2347af6c..80e0c30f11 100644 --- a/packages/slate-react/src/components/editable.tsx +++ b/packages/slate-react/src/components/editable.tsx @@ -55,6 +55,7 @@ import { EDITOR_TO_ELEMENT, EDITOR_TO_FORCE_RENDER, EDITOR_TO_PENDING_INSERTION_MARKS, + EDITOR_TO_STYLE_ELEMENT, EDITOR_TO_USER_MARKS, EDITOR_TO_USER_SELECTION, EDITOR_TO_WINDOW, @@ -835,6 +836,16 @@ export const Editable = (props: EditableProps) => { } }, []) + useEffect(() => { + const styleElement = document.createElement('style') + document.head.appendChild(styleElement) + EDITOR_TO_STYLE_ELEMENT.set(editor, styleElement) + return () => { + styleElement.remove() + EDITOR_TO_STYLE_ELEMENT.delete(editor) + } + }, []) + return ( diff --git a/packages/slate-react/src/components/leaf.tsx b/packages/slate-react/src/components/leaf.tsx index ef6ebf6490..d59c98ff2f 100644 --- a/packages/slate-react/src/components/leaf.tsx +++ b/packages/slate-react/src/components/leaf.tsx @@ -4,6 +4,7 @@ import String from './string' import { PLACEHOLDER_SYMBOL, EDITOR_TO_PLACEHOLDER_ELEMENT, + EDITOR_TO_STYLE_ELEMENT, } from '../utils/weak-maps' import { RenderLeafProps, RenderPlaceholderProps } from './editable' import { useSlateStatic } from '../hooks/use-slate-static' @@ -41,12 +42,17 @@ const Leaf = (props: { return } - editorEl.style.minHeight = `${placeholderEl.clientHeight}px` EDITOR_TO_PLACEHOLDER_ELEMENT.set(editor, placeholderEl) + const styleElement = EDITOR_TO_STYLE_ELEMENT.get(editor) + if (styleElement) { + styleElement.innerHTML = + `:where([data-slate-editor-id="${editor.id}"]) { min-height: ${minHeight}; }` + } return () => { - editorEl.style.minHeight = 'auto' EDITOR_TO_PLACEHOLDER_ELEMENT.delete(editor) + const styleElement = EDITOR_TO_STYLE_ELEMENT.get(editor) + if (styleElement) styleElement.innerHTML = '' } }, [placeholderRef, leaf]) diff --git a/packages/slate-react/src/utils/weak-maps.ts b/packages/slate-react/src/utils/weak-maps.ts index 8834d77783..7634ea9983 100644 --- a/packages/slate-react/src/utils/weak-maps.ts +++ b/packages/slate-react/src/utils/weak-maps.ts @@ -29,6 +29,10 @@ export const EDITOR_TO_KEY_TO_ELEMENT: WeakMap< Editor, WeakMap > = new WeakMap() +export const EDITOR_TO_STYLE_ELEMENT: WeakMap< + Editor, + HTMLStyleElement +> = new WeakMap() /** * Weak maps for storing editor-related state. From 4bce9f0d71af8be7992c76a8156d35b2fbeaa967 Mon Sep 17 00:00:00 2001 From: Kyle McLean Date: Thu, 1 Dec 2022 04:37:33 -0700 Subject: [PATCH 4/7] Make editor min-height respond to changes in placeholder height --- packages/slate-react/package.json | 1 + packages/slate-react/src/components/leaf.tsx | 30 +++++++++++++++----- packages/slate-react/test/index.spec.tsx | 8 ++++++ 3 files changed, 32 insertions(+), 7 deletions(-) diff --git a/packages/slate-react/package.json b/packages/slate-react/package.json index c375e02c15..85c2be7850 100644 --- a/packages/slate-react/package.json +++ b/packages/slate-react/package.json @@ -30,6 +30,7 @@ "@types/react": "^16.9.13", "@types/react-dom": "^16.9.4", "@types/react-test-renderer": "^16.8.0", + "@types/resize-observer-browser": "^0.1.7", "react": ">=16.8.0", "react-dom": ">=16.8.0", "react-test-renderer": ">=16.8.0", diff --git a/packages/slate-react/src/components/leaf.tsx b/packages/slate-react/src/components/leaf.tsx index d59c98ff2f..ba23089fe8 100644 --- a/packages/slate-react/src/components/leaf.tsx +++ b/packages/slate-react/src/components/leaf.tsx @@ -1,4 +1,4 @@ -import React, { useRef, useEffect } from 'react' +import React, { useRef, useEffect, useMemo } from 'react' import { Element, Text } from 'slate' import String from './string' import { @@ -34,6 +34,19 @@ const Leaf = (props: { const placeholderRef = useRef(null) const editor = useSlateStatic() + const placeholderResizeObserver = useMemo( + () => + new ResizeObserver(([{ target }]) => { + const styleElement = EDITOR_TO_STYLE_ELEMENT.get(editor) + if (!styleElement) return + + // Make the min-height the height of the placeholder. + const minHeight = `${target.clientHeight}px` + styleElement.innerHTML = `:where([data-slate-editor-id="${editor.id}"]) { min-height: ${minHeight}; }` + }), + [] + ) + useEffect(() => { const placeholderEl = placeholderRef?.current const editorEl = ReactEditor.toDOMNode(editor, editor) @@ -42,17 +55,20 @@ const Leaf = (props: { return } - EDITOR_TO_PLACEHOLDER_ELEMENT.set(editor, placeholderEl) - const styleElement = EDITOR_TO_STYLE_ELEMENT.get(editor) - if (styleElement) { - styleElement.innerHTML = - `:where([data-slate-editor-id="${editor.id}"]) { min-height: ${minHeight}; }` + if (placeholderEl !== EDITOR_TO_PLACEHOLDER_ELEMENT.get(editor)) { + EDITOR_TO_PLACEHOLDER_ELEMENT.set(editor, placeholderEl) + placeholderResizeObserver.disconnect() + placeholderResizeObserver.observe(placeholderEl) } return () => { EDITOR_TO_PLACEHOLDER_ELEMENT.delete(editor) + placeholderResizeObserver.disconnect() const styleElement = EDITOR_TO_STYLE_ELEMENT.get(editor) - if (styleElement) styleElement.innerHTML = '' + if (styleElement) { + // No min-height if there is no placeholder. + styleElement.innerHTML = '' + } } }, [placeholderRef, leaf]) diff --git a/packages/slate-react/test/index.spec.tsx b/packages/slate-react/test/index.spec.tsx index 0270c9b143..a226ea173a 100644 --- a/packages/slate-react/test/index.spec.tsx +++ b/packages/slate-react/test/index.spec.tsx @@ -8,7 +8,15 @@ const createNodeMock = () => ({ getRootNode: () => global.document, }) +class MockResizeObserver { + observe() {} + unobserve() {} + disconnect() {} +} + describe('slate-react', () => { + window.ResizeObserver = MockResizeObserver as any + describe('Editable', () => { describe('NODE_TO_KEY logic', () => { it('should not unmount the node that gets split on a split_node operation', async () => { From 474f26e55a002fb3b16ff1b36f88ece2c145cc41 Mon Sep 17 00:00:00 2001 From: Kyle McLean Date: Thu, 1 Dec 2022 04:38:59 -0700 Subject: [PATCH 5/7] Add changeset for stylesheet changes --- .changeset/strange-pens-lie.md | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .changeset/strange-pens-lie.md diff --git a/.changeset/strange-pens-lie.md b/.changeset/strange-pens-lie.md new file mode 100644 index 0000000000..94101c85e3 --- /dev/null +++ b/.changeset/strange-pens-lie.md @@ -0,0 +1,6 @@ +--- +'slate': minor +'slate-react': minor +--- + +Use stylesheet for default styles on Editable components From 4361432483ff735114924623d0a8682c645631f1 Mon Sep 17 00:00:00 2001 From: Kyle McLean Date: Thu, 1 Dec 2022 05:56:30 -0700 Subject: [PATCH 6/7] Prevent unnecessary creations of ResizeObservers --- packages/slate-react/src/components/leaf.tsx | 59 ++++++++++++-------- 1 file changed, 36 insertions(+), 23 deletions(-) diff --git a/packages/slate-react/src/components/leaf.tsx b/packages/slate-react/src/components/leaf.tsx index ba23089fe8..823a56c9b7 100644 --- a/packages/slate-react/src/components/leaf.tsx +++ b/packages/slate-react/src/components/leaf.tsx @@ -1,4 +1,4 @@ -import React, { useRef, useEffect, useMemo } from 'react' +import React, { useRef, useEffect } from 'react' import { Element, Text } from 'slate' import String from './string' import { @@ -8,7 +8,6 @@ import { } from '../utils/weak-maps' import { RenderLeafProps, RenderPlaceholderProps } from './editable' import { useSlateStatic } from '../hooks/use-slate-static' -import { ReactEditor } from '..' /** * Individual leaves in a text node with unique formatting. @@ -34,42 +33,56 @@ const Leaf = (props: { const placeholderRef = useRef(null) const editor = useSlateStatic() - const placeholderResizeObserver = useMemo( - () => - new ResizeObserver(([{ target }]) => { - const styleElement = EDITOR_TO_STYLE_ELEMENT.get(editor) - if (!styleElement) return + const placeholderResizeObserver = useRef(null) - // Make the min-height the height of the placeholder. - const minHeight = `${target.clientHeight}px` - styleElement.innerHTML = `:where([data-slate-editor-id="${editor.id}"]) { min-height: ${minHeight}; }` - }), - [] - ) + useEffect(() => { + return () => { + if (placeholderResizeObserver.current) { + placeholderResizeObserver.current.disconnect() + } + } + }, []) useEffect(() => { const placeholderEl = placeholderRef?.current - const editorEl = ReactEditor.toDOMNode(editor, editor) - if (!placeholderEl || !editorEl) { - return + if (placeholderEl) { + EDITOR_TO_PLACEHOLDER_ELEMENT.set(editor, placeholderEl) + } else { + EDITOR_TO_PLACEHOLDER_ELEMENT.delete(editor) } - if (placeholderEl !== EDITOR_TO_PLACEHOLDER_ELEMENT.get(editor)) { - EDITOR_TO_PLACEHOLDER_ELEMENT.set(editor, placeholderEl) - placeholderResizeObserver.disconnect() - placeholderResizeObserver.observe(placeholderEl) + if (placeholderResizeObserver.current) { + // Update existing observer. + placeholderResizeObserver.current.disconnect() + if (placeholderEl) + placeholderResizeObserver.current.observe(placeholderEl) + } else if (placeholderEl) { + // Create a new observer and observe the placeholder element. + placeholderResizeObserver.current = new ResizeObserver(([{ target }]) => { + const styleElement = EDITOR_TO_STYLE_ELEMENT.get(editor) + if (styleElement) { + // Make the min-height the height of the placeholder. + const minHeight = `${target.clientHeight}px` + styleElement.innerHTML = `:where([data-slate-editor-id="${editor.id}"]) { min-height: ${minHeight}; }` + } + }) + + placeholderResizeObserver.current.observe(placeholderEl) } - return () => { - EDITOR_TO_PLACEHOLDER_ELEMENT.delete(editor) - placeholderResizeObserver.disconnect() + if (!placeholderEl) { + // No placeholder element, so no need for a resize observer. const styleElement = EDITOR_TO_STYLE_ELEMENT.get(editor) if (styleElement) { // No min-height if there is no placeholder. styleElement.innerHTML = '' } } + + return () => { + EDITOR_TO_PLACEHOLDER_ELEMENT.delete(editor) + } }, [placeholderRef, leaf]) let children = ( From d04fcca518d6b51c78b928e715ae95ab051f6c1b Mon Sep 17 00:00:00 2001 From: Kyle McLean Date: Sat, 3 Dec 2022 07:11:41 -0700 Subject: [PATCH 7/7] Update yarn.lock --- yarn.lock | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/yarn.lock b/yarn.lock index b579292e8c..c8ac0a9aea 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3739,6 +3739,13 @@ __metadata: languageName: node linkType: hard +"@types/resize-observer-browser@npm:^0.1.7": + version: 0.1.7 + resolution: "@types/resize-observer-browser@npm:0.1.7" + checksum: 0377eaac8bb7a17b983b49a156006032380b459bfebefc54a5aa2f7f8a9786d2b60723e8837c61ef733330b478f4f26293e9edbdc8006238e4f80c878c56c988 + languageName: node + linkType: hard + "@types/resolve@npm:0.0.8": version: 0.0.8 resolution: "@types/resolve@npm:0.0.8" @@ -14235,6 +14242,7 @@ resolve@^2.0.0-next.3: "@types/react": ^16.9.13 "@types/react-dom": ^16.9.4 "@types/react-test-renderer": ^16.8.0 + "@types/resize-observer-browser": ^0.1.7 direction: ^1.0.3 is-hotkey: ^0.1.6 is-plain-object: ^5.0.0