dashorst commented on issue #1838:
URL: https://github.com/apache/wicket/issues/1838#issuecomment-6705995932

   ## Audit results: WCAG 2.2 AA baseline of wicket-core and wicket-extensions
   
   Measured on `master` at `987618e3ee` (2026-11-07).
   
   Six components or behaviours stop some users outright: ModalDialog in its 
default configuration, the Palette buttons, AjaxEditableLabel, RatingPanel, 
KittenCaptchaPanel, and links attached to non-interactive tags. Most other 
problems come from one gap that runs through both modules: they render almost 
no state or relationship semantics. Sort order, selected tab, expanded tree 
node, current page and field-in-error are expressed only as CSS classes. After 
an Ajax update, focus survives only when the focused element keeps its markup 
id. Nothing is ever announced. Outside the autocomplete, no component in either 
module renders a `role` or `aria-*` attribute, and the autocomplete still has 
two ARIA errors.
   
   Some things already hold up and should be kept. No shipped stylesheet 
suppresses the focus outline, so every focusable element showed the browser's 
focus ring. `ModalDialog.trapFocus()` moves focus in, wraps Tab and returns 
focus to the opener. `AutoCompleteTextField` is close to the ARIA 1.2 combobox 
pattern. `HeadersToolbar` renders `<th scope="col">`. `FormComponentLabel` and 
`wicket:for` produce working `<label for>` in the common order, and 
`RadioChoice` and `CheckBoxMultipleChoice` give every option its own label. 
Wicket's Ajax layer restores focus when the replaced element keeps its id.
   
   ### Slices
   
   | # | Slice | Findings | Worst severity |
   |---|---|---|---|
   | 1 | Forms and feedback | F01–F06 | hinders use |
   | 2 | Page structure | F07–F09 | **blocks use** (links on non-interactive 
tags) |
   | 3 | Ajax focus and announcements | F10–F13 | **blocks use** 
(AjaxEditableLabel) |
   | 4 | DataTable and navigators | F14–F17 | hinders use |
   | 5 | ModalDialog | F18–F19 | **blocks use** (default configuration) |
   | 6 | Tabs | F20 | hinders use |
   | 7 | Tree | F21–F23 | hinders use |
   | 8 | Autocomplete | F24–F25 | minor |
   | 9 | Wizard | F26 | minor |
   | 10 | Upload | F27–F28 | hinders use |
   | 11 | The rest: Palette, date/time fields, RatingPanel, KittenCaptchaPanel, 
BreadCrumbBar | F29–F33 | **blocks use** (Palette, RatingPanel, 
KittenCaptchaPanel) |
   
   ### Method
   
   Each component was rendered on a local harness of test pages: forms, links, 
Ajax, tables, modal, tabs, trees, autocomplete, wizard, upload and 
miscellaneous components, plus the built-in error pages. Every page has `lang`, 
a title, an `<h1>` and a `<main>`, so any page-level failure comes from a 
component. The pages use the components' default markup and CSS. The harness is 
not committed.
   
   - **Automated:** axe-core 4.10.3 in Playwright on Chromium, with the tags 
`wcag2a`, `wcag2aa`, `wcag21a`, `wcag21aa` and `wcag22aa`. It covered 33 
states: initial, failed submit (full-page and Ajax), Ajax-sorted and Ajax-paged 
tables, an empty table, dialogs open, tabs switched, trees expanded, the 
autocomplete open, wizard steps and errors, an upload row added, a breadcrumb 
navigated, and the error pages.
   - **Keyboard:** scripted in Playwright. It recorded tab order, the focus 
ring, target size, and where focus lands after each Ajax replace and each 
dialog open and close.
   - **Patterns:** tabs, dialog, tree, combobox and breadcrumb were compared 
with the WAI-ARIA Authoring Practices.
   - **Limits:** no real screen reader was used, so findings about what gets 
announced are inferred from the accessibility tree and the markup. Only 
Chromium was covered.
   
   "Markup change" says whether a fix would change what applications see: new 
or changed elements, attributes or classes that CSS selectors or `TagTester` 
assertions may rely on.
   
   ### Framework findings
   
   The findings are grouped by area. Each one gives what the component renders, 
the success criterion it fails, its severity, the source location, a proposed 
fix, and whether the fix changes markup.
   
   <details>
   <summary><b>Forms and feedback</b> (6 findings)</summary>
   
   **F01: Label placed after its component points at a missing id 
(`wicket:for`, `FormComponentLabel`)**
   - Renders, on first render, for `<input type="checkbox" wicket:id="agree"> 
<label wicket:for="agree">I agree</label>`:
     ```html
     <input type="checkbox" name="agree">
     <label id="ida-w-lbl" for="ida" class="required">I agree</label>
     ```
   - The resolver calls `setOutputMarkupId(true)` only when it reaches the 
label. By then the input has already rendered without an id. Only a warning is 
logged, and the second render is correct. Label-after is the normal order for 
checkboxes and radios.
   - Fails: 1.3.1 Info and Relationships; 4.1.2 Name, Role, Value (axe `label`, 
critical).
   - Severity: hinders use. The checkbox has no accessible name, and clicking 
the label does nothing.
   - Source: `wicket-core/.../markup/html/form/AutoLabelResolver.java:121-130`.
   - Fix: make components referenced by `wicket:for` output their markup id 
before rendering starts, for example with a markup-load-time pass or 
`onInitialize` resolution of `wicket:for` targets.
   - Markup change: yes. The referenced component gains an `id` attribute on 
first render.
   
   **F02: Validation messages name the component id, not the visible label**
   - With `<label wicket:for="name">Name</label>` and no explicit `setLabel`, 
the FeedbackPanel says `'name' is required.`, and an `AjaxEditableLabel` says 
`'editor' is required.` Only `<wicket:label>` copies the label text into the 
label model.
   - Fails: 3.3.1 Error Identification, because the field in error is not 
identifiable for users who don't know the developer's ids; 3.3.3 is related.
   - Severity: hinders use.
   - Source: `AutoLabelTextResolver.java:191-197` handles only 
`<wicket:label>`. `AutoLabelResolver` does not set the label model. 
`AjaxEditableLabel` never gives its editor a label unless `setLabel` is called 
(`AjaxEditableLabel.java:235-237`).
   - Fix: let `wicket:for` set the label model from the label's body text when 
the component has no label. `AjaxEditableLabel` should default the editor's 
label to something meaningful, or the docs should require `setLabel`.
   - Markup change: no. The text of feedback messages changes.
   
   **F03: Field errors are not programmatically tied to the field**
   - After a failed submit, the field and its label get class `error`. Nothing 
renders `aria-invalid`, and the message in `FeedbackPanel` is not referenced 
from the field through `aria-describedby`.
   - Fails: 1.3.1 Info and Relationships, and 3.3.1 Error Identification in 
