Skip to content

Commit

Permalink
Integrate with ElementInternals
Browse files Browse the repository at this point in the history
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
  • Loading branch information
seanpdoyle committed Sep 28, 2024
1 parent 457a834 commit 21b2b65
Show file tree
Hide file tree
Showing 3 changed files with 39 additions and 58 deletions.
22 changes: 21 additions & 1 deletion src/test/system/custom_element_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -471,12 +471,32 @@ 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", async () => {
const element = getEditorElement()

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

test("editor returns its type", async() => {
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(getEditorElement().labels, labels)
assert.deepEqual(Array.from(getEditorElement().labels), labels)
})

test("focuses when <label> clicked", () => {
Expand Down
8 changes: 3 additions & 5 deletions src/test/test_helpers/fixtures/editor_with_labels.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
export default () =>
`<label id="label-1" for="editor"><span>Label 1</span></label>
<label id="label-2">
Label 2
<trix-editor id="editor"></trix-editor>
</label>
<label id="label-3" for="editor">Label 3</label>`
<label id="label-2">Label 2</label>
<trix-editor id="editor"></trix-editor>
<label id="label-3" for="editor">Label 3</label>`
67 changes: 15 additions & 52 deletions src/trix/elements/trix_editor_element.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import * as config from "trix/config"

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

export default class TrixEditorElement extends HTMLElement {
static formAssociated = true

#internals

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

// Properties

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

get 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
return this.#internals.labels
}

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

get type() {
return "trix-editor"
}

// Controller delegate methods

notify(message, data) {
Expand Down Expand Up @@ -269,54 +268,18 @@ 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

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

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()
formResetCallback() {
this.reset()
}

reset() {
Expand Down

0 comments on commit 21b2b65

Please sign in to comment.