Skip to content

Commit

Permalink
Integrate with ElementInternals
Browse files Browse the repository at this point in the history
Closes [basecamp#1023][]

Replace the requirement for an `<input type="hidden">` element with
direct `<form>` integration through built-in support for
[ElementInternals][].

According to the [Form-associated custom elements][] section of [More
capable form controls][], various behaviors that the `<trix-editor>`
element was recreating are provided out of the box.

For example, the `<input type="hidden">`-`[input]` attribute pairing can
be achieved through [ElementInternals.setFormValue][]. Similarly, the
`<label>` element support can be achieved through
[ElementInternals.labels][].

[basecamp#1023]: basecamp#1023
[ElementInternals]: https://developer.mozilla.org/en-US/docs/Web/API/ElementInternals
[Form-associated custom elements]: https://web.dev/articles/more-capable-form-controls#form-associated_custom_elements
[More capable form controls]: https://web.dev/articles/more-capable-form-controls
[ElementInternals.setFormValue]: https://developer.mozilla.org/en-US/docs/Web/API/ElementInternals/setFormValue
[ElementInternals.labels]: https://developer.mozilla.org/en-US/docs/Web/API/ElementInternals/labels
  • Loading branch information
seanpdoyle committed Feb 9, 2024
1 parent bc24ac5 commit 0088e6c
Show file tree
Hide file tree
Showing 17 changed files with 227 additions and 31 deletions.
7 changes: 6 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,13 @@ on:

jobs:
build:
name: Browser tests
name: "Browser tests (ElementInternals: ${{ matrix.elementInternals }})"
runs-on: ubuntu-latest
strategy:
matrix:
elementInternals: [false, true]
env:
EDITOR_ELEMENT_INTERNALS: "${{ matrix.elementInternals }}"
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
Expand Down
58 changes: 55 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ This is the approach that all modern, production ready, WYSIWYG editors now take

<details><summary>Trix supports all evergreen, self-updating desktop and mobile browsers.</summary><img src="https://app.saucelabs.com/browser-matrix/basecamp_trix.svg"></details>

Trix is built with established web standards, notably [Custom Elements](https://developer.mozilla.org/en-US/docs/Web/Web_Components/Using_custom_elements), [Mutation Observer](https://dom.spec.whatwg.org/#mutation-observers), and [Promises](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise).
Trix is built with established web standards, notably [Custom Elements](https://developer.mozilla.org/en-US/docs/Web/Web_Components/Using_custom_elements), [Element Internals](https://developer.mozilla.org/en-US/docs/Web/API/ElementInternals), [Mutation Observer](https://dom.spec.whatwg.org/#mutation-observers), and [Promises](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise).

# Getting Started

Expand Down Expand Up @@ -49,12 +49,26 @@ document.addEventListener("trix-before-initialize", () => {

## Creating an Editor

Place an empty `<trix-editor></trix-editor>` tag on the page. Trix will automatically insert a separate `<trix-toolbar>` before the editor.
Place an empty `<trix-editor></trix-editor>` tag on the page. If the `<trix-editor>` element is rendered with a `[toolbar]` attribute that references the element by its `[id]`, it will treat that element as its toolbar:

```html
<trix-toolbar id="editor_toolbar"></trix-toolbar>

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

Otherwise, Trix will automatically insert a separate `<trix-toolbar>` before the editor.

Like an HTML `<textarea>`, `<trix-editor>` accepts `autofocus` and `placeholder` attributes. Unlike a `<textarea>`, `<trix-editor>` automatically expands vertically to fit its contents.

## Integrating With Forms

There are two styles of integrating with `<form>` element submissions.

### Legacy integration with `<input type="hidden">`

Legacy support is provided through an `<input type="hidden">` element paired with an `[input]` attribute on the `<trix-editor>` element.

To submit the contents of a `<trix-editor>` with a form, first define a hidden input field in the form and assign it an `id`. Then reference that `id` in the editor’s `input` attribute.

```html
Expand All @@ -66,7 +80,7 @@ To submit the contents of a `<trix-editor>` with a form, first define a hidden i

Trix will automatically update the value of the hidden input field with each change to the editor.

## Populating With Stored Content
#### Populating With Stored Content

To populate a `<trix-editor>` with stored content, include that content in the associated input element’s `value` attribute.

Expand All @@ -79,6 +93,44 @@ 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.

### Integration with Element Internals

Trix can also be configured to integrate with forms through the `<trix-editor>` element's `ElementInternals` instance.

First, configure Trix to opt-into its Element Internals support by rendering a `<meta>` element into the document's `<head>`:

```html
<head>
<!---->
<meta name="trix-config-editor-element-internals" content="true">
</head>
```

Then, to submit the contents of a `<trix-editor>` with a `<form>`, render the element with a `[name]` attribute and its initial value as its inner HTML.

```html
<form >
<trix-editor name="content"></trix-editor>
</form>
```

To associate the element with a `<form>` that isn't an ancestor, render the element with a `[form]` attribute that references the `<form>` element by its `[id]`:

```html
<form id="a-form-element" ></form>
<trix-editor name="content" form="a-form-element"></trix-editor>
```

#### Populating With Stored Content

To populate a `<trix-editor>` with stored content, include that content as HTML inside the element’s inner HTML.

```html
<form >
<trix-editor>Editor content goes here</trix-editor>
</form>
```

## 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:
Expand Down
21 changes: 20 additions & 1 deletion assets/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
box-sizing: border-box;
}

main {
main, nav {
margin: 20px auto;
max-width: 700px;
}
Expand Down Expand Up @@ -69,8 +69,27 @@
}
});
</script>
<script>
const searchParams = new URLSearchParams(location.search)

if (searchParams.get("editor") === "elementInternals") {
document.head.insertAdjacentHTML(
"beforeend",
`<meta name="trix-config-editor-element-internals" content="true">`
)

document.addEventListener("trix-change", function(event) {
var input = document.getElementById("input")
input.value = event.target.value
})
}
</script>
</head>
<body>
<nav>
<a href="/">Legacy support</a>
<a href="/?editor=elementInternals">ElementInternals support</a>
</nav>
<main>
<label for="editor">Input</label>
<trix-editor autofocus class="trix-content" input="input" id="editor"></trix-editor>
Expand Down
4 changes: 4 additions & 0 deletions karma.conf.js
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,10 @@ if (process.env.SAUCE_ACCESS_KEY) {
}
}

if (process.env.EDITOR_ELEMENT_INTERNALS === "true") {
config.files.unshift({ pattern: "src/test/test_helpers/fixtures/element_internals.js", watched: false, included: true })
}

function buildId() {
const { GITHUB_WORKFLOW, GITHUB_RUN_NUMBER, GITHUB_RUN_ID } = process.env
return GITHUB_WORKFLOW && GITHUB_RUN_NUMBER && GITHUB_RUN_ID
Expand Down
16 changes: 13 additions & 3 deletions src/test/system/accessibility_test.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { assert, test, testGroup, triggerEvent } from "test/test_helper"
import * as config from "trix/config"
import { assert, test, testGroup, testIf, testUnless, triggerEvent } from "test/test_helper"

testGroup("Accessibility attributes", { template: "editor_default_aria_label" }, () => {
test("sets the role to textbox", () => {
Expand All @@ -22,17 +23,26 @@ testGroup("Accessibility attributes", { template: "editor_default_aria_label" },
assert.equal(editor.getAttribute("aria-labelledby"), "aria-labelledby-id")
})

test("assigns aria-label to the text of the element's <label> elements", () => {
testUnless(config.editor.elementInternals, "assigns aria-label to the text of the element's <label> elements", () => {
const editor = document.getElementById("editor-with-labels")
assert.equal(editor.getAttribute("aria-label"), "Label 1 Label 2 Label 3")
})

test("updates the aria-label on focus", () => {
testUnless(config.editor.elementInternals, "updates the aria-label on focus", () => {
const editor = document.getElementById("editor-with-modified-label")
const label = document.getElementById("modified-label")

label.innerHTML = "<span>New Value</span>"
triggerEvent(editor, "focus")
assert.equal(editor.getAttribute("aria-label"), "New Value")
})

testIf(config.editor.elementInternals, "does not set [aria-label] for a <label> element", () => {
const editor = document.getElementById("editor-with-labels")
const labels = Array.from(editor.labels)
const text = labels.map((label) => label.textContent.trim())

assert.deepEqual(text, [ "Label 1", "Label 2", "Label 3" ])
assert.equal(editor.getAttribute("aria-label"), null)
})
})
6 changes: 4 additions & 2 deletions src/test/system/custom_element_test.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import * as config from "trix/config"
import { rangesAreEqual } from "trix/core/helpers"

import {
Expand All @@ -13,6 +14,7 @@ import {
test,
testGroup,
testIf,
testUnless,
triggerEvent,
typeCharacters,
typeInToolbarDialog,
Expand Down Expand Up @@ -510,12 +512,12 @@ testGroup("<label> support", { template: "editor_with_labels" }, () => {
assert.deepEqual(Array.from(getEditorElement().labels), labels)
})

test("focuses when <label> clicked", () => {
testUnless(config.editor.elementInternals, "focuses when <label> clicked", () => {
document.getElementById("label-1").click()
assert.equal(getEditorElement(), document.activeElement)
})

test("focuses when <label> descendant clicked", () => {
testUnless(config.editor.elementInternals, "focuses when <label> descendant clicked", () => {
document.getElementById("label-1").querySelector("span").click()
assert.equal(getEditorElement(), document.activeElement)
})
Expand Down
5 changes: 3 additions & 2 deletions src/test/system/installation_process_test.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import * as config from "trix/config"
import EditorController from "trix/controllers/editor_controller"

import { assert, test, testGroup } from "test/test_helper"
import { assert, test, testGroup, testUnless } from "test/test_helper"
import { nextFrame } from "../test_helpers/timing_helpers"

testGroup("Installation process", { template: "editor_html" }, () => {
Expand Down Expand Up @@ -31,7 +32,7 @@ testGroup("Installation process without specified elements", { template: "editor
assert.equal(editorElement.toolbarElement, toolbarElement)
})

test("creates identified input elements", () => {
testUnless(config.editor.elementInternals, "creates identified input elements", () => {
const editorElement = getEditorElement()

const inputId = editorElement.getAttribute("input")
Expand Down
9 changes: 7 additions & 2 deletions src/test/test_helpers/fixtures/editor_html.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
import * as config from "trix/config"

export default () =>
`<input id="my_input" type="hidden" value="&lt;div&gt;Hello world&lt;/div&gt;">
<trix-editor input="my_input" autofocus placeholder="Say hello..."></trix-editor>`
config.editor.elementInternals ?
`<trix-editor autofocus placeholder="Say hello..."><div>Hello world</div></trix-editor>
` :
`<input id="my_input" type="hidden" value="&lt;div&gt;Hello world&lt;/div&gt;">
<trix-editor input="my_input" autofocus placeholder="Say hello..."></trix-editor>`
8 changes: 6 additions & 2 deletions src/test/test_helpers/fixtures/editor_with_image.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import * as config from "trix/config"
import { TEST_IMAGE_URL } from "./test_image_url"

export default () =>
`<trix-editor input="my_input" autofocus placeholder="Say hello..."></trix-editor>
<input id="my_input" type="hidden" value="ab&lt;img src=&quot;${TEST_IMAGE_URL}&quot; width=&quot;10&quot; height=&quot;10&quot;&gt;">`
config.editor.elementInternals ?
`<trix-editor autofocus placeholder="Say hello...">ab<img src="${TEST_IMAGE_URL}" width="10" height="10"></trix-editor>
` :
`<trix-editor input="my_input" autofocus placeholder="Say hello..."></trix-editor>
<input id="my_input" type="hidden" value="ab&lt;img src=&quot;${TEST_IMAGE_URL}&quot; width=&quot;10&quot; height=&quot;10&quot;&gt;">`
18 changes: 13 additions & 5 deletions src/test/test_helpers/fixtures/editor_with_toolbar_and_input.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,14 @@
import * as config from "trix/config"

export default () =>
`<ul id="my_editor">
<li><trix-toolbar id="my_toolbar"></trix-toolbar></li>
<li><trix-editor toolbar="my_toolbar" input="my_input" autofocus placeholder="Say hello..."></trix-editor></li>
<li><input id="my_input" type="hidden" value="&lt;div&gt;Hello world&lt;/div&gt;"></li>
</ul>`
config.editor.elementInternals ?
`<ul id="my_editor">
<li><trix-toolbar id="my_toolbar"></trix-toolbar></li>
<li><trix-editor toolbar="my_toolbar" autofocus placeholder="Say hello..."><div>Hello world</div></trix-editor></li>
</ul>
` :
`<ul id="my_editor">
<li><trix-toolbar id="my_toolbar"></trix-toolbar></li>
<li><trix-editor toolbar="my_toolbar" input="my_input" autofocus placeholder="Say hello..."></trix-editor></li>
<li><input id="my_input" type="hidden" value="&lt;div&gt;Hello world&lt;/div&gt;"></li>
</ul>`
26 changes: 18 additions & 8 deletions src/test/test_helpers/fixtures/editors_with_forms.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,21 @@
import * as config from "trix/config"

export default () =>
`<form id="ancestor-form">
<trix-editor id="editor-with-ancestor-form"></trix-editor>
</form>
config.editor.elementInternals ?
`<form id="ancestor-form">
<trix-editor id="editor-with-ancestor-form"></trix-editor>
</form>
<form id="input-form"></form>
<trix-editor id="editor-with-input-form" form="input-form"></trix-editor>
<trix-editor id="editor-with-no-form"></trix-editor>
` :
`<form id="ancestor-form">
<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>
<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>`
<trix-editor id="editor-with-no-form"></trix-editor>`
4 changes: 4 additions & 0 deletions src/test/test_helpers/fixtures/element_internals.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
document.head.insertAdjacentHTML(
"beforeend",
`<meta name="trix-config-editor-element-internals" content="true">
`)
4 changes: 4 additions & 0 deletions src/test/test_helpers/test_helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,10 @@ export const testIf = function (condition, ...args) {
}
}

export const testUnless = function(condition, ...args) {
testIf(!condition, ...args)
}

export const { skip, test } = QUnit

const waitForTrixInit = async () => {
Expand Down
5 changes: 5 additions & 0 deletions src/trix/config/editor.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
const config = {
elementInternals: false
}

export default config
1 change: 1 addition & 0 deletions src/trix/config/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ export { default as attachments } from "./attachments"
export { default as blockAttributes } from "./block_attributes"
export { default as browser } from "./browser"
export { default as css } from "./css"
export { default as editor } from "./editor"
export { default as fileSize } from "./file_size_formatting"
export { default as input } from "./input"
export { default as keyNames } from "./key_names"
Expand Down
Loading

0 comments on commit 0088e6c

Please sign in to comment.