From 0e7c8cbb4cfa895727e34acab44379c802b23f81 Mon Sep 17 00:00:00 2001 From: Kyle McLean Date: Tue, 31 Jan 2023 14:00:32 -0700 Subject: [PATCH 1/5] Switch back to using inline styles for default editor styles --- .../slate-react/src/components/editable.tsx | 74 +++++++------------ packages/slate-react/src/components/leaf.tsx | 30 ++++---- packages/slate-react/src/utils/weak-maps.ts | 4 - .../src/utils/where-if-supported.ts | 22 ------ packages/slate/src/create-editor.ts | 3 - packages/slate/src/interfaces/editor.ts | 1 - 6 files changed, 40 insertions(+), 94 deletions(-) delete mode 100644 packages/slate-react/src/utils/where-if-supported.ts diff --git a/packages/slate-react/src/components/editable.tsx b/packages/slate-react/src/components/editable.tsx index adad6b3dd6..42886a5402 100644 --- a/packages/slate-react/src/components/editable.tsx +++ b/packages/slate-react/src/components/editable.tsx @@ -54,7 +54,7 @@ import { EDITOR_TO_ELEMENT, EDITOR_TO_FORCE_RENDER, EDITOR_TO_PENDING_INSERTION_MARKS, - EDITOR_TO_STYLE_ELEMENT, + EDITOR_TO_PLACEHOLDER_ELEMENT, EDITOR_TO_USER_MARKS, EDITOR_TO_USER_SELECTION, EDITOR_TO_WINDOW, @@ -66,7 +66,6 @@ import { NODE_TO_ELEMENT, PLACEHOLDER_SYMBOL, } from '../utils/weak-maps' -import { whereIfSupported } from '../utils/where-if-supported' import { RestoreDOM } from './restore-dom/restore-dom' import { useAndroidInputManager } from '../hooks/android-input-manager/use-android-input-manager' import { useTrackUserInput } from '../hooks/use-track-user-input' @@ -77,9 +76,6 @@ const Children = (props: Parameters[0]) => ( {useChildren(props)} ) -// The number of Editable components currently mounted. -let mountedCount = 0 - /** * `RenderElementProps` are passed to the `renderElement` handler. */ @@ -125,6 +121,7 @@ export type EditableProps = { renderPlaceholder?: (props: RenderPlaceholderProps) => JSX.Element scrollSelectionIntoView?: (editor: ReactEditor, domRange: DOMRange) => void as?: React.ElementType + disableDefaultStyles?: boolean } & React.TextareaHTMLAttributes /** @@ -142,8 +139,9 @@ export const Editable = (props: EditableProps) => { renderLeaf, renderPlaceholder = props => , scrollSelectionIntoView = defaultScrollSelectionIntoView, - style = {}, + style: userStyle = {}, as: Component = 'div', + disableDefaultStyles = false, ...attributes } = props const editor = useSlate() @@ -806,45 +804,9 @@ 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') - const selector = '[data-slate-editor]' - const defaultStyles = - // 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;` - defaultStylesElement.innerHTML = whereIfSupported(selector, defaultStyles) - - document.head.appendChild(defaultStylesElement) - } - - return () => { - mountedCount-- - - if (mountedCount <= 0) - document.querySelector('style[data-slate-default-styles]')?.remove() - } - }, []) - - 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) - } - }, []) + const placeholderHeight = EDITOR_TO_PLACEHOLDER_ELEMENT.get( + editor + )?.getBoundingClientRect()?.height return ( @@ -875,7 +837,6 @@ export const Editable = (props: EditableProps) => { : 'false' } data-slate-editor - data-slate-editor-id={editor.id} data-slate-node="value" // explicitly set this contentEditable={!readOnly} @@ -885,7 +846,26 @@ export const Editable = (props: EditableProps) => { zindex={-1} suppressContentEditableWarning ref={ref} - style={style} + style={{ + ...(disableDefaultStyles + ? {} + : { + // 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', + // Make the minimum height that of the placeholder. + ...(placeholderHeight + ? { minHeight: placeholderHeight } + : {}), + }), + // Allow for passed-in styles to override anything. + ...userStyle, + }} onBeforeInput={useCallback( (event: React.FormEvent) => { // COMPAT: Certain browsers don't support the `beforeinput` event, so we diff --git a/packages/slate-react/src/components/leaf.tsx b/packages/slate-react/src/components/leaf.tsx index bdf2c55757..cdae1cd1ac 100644 --- a/packages/slate-react/src/components/leaf.tsx +++ b/packages/slate-react/src/components/leaf.tsx @@ -5,11 +5,10 @@ import String from './string' import { PLACEHOLDER_SYMBOL, EDITOR_TO_PLACEHOLDER_ELEMENT, - EDITOR_TO_STYLE_ELEMENT, + EDITOR_TO_FORCE_RENDER, } from '../utils/weak-maps' import { RenderLeafProps, RenderPlaceholderProps } from './editable' import { useSlateStatic } from '../hooks/use-slate-static' -import { whereIfSupported } from '../utils/where-if-supported' /** * Individual leaves in a text node with unique formatting. @@ -32,6 +31,7 @@ const Leaf = (props: { renderLeaf = (props: RenderLeafProps) => , } = props + const lastPlaceholderRef = useRef(null) const placeholderRef = useRef(null) const editor = useSlateStatic() @@ -62,28 +62,24 @@ const Leaf = (props: { } else if (placeholderEl) { // Create a new observer and observe the placeholder element. const ResizeObserver = window.ResizeObserver || ResizeObserverPolyfill - 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 selector = `[data-slate-editor-id="${editor.id}"]` - const styles = `min-height: ${target.clientHeight}px;` - styleElement.innerHTML = whereIfSupported(selector, styles) - } + placeholderResizeObserver.current = new ResizeObserver(() => { + // Force a re-render of the editor so its min-height can be updated + // to the new height of the placeholder. + const forceRender = EDITOR_TO_FORCE_RENDER.get(editor) + forceRender?.() }) - placeholderResizeObserver.current.observe(placeholderEl) } - if (!placeholderEl) { + if (!placeholderEl && lastPlaceholderRef.current) { // 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 = '' - } + // Force a re-render of the editor so its min-height can be reset. + const forceRender = EDITOR_TO_FORCE_RENDER.get(editor) + forceRender?.() } + lastPlaceholderRef.current = placeholderRef.current + return () => { EDITOR_TO_PLACEHOLDER_ELEMENT.delete(editor) } diff --git a/packages/slate-react/src/utils/weak-maps.ts b/packages/slate-react/src/utils/weak-maps.ts index 7634ea9983..8834d77783 100644 --- a/packages/slate-react/src/utils/weak-maps.ts +++ b/packages/slate-react/src/utils/weak-maps.ts @@ -29,10 +29,6 @@ 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. diff --git a/packages/slate-react/src/utils/where-if-supported.ts b/packages/slate-react/src/utils/where-if-supported.ts deleted file mode 100644 index df6ae7b72b..0000000000 --- a/packages/slate-react/src/utils/where-if-supported.ts +++ /dev/null @@ -1,22 +0,0 @@ -/** - * Returns a set of rules that use the `:where` selector if it is supported, - * otherwise it falls back to the provided selector on its own. - * - * The `:where` selector is used to give a selector a lower specificity, - * allowing the rule to be overridden by a user-defined stylesheet. - * - * Older browsers do not support the `:where` selector. - * If it is not supported, the selector will be used without `:where`, - * which means that the rule will have a higher specificity and a user-defined - * stylesheet will not be able to override it easily. - */ -export function whereIfSupported(selector: string, styles: string): string { - return ( - `@supports (selector(:where(${selector}))) {` + - `:where(${selector}) { ${styles} }` + - `}` + - `@supports not (selector(:where(${selector}))) {` + - `${selector} { ${styles} }` + - `}` - ) -} diff --git a/packages/slate/src/create-editor.ts b/packages/slate/src/create-editor.ts index 874e41dcf9..b2cd9f512b 100644 --- a/packages/slate/src/create-editor.ts +++ b/packages/slate/src/create-editor.ts @@ -16,8 +16,6 @@ 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. */ @@ -28,7 +26,6 @@ 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 274e96bfc4..64d1aa852b 100644 --- a/packages/slate/src/interfaces/editor.ts +++ b/packages/slate/src/interfaces/editor.ts @@ -57,7 +57,6 @@ export interface BaseEditor { selection: Selection operations: Operation[] marks: EditorMarks | null - readonly id: number // Schema-specific node behaviors. isInline: (element: Element) => boolean From 5dfea1dd27244a0cafc9c6381629a4bce93bd97f Mon Sep 17 00:00:00 2001 From: Kyle McLean Date: Tue, 31 Jan 2023 14:01:55 -0700 Subject: [PATCH 2/5] Add example page and test for editor styling --- .../integration/examples/styling.test.ts | 113 ++++++++++++++++++ site/examples/styling.tsx | 47 ++++++++ site/pages/examples/[example].tsx | 2 + site/public/index.css | 11 ++ 4 files changed, 173 insertions(+) create mode 100644 playwright/integration/examples/styling.test.ts create mode 100644 site/examples/styling.tsx diff --git a/playwright/integration/examples/styling.test.ts b/playwright/integration/examples/styling.test.ts new file mode 100644 index 0000000000..2278771819 --- /dev/null +++ b/playwright/integration/examples/styling.test.ts @@ -0,0 +1,113 @@ +import { test, expect } from '@playwright/test' + +test.describe('styling example', () => { + test.beforeEach( + async ({ page }) => + await page.goto('http://localhost:3000/examples/styling') + ) + + test('applies styles to editor from style prop', async ({ page }) => { + page.waitForLoadState('domcontentloaded') + + const editor = page.locator('[data-slate-editor=true]').nth(0) + const styles = await editor.evaluate(el => { + const { + backgroundColor, + minHeight, + outlineWidth, + outlineStyle, + outlineColor, + position, + whiteSpace, + wordWrap, + } = window.getComputedStyle(el) + return { + backgroundColor, + minHeight, + outlineWidth, + outlineStyle, + outlineColor, + position, + whiteSpace, + wordWrap, + } + }) + + // Provided styles + expect(styles.backgroundColor).toBe('rgb(255, 230, 156)') + expect(styles.minHeight).toBe('200px') + expect(styles.outlineWidth).toBe('2px') + expect(styles.outlineStyle).toBe('solid') + expect(styles.outlineColor).toBe('rgb(0, 128, 0)') + + // Default styles + expect(styles.position).toBe('relative') + expect(styles.whiteSpace).toBe('pre-wrap') + expect(styles.wordWrap).toBe('break-word') + }) + + test('applies styles to editor from className prop', async ({ page }) => { + page.waitForLoadState('domcontentloaded') + + const editor = page.locator('[data-slate-editor=true]').nth(1) + const styles = await editor.evaluate(el => { + const { + backgroundColor, + paddingTop, + paddingRight, + paddingBottom, + paddingLeft, + fontSize, + minHeight, + outlineWidth, + outlineStyle, + outlineColor, + borderTopLeftRadius, + borderTopRightRadius, + borderBottomRightRadius, + borderBottomLeftRadius, + outlineOffset, + position, + whiteSpace, + wordWrap, + } = window.getComputedStyle(el) + return { + backgroundColor, + paddingTop, + paddingRight, + paddingBottom, + paddingLeft, + fontSize, + minHeight, + outlineWidth, + outlineStyle, + outlineColor, + borderTopLeftRadius, + borderTopRightRadius, + borderBottomRightRadius, + borderBottomLeftRadius, + outlineOffset, + position, + whiteSpace, + wordWrap, + } + }) + + expect(styles.backgroundColor).toBe('rgb(218, 225, 255)') + expect(styles.paddingTop).toBe('40px') + expect(styles.paddingRight).toBe('40px') + expect(styles.paddingBottom).toBe('40px') + expect(styles.paddingLeft).toBe('40px') + expect(styles.fontSize).toBe('20px') + expect(styles.minHeight).toBe('150px') + expect(styles.borderBottomLeftRadius).toBe('20px') + expect(styles.borderBottomRightRadius).toBe('20px') + expect(styles.borderTopLeftRadius).toBe('20px') + expect(styles.borderTopRightRadius).toBe('20px') + expect(styles.outlineOffset).toBe('-20px') + expect(styles.outlineWidth).toBe('3px') + expect(styles.outlineStyle).toBe('dashed') + expect(styles.outlineColor).toBe('rgb(0, 94, 128)') + expect(styles.whiteSpace).toBe('pre-wrap') + }) +}) diff --git a/site/examples/styling.tsx b/site/examples/styling.tsx new file mode 100644 index 0000000000..3c1251fa4f --- /dev/null +++ b/site/examples/styling.tsx @@ -0,0 +1,47 @@ +import React, { useMemo } from 'react' +import { createEditor } from 'slate' +import { Slate, Editable, withReact } from 'slate-react' +import { withHistory } from 'slate-history' + +const StylingExample = () => { + const editor1 = useMemo(() => withHistory(withReact(createEditor())), []) + const editor2 = useMemo(() => withHistory(withReact(createEditor())), []) + + return ( +
+ + + + + + + +
+ ) +} + +export default StylingExample diff --git a/site/pages/examples/[example].tsx b/site/pages/examples/[example].tsx index 9e0cffd307..ebeecb78fb 100644 --- a/site/pages/examples/[example].tsx +++ b/site/pages/examples/[example].tsx @@ -25,6 +25,7 @@ import ReadOnly from '../../examples/read-only' import RichText from '../../examples/richtext' import SearchHighlighting from '../../examples/search-highlighting' import ShadowDOM from '../../examples/shadow-dom' +import Styling from '../../examples/styling' import Tables from '../../examples/tables' import IFrames from '../../examples/iframe' import CustomPlaceholder from '../../examples/custom-placeholder' @@ -51,6 +52,7 @@ const EXAMPLES = [ ['Rich Text', RichText, 'richtext'], ['Search Highlighting', SearchHighlighting, 'search-highlighting'], ['Shadow DOM', ShadowDOM, 'shadow-dom'], + ['Styling', Styling, 'styling'], ['Tables', Tables, 'tables'], ['Rendering in iframes', IFrames, 'iframe'], ['Custom placeholder', CustomPlaceholder, 'custom-placeholder'], diff --git a/site/public/index.css b/site/public/index.css index e25878849e..bcd5333f9a 100644 --- a/site/public/index.css +++ b/site/public/index.css @@ -78,3 +78,14 @@ iframe { [data-slate-editor] > * + * { margin-top: 1em; } + +.fancy { + background-color: rgb(218, 225, 255); + padding: 40px; + font-size: 20px; + min-height: 150px; + outline: 3px dashed rgb(0, 94, 128); + border-radius: 20px; + outline-offset: -20px; + white-space: pre-wrap; +} From a5bbeb08a859f9d9fd6d7558a81b74632a1998ab Mon Sep 17 00:00:00 2001 From: Kyle McLean Date: Tue, 31 Jan 2023 14:02:28 -0700 Subject: [PATCH 3/5] Add section in docs for editor styling --- docs/concepts/09-rendering.md | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/docs/concepts/09-rendering.md b/docs/concepts/09-rendering.md index fd5d4bcbec..f6a79c8a22 100644 --- a/docs/concepts/09-rendering.md +++ b/docs/concepts/09-rendering.md @@ -136,3 +136,24 @@ const Toolbar = () => { ``` Because the `` uses the `useSlate` hook to retrieve the context, it will re-render whenever the editor changes, so that the active state of the buttons stays in sync. + +## Editor Styling + +Custom styles can be applied to the editor itself by using the `style` prop on the `` component. + +```jsx +const MyEditor = () => { + const [editor] = useState(() => withReact(createEditor())) + return ( + + + + ) +} +``` + +It is also possible to apply custom styles with a stylesheet and `className`. However, Slate uses inline styles to provide some default styles for the editor. Because inline styles take precedence over stylesheets, styles you provide using stylesheets will not override the default styles. If you are trying to use a stylesheet and your rules are not taking effect, do one of the following: + +- Provide your styles using the `style` prop instead of a stylesheet, which overrides the default inline styles. +- Pass the `disableDefaultStyles` prop to the `` component. +- Use `!important` in your stylesheet declarations to make them override the inline styles. From ca877a50868422b34253d74fa7f5ee91d6cc6111 Mon Sep 17 00:00:00 2001 From: Kyle McLean Date: Tue, 31 Jan 2023 14:02:49 -0700 Subject: [PATCH 4/5] Add test for editor height being set to placeholder height --- .../integration/examples/placeholder.test.ts | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/playwright/integration/examples/placeholder.test.ts b/playwright/integration/examples/placeholder.test.ts index 4e87a2fcb6..ede5de359b 100644 --- a/playwright/integration/examples/placeholder.test.ts +++ b/playwright/integration/examples/placeholder.test.ts @@ -14,4 +14,21 @@ test.describe('placeholder example', () => { 'renderPlaceholder' ) }) + + test('renders editor tall enough to fit placeholder', async ({ page }) => { + const slateEditor = page.locator('[data-slate-editor=true]') + const placeholderElement = page.locator('[data-slate-placeholder=true]') + + const editorBoundingBox = await slateEditor.boundingBox() + const placeholderBoundingBox = await placeholderElement.boundingBox() + + if (!editorBoundingBox) + throw new Error('Could not get bounding box for editor') + if (!placeholderBoundingBox) + throw new Error('Could not get bounding box for placeholder') + + expect(editorBoundingBox.height).toBeGreaterThanOrEqual( + placeholderBoundingBox.height + ) + }) }) From 6614bb6f5642c14ff91bd5132381bff2121c1929 Mon Sep 17 00:00:00 2001 From: Kyle McLean Date: Tue, 31 Jan 2023 14:16:08 -0700 Subject: [PATCH 5/5] Add changeset --- .changeset/shy-laws-argue.md | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .changeset/shy-laws-argue.md diff --git a/.changeset/shy-laws-argue.md b/.changeset/shy-laws-argue.md new file mode 100644 index 0000000000..8b31d92c17 --- /dev/null +++ b/.changeset/shy-laws-argue.md @@ -0,0 +1,6 @@ +--- +'slate-react': minor +'slate': patch +--- + +Revert to using inline styles for default editor styles