Which booking widgets are covered
The Hospitality Booking test is a specialised scan for the interactive widgets that drive hotel booking flows — search, dates, guests and room selection.
Most accessibility scanners only see the page as it first loads. The interesting parts of a booking form are hidden until you interact with them: the destination list, the calendar, the guests panel. This test opens each one the way a visitor would, and runs the accessibility checks inside the panel once it is open.
How a run works
When the test is pointed at a booking-flow URL, it works through this loop automatically, with no manual interaction:
| Step | What happens |
|---|---|
| 1. Page load | A browser opens the URL with the configured viewport, cookies and zoom level, and scopes the audit to the booking form rather than the whole page. |
| 2. Clear the way | Cookie banners and any modal dialogs already on screen are dismissed, so they do not pollute the audit. |
| 3. Widget discovery | The page is scanned for the fields and buttons that open booking panels, through nine layered detection passes (see below). |
| 4. Per-widget loop | For each one: open it (click or press Enter / Space / arrow keys), navigate through the panel with Tab and arrow keys recording every element that receives focus, check each of those elements, then close the panel before moving on. |
| 5. Checks per element | The same checkpoints the Detector runs elsewhere, applied inside the open panel: keyboard reachability, keyboard operability, focus visibility, text contrast (normal, hover and focus states), non-text contrast, and accessible name & role. |
| 6. Aggregation | Findings are deduplicated — the same failure across several widgets is collapsed into one — sorted in page order, and enriched with a suggested fix. |
Each finding carries a title, a description, the location on the page, the HTML involved, and the WCAG criterion it falls under.
Test variants
The same engine ships with several entry points, so a review can isolate one concern at a time:
| Test | Scope |
|---|---|
hospitality_booking | Full audit. |
hospitality_booking_navigation_only | Keyboard reachability only — no contrast. |
hospitality_booking_contrast_only | Text and non-text contrast inside widgets — no navigation findings. |
hospitality_booking_states_only | Hover and focus state contrast only. |
hospitality_booking_form | Combines the page-wide text contrast check with the full booking audit. |
What counts as a widget
An element is treated as a widget trigger when it matches at least one of the signals below. Detection runs in priority order — specific ARIA signals first, fallbacks last — and an element matched by an earlier pass is skipped by later ones, so proper semantic markup is always preferred over keyword guesses.
Primary signals (ARIA first)
| Signal | What is looked for |
|---|---|
aria-haspopup | The element declares that it opens a popup (menu, listbox, tree, grid, dialog). |
aria-expanded | The element controls a region that can expand and collapse. |
role="combobox" | An input that opens a listbox of options, optionally supported by aria-controls, aria-owns, aria-activedescendant or aria-autocomplete. |
Fallbacks, for custom components and pre-ARIA widgets
| Signal | Pattern |
|---|---|
| Native select | A <select> element (single-select, without multiple or size). |
| Pressable field | [data-role="field"][tabindex], used by some Polymer-based booking engines. |
| Input with placeholder | An <input> with a meaningful placeholder of three characters or more — common in Angular and other single-page forms. |
| Autocomplete input | jQuery-UI or custom autocomplete inputs, recognised by class name or data-source attribute. |
| Field button | A <div role="button"> or <span tabindex="0"> wrapper with a destination-like class or label. |
| Keyword field | A catch-all: any remaining input whose label, placeholder, id, name or text mentions a booking keyword — destination, hotel, check-in, dates, guests, rooms, personas, huéspedes, quartos. |
Once a trigger is found, the specialised handlers for destination, date picker and guests-and-rooms each get a chance to claim it. If none does, a generic open / navigate / close routine takes over.
Outside hospitality, the same philosophy backs general-purpose navigators for comboboxes, accordions, disclosure widgets and tab lists.
Widget types and how well each is covered
| Widget | Support | How it is detected |
|---|---|---|
| Date picker (check-in / check-out calendar) | ✅ Dedicated handler | aria-haspopup or aria-expanded on a date input or its wrapper; class-based fallback (.datepicker, .daterangepicker, jQuery-UI); keyword fallback. Past and disabled cells are excluded from the audit. |
| Dropdown selectors (destination, room type, rate plan) | ✅ Several paths | Native <select>; a <div role="button" aria-haspopup> wrapper; an ARIA combobox; Select2-enhanced selects, where the hidden native element is recognised so it is not audited twice. |
| Guest / room counter | ✅ Dedicated handler | Keyword match on guest, room, persona, huésped, quarto. Opens the popup and tabs through the increment, decrement and quantity controls. |
| Room cards / carousel | ⚠️ Partial | Cards with aria-expanded or aria-haspopup are treated as triggers and walked with Tab. There is no carousel-specific handler yet — no rotation-speed or skip-link checks — but keyboard reachability and contrast still run on each focusable child. |
| Step indicator / progress bar | ⚠️ Generic coverage | Steps built as buttons with aria-expanded are walked; otherwise the generic tab navigator covers them. No dedicated step-indicator semantics. |
| Modal dialogs (room details, cancellation policies, rate breakdowns) | ✅ Covered | Detected via role="dialog". Modals already open when the run starts are closed first so they do not shadow the booking form; modals opened during the audit are walked by the generic navigator. |
| Form fields (personal info, payment, special requests) | ✅ Base coverage | The standard name-and-role, keyboard-operability, focus-visibility and text-contrast checks. Form error association is covered by its own check. |
| Autocomplete / search-suggest (destination search) | ✅ Dedicated handler | aria-autocomplete="list" or "both" plus a popup role; class-based and data-source fallbacks. The handler types a seed query, walks the suggestion list, and records contrast and focus on each option. |
| Accordion panels (rate details, inclusions, policies) | ✅ General-purpose navigator | aria-expanded (required) plus a valid header element (button, <h1>–<h6>, or role="button") plus a panel with role="region" or aria-labelledby. At least two of these indicators are required, to suppress false positives. |
| Tab panels (room views, amenities, reviews) | ✅ General-purpose navigator | The native ARIA pattern: role="tablist" containing role="tab" elements with aria-selected. Tab moves between groups, arrow keys cycle within one. |
Why a general-purpose scanner misses this
This test introduces no new WCAG checks. It applies the existing ones — contrast, name and role, keyboard operability, focus visibility, non-text contrast — inside live popup contexts.
That is the whole point. Traditional automated scanners audit the static initial DOM, so everything behind a closed calendar or a collapsed guests panel is invisible to them. On a booking form, that is most of the interface a traveller actually has to operate.
Scoping
The audit is always scoped to the booking widget rather than the whole page. A root selector covering the entire document is rejected, so that the run stays focused on the booking form and does not drift into site chrome — headers, footers and navigation, which the page-wide checks already cover.