spirit. The text exists, but a screen reader user on the field doesn't hear it.
   - Severity: hinders use.
   - Source: `FormComponent.onComponentTag` (`FormComponent.java` ~1406). No 
`aria-invalid` exists anywhere in core.
   - Fix: render `aria-invalid="true"` on invalid form components. Offer opt-in 
`aria-describedby` wiring, either from 
`FormComponentFeedbackBorder`/`FencedFeedbackPanel` to the field or through a 
small `ErrorMessageBehavior`.
   - Markup change: yes. `aria-invalid` and `aria-describedby` attributes are 
added.
   
   **F04: FeedbackPanel content is not announced after an Ajax submit**
   - Renders:
     ```html
     <ul class="feedbackPanel"><li class="feedbackPanelERROR"><span>'name' is 
required.</span></li>…</ul>
     ```
     The `<ul>` is invisible until there are messages. With an Ajax submit, 
focus stays on the button and nothing is announced.
   - Fails: 4.1.3 Status Messages.
   - Severity: hinders use.
   - Source: `wicket-core/.../markup/html/panel/FeedbackPanel.html`, and 
`FeedbackPanel.java:142-150` (the container itself toggles visibility).
   - Fix: render a permanent wrapper with `role="alert"` for error level or 
`role="status"` for others, or with `aria-live`, and fill it. Alternatively 
move focus to the panel on Ajax errors, as an opt-in. The wrapper has to exist 
before the messages arrive, so `setVisible(anyMessage())` has to move from the 
wrapper to the list.
   - Markup change: yes. There is a new wrapper element or attribute, and the 
panel's outer tag becomes visible when empty.
   
   **F05: Error indicators are a bare asterisk**
   - `FormComponentFeedbackBorder` and `FormComponentFeedbackIndicator` render 
`<span class="feedbackBorderERROR">*</span>`. It is announced as "star", and 
the meaning is carried only by its colour or position.
   - Fails: 1.1.1 Non-text Content; 1.3.1 Info and Relationships.
   - Severity: minor.
   - Source: `FormComponentFeedbackBorder.html` and 
`FormComponentFeedbackIndicator.html`; class set at 
`FormComponentFeedbackBorder.java:83` and 
`FormComponentFeedbackIndicator.java:80`.
   - Fix: give the span a text alternative from a resource key (visually hidden 
text or `title` plus `aria-hidden` on the glyph). Or render the first error 
message.
   - Markup change: yes, a new child or attribute.
   
   **F06: `required` is exposed only on text components**
   - `TextField` and `TextArea` get `required="required"` (new in WICKET-7191). 
`DropDownChoice`, `CheckBox`, `RadioChoice`, `CheckBoxMultipleChoice`, 
`ListMultipleChoice` and `RadioGroup` render nothing for a required state. The 
asterisk class `required` on the label is visual only.
   - Fails: 1.3.1 Info and Relationships; 3.3.2 Labels or Instructions.
   - Severity: minor.
   - Source: `AbstractTextComponent.java:142-170` is the only place that sets 
it.
   - Fix: put `required` on `<select>` and the checkbox `<input>`, and 
`aria-required="true"` on the group container of RadioChoice, 
CheckBoxMultipleChoice, RadioGroup and CheckGroup. This also turns on the 
browser's native constraint validation for those elements, which applications 
will notice in the same way they noticed it for text fields.
   - Markup change: yes.
   
   The same commit also writes the label text into `placeholder` whenever a 
label model is set (`AbstractTextComponent.java:151-155`). That is not a WCAG 
failure, but it duplicates the visible label in every labelled text field. 
Combined with F02's fix, every `wicket:for` field would get a placeholder. 
Decide that together.
   
   </details>
   <details>
   <summary><b>Links and page structure</b> (3 findings)</summary>
   
   **F07: `Link`, `AjaxLink` and `ExternalLink` on non-interactive tags are 
mouse-only**
   - `<span wicket:id="link">` and `<div wicket:id="link">` render as `<span 
id="id1">Link on span</span>`, with a click handler only: no `role`, no 
`tabindex`, no key handler. They are absent from the tab order (tabIndex −1 in 
the run).
   - Fails: 2.1.1 Keyboard; 4.1.2 Name, Role, Value.
   - Severity: blocks use for keyboard and screen reader users of that control. 
The developer chose the tag, but Wicket accepts it silently.
   - Source: `Link.java:426-437`; `ExternalLink.java:202-214`; 
`AjaxLink.java:115-127` (handles a, area, link and button only). 
`AjaxFallbackLink` already rejects other tags (`AjaxFallbackLink.java:145-154`).
   - Fix: either reject non-interactive tags as `AjaxFallbackLink` does, or 
render `role="link"` (or `role="button"` for AjaxLink), `tabindex="0"` and an 
Enter/Space handler. A development-mode warning is the minimal step.
   - Markup change: yes with the attributes. An exception would break 
applications that do this today.
   
   **F08: Disabled links and the current page in navigators have no state**
   - A disabled `Link`/`AjaxLink` on `<a>` renders `<a>Disabled Link</a>`: no 
href, so it is plain text with no "disabled" or "current" state. 
`PagingNavigator`'s current page, `BreadCrumbBar`'s current crumb and the tree 
junction of a leaf all use this.
   - Fails: 1.3.1 Info and Relationships. The current page is shown only by the 
absence of a link.
   - Severity: minor.
   - Source: `AbstractLink.java:94-107` (`disableLink`); 
`PagingNavigationLink.java:56,135-138`.
   - Fix: for navigation, render `aria-current="page"` on the current page link 
and crumb, keeping or dropping the href by choice. For generic disabled links, 
`role="link" aria-disabled="true"`.
   - Markup change: yes, attributes.
   
   **F09: Built-in error pages lack `lang` and fail contrast**
   - `PageExpiredErrorPage`, `AccessDeniedPage`, `InternalErrorPage` and 
`ExceptionErrorPage` render `<html>` without `lang`, under an HTML 4.01 
doctype. Their headings are `#E9601A` on white (3.4:1) at 1.5em of a 9pt base, 
which is below the large-text size.
   - Fails: 3.1.1 Language of Page (axe `html-has-lang`); 1.4.3 Contrast 
(Minimum) (axe `color-contrast`).
   - Severity: hinders use. These pages show in production.
   - Source: 
`wicket-core/.../markup/html/pages/{PageExpiredErrorPage,InternalErrorPage}.html:18-20`,
 `{AccessDeniedPage,ExceptionErrorPage}.html:2-4`; `pages/error.css:25-27`.
   - Fix: switch to the HTML5 doctype, render `lang` from the session locale 
through `<html wicket:id>` or a page-level attribute modifier, darken the 
heading colour (for example `#B34700` is 5.5:1), and raise the base font size.
   - Markup change: yes. Doctype and `lang` change, plus CSS.
   
   </details>
   <details>
   <summary><b>Ajax: focus, busy state and announcements</b> (4 
findings)</summary>
   
   **F10: Focus is lost when an Ajax update replaces the focused element under 
