Skip to content

Commit 92dd79e

Browse files
committed
Integrate with ElementInternals
Closes [#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][]. [#1023]: #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
1 parent b86322f commit 92dd79e

17 files changed

+302
-77
lines changed

.github/workflows/ci.yml

+6-1
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,13 @@ on:
88

99
jobs:
1010
build:
11-
name: Browser tests
11+
name: "Browser tests (ElementInternals: ${{ matrix.elementInternals }})"
1212
runs-on: ubuntu-latest
13+
strategy:
14+
matrix:
15+
elementInternals: [false, true]
16+
env:
17+
EDITOR_ELEMENT_INTERNALS: "${{ matrix.elementInternals }}"
1318
steps:
1419
- uses: actions/checkout@v3
1520
- uses: actions/setup-node@v3

README.md

+82-3
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ This is the approach that all modern, production ready, WYSIWYG editors now take
1919

2020
<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>
2121

22-
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).
22+
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).
2323

2424
# Getting Started
2525

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

5050
## Creating an Editor
5151

52-
Place an empty `<trix-editor></trix-editor>` tag on the page. Trix will automatically insert a separate `<trix-toolbar>` before the editor.
52+
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:
53+
54+
```html
55+
<trix-toolbar id="editor_toolbar"></trix-toolbar>
56+
57+
<trix-editor toolbar="editor_toolbar"></trix-editor>
58+
```
59+
60+
Otherwise, Trix will automatically insert a separate `<trix-toolbar>` before the editor.
5361

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

5664
## Integrating With Forms
5765

66+
There are two styles of integrating with `<form>` element submissions.
67+
68+
### Legacy integration with `<input type="hidden">`
69+
70+
Legacy support is provided through an `<input type="hidden">` element paired with an `[input]` attribute on the `<trix-editor>` element.
71+
5872
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.
5973

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

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

69-
## Populating With Stored Content
83+
#### Populating With Stored Content
7084

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

@@ -79,6 +93,71 @@ To populate a `<trix-editor>` with stored content, include that content in the a
7993

8094
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.
8195

