-
Notifications
You must be signed in to change notification settings - Fork 424
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
While it borrows from [targets][] and [outlets][], the idea of `references` also draws inspiration from the WAI ARIA concept of an [ID Ref and ID Ref List][aria-ref] attribute, like: * [aria-activedescendant](https://www.w3.org/TR/wai-aria-1.2/#aria-activedescendant) * [aria-controls](https://www.w3.org/TR/wai-aria-1.2/#aria-controls) * [aria-describedby](https://www.w3.org/TR/wai-aria-1.2/#aria-describedby) * [aria-details](https://www.w3.org/TR/wai-aria-1.2/#aria-details) * [aria-errormessage](https://www.w3.org/TR/wai-aria-1.2/#aria-errormessage) * [aria-flowto](https://www.w3.org/TR/wai-aria-1.2/#aria-flowto) * [aria-labelledby](https://www.w3.org/TR/wai-aria-1.2/#aria-labelledby) * [aria-owns](https://www.w3.org/TR/wai-aria-1.2/#aria-owns) Providing built-in support from Stimulus for elements that a controller establishes an [`[id]`-based relationship][id-relationship] with through ARIA attributes could cultivate a virtuous cycle between assistive technologies (reliant on semantics and document-hierarchy driven relationships) and client-side feature development (reliant on low-friction DOM traversal and state change callbacks). [targets]: https://stimulus.hotwired.dev/reference/targets [outlets]: https://stimulus.hotwired.dev/reference/outlets [aria-ref]: https://www.w3.org/TR/wai-aria-1.2/#propcharacteristic_value [id-relationship]: https://www.w3.org/TR/wai-aria-1.2/#attrs_relationships
- Loading branch information
1 parent
2ff5440
commit da0f125
Showing
12 changed files
with
658 additions
and
5 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,6 +1,6 @@ | ||
--- | ||
permalink: /reference/css-classes.html | ||
order: 06 | ||
order: 07 | ||
--- | ||
|
||
# CSS Classes | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,141 @@ | ||
--- | ||
permalink: /reference/references.html | ||
order: 05 | ||
--- | ||
|
||
# References | ||
|
||
_References_ provide direct access to _elements_ within (and without!) a Controller's scope based on their `[id]` attribute's value. | ||
|
||
They are conceptually similar to [Stimulus Targets](https://stimulus.hotwired.dev/reference/targets) and [Stimulus Outlets](https://stimulus.hotwired.dev/reference/outlets), but provide access regardless of where they occur in the document. | ||
|
||
[aria-ref]: https://www.w3.org/TR/wai-aria-1.2/#propcharacteristic_value | ||
[id-relationship]: https://www.w3.org/TR/wai-aria-1.2/#attrs_relationships | ||
|
||
<meta data-controller="callout" data-callout-text-value='aria-controls="accordion"'> | ||
<meta data-controller="callout" data-callout-text-value='id="accordion"'> | ||
|
||
|
||
```html | ||
<button aria-controls="accordion" aria-expanded="false" | ||
data-controller="disclosure" data-action="click->disclosure#toggle"> | ||
Show #accordion | ||
</button> | ||
|
||
... | ||
|
||
<div id="accordion" hidden> | ||
... | ||
</div> | ||
``` | ||
|
||
While a **target** is a specifically marked element **within the scope** of its own controller element, a **reference** can be located **anywhere on the page**. | ||
|
||
|
||
## Definitions | ||
|
||
A Controller class can define a `static references` array to declare which of | ||
its element's attribute names to use to resolve its references. | ||
|
||
By default, a Controller's `static references` property is defined to include a | ||
list of [ARIA ID reference and ID reference list attributes][aria-ref] to | ||
establish [`[id]`-based relationships][id-relationship] out-of-the-box, | ||
including: | ||
|
||
* [aria-activedescendant](https://www.w3.org/TR/wai-aria-1.2/#aria-activedescendant) | ||
* [aria-controls](https://www.w3.org/TR/wai-aria-1.2/#aria-controls) | ||
* [aria-describedby](https://www.w3.org/TR/wai-aria-1.2/#aria-describedby) | ||
* [aria-details](https://www.w3.org/TR/wai-aria-1.2/#aria-details) | ||
* [aria-errormessage](https://www.w3.org/TR/wai-aria-1.2/#aria-errormessage) | ||
* [aria-flowto](https://www.w3.org/TR/wai-aria-1.2/#aria-flowto) | ||
* [aria-labelledby](https://www.w3.org/TR/wai-aria-1.2/#aria-labelledby) | ||
* [aria-owns](https://www.w3.org/TR/wai-aria-1.2/#aria-owns) | ||
|
||
|
||
Define attribute names in your controller class using the `static references` array: | ||
|
||
<meta data-controller="callout" data-callout-text-value='static references'> | ||
<meta data-controller="callout" data-callout-text-value='"aria-controls"'> | ||
|
||
|
||
```js | ||
// disclosure_controller.js | ||
|
||
export default class extends Controller { | ||
static references = [ "aria-controls" ] | ||
|
||
toggle() { | ||
const expanded = this.element.getAttribute("aria-expanded") | ||
|
||
for (const ariaControlsReference of this.ariaControlsReferences) { | ||
ariaControlsReference.hidden = expanded != "true" | ||
} | ||
} | ||
} | ||
``` | ||
|
||
## Properties | ||
|
||
For each attribute name defined in the `static references` array, Stimulus adds three properties to your controller, where `[name]` corresponds to an attribute's name: | ||
|
||
| Kind | Property name | Return Type | Effect | ||
| ---- | ------------- | ----------- | ----------- | ||
| Existential | `has[Name]Reference` | `Boolean` | Tests for presence of an element with `[id="${name}"]` | ||
| Singular | `[name]Reference` | `Element` | Returns the first `Element` whose `[id]` value is included in the `[name]` attribute's token or throws an exception if none are present | ||
| Plural | `[name]References` | `Array<Element>` | Returns all `Element`s whose `[id]` values are included in the `[name]` attribute's tokens | ||
|
||
Kebab-case attribute names will be transformed to camelCase and TitleCase. For example, `aria-controls` will transform into `ariaControls` and `AriaControls`. | ||
|
||
## Reference Callbacks | ||
|
||
Reference callbacks are specially named functions called by Stimulus to let you respond to whenever a referenced element is added or removed from the document. | ||
|
||
To observe reference changes, define a method named `[name]ReferenceConnected()` or `[name]ReferenceDisconnected()`. | ||
|
||
<meta data-controller="callout" data-callout-text-value='"aria-activedescendant"'> | ||
<meta data-controller="callout" data-callout-text-value="ariaActivedescendantReferenceConnected(element)"> | ||
<meta data-controller="callout" data-callout-text-value="ariaActivedescendantReferenceDisconnected(element)"> | ||
|
||
```js | ||
// combobox_controller.js | ||
|
||
export default class extends Controller { | ||
static references = [ "aria-activedescendant" ] | ||
static target = [ "selected" ] | ||
|
||
ariaActivedescendantReferenceConnected(element) { | ||
this.selectedTarget.innerHTML = element.textContent | ||
} | ||
|
||
ariaActivedescendantReferenceDisconnected(element) { | ||
this.selectedTarget.innerHTML = "No selection" | ||
} | ||
} | ||
``` | ||
|
||
### References are Assumed to be Present | ||
|
||
When you access a Reference property in a Controller, you assert that at least one corresponding Reference is present. If the declaration is missing and no matching reference is found Stimulus will throw an exception: | ||
|
||
```html | ||
Missing element referenced by "[aria-controls]" for "disclosure" controller | ||
``` | ||
|
||
### Optional references | ||
|
||
If a Reference is optional or you want to assert that at least one Reference is present, you must first check the presence of the Reference using the existential property: | ||
|
||
```js | ||
if (this.hasAriaControlsReference) { | ||
this.safelyCallSomethingOnTheReference(this.ariaControlsReference) | ||
} | ||
``` | ||
|
||
Alternatively, looping over an empty Array of references would have the same | ||
result: | ||
|
||
```js | ||
for (const ariaControlsReference of this.ariaControlsReferences) { | ||
this.safelyCallSomethingOnTheReference(this.ariaControlsReference) | ||
} | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,6 +1,6 @@ | ||
--- | ||
permalink: /reference/using-typescript.html | ||
order: 07 | ||
order: 08 | ||
--- | ||
|
||
# Using Typescript | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,122 @@ | ||
import { Multimap } from "../multimap" | ||
import { ElementObserver, ElementObserverDelegate } from "../mutation-observers/element_observer" | ||
import { | ||
Token, | ||
TokenListObserver, | ||
TokenListObserverDelegate, | ||
parseTokenString, | ||
} from "../mutation-observers/token_list_observer" | ||
|
||
export interface ReferenceObserverDelegate { | ||
referenceConnected(element: Element, attributeName: string): void | ||
referenceDisconnected(element: Element, attributeName: string): void | ||
} | ||
|
||
export class ReferenceObserver implements ElementObserverDelegate, TokenListObserverDelegate { | ||
readonly delegate: ReferenceObserverDelegate | ||
readonly element: Element | ||
readonly root: Document | ||
readonly attributeNames: string[] | ||
readonly elementObserver: ElementObserver | ||
readonly tokenListObservers: TokenListObserver[] = [] | ||
readonly elementsByAttributeName = new Multimap<string, Element>() | ||
|
||
constructor(element: Element, root: Document, attributeNames: string[], delegate: ReferenceObserverDelegate) { | ||
this.delegate = delegate | ||
this.element = element | ||
this.root = root | ||
this.attributeNames = attributeNames | ||
|
||
this.elementObserver = new ElementObserver(root.body, this) | ||
for (const attributeName of attributeNames) { | ||
this.tokenListObservers.push(new TokenListObserver(element, attributeName, this)) | ||
} | ||
} | ||
|
||
start() { | ||
if (this.elementObserver.started) return | ||
|
||
this.elementObserver.start() | ||
for (const observer of this.tokenListObservers) observer.start() | ||
} | ||
|
||
stop() { | ||
if (this.elementObserver.started) { | ||
this.disconnectAllElements() | ||
for (const observer of this.tokenListObservers) observer.stop() | ||
this.elementObserver.stop() | ||
} | ||
} | ||
|
||
// Element observer delegate | ||
|
||
matchElement(element: Element) { | ||
return element.hasAttribute("id") | ||
} | ||
|
||
matchElementsInTree(tree: Element) { | ||
const match = this.matchElement(tree) ? [tree] : [] | ||
const matches = Array.from(tree.querySelectorAll("[id]")) | ||
return match.concat(matches) | ||
} | ||
|
||
elementMatched(element: Element) { | ||
for (const attributeName of this.attributeNames) { | ||
const tokens = this.element.getAttribute(attributeName) || "" | ||
for (const token of parseTokenString(tokens, this.element, attributeName)) { | ||
if (token.content == element.id) this.connectReference(element, attributeName) | ||
} | ||
} | ||
} | ||
|
||
elementUnmatched(element: Element) { | ||
for (const attributeName of this.attributeNames) { | ||
const tokens = this.element.getAttribute(attributeName) || "" | ||
for (const token of parseTokenString(tokens, this.element, attributeName)) { | ||
if (token.content == element.id) this.disconnectReference(element, attributeName) | ||
} | ||
} | ||
} | ||
|
||
elementAttributeChanged() {} | ||
|
||
// Token list observer delegate | ||
|
||
tokenMatched({ element, attributeName, content }: Token) { | ||
if (element == this.element && this.attributeNames.includes(attributeName)) { | ||
const relatedElement = this.root.getElementById(content) | ||
|
||
if (relatedElement) this.connectReference(relatedElement, attributeName) | ||
} | ||
} | ||
|
||
tokenUnmatched({ element, attributeName, content }: Token) { | ||
if (element == this.element && this.attributeNames.includes(attributeName)) { | ||
const relatedElement = this.root.getElementById(content) | ||
|
||
if (relatedElement) this.disconnectReference(relatedElement, attributeName) | ||
} | ||
} | ||
|
||
private connectReference(element: Element, attributeName: string) { | ||
if (!this.elementsByAttributeName.has(attributeName, element)) { | ||
this.elementsByAttributeName.add(attributeName, element) | ||
this.delegate.referenceConnected(element, attributeName) | ||
} | ||
} | ||
|
||
private disconnectReference(element: Element, attributeName: string) { | ||
if (this.elementsByAttributeName.has(attributeName, element)) { | ||
this.elementsByAttributeName.delete(attributeName, element) | ||
this.delegate.referenceDisconnected(element, attributeName) | ||
} | ||
} | ||
|
||
private disconnectAllElements() { | ||
for (const attributeName of this.elementsByAttributeName.keys) { | ||
for (const element of this.elementsByAttributeName.getValuesForKey(attributeName)) { | ||
this.disconnectReference(element, attributeName) | ||
} | ||
} | ||
} | ||
} |
Oops, something went wrong.