Skip to content

Commit e208190

Browse files
committed
Integrate with ElementInternals
Closes [basecamp#1023][] Integrate with `<form>` elements directly 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 `<label>` element support can be achieved through [ElementInternals.labels][]. Similarly, a `formResetCallback()` will fire whenever the associated `<form>` element resets. For now, keep the changes minimal. Future changes will handle integrating with more parts of `ElementInternals`. TODO after merging: --- - [ ] Integrate with [ElementInternals.willValidate](https://developer.mozilla.org/en-US/docs/Web/API/ElementInternals/willValidate), [ElementInternals.validity](https://developer.mozilla.org/en-US/docs/Web/API/ElementInternals/validity), [ElementInternals.validationMessage](https://developer.mozilla.org/en-US/docs/Web/API/ElementInternals/validationMessage) - [ ] [Form callbacks](https://web.dev/articles/more-capable-form-controls#lifecycle_callbacks) like `void formDisabledCallback(disabled)` to support `[disabled]` - [ ] [Instance properties included from ARIA](https://developer.mozilla.org/en-US/docs/Web/API/ElementInternals#instance_properties_included_from_aria) [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
1 parent 457a834 commit e208190

File tree

4 files changed

+66
-58
lines changed

4 files changed

+66
-58
lines changed

Diff for: README.md

+27
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,33 @@ To populate a `<trix-editor>` with stored content, include that content in the a
7979

8080
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.
8181

82+
## Providing an Accessible Name
83+
84+
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:
85+
86+
1. render the `<trix-editor>` element with an `[id]` attribute that the `<label>` element references through its `[for]` attribute:
87+
88+
```html
89+
<label for="editor">Editor</label>
90+
<trix-editor id="editor"></trix-editor>
91+
```
92+
93+
2. render the `<trix-editor>` element as a child of the `<label>` element:
94+
95+
```html
96+
<trix-toolbar id="editor-toolbar"></trix-toolbar>
97+
<label>
98+
Editor
99+
100+
<trix-editor toolbar="editor-toolbar"></trix-editor>
101+
</label>
102+
```
103+
104+
> [!WARNING]
105+
> 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.
106+
107+
In addition to integrating with `<label>` elements, `<trix-editor>` elements support `[aria-label]` and `[aria-labelledby]` attributes.
108+
82109
## Styling Formatted Content
83110

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

Diff for: src/test/system/custom_element_test.js

+21-1
Original file line numberDiff line numberDiff line change
@@ -471,12 +471,32 @@ testGroup("Custom element API", { template: "editor_empty" }, () => {
471471
form.removeEventListener("reset", preventDefault, false)
472472
expectDocument("hello\n")
473473
})
474+
475+
test("editor resets to its original value on element reset", async () => {
476+
const element = getEditorElement()
477+
478+
await typeCharacters("hello")
479+
element.reset()
480+
expectDocument("\n")
481+
})
482+
483+
test("element returns empty string when value is missing", async () => {
484+
const element = getEditorElement()
485+
486+
assert.equal(element.value, "")
487+
})
488+
489+
test("editor returns its type", async() => {
490+
const element = getEditorElement()
491+
492+
assert.equal("trix-editor", element.type)
493+
})
474494
})
475495

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

482502
test("focuses when <label> clicked", () => {

Diff for: src/test/test_helpers/fixtures/editor_with_labels.js

+3-5
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
export default () =>
22
`<label id="label-1" for="editor"><span>Label 1</span></label>
3-
<label id="label-2">
4-
Label 2
5-
<trix-editor id="editor"></trix-editor>
6-
</label>
7-
<label id="label-3" for="editor">Label 3</label>`
3+
<label id="label-2">Label 2</label>
4+
<trix-editor id="editor"></trix-editor>
5+
<label id="label-3" for="editor">Label 3</label>`

Diff for: src/trix/elements/trix_editor_element.js

+15-52
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import * as config from "trix/config"
22

33
import {
4-
findClosestElementFromNode,
54
handleEvent,
65
handleEventOnce,
76
installDefaultCSSForTagName,
@@ -161,6 +160,14 @@ installDefaultCSSForTagName("trix-editor", `\
161160
}`)
162161

163162
export default class TrixEditorElement extends HTMLElement {
163+
static formAssociated = true
164+
165+
#internals
166+
167+
constructor() {
168+
super()
169+
this.#internals = this.attachInternals()
170+
}
164171

165172
// Properties
166173

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

176183
get labels() {
177-
const labels = []
178-
if (this.id && this.ownerDocument) {
179-
labels.push(...Array.from(this.ownerDocument.querySelectorAll(`label[for='${this.id}']`) || []))
180-
}
181-
182-
const label = findClosestElementFromNode(this, { matchingSelector: "label" })
183-
if (label) {
184-
if ([ this, null ].includes(label.control)) {
185-
labels.push(label)
186-
}
187-
}
188-
189-
return labels
184+
return this.#internals.labels
190185
}
191186

192187
get toolbarElement() {
@@ -238,6 +233,10 @@ export default class TrixEditorElement extends HTMLElement {
238233
this.editor?.loadHTML(this.defaultValue)
239234
}
240235

236+
get type() {
237+
return "trix-editor"
238+
}
239+
241240
// Controller delegate methods
242241

243242
notify(message, data) {
@@ -269,54 +268,18 @@ export default class TrixEditorElement extends HTMLElement {
269268
requestAnimationFrame(() => triggerEvent("trix-initialize", { onElement: this }))
270269
}
271270
this.editorController.registerSelectionManager()
272-
this.registerResetListener()
273-
this.registerClickListener()
274271
autofocus(this)
275272
}
276273
}
277274

278275
disconnectedCallback() {
279276
this.editorController?.unregisterSelectionManager()
280-
this.unregisterResetListener()
281-
return this.unregisterClickListener()
282277
}
283278

284279
// Form support
285280

286-
registerResetListener() {
287-
this.resetListener = this.resetBubbled.bind(this)
288-
return window.addEventListener("reset", this.resetListener, false)
289-
}
290-
291-
unregisterResetListener() {
292-
return window.removeEventListener("reset", this.resetListener, false)
293-
}
294-
295-
registerClickListener() {
296-
this.clickListener = this.clickBubbled.bind(this)
297-
return window.addEventListener("click", this.clickListener, false)
298-
}
299-
300-
unregisterClickListener() {
301-
return window.removeEventListener("click", this.clickListener, false)
302-
}
303-
304-
resetBubbled(event) {
305-
if (event.defaultPrevented) return
306-
if (event.target !== this.form) return
307-
return this.reset()
308-
}
309-
310-
clickBubbled(event) {
311-
if (event.defaultPrevented) return
312-
if (this.contains(event.target)) return
313-
314-
const label = findClosestElementFromNode(event.target, { matchingSelector: "label" })
315-
if (!label) return
316-
317-
if (!Array.from(this.labels).includes(label)) return
318-
319-
return this.focus()
281+
formResetCallback() {
282+
this.reset()
320283
}
321284

322285
reset() {

0 commit comments

Comments
 (0)