Skip to content

Commit

Permalink
Use stylesheets to give Editable components default styles (#5206)
Browse files Browse the repository at this point in the history
* Use stylesheet to give Editable components a default style

* Give Editors a unique id

* Use per-editor stylesheets to give editors a min-height

* Make editor min-height respond to changes in placeholder height

* Add changeset for stylesheet changes

* Prevent unnecessary creations of ResizeObservers

* Update yarn.lock
  • Loading branch information
kylemclean authored Dec 4, 2022
1 parent d1f90eb commit 96b7fcd
Show file tree
Hide file tree
Showing 9 changed files with 119 additions and 19 deletions.
6 changes: 6 additions & 0 deletions .changeset/strange-pens-lie.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'slate': minor
'slate-react': minor
---

Use stylesheet for default styles on Editable components
1 change: 1 addition & 0 deletions packages/slate-react/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
58 changes: 46 additions & 12 deletions packages/slate-react/src/components/editable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -76,6 +77,9 @@ 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 @@ -802,6 +806,46 @@ 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()
}
}, [])

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 (
<ReadOnlyContext.Provider value={readOnly}>
<DecorateContext.Provider value={decorate}>
Expand Down Expand Up @@ -831,6 +875,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}
Expand All @@ -840,18 +885,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<HTMLDivElement>) => {
// COMPAT: Certain browsers don't support the `beforeinput` event, so we
Expand Down
49 changes: 42 additions & 7 deletions packages/slate-react/src/components/leaf.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@ 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'
import { ReactEditor } from '..'

/**
* Individual leaves in a text node with unique formatting.
Expand All @@ -33,19 +33,54 @@ const Leaf = (props: {
const placeholderRef = useRef<HTMLSpanElement | null>(null)
const editor = useSlateStatic()

const placeholderResizeObserver = useRef<ResizeObserver | null>(null)

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)
}

editorEl.style.minHeight = `${placeholderEl.clientHeight}px`
EDITOR_TO_PLACEHOLDER_ELEMENT.set(editor, 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)
}

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 () => {
editorEl.style.minHeight = 'auto'
EDITOR_TO_PLACEHOLDER_ELEMENT.delete(editor)
}
}, [placeholderRef, leaf])
Expand Down
4 changes: 4 additions & 0 deletions packages/slate-react/src/utils/weak-maps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@ 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
8 changes: 8 additions & 0 deletions packages/slate-react/test/index.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => {
Expand Down
3 changes: 3 additions & 0 deletions packages/slate/src/create-editor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*/
Expand All @@ -26,6 +28,7 @@ export const createEditor = (): Editor => {
operations: [],
selection: null,
marks: null,
id: nextEditorId++,
isInline: () => false,
isVoid: () => false,
markableVoid: () => false,
Expand Down
1 change: 1 addition & 0 deletions packages/slate/src/interfaces/editor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 8 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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
Expand Down

0 comments on commit 96b7fcd

Please sign in to comment.