a new id**
   - Repeater children get fresh markup ids on every render. Focus went to 
`<body>` after Enter on an `AjaxFallbackDefaultDataTable` sort header (`id63` 
became `id86`) and after Enter on an `AjaxTabbedPanel` tab (`ida1` became 
`ida5`). It also went to `<body>` after an `AjaxLink` hid itself, after 
`AjaxEditableLabel` saved, and after a `MultiFileUploadField` row was added or 
deleted.
   - Focus did survive in three cases: an input replacing itself, a button 
inside a replaced container (stable ids), and the paging links of 
`AjaxPagingNavigator` and `AjaxNavigationToolbar`, whose ids are stable.
   - Fails: 2.4.3 Focus Order. Focus restarts at the top of the document.
   - Severity: hinders use. This is the most frequent keyboard problem in the 
audit.
   - Source: `wicket-ajax-jquery.js:2173-2188` (`checkFocusedComponentReplaced` 
drops the focus when the id is gone); `AjaxTabbedPanel.java:82`; 
`AjaxFallbackHeadersToolbar.java:74` (`target.add(getTable())`). The repeaters 
regenerate ids.
   - Fix: there are two parts. In the components, give tab links and sort links 
stable markup ids derived from index or property, or have the component call 
`target.focusComponent(...)` on the new link. In the Ajax layer, offer a 
fallback: when the focused id vanishes, focus the nearest replaced ancestor 
(made programmatically focusable) rather than nothing.
   - Markup change: possibly. Stable ids replace generated ones, and a fallback 
may add `tabindex="-1"` on containers.
   
   **F11: The Ajax busy indicator and the lazy-load placeholder are silent**
   - `IndicatingAjaxLink` and `IndicatingAjaxButton` render
     ```html
     <span hidden="" class="wicket-ajax-indicator" id="…--ajax-indicator"><img 
src="…indicator.gif" alt=""></span>
     ```
     and just unhide it. `AjaxLazyLoadPanel` renders `<img alt="Loading..." 
src="…indicator.gif"/>`, with the English text hard-coded, and swaps in the 
content without notice. Nothing sets `aria-busy`, and the replaced region 
announces nothing.
   - Fails: 4.1.3 Status Messages.
   - Severity: minor. For long operations it hinders use.
   - Source: `AjaxIndicatorAppender.java:95-103`; 
`AjaxLazyLoadPanel.java:106-112`; `wicket-ajax-jquery.js:709-712, 738-740` 
(show and hide the indicator).
   - Fix: put a localized text alternative on the indicator, and set 
`aria-busy="true"` on the updated region while the request runs, through the 
indicator logic in `wicket-ajax-jquery.js`. Move the lazy-load text to a 
resource key and render it in a `role="status"` wrapper.
   - Markup change: yes, attributes and the placeholder markup.
   
   **F12: No framework-level way to announce an Ajax result**
   - Applications have no API that puts text into a live region. Every 
component that should announce something, such as feedback, sorting, tabs or 
the autocomplete result count, would need its own region.
   - Fails: an enabler for 4.1.3 Status Messages rather than a failure on its 
own.
   - Severity: hinders use, indirectly.
   - Source: absent. `grep aria-live|role="status"` over both modules finds 
nothing.
   - Fix: add one page-level polite live region, rendered on demand, and 
`IPartialPageRequestHandler.announce(String)` (or similar), which F04, F10 and 
the table and tabs slices can use.
   - Markup change: yes, a new element at the end of `<body>` when used.
   
   **F13: AjaxEditableLabel and its variants cannot be operated by keyboard and 
report errors invisibly**
   - Renders `<span id="…">edit me</span>`, with no `tabindex`, no `role` and a 
`click` trigger only. It is not in the tab order. When empty, the text is 
`...`. The editor `<input type="text" name="editable:editor">` has no label 
(axe `label`, critical). Validation errors go to `window.status`, which no 
browser shows. After a save, focus drops to `<body>`.
   - Fails: 2.1.1 Keyboard; 4.1.2 Name, Role, Value; 3.3.1 Error 
Identification; 2.4.3 Focus Order.
   - Severity: blocks use for keyboard users.
   - Source: `AjaxEditableLabel.html`, `AjaxEditableMultiLineLabel.html`, 
`AjaxEditableChoiceLabel.html`; `AjaxEditableLabel.java:428-431` (`click`), 
`514-522` (`window.status`), `536-543` and `482-488` (no focus call after 
submit or cancel), `632-635` (`...`).
   - Fix: render the label as a `<button type="button">` (or `role="button" 
tabindex="0"` with Enter/Space). Give the editor an `aria-label` or 
`aria-labelledby` from `setLabel` or the label text. Report errors through the 
feedback API and `aria-invalid` instead of `window.status`. Focus the label 
again after save or cancel.
   - Markup change: yes. The element changes or gains attributes, so CSS on 
`span` will notice.
   
   </details>
   <details>
   <summary><b>DataTable and navigators</b> (4 findings)</summary>
   
   **F14: Sort state is only a CSS class**
   - Renders `<th class="wicket_orderDown" scope="col"><a 
href="…orderByLink"><span>Name</span></a></th>`, with no `aria-sort`. The link 
name is only the column title, which says nothing about the action or the order.
   - Fails: 1.3.1 Info and Relationships.
   - Severity: hinders use.
   - Source: `OrderByBorder.java:110-134`; classes from 
`wicket-extensions/.../Initializer.properties:25-27`.
   - Fix: render `aria-sort="ascending|descending"` on the `<th>` of the sorted 
column, and leave it off unsorted ones as APG recommends. Optionally add 
localized hidden text such as "sort ascending" in the link.
   - Markup change: yes, an attribute.
   
   **F15: Filter toolbar inputs are unlabelled, and the choice filter submits 
on change**
   - `TextFilter` renders `<input type="text" 
name="…filters:0:filter:filter">`, and `ChoiceFilter` renders `<select … 
onchange="this.form.submit();">`. Neither has a label or a reference to the 
column header. `ChoiceFilteredPropertyColumn.enableAutoSubmit()` returns `true` 
by default, so selecting with the arrow keys in some browsers, or any change, 
reloads the page.
   - Fails: 1.3.1 Info and Relationships; 4.1.2 Name, Role, Value (axe `label` 
and `select-name`, critical); 3.3.2 Labels or Instructions; 3.2.2 On Input.
   - Severity: hinders use.
   - Source: `TextFilter.html`, `ChoiceFilter.html`; 
`ChoiceFilter.java:131-134`; `ChoiceFilteredPropertyColumn.java:123-126`.
   - Fix: give filter components an `aria-label` from the column's display 
model, through `FilterToolbar` or the `IFilteredColumn` contract. Default 
`enableAutoSubmit()` to `false`, or keep `true` and document the 3.2.2 
consequence. `GoAndClearFilter` already supplies explicit buttons.
   - Markup change: yes. `aria-label` is added, and the `onchange` attribute 
