[DOC] skills: update odoo-javascript skill documentation

This commit is contained in:
GitHub Copilot 2026-08-15 13:00:56 +02:00
parent 158933e96e
commit 162ba0b570
6 changed files with 875 additions and 1 deletions

View file

@ -0,0 +1,245 @@
# Accessibility patterns (QWeb + CSS + JS)
WCAG 2.2 AA. Snippets use this repo's conventions: 4-space indent, Bootstrap 5.3 utilities,
translatable attributes, `window.i18nManager.get(key)` for JS strings.
## Page skeleton
```xml
<t t-call="website.layout">
<a href="#eskaera-main" class="visually-hidden-focusable btn btn-primary m-2">
Skip to main content
</a>
<div id="wrap" class="eskaera-shop-page">
<main id="eskaera-main" tabindex="-1">
<h1 class="eskaera-page-title" t-esc="page_title"/>
...
</main>
</div>
</t>
```
- Exactly one `<h1>` per page; never skip heading levels to get a font size — that's what classes are for.
- `<main>`, `<nav aria-label="...">`, `<aside>` instead of anonymous `<div>`s.
- `tabindex="-1"` on the skip-link target so focus actually lands there.
## Buttons vs links
| Intent | Element |
| --- | --- |
| Navigates to a URL | `<a href="...">` |
| Changes state on this page | `<button type="button">` |
| Submits a form | `<button type="submit">` |
Never `<div t-on-click>` / `<span onclick>` — no keyboard, no role, no focus.
Icon-only:
```xml
<button type="button" class="btn btn-outline-secondary qty-decrease"
aria-label="Decrease quantity">
<i class="fa fa-minus" aria-hidden="true"/>
</button>
```
`aria-label` is auto-translated; `aria-hidden` on the icon stops the screen reader announcing
"minus icon" twice. Alternative when the label is long: visible `<span class="visually-hidden">`.
## Quantity stepper (live-announced)
```xml
<div class="qty-control" role="group" aria-label="Quantity">
<button type="button" class="btn qty-decrease" aria-label="Decrease quantity">
<i class="fa fa-minus" aria-hidden="true"/>
</button>
<input type="number" class="form-control qty-input"
t-att-value="line.qty" min="0" step="1"
aria-label="Quantity" inputmode="numeric"/>
<button type="button" class="btn qty-increase" aria-label="Increase quantity">
<i class="fa fa-plus" aria-hidden="true"/>
</button>
</div>
<p class="visually-hidden" aria-live="polite" id="qty_status"/>
```
```js
// after the server confirms the new qty
const status = document.getElementById("qty_status");
status.textContent = window.i18nManager.get("qty_updated_announcement");
```
Buttons must stay ≥ 24×24 px (WCAG 2.2 AA §2.5.8) everywhere, and ≥ 44×44 px under
`@media (pointer: coarse)` — including at 320 px width.
## Forms and errors
```xml
<div class="mb-3">
<label class="form-label" for="delivery_note">Delivery note</label>
<textarea id="delivery_note" name="delivery_note" class="form-control"
t-att-aria-invalid="'true' if errors.get('delivery_note') else None"
t-att-aria-describedby="'delivery_note_help delivery_note_error'
if errors.get('delivery_note')
else 'delivery_note_help'"/>
<small id="delivery_note_help" class="form-text">Optional, visible to the group manager.</small>
<p t-if="errors.get('delivery_note')" id="delivery_note_error"
class="invalid-feedback d-block" t-esc="errors['delivery_note']"/>
</div>
```
- The `for`/`id` pair is mandatory; wrapping alone is not enough for every AT.
- Error text is referenced by `aria-describedby`, and never colour-only — keep the message.
- The error summary at the top of a failed form should be focused on submit
(`container.focus()` on a `tabindex="-1"` heading).
- Required fields: `required` + a visible "(required)" or `*` with a legend explaining it.
## Product card — one link, no duplicates
```xml
<article class="product-card">
<img class="product-card-img" t-att-src="product['image_url']"
t-att-alt="product['name']" loading="lazy" width="400" height="400"/>
<h3 class="product-card-title">
<a t-att-href="product['url']" class="stretched-link" t-esc="product['name']"/>
</h3>
<p class="product-card-price">
<span class="visually-hidden">Price</span>
<span t-esc="product['price_formatted']"/>
</p>
</article>
```
- `stretched-link` makes the whole card clickable with a **single** link in the a11y tree — do not
also wrap the image in an `<a>`, that produces two identical stops.
- `width`/`height` reserve space (no layout shift); CSS `object-fit: cover` handles the ratio.
- Decorative product placeholder → `alt=""`.
## Filters as toggle buttons
```xml
<div class="tag-filter" role="group" aria-label="Filter by category">
<button t-foreach="categories" t-as="cat" t-key="cat['id']"
type="button" class="tag-filter-badge"
t-att-aria-pressed="'true' if cat['active'] else 'false'"
t-att-data-category-id="cat['id']">
<span t-esc="cat['name']"/>
</button>
</div>
```
JS flips `aria-pressed` together with the active class — the class alone announces nothing.
If the filters are a true multi-select set, `<fieldset><legend>` + checkboxes is better still.
## Search with results announcement
```xml
<label class="visually-hidden" for="product_search">Search products</label>
<input id="product_search" type="search" class="form-control"
placeholder="Search products…" autocomplete="off"
aria-describedby="search_results_count"/>
<p id="search_results_count" class="visually-hidden" aria-live="polite" aria-atomic="true"/>
```
Debounce ≥ 200 ms, then set the text once per settled query — announcing on every keystroke makes
the page unusable with a screen reader.
## Loading, infinite scroll, empty states
```xml
<div id="products_grid" class="products-grid" aria-busy="false"></div>
<p class="visually-hidden" aria-live="polite" id="load_status"/>
<button type="button" id="load_more" class="btn btn-outline-primary">Load more products</button>
```
- Set `aria-busy="true"` on the grid while fetching, back to `false` when done.
- Announce `"N more products loaded"` via `load_status`; don't move focus mid-scroll.
- **Always keep a real "Load more" button** alongside scroll-triggered loading — infinite scroll
alone is unreachable by keyboard and traps users away from the footer.
- Empty state is content, not a blank div: heading + explanation + the action that fixes it.
## Modals
Use Bootstrap's modal (focus trap, Esc, `aria-modal` come free) and give it a label:
```xml
<div class="modal fade" id="confirm_order_modal" tabindex="-1"
aria-labelledby="confirm_order_title" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h2 class="modal-title fs-5" id="confirm_order_title">Confirm your order</h2>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"/>
</div>
</div>
</div>
</div>
```
Return focus to the trigger on close (Bootstrap does this if the trigger is a real button).
## Order tables
```xml
<table class="table checkout-summary-table">
<caption class="visually-hidden">Order lines</caption>
<thead>
<tr>
<th scope="col">Product</th>
<th scope="col" class="text-end">Quantity</th>
<th scope="col" class="text-end">Subtotal</th>
</tr>
</thead>
<tbody>
<tr t-foreach="lines" t-as="line" t-key="line['id']">
<th scope="row" t-esc="line['name']"/>
<td class="text-end" t-esc="line['qty']"/>
<td class="text-end" t-esc="line['subtotal_formatted']"/>
</tr>
</tbody>
</table>
```
Never use a table for layout, and never drop `<thead>`/`scope` when stacking to cards on mobile —
restyle, don't restructure.
## Focus and motion in CSS
```css
/* One ring, everywhere, on keyboard focus only. */
.eskaera-page :focus-visible {
outline: 2px solid var(--ac-color-focus, var(--focus-ring-color, #0d6efd));
outline-offset: 2px;
border-radius: var(--ac-radius-sm);
}
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
scroll-behavior: auto !important;
}
}
```
This is the one legitimate `!important` in the codebase — a user preference must beat every component.
## Multilingual
The addon runs es / eu / ca. `website.layout` sets `<html lang>`; when a string stays in another
language inside a translated page (a supplier name, a product origin), mark it: `<span lang="eu">`.
Never build a sentence by concatenating translated fragments — pass the whole string with `%s`
placeholders so translators can reorder.
## Verification
1. **Keyboard only**: Tab through the page. Visible focus at every stop, order matches the visual
order, no trap, every action reachable, Esc closes overlays.
2. **Zoom 200 %** and **320 px width**: no horizontal scroll, nothing clipped.
3. **Contrast**: check rendered colours, including disabled states and placeholder text.
4. **Screen reader** smoke test on the cart flow (Orca on Linux, NVDA on Windows).
5. Lighthouse/axe in DevTools catches the mechanical half — it does not catch focus order,
announcement quality, or a label that lies.