96+
### Integration with Element Internals
97+
98+
Trix can also be configured to integrate with forms through the `<trix-editor>` element's `ElementInternals` instance.
99+
100+
First, configure Trix to opt-into its Element Internals support by rendering a `<meta>` element into the document's `<head>`:
101+
102+
```html
103+
<head>
104+
<!---->
105+
<meta name="trix-config-editor-element-internals" content="true">
106+
</head>
107+
```
108+
109+
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.
110+
111+
```html
112+
<form >
113+
<trix-editor name="content"></trix-editor>
114+
</form>
115+
```
116+
117+
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]`:
118+
119+
```html
120+
<form id="a-form-element" ></form>
121+
<trix-editor name="content" form="a-form-element"></trix-editor>
122+
```
123+
124+
#### Populating With Stored Content
125+
126+
To populate a `<trix-editor>` with stored content, include that content as HTML inside the element’s inner HTML.
127+
128+
```html
129+
<form >
130+
<trix-editor>Editor content goes here</trix-editor>
131+
</form>
132+
```
133+
134+
## Providing an Accessible Name
135+
136+
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:
137+
138+
1. render the `<trix-editor>` element with an `[id]` attribute that the `<label>` element references through its `[for]` attribute:
139+
140+
```html
141+
<label for="editor">Editor</label>
142+
<trix-editor id="editor"></trix-editor>
143+
```
144+
145+
2. render the `<trix-editor>` element as a child of the `<label>` element:
146+
147+
```html
148+
<trix-toolbar id="editor-toolbar"></trix-toolbar>
149+
<label>
150+
Editor
151+
152+
<trix-editor toolbar="editor-toolbar"></trix-editor>
153+
</label>
154+
```
155+
156+
> [!WARNING]
157+
> 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.
158+
159+
In addition to integrating with `<label>` elements, `<trix-editor>` elements support `[aria-label]` and `[aria-labelledby]` attributes.
160+
82161
## Styling Formatted Content
83162

84163
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.

assets/index.html

+22-2
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
box-sizing: border-box;
1313
}
1414

15-
main {
15+
main, nav {
1616
margin: 20px auto;
1717
max-width: 700px;
1818
}
@@ -69,10 +69,30 @@
6969
}
7070
});
7171
</script>
72+
<script>
73+
const searchParams = new URLSearchParams(location.search)
74+
75+
if (searchParams.get("editor") === "elementInternals") {
76+
document.head.insertAdjacentHTML(
77+
"beforeend",
78+
`<meta name="trix-config-editor-element-internals" content="true">`
79+
)
80+
81+
document.addEventListener("trix-change", function(event) {
82+
var input = document.getElementById("input")
83+
input.value = event.target.value
84+
})
85+
}
86+
</script>
7287
</head>
7388
<body>
89+
<nav>
90+
<a href="/">Legacy support</a>
91+
<a href="/?editor=elementInternals">ElementInternals support</a>
92+
</nav>
7493
<main>
75-
<trix-editor autofocus class="trix-content" input="input"></trix-editor>
94+
<label for="editor">Input</label>
95+
<trix-editor autofocus class="trix-content" id="editor"></trix-editor>
7696
<details id="output">
7797
<summary>Output</summary>
7898
<textarea readonly id="input"></textarea>

karma.conf.js

+5-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ const config = {
33
frameworks: [ "qunit" ],
44
files: [
55
{ pattern: "dist/test.js", watched: false },
6-
{ pattern: "src/test_helpers/fixtures/*.png", watched: false, included: false, served: true }
6+
{ pattern: "src/test/test_helpers/fixtures/*.png", watched: false, included: false, served: true }
77
],
88
proxies: {
99
"/test_helpers/fixtures/": "/base/src/test_helpers/fixtures/"
@@ -101,6 +101,10 @@ if (process.env.SAUCE_ACCESS_KEY) {
101101
}
102102
}
103103

104+
if (process.env.EDITOR_ELEMENT_INTERNALS === "true") {
105+
config.files.unshift({ pattern: "src/test/test_helpers/fixtures/element_internals.js", watched: false, included: true })
106+
}
107+
104108
function buildId() {
105109
const { GITHUB_WORKFLOW, GITHUB_RUN_NUMBER, GITHUB_RUN_ID } = process.env
106110
return GITHUB_WORKFLOW && GITHUB_RUN_NUMBER && GITHUB_RUN_ID

src/test/system/accessibility_test.js

+13-3
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import { assert, test, testGroup, triggerEvent } from "test/test_helper"
1+
import * as config from "trix/config"
2+
import { assert, test, testGroup, testIf, testUnless, triggerEvent } from "test/test_helper"
23

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

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

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

3435
label.innerHTML = "<span>New Value</span>"
3536
triggerEvent(editor, "focus")
3637
assert.equal(editor.getAttribute("aria-label"), "New Value")
3738
})
39+
40+
testIf(config.editor.elementInternals, "does not set [aria-label] for a <label> element", () => {
41+
const editor = document.getElementById("editor-with-labels")
42+
const labels = Array.from(editor.labels)
43+
const text = labels.map((label) => label.textContent.trim())
44+
45+
assert.deepEqual(text, [ "Label 1", "Label 2", "Label 3" ])
46+
assert.equal(editor.getAttribute("aria-label"), null)
47+
})
3848
})

src/test/system/custom_element_test.js

+26-24
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import * as config from "trix/config"
12
import { rangesAreEqual } from "trix/core/helpers"
23

34
import {
@@ -13,6 +14,7 @@ import {
1314
test,
1415
testGroup,
1516
testIf,
17+
testUnless,
1618
triggerEvent,
1719
typeCharacters,
1820
typeInToolbarDialog,
@@ -391,12 +393,6 @@ testGroup("Custom element API", { template: "editor_empty" }, () => {
391393
})
392394
})
393395

394-
test("element returns empty string when value is missing", async () => {
395-
const element = getEditorElement()
396-
397-
assert.equal(element.value, "")
398-
})
399-
400396
test("element serializes HTML after attribute changes", async () => {
401397
const element = getEditorElement()
402398
let serializedHTML = element.value
@@ -446,14 +442,6 @@ testGroup("Custom element API", { template: "editor_empty" }, () => {
446442
return promise
447443
})
448444

449-
test("editor resets to its original value on element reset", async () => {
450-
const element = getEditorElement()
451-
452-
await typeCharacters("hello")
453-
element.reset()
454-
expectDocument("\n")
455-
})
456-
457445
test("editor resets to its original value on form reset", async () => {
458446
const element = getEditorElement()
459447
const { form } = element
@@ -485,6 +473,26 @@ testGroup("Custom element API", { template: "editor_empty" }, () => {
485473
form.removeEventListener("reset", preventDefault, false)
486474
expectDocument("hello\n")
487475
})
476+
477+
test("editor resets to its original value on element reset", async () => {
478+
const element = getEditorElement()
479+
480+
await typeCharacters("hello")
481+
element.reset()
482+
expectDocument("\n")
483+
})
484+
485+
test("element returns empty string when value is missing", async () => {
486+
const element = getEditorElement()
487+
488+
assert.equal(element.value, "")
489+
})
490+
491+
test("editor returns its type", async() => {
492+
const element = getEditorElement()
493+
494+
assert.equal("trix-editor", element.type)
495+
})
488496
})
489497

490498
testGroup("HTML sanitization", { template: "editor_html" }, () => {
@@ -501,15 +509,15 @@ testGroup("HTML sanitization", { template: "editor_html" }, () => {
501509
testGroup("<label> support", { template: "editor_with_labels" }, () => {
502510
test("associates all label elements", () => {
503511
const labels = [ document.getElementById("label-1"), document.getElementById("label-3") ]
504-
assert.deepEqual(getEditorElement().labels, labels)
512+
assert.deepEqual(Array.from(getEditorElement().labels), labels)
505513
})
506514

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

512-
test("focuses when <label> descendant clicked", () => {
520+
testUnless(config.editor.elementInternals, "focuses when <label> descendant clicked", () => {
513521
document.getElementById("label-1").querySelector("span").click()
514522
assert.equal(getEditorElement(), document.activeElement)
515523
})
@@ -529,7 +537,7 @@ testGroup("form property references its <form>", { template: "editors_with_forms
529537
assert.equal(editor.form, form)
530538
})
531539

532-
test("transitively accesses its related <input> element's <form>", () => {
540+
test("transitively accesses its related <form>", () => {
533541
const form = document.getElementById("input-form")
534542
const editor = document.getElementById("editor-with-input-form")
535543
assert.equal(editor.form, form)
@@ -539,10 +547,4 @@ testGroup("form property references its <form>", { template: "editors_with_forms
539547
const editor = document.getElementById("editor-with-no-form")
540548
assert.equal(editor.form, null)
541549
})
542-
543-
test("editor returns its type", async() => {
544-
const element = getEditorElement()
545-
546-
assert.equal("trix-editor", element.type)
547-
})
548550
})

src/test/system/installation_process_test.js

+9-4
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1+
import * as config from "trix/config"
12
import EditorController from "trix/controllers/editor_controller"
23

3-
import { assert, test, testGroup } from "test/test_helper"
4+
import { assert, test, testGroup, testUnless } from "test/test_helper"
45
import { nextFrame } from "../test_helpers/timing_helpers"
56

67
testGroup("Installation process", { template: "editor_html" }, () => {
@@ -20,23 +21,27 @@ testGroup("Installation process", { template: "editor_html" }, () => {
2021
})
2122
})
2223

23-
testGroup("Installation process without specified elements", { template: "editor_empty" }, () =>
24-
test("creates identified toolbar and input elements", () => {
24+
testGroup("Installation process without specified elements", { template: "editor_empty" }, () => {
25+
test("creates identified toolbar", () => {
2526
const editorElement = getEditorElement()
2627

2728
const toolbarId = editorElement.getAttribute("toolbar")
2829
assert.ok(/trix-toolbar-\d+/.test(toolbarId), `toolbar id not assert.ok ${JSON.stringify(toolbarId)}`)
2930
const toolbarElement = document.getElementById(toolbarId)
3031
assert.ok(toolbarElement, "toolbar element not assert.ok")
3132
assert.equal(editorElement.toolbarElement, toolbarElement)
33+
})
34+
35+
testUnless(config.editor.elementInternals, "creates identified input elements", () => {
36+
const editorElement = getEditorElement()
3237

3338
const inputId = editorElement.getAttribute("input")
3439
assert.ok(/trix-input-\d+/.test(inputId), `input id not assert.ok ${JSON.stringify(inputId)}`)
3540
const inputElement = document.getElementById(inputId)
3641
assert.ok(inputElement, "input element not assert.ok")
3742
assert.equal(editorElement.inputElement, inputElement)
3843
})
39-
)
44+
})
4045

4146
testGroup("Installation process with specified elements", { template: "editor_with_toolbar_and_input" }, () => {
4247
test("uses specified elements", () => {
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
import * as config from "trix/config"
2+
13
export default () =>
2-
`<input id="my_input" type="hidden" value="&lt;div&gt;Hello world&lt;/div&gt;">
3-
<trix-editor input="my_input" autofocus placeholder="Say hello..."></trix-editor>`
4+
config.editor.elementInternals ?
5+
`<trix-editor autofocus placeholder="Say hello..."><div>Hello world</div></trix-editor>
6+
` :
7+
`<input id="my_input" type="hidden" value="&lt;div&gt;Hello world&lt;/div&gt;">
8+
<trix-editor input="my_input" autofocus placeholder="Say hello..."></trix-editor>`
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
1+
import * as config from "trix/config"
12
import { TEST_IMAGE_URL } from "./test_image_url"
23

34
export default () =>
4-
`<trix-editor input="my_input" autofocus placeholder="Say hello..."></trix-editor>
5-
<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;">`
5+
config.editor.elementInternals ?
6+
`<trix-editor autofocus placeholder="Say hello...">ab<img src="${TEST_IMAGE_URL}" width="10" height="10"></trix-editor>
7+
` :
8+
`<trix-editor input="my_input" autofocus placeholder="Say hello..."></trix-editor>
9+
<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;">`

0 commit comments

Comments
 (0)