disappears if the default changes, which also changes behaviour.
   
   **F16: PagingNavigator's names are glyphs, and the navigator has no 
landmark**
   - Renders
     ```html
     <a class="first" title="Go to first page">&lt;&lt;</a> <a rel="prev" 
class="prev" title="Go to previous page">&lt;</a>
     <span class="goto"><a title="Go to page 1"><span>1</span></a> …
     ```
     The accessible name is the content (`<<`, `<`), not the title, so a screen 
reader says "less than less than". Nothing wraps the controls in `<nav>`, and 
the current page has no `aria-current` (see F08). The `next` link measured 9×17 
px. axe reports 2.5.8 only where spacing does not save it.
   - Fails: 2.4.4 Link Purpose (In Context); 1.3.1 Info and Relationships, 
minor.
   - Severity: hinders use.
   - Source: 
`wicket-core/.../markup/html/navigation/paging/PagingNavigator.html`; titles at 
`PagingNavigator.java:192` and `PagingNavigation.java:438`.
   - Fix: put the localized title text in an `aria-label` (or visually hidden 
text) and mark the glyphs `aria-hidden`. Wrap the navigator in `<nav 
aria-label="…">` and render `aria-current="page"`.
   - Markup change: yes. `<nav>` changes the element structure that CSS 
selectors may target.
   
   **F17: Empty and navigation rows are plain cells**
   - `NoRecordsToolbar` and `NavigationToolbar` render inside `<td colspan>` in 
`<thead>` and `<tfoot>`. That is valid and passed axe. The page summary 
"Showing 6 to 10 of 43" is not announced when Ajax paging replaces it, which 
belongs to F12.
   - Severity: minor. No separate fix is needed beyond F12.
   
   </details>
   <details>
   <summary><b>ModalDialog</b> (2 findings)</summary>
   
   **F18: ModalDialog has no dialog semantics**
   - Renders
     ```html
     <div class="modal-dialog-overlay"><div class="modal-dialog"><div 
class="modal-dialog-content">…</div></div></div>
     ```
     There is no `role="dialog"`, `aria-modal`, `aria-labelledby` or 
`aria-label`, and the background is not made `inert`. A screen reader's virtual 
cursor moves freely behind the overlay.
   - Fails: 4.1.2 Name, Role, Value; 1.3.1 Info and Relationships. axe raises 
nothing, because a `<div>` is not checked against the dialog rules.
   - Severity: hinders use.
   - Source: `ModalDialog.html:19-23`; `ModalDialog.java:92-107`.
   - Fix: render `role="dialog" aria-modal="true"` on `.modal-dialog`, with 
`aria-labelledby` pointing at a heading id the content can set 
(`ModalDialog#setLabelledBy(Component)` or a title model). Alternatively, move 
to a native `<dialog>` with `showModal()`, which gives inertness, Escape and 
top layer for free.
   - Markup change: yes. Attributes are added, and with `<dialog>` the element 
changes.
   
   **F19: The default configuration does not move focus, trap it, close on 
Escape or return focus**
   - `new ModalDialog(id)` without `trapFocus()` and `closeOnEscape()`: after 
opening, focus stays on the opener behind the overlay. Tab walks from the 
opener through the dialog and on into the page behind it. Escape does nothing. 
After closing, focus is on `<body>`.
   - With both opt-ins, focus moves to the first field, Tab and Shift+Tab wrap 
inside the dialog, Escape closes it, and focus returns to the opener. That 
matches APG.
   - Fails (default configuration): 2.4.3 Focus Order; 2.1.1 Keyboard, since 
there is no keyboard equivalent of `closeOnClick()`.
   - Severity: blocks use in the default configuration.
   - Source: `ModalDialog.java:224-233` (`closeOnEscape`), `260-265` 
(`trapFocus`); `trap-focus.js:60, 102-105, 131-138`.
   - Fix: make `trapFocus` and `closeOnEscape` the default, with opt-outs, so 
the accessible behaviour is what applications get. Attach the trap to 
`.modal-dialog` rather than the overlay. It currently puts `tabindex="0"` on 
the overlay (`trap-focus.js:64`), which adds a focus stop.
   - Markup change: no markup change, but the behaviour does change for 
applications that relied on the defaults.
   
   </details>
   <details>
   <summary><b>TabbedPanel and AjaxTabbedPanel</b> (1 finding)</summary>
   
   **F20: Tabs are a list of links, and the selection is only a class**
   - Renders
     ```html
     <div class="tab-row"><ul><li class="tab0"><a 
href="…"><span>First</span></a></li><li class="tab1 selected"><a href="…">…
     </ul></div><div class="tab-panel">…</div>
     ```
     The selected tab is still an active link, with no `aria-current` or 
`aria-selected`. APG tabs pattern comparison: there is no `tablist`, `tab` or 
`tabpanel`, no `aria-controls`, and no arrow-key navigation (ArrowRight does 
nothing). After an Ajax switch, focus goes to `<body>` (F10).
   - Fails: 1.3.1 Info and Relationships; 4.1.2 Name, Role, Value for the 
selected state.
   - Severity: hinders use.
   - Source: `TabbedPanel.html:19-24`; `TabbedPanel.java:231-241` (classes), 
`362-374` (link); `AjaxTabbedPanel.java:72-86`.
   - Fix: there are two options. The minimal fix keeps the navigation semantics 
(links that load content, which is legitimate for server round-trips): add 
`aria-current="true"` on the selected link, give the panel an id and 
`aria-labelledby`, and restore focus to the selected link after an Ajax switch. 
The full fix is an opt-in APG tabs mode: `role="tablist"`, `tab` and 
`tabpanel`, `aria-selected`, `aria-controls`, roving `tabindex`, and arrow, 
Home and End keys with manual activation. Mixing the two models is worse than 
either.
   - Markup change: yes, attributes. The opt-in mode changes the roles and tab 
order.
   
   </details>
   <details>
   <summary><b>Tree components</b> (3 findings)</summary>
   
   **F21: The tree junction is an empty link with no name and no state**
   - Renders `<a id="ida7" href="…junction" 
class="tree-junction-collapsed">&nbsp;</a>`, an 18×18 px background image. In 
`DefaultTableTree` it rendered 4×17 px. Expanded and collapsed are known only 
from the class. The link for a leaf has no href.
   - Fails: 2.4.4 Link Purpose; 4.1.2 Name, Role, Value (axe `link-name`, 
serious); 2.5.8 Target Size (Minimum) (axe `target-size`).
   - Severity: hinders use. The folder label link toggles too, but the state is 
invisible to assistive technology.
   - Source: `Node.html:22`; `Node.java:119-137, 178-193, 228`; 
