Skip to content
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

Simplify implementation of custom editor styling #5278

Merged
merged 5 commits into from
Feb 1, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/shy-laws-argue.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'slate-react': minor
'slate': patch
---

Revert to using inline styles for default editor styles
21 changes: 21 additions & 0 deletions docs/concepts/09-rendering.md
Original file line number Diff line number Diff line change
Expand Up @@ -136,3 +136,24 @@ const Toolbar = () => {
```

Because the `<Toolbar>` 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 `<Editable>` component.

```jsx
const MyEditor = () => {
const [editor] = useState(() => withReact(createEditor()))
return (
<Slate editor={editor}>
<Editable style={{ minHeight: '200px', backgroundColor: 'lime' }} />
</Slate>
)
}
```

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 `<Editable>` component.
- Use `!important` in your stylesheet declarations to make them override the inline styles.
74 changes: 27 additions & 47 deletions packages/slate-react/src/components/editable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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'
Expand All @@ -77,9 +76,6 @@ const Children = (props: Parameters<typeof useChildren>[0]) => (
<React.Fragment>{useChildren(props)}</React.Fragment>
)

// The number of Editable components currently mounted.
let mountedCount = 0

/**
* `RenderElementProps` are passed to the `renderElement` handler.
*/
Expand Down Expand Up @@ -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<HTMLDivElement>

/**
Expand All @@ -142,8 +139,9 @@ export const Editable = (props: EditableProps) => {
renderLeaf,
renderPlaceholder = props => <DefaultPlaceholder {...props} />,
scrollSelectionIntoView = defaultScrollSelectionIntoView,
style = {},
style: userStyle = {},
as: Component = 'div',
disableDefaultStyles = false,
...attributes
} = props
const editor = useSlate()
Expand Down Expand Up @@ -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 (
<ReadOnlyContext.Provider value={readOnly}>
Expand Down Expand Up @@ -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}
Expand All @@ -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<HTMLDivElement>) => {
// COMPAT: Certain browsers don't support the `beforeinput` event, so we
Expand Down
30 changes: 13 additions & 17 deletions packages/slate-react/src/components/leaf.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -32,6 +31,7 @@ const Leaf = (props: {
renderLeaf = (props: RenderLeafProps) => <DefaultLeaf {...props} />,
} = props

const lastPlaceholderRef = useRef<HTMLSpanElement | null>(null)
const placeholderRef = useRef<HTMLSpanElement | null>(null)
const editor = useSlateStatic()

Expand Down Expand Up @@ -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)
}
Expand Down
4 changes: 0 additions & 4 deletions packages/slate-react/src/utils/weak-maps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,6 @@ export const EDITOR_TO_KEY_TO_ELEMENT: WeakMap<
Editor,
WeakMap<Key, HTMLElement>
> = new WeakMap()
export const EDITOR_TO_STYLE_ELEMENT: WeakMap<
Editor,
HTMLStyleElement
> = new WeakMap()

/**
* Weak maps for storing editor-related state.
Expand Down
22 changes: 0 additions & 22 deletions packages/slate-react/src/utils/where-if-supported.ts

This file was deleted.

3 changes: 0 additions & 3 deletions packages/slate/src/create-editor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*/
Expand All @@ -28,7 +26,6 @@ export const createEditor = (): Editor => {
operations: [],
selection: null,
marks: null,
id: nextEditorId++,
isInline: () => false,
isVoid: () => false,
markableVoid: () => false,
Expand Down
1 change: 0 additions & 1 deletion packages/slate/src/interfaces/editor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
17 changes: 17 additions & 0 deletions playwright/integration/examples/placeholder.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
)
})
})
Loading