+ `;
+ htmx.process(playground());
+ let button = playground().querySelector('button');
+ button.click();
+ playground().querySelector('div').getAttribute('aria-valuenow').should.equal('51');
+ playground().querySelector('div').hasAttribute('aria-current').should.equal(false);
+ button.getAttribute('aria-label').should.equal('Save');
+ });
+
+ it('q(this).aria only accesses the current element', function() {
+ playground().innerHTML = `
+
+
+
+ `;
+ htmx.process(playground());
+ let button = playground().querySelector('button');
+ button.click();
+ window.__localState.should.deep.equal([undefined, true]);
+ window.__localAfter.should.equal(false);
+ button.getAttribute('aria-busy').should.equal('false');
+ playground().querySelector('section').getAttribute('aria-busy').should.equal('true');
+ delete window.__localState;
+ delete window.__localAfter;
+ });
+
+ it('q(this).aria preserves application element properties after await', async function() {
+ playground().innerHTML = `
+
+
+
+ `;
+ htmx.process(playground());
+ let button = playground().querySelector('button');
+ let applicationState = { owner: 'app' };
+ button.aria = applicationState;
+ button.click();
+ await htmx.timeout(10);
+ window.__sameThis.should.equal(true);
+ window.__closestId.should.equal('owner');
+ button.aria.should.equal(applicationState);
+ button.getAttribute('aria-busy').should.equal('true');
+ delete window.__sameThis;
+ delete window.__closestId;
+ });
+
// -------------------------------------------------------------------------
// cascading data proxy
// -------------------------------------------------------------------------
@@ -1704,7 +1957,7 @@ describe('hx-live extension', function () {
btn.hasAttribute('disabled').should.equal(true);
});
- it(':aria-expanded writes "true"/"false", never removes', async function() {
+ it(':aria-expanded writes boolean strings', async function() {
playground().innerHTML = `
diff --git a/www/src/content/extensions/06-hx-live.md b/www/src/content/extensions/06-hx-live.md
index 1680fedde..7fa6a7400 100644
--- a/www/src/content/extensions/06-hx-live.md
+++ b/www/src/content/extensions/06-hx-live.md
@@ -242,11 +242,15 @@ attr('.active') // has class .active?
attr('.active', q('#src').checked) // add/remove class
attr('class', 'foo bar') // multi-class string
attr('class', { active: matches('.tab') }) // multi-class object
-attr('aria-expanded', matches('.open')) // any aria-*: writes "true"/"false"
+attr('aria-expanded') // raw string or null
+attr('aria-expanded', false) // write "false"
+attr('aria-expanded', null) // remove
attr('value', 'hello') // value/checked/selected: syncs property + attribute
attr('data-x', null) // remove
```
+Use [`aria.*`](#aria) to read booleans, numbers, and lists.
+
### `toggle(name, values?)`
Toggle (no `values`) or cycle (with `values`) a class or attribute on this element.
@@ -272,6 +276,126 @@ take('aria-current', 'nav a') // become the current nav item
take('.active') // implicit scope: parent element's subtree
```
+### `aria`
+
+Read and write ARIA attributes on this element or an ancestor:
+
+```html
+
+
+
+
+```
+
+Both `aria.busy` expressions use `aria-busy` on the div. If no element has the attribute, a write adds it to the current element:
+
+```html
+
+
+
+
+
+```
+
+Use bare `aria` for shared state. Use `q()` for one element:
+
+```js
+aria.busy // closest aria-busy, starting at this
+q(this).aria.busy // aria-busy on this
+q('#form').aria.busy // aria-busy on the selected form
+```
+
+Each form uses the same value rules. You can use these values as booleans, numbers, and arrays:
+
+```html
+
+
+...
+
...
+
+
+```
+
+After one click:
+
+```html
+
+
+```
+
+Use either form to remove an attribute:
+
+```js
+aria.current = null
+delete aria.current
+```
+
+#### Value types
+
+hx-live uses the value types from [WAI-ARIA 1.2](https://www.w3.org/TR/wai-aria-1.2/).
+
+**Boolean**
+
+- `aria-atomic`
+- `aria-busy`
+- `aria-checked`
+- `aria-current`
+- `aria-disabled`
+- `aria-expanded`
+- `aria-grabbed`
+- `aria-haspopup`
+- `aria-hidden`
+- `aria-invalid`
+- `aria-modal`
+- `aria-multiline`
+- `aria-multiselectable`
+- `aria-pressed`
+- `aria-readonly`
+- `aria-required`
+- `aria-selected`
+
+**Number**
+
+- `aria-colcount`
+- `aria-colindex`
+- `aria-colspan`
+- `aria-level`
+- `aria-posinset`
+- `aria-rowcount`
+- `aria-rowindex`
+- `aria-rowspan`
+- `aria-setsize`
+- `aria-valuemax`
+- `aria-valuemin`
+- `aria-valuenow`
+
+**Token list (`string[]`)**
+
+- `aria-dropeffect`
+- `aria-relevant`
+
+**ID reference list (`string[]`)**
+
+- `aria-controls`
+- `aria-describedby`
+- `aria-flowto`
+- `aria-labelledby`
+- `aria-owns`
+
+All other `aria-*` attributes remain strings.
+
+You can use `aria.*` in `hx-live`, bindings, `hx-on`, `js:` attribute values, and `hx-trigger` filters.
+
### `data`
Read or write `data-*` attributes on the closest ancestor that has them. Lets components share state up the tree.
@@ -455,7 +579,7 @@ For a single inline section, native [``](https://developer.mozilla.org/
-
+
```
**Toggle button.**