`tree/theme/windows/theme.css:49-56` and `human/theme.css`.
   - Fix: render the junction as `<button type="button" 
aria-expanded="true|false" aria-label="Expand/Collapse {node}">` (localized), 
or as `<a>` with the same attributes. Hide it from assistive technology for 
leaves. Make the theme's hit area at least 24×24.
   - Markup change: yes. The element and attributes change, and the themes' CSS 
selectors are `a.tree-junction…`.
   
   **F22: No tree semantics or keyboard model**
   - The nested tree is `<div class="tree-node">`/`<div class="tree-subtree">`, 
and the table tree is plain rows. There is no `role="tree"`, `treeitem` or 
`group`, and no `aria-level`, `aria-expanded` or `aria-selected`. Arrow keys do 
nothing. Selection (`tree-folder-…` / `selected`) is shown only by colour 
(`theme.css:113-114, 135-136`).
   - APG treeview comparison: none of the roles, states or keyboard 
interactions are present. The tree works as nested lists of links.
   - Fails: 1.3.1 Info and Relationships; 1.4.1 Use of Color for the selection.
   - Severity: hinders use.
   - Source: `nested/Subtree.html:26-28`; `NestedTree.html`; `TableTree.html`.
   - Fix: the lowest-risk accessible structure is nested `<ul role="list">` (or 
`role="group"`) with the button junction from F21, which gives levels and 
expansion without a roving-focus widget. A full APG `role="tree"` with arrow 
keys is a larger, opt-in change. For `TableTree`, the APG pattern is `treegrid`.
   - Markup change: yes. `div` becomes `ul`/`li` in the opt-in, or attributes 
are added.
   
   **F23: CheckedFolder and CheckFolder checkboxes are unlabelled**
   - Renders `<input type="checkbox" name="…content:checkbox" id="idab">` next 
to `<a><span class="tree-label">Root</span></a>`. No `for` or `aria-labelledby` 
connects them. The checkbox measured 13×13 px.
   - Fails: 4.1.2 Name, Role, Value; 1.3.1 Info and Relationships (axe `label`, 
critical); 2.5.8 Target Size (Minimum).
   - Severity: hinders use.
   - Source: `content/CheckedFolder.html:26`, `CheckFolder.html:26`; 
`CheckedFolder.java:60`.
   - Fix: put `aria-labelledby` on the checkbox, pointing at the label span, 
with an output id.
   - Markup change: yes, attributes.
   
   </details>
   <details>
   <summary><b>AutoCompleteTextField</b> (2 findings)</summary>
   
   **F24: The combobox's initial state is invalid ARIA**
   - Before the first popup, the input renders
     ```html
     <input type="text" name="city" id="idcd" autocomplete="off" 
role="combobox" aria-autocomplete="list" 
aria-controls="wicket-autocomplete-listbox-idcd">
     ```
     `aria-expanded` is missing, though it is required for `combobox`. 
`aria-controls` points at an element that does not exist until the first fetch. 
While open, the pattern is correct: `aria-expanded="true"`, 
`aria-activedescendant` follows the arrow keys, there is a `listbox` and 
`option`s with `aria-selected`, Escape closes the popup, and Enter selects.
   - Fails: 4.1.2 Name, Role, Value (axe `aria-required-attr` and 
`aria-valid-attr-value`, critical).
   - Severity: minor. It works in practice, but the validators flag it.
   - Source: `AutoCompleteTextField.java:286-289`; 
`wicket-autocomplete.js:321-340, 433, 476-477`.
   - Fix: render `aria-expanded="false"` on the server. Render the (hidden) 
listbox container with its id on the server, or only set `aria-controls` from 
JS when the popup is created.
   - Markup change: yes, an attribute and possibly an element.
   
   **F25: The number of results is not announced**
   - APG says this is optional, but the popup's appearance and its result count 
are not conveyed to screen readers that ignore `aria-expanded` changes.
   - Severity: minor.
   - Source: `wicket-autocomplete.js`, which has no live region.
   - Fix: announce "N suggestions" politely through F12's region, as an opt-in 
setting.
   - Markup change: no, apart from F12's region.
   
   </details>
   <details>
   <summary><b>Wizard</b> (1 finding)</summary>
   
   **F26: The step title is not a heading, and the step position is not 
exposed**
   - Renders `<div class="wizard-step-title"><div>Your name</div></div>` and 
`<div class="wizard-step-summary">…`. There is no heading, no "step 1 of 2", 
and the overview is a plain `div`. The buttons are fine: `<input type="submit" 
value="Next >">` and so on, from `Wizard.properties`.
   - Fails: 1.3.1 Info and Relationships; 2.4.6 Headings and Labels.
   - Severity: minor.
   - Source: `WizardStep$Header.html`; `Wizard.html`.
   - Fix: render the title as a heading. The level has to be configurable, 
because the wizard does not know its context. Expose the step position in text 
(a resource key) and, if an overview is shown, mark the current step with 
`aria-current="step"`.
   - Markup change: yes. The element changes from `div` to `hN`.
   
   </details>
   <details>
   <summary><b>Upload</b> (2 findings)</summary>
   
   **F27: MultiFileUploadField adds unlabelled inputs, ambiguous delete buttons 
and loses focus**
   - After a file is picked, JS adds `<input type="file" name="multi_mf_1" 
style="display: block;">` with no label. The used input moves off-screen at 
`left:-1000px` but stays in the accessibility tree and the tab order. Each row 
gets `<input type="button" value="Delete">` with no file name in its accessible 
name. After picking and after deleting, focus is on `<body>`. The caption 
"Files (maximum 3):" is a plain div that labels nothing.
   - Fails: 4.1.2 Name, Role, Value; 3.3.2 Labels or Instructions (axe `label`, 
critical); 2.4.6 Headings and Labels; 2.4.3 Focus Order.
   - Severity: hinders use.
   - Source: 
`wicket-core/.../markup/html/form/upload/MultiFileUploadField.html:21`; 
`MultiFileUploadField.js:86-87, 99-101, 132-134, 161`.
   - Fix: give each new input `aria-labelledby` pointing at the caption (with 
an id) and the field's label. Hide the used input with `hidden` or 
`display:none` rather than moving it. Label the delete buttons "Delete 
{filename}" through a resource with a parameter. Focus the next row's button or 
the active input after a delete.
   - Markup change: yes. Attributes are added and the hiding technique changes.
   
   **F28: UploadProgressBar progress is not exposed**
   - Progress is the CSS width of `.wupb-foreground`. The status text is 
rewritten every second into a newly created `<label>` element, which is the 
wrong semantics. There is no `role="progressbar"`, no `aria-valuenow` and no 
live region.
   - Fails: 4.1.2 Name, Role, Value; 4.1.3 Status Messages.
   - Severity: hinders use for long uploads.
   - Source: `UploadProgressBar.html`; `progressbar.js:73-82, 89, 97`.
   - Fix: put `role="progressbar"` with `aria-valuemin`, `aria-valuemax`, 
