Skip to content

Commit

Permalink
Merge pull request basecamp#1192 from basecamp/revert-1188-editor-ele…
Browse files Browse the repository at this point in the history
…ment-internals

Revert "Integrate with `ElementInternals`"
  • Loading branch information
jorgemanrubia authored Oct 3, 2024
2 parents 2f8c59d + a44dad9 commit 2b7f980
Show file tree
Hide file tree
Showing 5 changed files with 59 additions and 183 deletions.
57 changes: 0 additions & 57 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -148,63 +148,6 @@ To populate a `<trix-editor>` with stored content, include that content in the a

Always use an associated input element to safely populate an editor. Trix won’t load any HTML content inside a `<trix-editor>…</trix-editor>` tag.

## Disabling the Editor

To disable the `<trix-editor>`, render it with the `[disabled]` attribute:

```html
<trix-editor disabled></trix-editor>
```

Disabled editors are not editable, cannot receive focus, and their values will
be ignored when their related `<form>` element is submitted.

To change whether or not an editor is disabled, either toggle the `[disabled]`
attribute or assign a boolean to the `.disabled` property:

```html
<trix-editor id="editor" disabled></trix-editor>

<script>
const editor = document.getElementById("editor")
editor.toggleAttribute("disabled", false)
editor.disabled = true
</script>
```

When disabled, the editor will match the [:disabled CSS
pseudo-class][:disabled].

[:disabled]: https://developer.mozilla.org/en-US/docs/Web/CSS/:disabled

## Providing an Accessible Name

Like other form controls, `<trix-editor>` elements should have an accessible name. The `<trix-editor>` element integrates with `<label>` elements and The `<trix-editor>` supports two styles of integrating with `<label>` elements:

1. render the `<trix-editor>` element with an `[id]` attribute that the `<label>` element references through its `[for]` attribute:

```html
<label for="editor">Editor</label>
<trix-editor id="editor"></trix-editor>
```

2. render the `<trix-editor>` element as a child of the `<label>` element:

```html
<trix-toolbar id="editor-toolbar"></trix-toolbar>
<label>
Editor

<trix-editor toolbar="editor-toolbar"></trix-editor>
</label>
```

> [!WARNING]
> When rendering the `<trix-editor>` element as a child of the `<label>` element, [explicitly render](#creating-an-editor) the corresponding `<trix-toolbar>` element outside of the `<label>` element.
In addition to integrating with `<label>` elements, `<trix-editor>` elements support `[aria-label]` and `[aria-labelledby]` attributes.

## Styling Formatted Content

To ensure what you see when you edit is what you see when you save, use a CSS class name to scope styles for Trix formatted content. Apply this class name to your `<trix-editor>` element, and to a containing element when you render stored Trix content for display in your application.
Expand Down
96 changes: 2 additions & 94 deletions src/test/system/custom_element_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -495,32 +495,12 @@ testGroup("Custom element API", { template: "editor_empty" }, () => {
form.removeEventListener("reset", preventDefault, false)
expectDocument("hello\n")
})

test("editor resets to its original value on element reset", async () => {
const element = getEditorElement()

await typeCharacters("hello")
element.reset()
expectDocument("\n")
})

test("element returns empty string when value is missing", () => {
const element = getEditorElement()

assert.equal(element.value, "")
})

test("editor returns its type", () => {
const element = getEditorElement()

assert.equal("trix-editor", element.type)
})
})