`aria-valuenow` and `aria-label` on the bar, or use `<progress>`. Write the 
status into a `role="status"` element by updating its text, not by replacing a 
`<label>`. Throttle announcements.
   - Markup change: yes. The bar gains attributes or becomes `<progress>`, and 
the status's child element changes.
   
   </details>
   <details>
   <summary><b>The rest</b> (6 findings)</summary>
   
   **F29: The Palette buttons have no accessible name, and its lists are 
unlabelled**
   - Renders `<button type="button" class="palette-add"><div></div></button>` 
×4 (×6 with add-all and remove-all). With the default theme the only content is 
a CSS `::after` glyph (`⇒`, `⇐`, `⇑`, `⇓`), and without the theme the button 
has no name at all. The two `<select multiple>` sit next to `<span 
class="palette-header">Available</span>` and `Selected` without any 
association. Unthemed, the buttons measured 16×6 px.
   - Fails: 4.1.2 Name, Role, Value (axe `button-name` and `select-name`, 
critical); 1.3.1 Info and Relationships; 2.5.8 Target Size (Minimum), unthemed 
only.
   - Severity: blocks use for screen reader users. Keyboard operation works: 
the lists and buttons are reachable and Space activates.
   - Source: `Palette.html:6-23`; `Palette.java:264, 277, 682-690`; 
`palette/theme/palette.css:52-76`.
   - Fix: give each button a localized `title` and accessible name (visually 
hidden text, or `aria-label` from new resource keys `palette.add` and so on), 
with the glyph `aria-hidden`. Render the headers as `<label for>` or link them 
with `aria-labelledby`, and give the button group `role="group"` with a label.
   - Markup change: yes. Attributes are added, and the header may change from 
`span` to `label`.
   
   **F30: Date and time fields render unlabelled sub-inputs**
   - `TimeField` renders `<input type="number" name="time:hours" 
class="time-hours" min="1" max="12">`, a `:` span, `minutes`, and an AM/PM 
`<select>`, none of them labelled. `LocalDateTimeField` and 
`ZonedDateTimeField` add an unlabelled `datetime-date` input. A `wicket:for` 
label on the panel points at the wrapping `<span>`, which labels nothing. There 
is no group.
   - Fails: 1.3.1 Info and Relationships; 4.1.2 Name, Role, Value; 3.3.2 Labels 
or Instructions (axe `label` ×8 and `select-name` ×3, critical).
   - Severity: hinders use. The application cannot label the sub-fields without 
subclassing.
   - Source: `TimeField.html:20-23`; `TimeField.java:140, 181-184, 231-234`; 
`AbstractDateTimeField.html:19-23`; `AbstractDateTimeField.java:195, 219`.
   - Fix: render the wrapper as `role="group"` with `aria-labelledby` pointing 
at the panel's label, which `ILabelProviderLocator` can provide. Give each 
sub-input an `aria-label` from resource keys ("Hours", "Minutes", "AM/PM", 
"Date"). Make the panel's `wicket:for` target the first sub-input.
   - Markup change: yes, attributes.
   
   **F31: RatingPanel stars have no names, and the rating is visual only**
   - Renders `<a href="…element-0-link"><img src="…star1.gif"></a>` ×5. The 
images have no alt, so the links have no name. Whether a star is active is 
shown only by the image.
   - Fails: 1.1.1 Non-text Content; 2.4.4 Link Purpose; 4.1.2 Name, Role, Value 
(axe `image-alt` and `link-name`).
   - Severity: blocks use for screen reader users.
   - Source: `RatingPanel.html:24`; `RatingPanel.java:150-152`.
   - Fix: give each star's image an alt from a resource ("Rate {n} of {max}"). 
Expose the current value in text, or as `aria-pressed` or `aria-current` on the 
active stars. A radio-group rendering is the more robust option.
   - Markup change: yes, attributes.
   
   **F32: KittenCaptchaPanel is mouse-only and has no text alternative**
   - Renders `<img id="ide8" src="…kittens-image">` with no alt, no tabindex 
and a `click` handler that sends mouse coordinates. No keyboard or non-visual 
alternative exists.
   - Fails: 2.1.1 Keyboard; 1.1.1 Non-text Content. Object recognition is 
allowed under 3.3.8, but only with these other criteria met.
   - Severity: blocks use for keyboard and blind users.
   - Source: `KittenCaptchaPanel.html:24`; `KittenCaptchaPanel.java:185-203`.
   - Fix: there is no small fix. Document that the panel is not accessible and 
needs an alternative path, or deprecate it. A keyboard variant (focusable 
regions) still leaves blind users out.
   - Markup change: not applicable.
   
   **F33: BreadCrumbBar has no navigation landmark or current marker, and its 
separator is spoken**
   - Renders, after navigating one level down:
     ```html
     <div class="wicketExtensionsBreadCrumbBar"><span 
class="wicketExtensionsBreadCrumbBarCrumb"><span> <a 
href="…crumb-link">Home</a> </span></span><span 
class="wicketExtensionsBreadCrumbBarCrumb"><span> /<a>Home &gt; child</a> 
</span></span></div>
     ```
     The "/" separator is announced. The current crumb is an href-less `<a>`.
   - Fails: 1.3.1 Info and Relationships.
   - Severity: minor.
   - Source: `BreadCrumbBar.html:18`; `BreadCrumbBar.java:77, 89, 257-260, 279`.
   - Fix: follow the APG breadcrumb pattern. Render `<nav 
aria-label="Breadcrumb"><ol>`, with the separators added by CSS or 
`aria-hidden`, and `aria-current="page"` on the last crumb.
   - Markup change: yes. The structure changes from `div`/`span` to 
`nav`/`ol`/`li`.
   
   **F34: AjaxLazyLoadPanel placeholder text is hard-coded English**
   - This is covered in F11. It is listed here so the "rest" slice can 
reference it if F11 is split.
   
   </details>
   
   ### Application-side findings
   
   These come from the application or the harness, not from Wicket. Each has a 
note where the framework could make the job easier.
   
   - **Target size (2.5.8).** The tab links, sort links, wizard buttons, 
`ExternalLink` and `AjaxFallbackLink` in the harness are text-sized because the 
harness has no CSS. Wicket ships no CSS for them, so meeting 24×24 is the 
application's job. The exceptions are the tree theme's junction (F21) and the 
unthemed Palette buttons (F29), where Wicket's own CSS or markup sets the size.
   - **`CaptchaImageResource`.** The application renders the `<img>`, so alt 
text is the application's job. A distorted-text CAPTCHA fails 1.1.1 unless an 
alternative modality is offered, and it fails 3.3.8 if it guards 
authentication. Wicket offers no audio or alternative variant, so the 
documentation should say so.
   - **Unlabelled fields.** A `TextField` without `wicket:for`, `<label>` or 
`setLabel` is unlabelled. That is an application issue, but see F02 for the 
error message text.
   - **Headings, page titles, `lang` and landmarks** on application pages are 
the application's job. The harness sets them, and axe found nothing there.
   - **Link on `<span>`/`<div>` (F07)** is partly an application choice. It is 
listed as a framework finding because Wicket accepts the markup without protest 
and renders an inoperable control.
   - **ModalDialog content.** The dialog's heading and the field labels inside 
it are application content. Labelling the dialog needs F18's hook.
   
   ### Proposed follow-up issues
   
   Severity per slice is the worst finding in it.
   <details>
   <summary><b>1. Forms and feedback (F01–F06), hinders use</b></summary>
   
   **Title:** Make form labels, required state and validation errors accessible 
by default
   
   **Body:**
   Form components render labels and errors in a way that works visually but 
loses information for assistive technology. The WCAG 2.2 audit in #1838 found 
six problems.
   
   A `wicket:for` label placed after its component, the usual order for 
checkboxes, points at an id the component never rendered on the first render. 
When the label is a plain `wicket:for` label, validation messages use the 
component id (`'name' is required.`). Invalid components get no `aria-invalid`, 
and nothing ties a message to its field. A FeedbackPanel filled by an Ajax 
submit is not announced. The FormComponentFeedbackBorder and 
FormComponentFeedbackIndicator indicator is a bare `*`. Only text components 
expose `required`.
   
   The fix makes `wicket:for` targets output their id before rendering, and 
lets `wicket:for` supply the label model when none is set. It renders 
`aria-invalid` on invalid components and gives FeedbackPanel a permanent live 
region (`role="alert"` for errors). It adds a text alternative to the 
indicators and renders `required` or `aria-required` on choice components and 
groups. The automatic placeholder from WICKET-7191 should be decided at the 
same time.
   
   Applications will see new attributes on form components, a FeedbackPanel 
wrapper that is present while empty, and different message texts where labels 
were missing. Deliverables: the fix, reference documentation of what each form 
component renders, a how-to on accessible forms and error reporting, an 
example, and an automated a11y check of the example in a failed-submit state.
   
   </details>
   <details>
   <summary><b>2. Page structure (F07–F09), blocks use</b></summary>
   
   **Title:** Stop rendering inoperable links on non-interactive tags, expose 
disabled and current link states, and fix the built-in error pages
   
   **Body:**
   `Link`, `AjaxLink` and `ExternalLink` accept any tag. On a `<span>` or 
`<div>` they render a click handler only, with no role, tabindex or keyboard 
handler, so the control cannot be reached by keyboard. `AjaxFallbackLink` 
already rejects such tags. Either reject them consistently or render `role`, 
`tabindex="0"` and an Enter/Space handler. A development-mode warning is the 
minimum.
   
   Disabled links become an href-less `<a>` with no exposed state. Navigation 
components that use this for the current item should render `aria-current`.
   
   The error pages (`PageExpiredErrorPage`, `AccessDeniedPage`, 
`InternalErrorPage`, `ExceptionErrorPage`) have no `lang` attribute, use an 
HTML 4.01 doctype, and draw headings at 3.4:1 contrast. They appear in 
production, so they need the HTML5 doctype, `lang` from the session locale and 
a compliant colour.
   
   Applications will see changed doctype and attributes on the error pages, and 
either new attributes on, or exceptions for, links on non-interactive tags. 
Deliverables: the fix, reference docs on which tags each link type supports, a 
how-to, an example and an a11y check.
   
   </details>
   <details>
   <summary><b>3. Ajax focus and announcements (F10–F13), blocks use 
(AjaxEditableLabel)</b></summary>
   
   **Title:** Keep keyboard focus across Ajax updates and give Ajax a way to 
announce results
   
   **Body:**
   Wicket restores focus after an Ajax update only when the focused element 
comes back with the same markup id. Repeater children get new ids on every 
render, so focus falls to `<body>` after an Ajax sort in a DataTable, after 
switching an AjaxTabbedPanel tab, after an AjaxEditableLabel save, after a 
component hides itself, and in MultiFileUploadField. Components should keep 
stable ids for their controls or focus the new control explicitly. 
`wicket-ajax-jquery.js` should fall back to the nearest replaced container 
instead of dropping focus.
   
   Nothing in Wicket announces Ajax results. There is no live region and no 
`aria-busy`. The indicator image has an empty alt, and AjaxLazyLoadPanel's 
placeholder is a hard-coded English "Loading...". Add a page-level polite live 
region with an API on the Ajax request handler, set `aria-busy` while a request 
runs, and localize the placeholder.
   
   AjaxEditableLabel and its variants are reachable only by mouse click. They 
are a `<span>` or `<div>` without tabindex. Their editor has no accessible 
name, and they report validation errors through `window.status`, which no 
browser shows. They should render a button-like trigger, label the editor, 
report errors through feedback with `aria-invalid`, and return focus after save 
or cancel.
   
   Applications will see new attributes, a live-region element when it is used, 
stable ids instead of generated ones on some links, and a different element for 
AjaxEditableLabel's label. Deliverables: the fix, reference docs on focus 
handling and announcements, a how-to, an example and an a11y check that 
includes post-update focus.
   
   </details>
   <details>
   <summary><b>4. DataTable and navigators (F14–F17), hinders use</b></summary>
   
   **Title:** Expose sort state, label filters and name the paging controls in 
DataTable and PagingNavigator
   
   **Body:**
   The sort state is only a CSS class (`wicket_orderUp`/`wicket_orderDown`). 
Render `aria-sort` on the sorted `<th>`.
   
   FilterToolbar inputs have no label. Derive an `aria-label` from the column 
header. ChoiceFilteredPropertyColumn submits the form on every change by 
default (`onchange="this.form.submit()"`), which conflicts with 3.2.2 On Input. 
Change the default or document it.
   
   PagingNavigator's links are named by their glyphs (`<<`, `<`, `>`, `>>`), 
because the title does not override content. Give them proper accessible names, 
wrap the navigator in `<nav>`, and mark the current page with 
`aria-current="page"`.
   
   After an Ajax sort the focus is lost, which is covered by the Ajax slice. 
The row summary is not announced after Ajax paging and should use that slice's 
announcement API.
   
   Applications will see new attributes, a `<nav>` element around 
PagingNavigator, and possibly a changed auto-submit default. Deliverables: the 
fix, reference docs, a how-to for accessible tables, an example, and an a11y 
check covering the sorted, filtered, paged and empty states.
   
   </details>
   <details>
   <summary><b>5. ModalDialog (F18–F19), blocks use (default 
configuration)</b></summary>
   
   **Title:** Give ModalDialog dialog semantics and accessible focus behaviour 
by default
   
   **Body:**
   ModalDialog renders three nested `<div>`s with no `role="dialog"`, 
`aria-modal` or accessible name, and leaves the background reachable to 
assistive technology.
   
   In its default configuration it also leaves focus on the opener behind the 