testGroup("<label> support", { template: "editor_with_labels" }, () => {
test("associates all label elements", () => {
const labels = [ document.getElementById("label-1"), document.getElementById("label-3") ]
assert.deepEqual(Array.from(getEditorElement().labels), labels)
assert.deepEqual(getEditorElement().labels, labels)
})

test("focuses when <label> clicked", () => {
Expand All @@ -541,7 +521,7 @@ testGroup("<label> support", { template: "editor_with_labels" }, () => {
})
})

testGroup("integrates with its <form>", { template: "editors_with_forms", container: "div" }, () => {
testGroup("form property references its <form>", { template: "editors_with_forms", container: "div" }, () => {
test("accesses its ancestor form", () => {
const form = document.getElementById("ancestor-form")
const editor = document.getElementById("editor-with-ancestor-form")
Expand All @@ -558,76 +538,4 @@ testGroup("integrates with its <form>", { template: "editors_with_forms", contai
const editor = document.getElementById("editor-with-no-form")
assert.equal(editor.form, null)
})

test("adds [disabled] attribute based on .disabled property", () => {
const editor = document.getElementById("editor-with-ancestor-form")

editor.disabled = true

assert.equal(editor.hasAttribute("disabled"), true, "adds [disabled] attribute")

editor.disabled = false

assert.equal(editor.hasAttribute("disabled"), false, "removes [disabled] attribute")
})

test("removes [contenteditable] and disables input when editor element has [disabled]", () => {
const editor = document.getElementById("editor-with-no-form")

editor.setAttribute("disabled", "")

assert.equal(editor.matches(":disabled"), true, "sets :disabled CSS pseudostate")
assert.equal(editor.inputElement.disabled, true, "disables input")
assert.equal(editor.disabled, true, "exposes [disabled] attribute as .disabled property")
assert.equal(editor.hasAttribute("contenteditable"), false, "removes [contenteditable] attribute")

editor.removeAttribute("disabled")

assert.equal(editor.matches(":disabled"), false, "removes sets :disabled pseudostate")
assert.equal(editor.inputElement.disabled, false, "enabled input")
assert.equal(editor.disabled, false, "updates .disabled property")
assert.equal(editor.hasAttribute("contenteditable"), true, "adds [contenteditable] attribute")
})

test("removes [contenteditable] and disables input when editor element is :disabled", () => {
const editor = document.getElementById("editor-within-fieldset")
const fieldset = document.getElementById("fieldset")

fieldset.disabled = true

assert.equal(editor.matches(":disabled"), true, "sets :disabled CSS pseudostate")
assert.equal(editor.inputElement.disabled, true, "disables input")
assert.equal(editor.disabled, true, "infers disabled state from ancestor")
assert.equal(editor.hasAttribute("disabled"), false, "does not set [disabled] attribute")
assert.equal(editor.hasAttribute("contenteditable"), false, "removes [contenteditable] attribute")

fieldset.disabled = false

assert.equal(editor.matches(":disabled"), false, "removes sets :disabled pseudostate")
assert.equal(editor.inputElement.disabled, false, "enabled input")
assert.equal(editor.disabled, false, "updates .disabled property")
assert.equal(editor.hasAttribute("disabled"), false, "does not set [disabled] attribute")
assert.equal(editor.hasAttribute("contenteditable"), true, "adds [contenteditable] attribute")
})

test("does not receive focus when :disabled", () => {
const activeEditor = document.getElementById("editor-with-input-form")
const editor = document.getElementById("editor-within-fieldset")

activeEditor.focus()
editor.disabled = true
editor.focus()

assert.equal(activeEditor, document.activeElement, "disabled editor does not receive focus")
})

test("disabled editor does not encode its value when the form is submitted", () => {
const editor = document.getElementById("editor-with-ancestor-form")
const form = editor.form

editor.inputElement.value = "Hello world"
editor.disabled = true

assert.deepEqual({}, Object.fromEntries(new FormData(form).entries()), "does not write to FormData")
})
})
8 changes: 5 additions & 3 deletions src/test/test_helpers/fixtures/editor_with_labels.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
export default () =>
`<label id="label-1" for="editor"><span>Label 1</span></label>
<label id="label-2">Label 2</label>
<trix-editor id="editor"></trix-editor>
<label id="label-3" for="editor">Label 3</label>`
<label id="label-2">
Label 2
<trix-editor id="editor"></trix-editor>
</label>
<label id="label-3" for="editor">Label 3</label>`
5 changes: 2 additions & 3 deletions src/test/test_helpers/fixtures/editors_with_forms.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
export default () =>
`<form id="ancestor-form">
<trix-editor id="editor-with-ancestor-form" name="editor-with-ancestor-form"></trix-editor>
<trix-editor id="editor-with-ancestor-form"></trix-editor>
</form>
<form id="input-form">
<input type="hidden" id="hidden-input">
</form>
<trix-editor id="editor-with-input-form" input="hidden-input"></trix-editor>
<trix-editor id="editor-with-no-form"></trix-editor>
<fieldset id="fieldset"><trix-editor id="editor-within-fieldset"></fieldset>`
<trix-editor id="editor-with-no-form"></trix-editor>`
76 changes: 50 additions & 26 deletions src/trix/elements/trix_editor_element.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import * as config from "trix/config"

import {
findClosestElementFromNode,
handleEvent,
handleEventOnce,
installDefaultCSSForTagName,
Expand Down Expand Up @@ -160,14 +161,6 @@ installDefaultCSSForTagName("trix-editor", `\
}`)

export default class TrixEditorElement extends HTMLElement {
static formAssociated = true

#internals

constructor() {
super()
this.#internals = this.attachInternals()
}

// Properties

Expand All @@ -181,7 +174,19 @@ export default class TrixEditorElement extends HTMLElement {
}

get labels() {
return this.#internals.labels
const labels = []
if (this.id && this.ownerDocument) {
labels.push(...Array.from(this.ownerDocument.querySelectorAll(`label[for='${this.id}']`) || []))
}

const label = findClosestElementFromNode(this, { matchingSelector: "label" })
if (label) {
if ([ this, null ].includes(label.control)) {
labels.push(label)
}
}

return labels
}

get toolbarElement() {
Expand Down Expand Up @@ -233,18 +238,6 @@ export default class TrixEditorElement extends HTMLElement {
this.editor?.loadHTML(this.defaultValue)
}

get disabled() {
return this.inputElement.disabled
}

set disabled(value) {
this.toggleAttribute("disabled")
}

get type() {
return this.localName
}

// Controller delegate methods

notify(message, data) {
Expand Down Expand Up @@ -276,23 +269,54 @@ export default class TrixEditorElement extends HTMLElement {
requestAnimationFrame(() => triggerEvent("trix-initialize", { onElement: this }))
}
this.editorController.registerSelectionManager()
this.registerResetListener()
this.registerClickListener()
autofocus(this)
}
}

disconnectedCallback() {
this.editorController?.unregisterSelectionManager()
this.unregisterResetListener()
return this.unregisterClickListener()
}

// Form support

formDisabledCallback(disabled) {
this.inputElement.disabled = disabled
this.toggleAttribute("contenteditable", !disabled)
registerResetListener() {
this.resetListener = this.resetBubbled.bind(this)
return window.addEventListener("reset", this.resetListener, false)
}

unregisterResetListener() {
return window.removeEventListener("reset", this.resetListener, false)
}

registerClickListener() {
this.clickListener = this.clickBubbled.bind(this)
return window.addEventListener("click", this.clickListener, false)
}

unregisterClickListener() {
return window.removeEventListener("click", this.clickListener, false)
}

resetBubbled(event) {
if (event.defaultPrevented) return
if (event.target !== this.form) return
return this.reset()
}

formResetCallback() {
this.reset()
clickBubbled(event) {
if (event.defaultPrevented) return
if (this.contains(event.target)) return

const label = findClosestElementFromNode(event.target, { matchingSelector: "label" })
if (!label) return

if (!Array.from(this.labels).includes(label)) return

return this.focus()
}

reset() {
Expand Down

0 comments on commit 2b7f980

Please sign in to comment.