overlay, lets Tab move into the page behind it, ignores Escape, and drops focus 
to `<body>` on close. `trapFocus()` and `closeOnEscape()` already implement the 
APG behaviour correctly: focus in, Tab wraps, Escape closes, focus returns to 
the opener. They are opt-in, though, and the trap puts `tabindex="0"` on the 
overlay.
   
   The fix renders `role="dialog" aria-modal="true"` with `aria-labelledby` (a 
new API to name the title component) and makes the focus trap and Escape the 
default, with opt-outs. It should also evaluate a native `<dialog>` with 
`showModal()`.
   
   Applications will see new attributes, possibly a different element, and 
different default behaviour. Deliverables: the fix, reference docs, a how-to, 
an example and an a11y check with the dialog open.
   
   </details>
   <details>
   <summary><b>6. Tabs (F20), hinders use</b></summary>
   
   **Title:** Expose the selected tab in TabbedPanel and offer the WAI-ARIA 
tabs pattern
   
   **Body:**
   TabbedPanel and AjaxTabbedPanel render a list of links. The selected tab is 
marked only by the `selected` CSS class and remains an active link. There are 
no tab roles and no arrow-key support, and AjaxTabbedPanel loses focus after a 
switch.
   
   A server round-trip per tab fits the navigation model fine, so there are two 
steps. First, the default keeps links but adds `aria-current` on the selected 
link, an accessible relationship to the panel, and focus restoration. Second, 
an opt-in mode implements the APG tabs pattern: `tablist`, `tab` and 
`tabpanel`, `aria-selected`, `aria-controls`, roving tabindex, and arrow, Home 
and End keys with manual activation.
   
   Applications will see new attributes, and different roles and tab order in 
the opt-in mode. Deliverables: the fix, reference docs on both modes, a how-to, 
an example and an a11y check.
   
   </details>
   <details>
   <summary><b>7. Tree (F21–F23), hinders use</b></summary>
   
   **Title:** Give tree junctions a name and expanded state, and label tree 
checkboxes
   
   **Body:**
   The tree junction is an `<a>` containing only `&nbsp;` with a background 
image. It has no accessible name, its expanded or collapsed state is only a 
class, and its hit area is 18×18 px (4 px wide in DefaultTableTree). The nested 
tree has no list or tree structure and no keyboard model. Selection is shown 
only by colour. CheckedFolder and CheckFolder render unlabelled checkboxes.
   
   The fix renders the junction as a button with `aria-expanded` and a 
localized label, hidden from assistive technology for leaves. It enlarges the 
themes' hit areas, links the checkboxes to their labels, and gives the nested 
tree a list structure. A full APG `tree`/`treegrid` mode with arrow-key 
navigation can follow as an opt-in.
   
   Applications will see a changed junction element, which affects CSS 
selectors in custom themes, plus new attributes and possibly `ul`/`li` instead 
of `div`. Deliverables: the fix, reference docs, a how-to, an example and an 
a11y check in the collapsed and expanded states.
   
   </details>
   <details>
   <summary><b>8. Autocomplete (F24–F25), minor</b></summary>
   
   **Title:** Fix AutoCompleteTextField's initial ARIA state
   
   **Body:**
   AutoCompleteTextField follows the ARIA 1.2 combobox pattern while its popup 
is open. Before the first popup, though, the input lacks the required 
`aria-expanded`, and its `aria-controls` references an element that does not 
exist yet. Both are critical axe errors.
   
   The fix renders `aria-expanded="false"` server-side and either renders the 
empty listbox container or sets `aria-controls` only once the popup exists. As 
an opt-in, the number of suggestions can be announced through the announcement 
API from the Ajax slice.
   
   Applications will see one new attribute and possibly an empty hidden 
container. Deliverables: the fix, reference docs, a how-to, an example and an 
a11y check with the popup closed and open.
   
   </details>
   <details>
   <summary><b>9. Wizard (F26), minor</b></summary>
   
   **Title:** Render the wizard step title as a heading and expose the step 
position
   
   **Body:**
   Wizard renders the step title and summary as nested `<div>`s, gives no 
indication of the current position, and its overview has no current-step 
marker. Render the title as a heading with a configurable level, add a 
localized "step X of Y" text, and mark the current step in an overview with 
`aria-current="step"`.
   
   The buttons and the error feedback, which uses FeedbackPanel and benefits 
from the forms slice, need no wizard-specific change.
   
   Applications will see a heading element instead of a `div`. Deliverables: 
the fix, reference docs, a how-to, an example and an a11y check including a 
failed step.
   
   </details>
   <details>
   <summary><b>10. Upload (F27–F28), hinders use</b></summary>
   
   **Title:** Label MultiFileUploadField's generated inputs and expose 
UploadProgressBar progress
   
   **Body:**
   MultiFileUploadField's JavaScript creates file inputs without labels and 
moves used inputs off-screen while they stay focusable. It labels every delete 
button just "Delete", and drops focus to `<body>` after a file is picked or a 
row is deleted.
   
   UploadProgressBar shows progress only as a CSS width, and rewrites its 
status into a freshly created `<label>` element every second. There is no 
`progressbar` role and no status region.
   
   The fix labels the inputs from the caption and the field label, really hides 
the used inputs, names the delete buttons after the file, and manages focus. It 
renders `role="progressbar"` (or `<progress>`) with values and a 
`role="status"` text.
   
   Applications will see new attributes and changed elements in the generated 
markup. Deliverables: the fix, reference docs, a how-to, an example and an a11y 
check with a file selected and during an upload.
   
   </details>
   <details>
   <summary><b>11. The rest: Palette, date and time fields, RatingPanel, 
KittenCaptchaPanel, BreadCrumbBar (F29–F33), blocks use</b></summary>
   
   **Title:** Name the controls of Palette, the date and time fields and 
RatingPanel, fix BreadCrumbBar semantics, and decide on KittenCaptchaPanel
   
   **Body:**
   Palette's move buttons contain only an empty `<div>` with a CSS glyph, so 
screen readers get no name. Its two lists are not associated with their 
"Available" and "Selected" headers.
   
   TimeField, LocalDateTimeField and ZonedDateTimeField render hours, minutes, 
AM/PM and date sub-inputs without labels or a group, and the panel's 
`wicket:for` label targets a `<span>`.
   
   RatingPanel's stars are links around images without alt, so they have no 
name, and the current rating is visual only.
   
   KittenCaptchaPanel can only be operated by mouse clicks on an image without 
alt. It needs at least a documented warning and probably deprecation.
   
   BreadCrumbBar should follow the APG breadcrumb pattern: a `<nav>` with a 
list, separators hidden from assistive technology, and `aria-current="page"`.
   
   Applications will see new attributes, and changed elements in BreadCrumbBar 
and possibly in the Palette headers. Deliverables per component: the fix, 
reference docs, a how-to, an example and an a11y check.
   
   </details>
   
   ---
   <sub>Generated-by: Claude Code (Claude Opus 5.5). The harness, the axe runs 
and the keyboard pass were run, and this report was drafted, with AI 
assistance.</sub>
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to