You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@causeway.apache.org by da...@apache.org on 2026/09/01 04:19:09 UTC

(causeway) branch htmx updated (c4fc92770d4 -> 9df6557c868)

This is an automated email from the ASF dual-hosted git repository.

danhaywood pushed a change to branch htmx
in repository https://gitbox.apache.org/repos/asf/causeway.git


    from c4fc92770d4 docs(openspec): archive time and multiline editor usability
     new 1fdd188c5c9 docs(pi): add htmx viewer session retrospective
     new 6a064ed78a7 docs(openspec): plan editor and pager cosmetic fixes
     new 5d46a3bd7b5 fix(webcomponents): correct picker textarea and pager presentation
     new 9df6557c868 docs(openspec): archive editor and pager presentation fixes

The 4 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.


Summary of changes:
 .../htmx-viewer-openai-session-retrospective.adoc  |  82 +++++++++++++++++
 .../.openspec.yaml                                 |   0
 .../design.md                                      | 100 +++++++++++++++++++++
 .../proposal.md                                    |  31 +++++++
 .../specs/domain-web-components/spec.md            |  60 +++++++++++++
 .../generic-htmx-web-component-viewer/spec.md      |  23 +++++
 .../specs/vaadin-collection-grid-adapter/spec.md   |  20 +++++
 .../specs/vaadin-semantic-editor-families/spec.md  |  64 +++++++++++++
 .../tasks.md                                       |  30 +++++++
 openspec/specs/domain-web-components/spec.md       |  66 +++++++++++---
 .../generic-htmx-web-component-viewer/spec.md      |  26 +++++-
 .../specs/vaadin-collection-grid-adapter/spec.md   |  19 ++++
 .../specs/vaadin-semantic-editor-families/spec.md  |  50 +++++++++--
 viewers/webcomponents/foundation/USAGE.adoc        |   5 +-
 .../foundation/src/collection-element.mjs          |   5 +-
 .../webcomponents/foundation/src/field-widget.mjs  |  12 ++-
 viewers/webcomponents/foundation/src/theme.css     |  12 +--
 .../test/collection-grid-responsive.test.mjs       |  47 +++++++++-
 .../foundation/test/component-styles.test.mjs      |   3 +-
 .../foundation/test/field-widget.test.mjs          |  37 ++++++--
 .../foundation/vaadin-fields/audit.mjs             |  63 +++++++------
 .../petclinic/PetClinicHtmxPlaywrightTest.java     |  68 +++++++++++---
 22 files changed, 732 insertions(+), 91 deletions(-)
 create mode 100644 .pi/notes/htmx-viewer-openai-session-retrospective.adoc
 copy openspec/changes/archive/{2026-08-31-add-domain-object-link-icons => 2026-08-31-fix-time-picker-multiline-and-collection-pagination}/.openspec.yaml (100%)
 create mode 100644 openspec/changes/archive/2026-08-31-fix-time-picker-multiline-and-collection-pagination/design.md
 create mode 100644 openspec/changes/archive/2026-08-31-fix-time-picker-multiline-and-collection-pagination/proposal.md
 create mode 100644 openspec/changes/archive/2026-08-31-fix-time-picker-multiline-and-collection-pagination/specs/domain-web-components/spec.md
 create mode 100644 openspec/changes/archive/2026-08-31-fix-time-picker-multiline-and-collection-pagination/specs/generic-htmx-web-component-viewer/spec.md
 create mode 100644 openspec/changes/archive/2026-08-31-fix-time-picker-multiline-and-collection-pagination/specs/vaadin-collection-grid-adapter/spec.md
 create mode 100644 openspec/changes/archive/2026-08-31-fix-time-picker-multiline-and-collection-pagination/specs/vaadin-semantic-editor-families/spec.md
 create mode 100644 openspec/changes/archive/2026-08-31-fix-time-picker-multiline-and-collection-pagination/tasks.md


(causeway) 01/04: docs(pi): add htmx viewer session retrospective

Posted by da...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

danhaywood pushed a commit to branch htmx
in repository https://gitbox.apache.org/repos/asf/causeway.git

commit 1fdd188c5c9bbbbe1f70a6f637a0c561802eb56f
Author: Dan Haywood <da...@haywood-associates.co.uk>
AuthorDate: Mon Aug 31 21:35:29 2026 +0200

    docs(pi): add htmx viewer session retrospective
---
 .../htmx-viewer-openai-session-retrospective.adoc  | 82 ++++++++++++++++++++++
 1 file changed, 82 insertions(+)

diff --git a/.pi/notes/htmx-viewer-openai-session-retrospective.adoc b/.pi/notes/htmx-viewer-openai-session-retrospective.adoc
new file mode 100644
index 00000000000..d04f9563faa
--- /dev/null
+++ b/.pi/notes/htmx-viewer-openai-session-retrospective.adoc
@@ -0,0 +1,82 @@
+= Token-saving notes for HTMX viewer sessions
+
+== Biggest sources of waste observed
+
+The historical pi sessions accumulated 54 compactions, which repeatedly forced rediscovery of skills, repository structure, active OpenSpec state, JDK settings, and test commands.
+The principal OpenSpec skills were reread unchanged 124 times.
+One unchanged Petclinic Playwright command was run 14 times, returning the same failure 13 times before succeeding.
+Large browser snapshots and command output were sometimes returned directly into model context.
+Git status, diff checks, OpenSpec listing, and validation were often run separately and repeatedly.
+
+== Rules for future sessions
+
+Use a fresh pi session for one coherent change or a small set of closely related changes.
+Treat the first compaction as a warning and the second as a reason to finish the current atomic step and restart with a short handoff.
+
+Read the required workflow skill once when its workflow begins.
+Keep a short digest of its commands and rules instead of rereading it after compaction.
+
+Create a working dossier at session start containing only:
+
+* the active OpenSpec change;
+* the relevant specifications and source files;
+* the selected JDK;
+* the focused test commands;
+* the current failure or next task;
+* the current Git state.
+
+Update the dossier when a fact changes rather than rediscovering stable information.
+
+Batch OpenSpec list and status, Git status, recent commits, JDK discovery, and relevant file checks into one startup operation.
+Batch `git diff --check`, `git status`, task counting, and OpenSpec validation at lifecycle boundaries.
+Do not run these checks between edits unless their result can affect the next decision.
+
+After a test failure, save the complete output, extract the first causal error, form a hypothesis, and change something observable before rerunning.
+Do not rerun an identical expensive command merely to see whether the result changes unless explicitly diagnosing flakiness.
+
+Run the narrowest relevant Node, Maven, or Playwright test while editing.
+Run module tests only after the focused test passes.
+Run broad Maven and Playwright gates at implementation or archive boundaries.
+
+Write browser snapshots, console output, network traces, Maven output, and test logs to files.
+Use context-mode to process those files and return only a diagnostic summary to the model.
+
+Use Context7 once for a third-party API question and retain the returned `docRef`.
+Reuse the cached document instead of issuing a semantically equivalent query after compaction.
+
+When multiple IntelliJ projects prevent project inference, switch immediately to repository-scoped `read`, `ctx_execute`, or `ctx_execute_file` calls.
+Do not retry an unchanged IDE call that already failed to identify the project.
+
+Use semantic search once to locate a symbol, then read the known file or offset directly.
+Do not alternate repeatedly among symbol search, text search, regex search, and shell grep for the same question.
+
+Prefer `edit` for small patches.
+Use `write` only for new files or deliberate complete rewrites, and preserve content not explicitly superseded.
+
+== Minimal repository map
+
+The framework-neutral components are in `viewers/webcomponents/foundation/`.
+The generic viewer is in `viewers/webcomponents/htmx/`.
+The principal sample is in `viewers/webcomponents/sample-htmx-petclinic/`.
+The broad regression fixture is in `regressiontests/referenceapp/htmx/`.
+The roadmap and deferred proposals are in `openspec/planned-changes/web-component-viewer/`.
+The canonical accumulated specifications are in `openspec/specs/`.
+
+A focused foundation check normally starts with `cd viewers/webcomponents/foundation && node --test` or a narrowed `--test-name-pattern`.
+Determine the required JDK once per session and retain the exact Maven command in the dossier.
+
+== Copy-paste briefing
+
+[source,text]
+----
+Work from one explicit OpenSpec change at a time.
+Read each required workflow skill once and retain a short digest.
+Maintain a dossier with the active change, relevant specs and files, JDK, focused tests, current failure, and Git state.
+Batch discovery and lifecycle checks.
+After a failure, inspect the saved full log and make a concrete change before rerunning.
+Use focused tests during implementation and broad gates only after they pass.
+Save large browser and build output to files and summarize it with context-mode.
+Reuse Context7 docRefs and other cached static information.
+Restart with a short handoff after a second compaction.
+Preserve existing content unless the task explicitly supersedes it.
+----


(causeway) 02/04: docs(openspec): plan editor and pager cosmetic fixes

Posted by da...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

danhaywood pushed a commit to branch htmx
in repository https://gitbox.apache.org/repos/asf/causeway.git

commit 6a064ed78a771ce36906cbc8d5e91950c8469d99
Author: Dan Haywood <da...@haywood-associates.co.uk>
AuthorDate: Mon Aug 31 22:10:03 2026 +0200

    docs(openspec): plan editor and pager cosmetic fixes
---
 .../.openspec.yaml                                 |  2 +
 .../design.md                                      | 83 ++++++++++++++++++++++
 .../proposal.md                                    | 30 ++++++++
 .../specs/domain-web-components/spec.md            | 39 ++++++++++
 .../generic-htmx-web-component-viewer/spec.md      | 23 ++++++
 .../specs/vaadin-collection-grid-adapter/spec.md   | 20 ++++++
 .../specs/vaadin-semantic-editor-families/spec.md  | 39 ++++++++++
 .../tasks.md                                       | 29 ++++++++
 8 files changed, 265 insertions(+)

diff --git a/openspec/changes/fix-time-picker-multiline-and-collection-pagination/.openspec.yaml b/openspec/changes/fix-time-picker-multiline-and-collection-pagination/.openspec.yaml
new file mode 100644
index 00000000000..ecf3b45d38b
--- /dev/null
+++ b/openspec/changes/fix-time-picker-multiline-and-collection-pagination/.openspec.yaml
@@ -0,0 +1,2 @@
+schema: spec-driven
+created: 2026-08-31
diff --git a/openspec/changes/fix-time-picker-multiline-and-collection-pagination/design.md b/openspec/changes/fix-time-picker-multiline-and-collection-pagination/design.md
new file mode 100644
index 00000000000..bda5ec86cb3
--- /dev/null
+++ b/openspec/changes/fix-time-picker-multiline-and-collection-pagination/design.md
@@ -0,0 +1,83 @@
+## Context
+
+The shared picker bridge currently moves focus from a time input to Vaadin's shadow toggle, consumes Enter or Space, and schedules `open()` with `setTimeout` after returning focus to the input.
+That delay made a state-based Playwright assertion pass but separates overlay creation from the trusted keyboard event and permits asynchronous prompt replacement to supersede the target.
+A robust browser contract must verify the actual Vaadin overlay is visibly presented, not merely that a Boolean briefly becomes true.
+
+The application theme treats `textarea:not([slot="input"])` as native.
+Vaadin Text Area uses an internal `<textarea slot="textarea">`, so the selector applies native border, padding, sizing, and focus rules inside the toolkit control.
+The desired outer orange boundary in the screenshot is Vaadin's own focus ring; the inner textarea boundary is the leaked application-native styling.
+
+Bounded collection windows already carry normalized `totalCount` and `countAvailable` metadata.
+The pager currently renders only `Items rangeStart–rangeEnd`, even when a safe authoritative total is present, while correctly avoiding invented totals when metadata is unavailable.
+
+## Goals / Non-Goals
+
+**Goals:**
+
+- Make Enter and Space on a focused clock affordance synchronously present the real time overlay.
+- Verify the overlay itself is open and visible in a real browser.
+- Leave exactly the toolkit-owned multiline boundary and focus ring around Vaadin Text Area.
+- Include a safe authoritative total in bounded collection range labels on every page.
+- Preserve unknown-total wording and all existing interaction, paging, validation, and fallback semantics.
+
+**Non-Goals:**
+
+- Do not replace Vaadin's time overlay or implement a Causeway-owned time list.
+- Do not remove focus indication or borders from native fallback textareas.
+- Do not infer collection totals from offsets, page size, navigation flags, or loaded rows.
+- Do not change GraphQL collection window metadata or requests.
+
+## Decisions
+
+### Open the picker synchronously during keyboard activation
+
+The qualified toggle will consume Enter and Space and, during the trusted key event, return focus to the picker input and invoke the pinned public `open()` method.
+The fallback branch used by test adapters may set `opened`, but production Vaadin controls use `open()`.
+No timer or microtask will defer activation.
+
+Returning focus to the input matches Vaadin pointer handling, which prevents toggle mousedown from blurring the input before opening.
+It also lets the picker keep its overlay open rather than interpreting toggle focus as an external blur.
+
+Calling `open()` from a timer was rejected because the control can be superseded and browser popover presentation can diverge from property state.
+Synthesizing pointer events was rejected because untrusted synthetic clicks are not equivalent to user activation.
+
+### Assert visible overlay presentation
+
+Foundation browser audit and Petclinic Playwright coverage will require the nested time picker to report `opened`, expose its Vaadin overlay as `:popover-open`, and have a non-empty visible rectangle after keyboard activation.
+The tests will stabilize any prior parameter preparation before interacting so they exercise the current generation rather than a replaced control.
+
+Property-only tests of `opened` were rejected because they reproduced the false positive reported by the user.
+
+### Scope native textarea styling to unslotted controls
+
+Every application-theme selector that currently uses `textarea:not([slot="input"])` will use `textarea:not([slot])` instead.
+This retains the application border, padding, sizing, and focus outline for native fallback textareas while excluding Vaadin's `slot="textarea"` implementation detail.
+The Vaadin input container remains responsible for the one visible boundary and focus ring.
+
+Broadly removing textarea borders was rejected because native toolkit rollback must remain visibly operable.
+
+### Add totals only when authoritative and safe
+
+The bounded pager will append ` of <totalCount>` when `totalCount` is a non-negative safe integer.
+When no items are returned, established `No items` wording remains.
+When total metadata is absent or invalid, the pager retains range-only wording and does not infer a value.
+
+This applies equally to native bounded tables and bounded Grid mode because the collection host owns the pager.
+
+## Risks / Trade-offs
+
+- [Synchronous focus transfer could alter keyboard focus expectations] → Preserve the explicit toggle Tab stop, open only on Enter or Space, and verify reverse Tab and prompt containment separately.
+- [Vaadin changes its overlay implementation] → Keep the browser assertion inside the pinned adapter audit so dependency drift fails qualification.
+- [Changing textarea selectors could affect other slotted controls] → Scope only the textarea selector and verify native fallback and Vaadin multiline controls independently.
+- [A malformed total could leak into UI] → Require a non-negative safe integer and retain unknown-total wording otherwise.
+
+## Migration Plan
+
+The behavior applies automatically to existing qualified controls and bounded collections.
+No application markup, GraphQL query, configuration, or persisted data migration is required.
+Reverting restores delayed keyboard activation, the broader textarea selector, and range-only pager labels.
+
+## Open Questions
+
+None.
diff --git a/openspec/changes/fix-time-picker-multiline-and-collection-pagination/proposal.md b/openspec/changes/fix-time-picker-multiline-and-collection-pagination/proposal.md
new file mode 100644
index 00000000000..e10bbb484ce
--- /dev/null
+++ b/openspec/changes/fix-time-picker-multiline-and-collection-pagination/proposal.md
@@ -0,0 +1,30 @@
+## Why
+
+The prior time and multiline usability change made the clock affordance focusable and attempted to remove a redundant host outline, but real browser use still exposes three presentation defects.
+Keyboard activation can set time-picker state without presenting a usable visible overlay, Vaadin's internal `slot="textarea"` still matches application native-textarea styling and produces a second border, and bounded collection pagers omit an available authoritative total from their range label.
+
+## What Changes
+
+- Open the real Vaadin time-picker overlay synchronously from Enter or Space activation while the trusted keyboard event is active, and verify visible popover presentation rather than only the `opened` property.
+- Preserve the clock trigger's Tab and reverse-Tab accessibility without using delayed activation that can lose browser activation context or target a superseded control.
+- Restrict application native textarea styling to unslotted native controls so Vaadin's internal textarea receives only toolkit-owned boundaries and focus indication.
+- Render bounded collection ranges as `Items <start>–<end> of <total>` whenever a valid authoritative total is available, while retaining range-only wording when totals are unavailable.
+- Add foundation browser and unit coverage plus Petclinic acceptance for visible keyboard-opened time overlays, a single multiline border, and correct collection totals across pages.
+
+## Capabilities
+
+### New Capabilities
+
+None.
+
+### Modified Capabilities
+
+- `vaadin-semantic-editor-families`: Require trusted keyboard activation to present a visible time overlay and prevent native textarea chrome from crossing into Vaadin multiline internals.
+- `domain-web-components`: Require accessible time-picker activation and bounded collection range labels to reflect available authoritative totals.
+- `vaadin-collection-grid-adapter`: Require bounded pager labels to include safe authoritative totals without inventing unavailable counts.
+- `generic-htmx-web-component-viewer`: Qualify the corrected time overlay, multiline boundary, and paged Petclinic collection labels in the executable browser profile.
+
+## Impact
+
+The change affects the shared field-widget keyboard bridge, application theme selectors, bounded collection pager rendering, foundation and Vaadin audit tests, and Petclinic Playwright assertions.
+It changes no GraphQL schema, domain model, route, persisted value, dependency version, or collection window request semantics.
diff --git a/openspec/changes/fix-time-picker-multiline-and-collection-pagination/specs/domain-web-components/spec.md b/openspec/changes/fix-time-picker-multiline-and-collection-pagination/specs/domain-web-components/spec.md
new file mode 100644
index 00000000000..954b01eafea
--- /dev/null
+++ b/openspec/changes/fix-time-picker-multiline-and-collection-pagination/specs/domain-web-components/spec.md
@@ -0,0 +1,39 @@
+## ADDED Requirements
+
+### Requirement: User-visible time-picker keyboard activation
+Qualified `<cw-property>` and `<cw-parameter>` time editors SHALL make their labelled clock affordance visibly open the current time-selection overlay from Enter or Space.
+Focus transfer and overlay activation MUST preserve Causeway-owned pending values, validation, cancellation, property save, parameter preparation, and action invocation.
+
+#### Scenario: Property clock is keyboard activated
+- **WHEN** a user presses Enter or Space on an editable property's current clock trigger
+- **THEN** the visible minute-resolution time overlay opens
+- **AND** the property is not saved merely by opening the overlay
+
+#### Scenario: Parameter clock is keyboard activated
+- **WHEN** a user presses Enter or Space on an editable action parameter's current clock trigger
+- **THEN** the visible minute-resolution time overlay opens within the prompt lifecycle
+- **AND** the action is not invoked merely by opening the overlay
+
+### Requirement: Authoritative bounded collection range totals
+A bounded collection pager SHALL include the authoritative total count in its live range label whenever a valid total is available.
+It MUST retain bounded range wording without a total when count metadata is unavailable and MUST NOT infer a total from loaded rows or navigation state.
+
+#### Scenario: First bounded page has a total
+- **WHEN** a collection window reports rows 1 through 10 and authoritative total count 23
+- **THEN** the pager announces `Items 1–10 of 23`
+- **AND** Next and Previous availability continues to follow authoritative window metadata
+
+#### Scenario: Later bounded page has a total
+- **WHEN** a collection window reports rows 21 through 23 and authoritative total count 23
+- **THEN** the pager announces `Items 21–23 of 23`
+- **AND** the displayed total remains stable across page navigation
+
+#### Scenario: Total count is unavailable
+- **WHEN** a bounded collection window has no valid authoritative total
+- **THEN** the pager announces only its available current range
+- **AND** it does not fabricate an `of` count
+
+#### Scenario: Collection is empty
+- **WHEN** the current authoritative window returns no items
+- **THEN** the pager retains its established `No items` wording
+- **AND** navigation availability remains metadata-driven
diff --git a/openspec/changes/fix-time-picker-multiline-and-collection-pagination/specs/generic-htmx-web-component-viewer/spec.md b/openspec/changes/fix-time-picker-multiline-and-collection-pagination/specs/generic-htmx-web-component-viewer/spec.md
new file mode 100644
index 00000000000..5eeb4ee57f8
--- /dev/null
+++ b/openspec/changes/fix-time-picker-multiline-and-collection-pagination/specs/generic-htmx-web-component-viewer/spec.md
@@ -0,0 +1,23 @@
+## ADDED Requirements
+
+### Requirement: Petclinic corrected editor and pager presentation
+The Petclinic browser profile SHALL verify user-visible keyboard time selection, one multiline editor boundary, and authoritative paged collection totals through the public HTMX viewer.
+
+#### Scenario: Visit time overlay is keyboard opened
+- **WHEN** the browser tabs from the visit time input to its clock trigger and presses Enter or Space
+- **THEN** the real Vaadin time overlay is visibly open with minute-resolution choices
+- **AND** no booking mutation occurs until the user invokes the action
+
+#### Scenario: Visit reason has one boundary
+- **WHEN** the multiline visit reason receives keyboard focus
+- **THEN** only the toolkit input-container boundary and focus ring are visible
+- **AND** the slotted internal textarea contributes no nested native border or outline
+
+#### Scenario: Visit pages show authoritative totals
+- **WHEN** the browser navigates first, middle, or final pages of a multi-page Visits collection
+- **THEN** each live range label includes the same authoritative total
+- **AND** its start and end positions match the rows shown on that page
+
+#### Scenario: Corrected journeys remain clean
+- **WHEN** the editor and collection journeys complete or cancel
+- **THEN** no unexpected mutation, invocation, focus loss, console error, page error, CSP violation, external request, overlay leak, or horizontal overflow occurs
diff --git a/openspec/changes/fix-time-picker-multiline-and-collection-pagination/specs/vaadin-collection-grid-adapter/spec.md b/openspec/changes/fix-time-picker-multiline-and-collection-pagination/specs/vaadin-collection-grid-adapter/spec.md
new file mode 100644
index 00000000000..eeac62d58f8
--- /dev/null
+++ b/openspec/changes/fix-time-picker-multiline-and-collection-pagination/specs/vaadin-collection-grid-adapter/spec.md
@@ -0,0 +1,20 @@
+## ADDED Requirements
+
+### Requirement: Bounded pager authoritative total presentation
+The collection-owned bounded pager SHALL append a valid authoritative total count to its current range for both native-table and bounded-Grid presentation.
+Unavailable or invalid totals MUST remain undisclosed rather than being derived from page offsets, returned rows, requested size, or navigation flags.
+
+#### Scenario: Bounded Grid has an authoritative total
+- **WHEN** bounded Grid presentation renders a non-empty normalized window with a non-negative safe-integer total
+- **THEN** the live pager range includes `of <total>`
+- **AND** responsive adapter changes do not alter that count
+
+#### Scenario: Native bounded presentation has an authoritative total
+- **WHEN** the same bounded window renders through native table presentation
+- **THEN** it exposes the same range and total wording
+- **AND** the displayed count remains toolkit-neutral
+
+#### Scenario: Bounded window has no authoritative total
+- **WHEN** total metadata is absent, null, negative, unsafe, or otherwise invalid
+- **THEN** the pager omits the `of` suffix
+- **AND** continues to use authoritative previous and next offsets without inventing collection size
diff --git a/openspec/changes/fix-time-picker-multiline-and-collection-pagination/specs/vaadin-semantic-editor-families/spec.md b/openspec/changes/fix-time-picker-multiline-and-collection-pagination/specs/vaadin-semantic-editor-families/spec.md
new file mode 100644
index 00000000000..1c1909d423e
--- /dev/null
+++ b/openspec/changes/fix-time-picker-multiline-and-collection-pagination/specs/vaadin-semantic-editor-families/spec.md
@@ -0,0 +1,39 @@
+## ADDED Requirements
+
+### Requirement: Visibly presented keyboard time overlay
+Enter or Space activation of an editable qualified time-picker clock trigger SHALL synchronously present the current Vaadin time overlay while processing the trusted keyboard event.
+The qualification contract MUST verify visible overlay presentation and MUST NOT treat the `opened` property alone as sufficient evidence.
+
+#### Scenario: User opens time choices with Enter
+- **WHEN** keyboard focus is on the current clock trigger and the user presses Enter
+- **THEN** focus returns to the associated time input and the Vaadin overlay is visibly presented
+- **AND** minute-resolution choices are available without committing or cancelling the Causeway interaction
+
+#### Scenario: User opens time choices with Space
+- **WHEN** keyboard focus is on the current clock trigger and the user presses Space
+- **THEN** the same visible overlay is presented synchronously
+- **AND** page scrolling, action invocation, property save, and prompt submission do not occur
+
+#### Scenario: Trigger generation is obsolete
+- **WHEN** a trigger or picker has disconnected or been superseded before activation
+- **THEN** it does not open an obsolete overlay or move focus
+- **AND** the current editor generation remains authoritative
+
+#### Scenario: Overlay state is not visibly presented
+- **WHEN** keyboard activation changes picker state but the pinned Vaadin overlay is not `:popover-open` with visible geometry
+- **THEN** browser qualification fails
+- **AND** the adapter does not report keyboard time selection as operable
+
+### Requirement: Toolkit-owned multiline boundary
+Vaadin multiline editors SHALL exclude their slotted internal textarea from application-native textarea border, padding, sizing, and focus-outline selectors.
+The toolkit SHALL remain responsible for the one visible editor boundary and focus ring.
+
+#### Scenario: Vaadin multiline editor receives focus
+- **WHEN** a qualified multiline property or action parameter receives keyboard focus
+- **THEN** the Vaadin input container presents one visible boundary and focus ring
+- **AND** its internal `slot="textarea"` element has no application-native border or outline
+
+#### Scenario: Native multiline fallback receives focus
+- **WHEN** toolkit policy or family fallback renders an unslotted native textarea
+- **THEN** application-native border, padding, sizing, resizing, and focus indication remain available
+- **AND** the Vaadin-specific exclusion does not suppress native usability
diff --git a/openspec/changes/fix-time-picker-multiline-and-collection-pagination/tasks.md b/openspec/changes/fix-time-picker-multiline-and-collection-pagination/tasks.md
new file mode 100644
index 00000000000..39b8efb9ab5
--- /dev/null
+++ b/openspec/changes/fix-time-picker-multiline-and-collection-pagination/tasks.md
@@ -0,0 +1,29 @@
+## 1. Visible keyboard time overlay
+
+- [ ] 1.1 Replace delayed clock-trigger activation with synchronous trusted-event focus transfer and Vaadin `open()` invocation for Enter and Space.
+- [ ] 1.2 Preserve Tab, reverse Tab, disabled/read-only omission, current-generation safety, pointer behavior, and minute resolution.
+- [ ] 1.3 Strengthen foundation unit and Vaadin browser-audit coverage to require an actually visible `:popover-open` time overlay rather than only picker state.
+
+## 2. Single multiline boundary
+
+- [ ] 2.1 Restrict application native textarea selectors to unslotted controls so Vaadin's `slot="textarea"` avoids native border, padding, sizing, and focus chrome.
+- [ ] 2.2 Update structural style tests and real-browser assertions for one Vaadin boundary while retaining native fallback textarea presentation.
+
+## 3. Authoritative collection totals
+
+- [ ] 3.1 Append valid non-negative safe-integer `totalCount` metadata to non-empty bounded pager range labels.
+- [ ] 3.2 Preserve `No items` and unknown-total behavior without deriving counts from offsets, loaded rows, page size, or navigation flags.
+- [ ] 3.3 Extend collection unit and responsive tests across first, middle, final, unavailable-total, empty, and invalid-total windows.
+
+## 4. Petclinic acceptance and documentation
+
+- [ ] 4.1 Extend Petclinic Playwright coverage to prove Enter and Space visibly open the current visit time overlay without premature booking.
+- [ ] 4.2 Verify the visit reason's slotted textarea has no nested native border or outline while the Vaadin input container remains visibly bounded.
+- [ ] 4.3 Verify paged Visits collection labels show correct ranges and the same authoritative total across pages.
+- [ ] 4.4 Update component usage documentation for visible keyboard time activation, unslotted native textarea styling, and authoritative pager totals.
+
+## 5. Validation
+
+- [ ] 5.1 Run complete foundation Node and Maven suites plus the Vaadin field browser audit.
+- [ ] 5.2 Run focused Petclinic Playwright acceptance and verify console, page-error, CSP, external-request, overlay, focus, and overflow diagnostics.
+- [ ] 5.3 Run relevant IDE inspections or project compilation, strict OpenSpec validation, and diff checks.


(causeway) 04/04: docs(openspec): archive editor and pager presentation fixes

Posted by da...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

danhaywood pushed a commit to branch htmx
in repository https://gitbox.apache.org/repos/asf/causeway.git

commit 9df6557c8689e79074d4312958ae8c06db1dec3e
Author: Dan Haywood <da...@haywood-associates.co.uk>
AuthorDate: Tue Sep 1 00:09:48 2026 +0200

    docs(openspec): archive editor and pager presentation fixes
---
 .../.openspec.yaml                                 |  0
 .../design.md                                      |  0
 .../proposal.md                                    |  0
 .../specs/domain-web-components/spec.md            |  0
 .../generic-htmx-web-component-viewer/spec.md      |  0
 .../specs/vaadin-collection-grid-adapter/spec.md   |  0
 .../specs/vaadin-semantic-editor-families/spec.md  |  0
 .../tasks.md                                       |  0
 openspec/specs/domain-web-components/spec.md       | 66 +++++++++++++++++-----
 .../generic-htmx-web-component-viewer/spec.md      | 26 ++++++++-
 .../specs/vaadin-collection-grid-adapter/spec.md   | 19 +++++++
 .../specs/vaadin-semantic-editor-families/spec.md  | 50 ++++++++++++++--
 12 files changed, 139 insertions(+), 22 deletions(-)

diff --git a/openspec/changes/fix-time-picker-multiline-and-collection-pagination/.openspec.yaml b/openspec/changes/archive/2026-08-31-fix-time-picker-multiline-and-collection-pagination/.openspec.yaml
similarity index 100%
rename from openspec/changes/fix-time-picker-multiline-and-collection-pagination/.openspec.yaml
rename to openspec/changes/archive/2026-08-31-fix-time-picker-multiline-and-collection-pagination/.openspec.yaml
diff --git a/openspec/changes/fix-time-picker-multiline-and-collection-pagination/design.md b/openspec/changes/archive/2026-08-31-fix-time-picker-multiline-and-collection-pagination/design.md
similarity index 100%
rename from openspec/changes/fix-time-picker-multiline-and-collection-pagination/design.md
rename to openspec/changes/archive/2026-08-31-fix-time-picker-multiline-and-collection-pagination/design.md
diff --git a/openspec/changes/fix-time-picker-multiline-and-collection-pagination/proposal.md b/openspec/changes/archive/2026-08-31-fix-time-picker-multiline-and-collection-pagination/proposal.md
similarity index 100%
rename from openspec/changes/fix-time-picker-multiline-and-collection-pagination/proposal.md
rename to openspec/changes/archive/2026-08-31-fix-time-picker-multiline-and-collection-pagination/proposal.md
diff --git a/openspec/changes/fix-time-picker-multiline-and-collection-pagination/specs/domain-web-components/spec.md b/openspec/changes/archive/2026-08-31-fix-time-picker-multiline-and-collection-pagination/specs/domain-web-components/spec.md
similarity index 100%
rename from openspec/changes/fix-time-picker-multiline-and-collection-pagination/specs/domain-web-components/spec.md
rename to openspec/changes/archive/2026-08-31-fix-time-picker-multiline-and-collection-pagination/specs/domain-web-components/spec.md
diff --git a/openspec/changes/fix-time-picker-multiline-and-collection-pagination/specs/generic-htmx-web-component-viewer/spec.md b/openspec/changes/archive/2026-08-31-fix-time-picker-multiline-and-collection-pagination/specs/generic-htmx-web-component-viewer/spec.md
similarity index 100%
rename from openspec/changes/fix-time-picker-multiline-and-collection-pagination/specs/generic-htmx-web-component-viewer/spec.md
rename to openspec/changes/archive/2026-08-31-fix-time-picker-multiline-and-collection-pagination/specs/generic-htmx-web-component-viewer/spec.md
diff --git a/openspec/changes/fix-time-picker-multiline-and-collection-pagination/specs/vaadin-collection-grid-adapter/spec.md b/openspec/changes/archive/2026-08-31-fix-time-picker-multiline-and-collection-pagination/specs/vaadin-collection-grid-adapter/spec.md
similarity index 100%
rename from openspec/changes/fix-time-picker-multiline-and-collection-pagination/specs/vaadin-collection-grid-adapter/spec.md
rename to openspec/changes/archive/2026-08-31-fix-time-picker-multiline-and-collection-pagination/specs/vaadin-collection-grid-adapter/spec.md
diff --git a/openspec/changes/fix-time-picker-multiline-and-collection-pagination/specs/vaadin-semantic-editor-families/spec.md b/openspec/changes/archive/2026-08-31-fix-time-picker-multiline-and-collection-pagination/specs/vaadin-semantic-editor-families/spec.md
similarity index 100%
rename from openspec/changes/fix-time-picker-multiline-and-collection-pagination/specs/vaadin-semantic-editor-families/spec.md
rename to openspec/changes/archive/2026-08-31-fix-time-picker-multiline-and-collection-pagination/specs/vaadin-semantic-editor-families/spec.md
diff --git a/openspec/changes/fix-time-picker-multiline-and-collection-pagination/tasks.md b/openspec/changes/archive/2026-08-31-fix-time-picker-multiline-and-collection-pagination/tasks.md
similarity index 100%
rename from openspec/changes/fix-time-picker-multiline-and-collection-pagination/tasks.md
rename to openspec/changes/archive/2026-08-31-fix-time-picker-multiline-and-collection-pagination/tasks.md
diff --git a/openspec/specs/domain-web-components/spec.md b/openspec/specs/domain-web-components/spec.md
index 8a74f433483..8e65a0af825 100644
--- a/openspec/specs/domain-web-components/spec.md
+++ b/openspec/specs/domain-web-components/spec.md
@@ -2253,23 +2253,23 @@ A modal action prompt whose first applicable parameter is an asynchronously upgr
 - **AND** a subsequent Escape from the closed editor cancels the prompt
 
 ### Requirement: Shared minute-precision temporal entry
-Editable `<cw-property>` and `<cw-parameter>` local-time and local-date-time values SHALL use the same qualified one-minute entry precision when rendered through the Vaadin field adapter.
-Causeway codecs and interaction owners MUST remain authoritative for lexical parsing, validation, pending state, cancellation, property mutation, and action invocation.
+Editable Web Component properties and action parameters backed by qualified Vaadin `LocalTime` or `LocalDateTime` controls SHALL present time without seconds or fractional seconds and SHALL use Vaadin's supported 15-minute dropdown interval.
+The behavior SHALL remain consistent across direct property editing, inline prompts, modal prompts, and sidebar prompts.
 
-#### Scenario: Property time is entered
-- **WHEN** an editable local-time or local-date-time property uses the qualified toolkit adapter
-- **THEN** its time entry and picker choices are limited to minute precision
-- **AND** successful save submits the minute-aligned local lexical value through the established property interaction
+#### Scenario: Property time enters editing
+- **WHEN** a qualified `LocalTime` or `LocalDateTime` property enters editing
+- **THEN** its visible time entry omits seconds and fractional seconds
+- **AND** its picker offers quarter-hour choices
 
-#### Scenario: Parameter time is entered
-- **WHEN** a local-time or local-date-time action parameter uses the qualified toolkit adapter
-- **THEN** its time entry and picker choices are limited to minute precision
-- **AND** successful invocation submits the minute-aligned local lexical value through the established parameter negotiation path
+#### Scenario: Action time parameter is prepared
+- **WHEN** a qualified `LocalTime` or `LocalDateTime` action parameter is prepared in any supported prompt style
+- **THEN** its visible time entry uses the same 15-minute dropdown interval
+- **AND** no default, validation, confirmation, or invocation semantics change
 
-#### Scenario: Temporal edit is cancelled
-- **WHEN** a property or parameter temporal editor is cancelled
-- **THEN** its authoritative or prepared value remains unchanged
-- **AND** no minute normalization is persisted or invoked solely because the editor was displayed
+#### Scenario: Precise value remains authoritative without commit
+- **WHEN** an accepted authoritative value contains seconds, fractional seconds, or a minute outside a quarter-hour choice and the interaction is only opened or cancelled
+- **THEN** the authoritative value retains its accepted precision
+- **AND** no rounded value is submitted
 
 ### Requirement: Semantic time-picker affordance
 Qualified property and action-parameter time editors SHALL provide equivalent pointer and keyboard access to their visible clock affordance without exposing Vaadin APIs in application markup.
@@ -2283,3 +2283,41 @@ Qualified property and action-parameter time editors SHALL provide equivalent po
 - **WHEN** a user reaches or clicks an action parameter's clock trigger
 - **THEN** the time picker opens with an accessible name derived from the effective parameter label
 - **AND** existing prompt focus containment, validation, submission, and cancellation remain controller-owned
+
+### Requirement: User-visible time-picker keyboard activation
+Qualified `<cw-property>` and `<cw-parameter>` time editors SHALL make their labelled clock affordance visibly open the current time-selection overlay from Enter or Space.
+Focus transfer and overlay activation MUST preserve Causeway-owned pending values, validation, cancellation, property save, parameter preparation, and action invocation.
+
+#### Scenario: Property clock is keyboard activated
+- **WHEN** a user presses Enter or Space on an editable property's current clock trigger
+- **THEN** the visible time overlay opens with quarter-hour choices formatted to minutes
+- **AND** the property is not saved merely by opening the overlay
+
+#### Scenario: Parameter clock is keyboard activated
+- **WHEN** a user presses Enter or Space on an editable action parameter's current clock trigger
+- **THEN** the visible time overlay opens with quarter-hour choices within the prompt lifecycle
+- **AND** the action is not invoked merely by opening the overlay
+
+### Requirement: Authoritative bounded collection range totals
+A bounded collection pager SHALL include the authoritative total count in its live range label whenever a valid total is available.
+It MUST retain bounded range wording without a total when count metadata is unavailable and MUST NOT infer a total from loaded rows or navigation state.
+
+#### Scenario: First bounded page has a total
+- **WHEN** a collection window reports rows 1 through 10 and authoritative total count 23
+- **THEN** the pager announces `Items 1–10 of 23`
+- **AND** Next and Previous availability continues to follow authoritative window metadata
+
+#### Scenario: Later bounded page has a total
+- **WHEN** a collection window reports rows 21 through 23 and authoritative total count 23
+- **THEN** the pager announces `Items 21–23 of 23`
+- **AND** the displayed total remains stable across page navigation
+
+#### Scenario: Total count is unavailable
+- **WHEN** a bounded collection window has no valid authoritative total
+- **THEN** the pager announces only its available current range
+- **AND** it does not fabricate an `of` count
+
+#### Scenario: Collection is empty
+- **WHEN** the current authoritative window returns no items
+- **THEN** the pager retains its established `No items` wording
+- **AND** navigation availability remains metadata-driven
diff --git a/openspec/specs/generic-htmx-web-component-viewer/spec.md b/openspec/specs/generic-htmx-web-component-viewer/spec.md
index 3bc40bb7d4d..5efb8266ac9 100644
--- a/openspec/specs/generic-htmx-web-component-viewer/spec.md
+++ b/openspec/specs/generic-htmx-web-component-viewer/spec.md
@@ -927,11 +927,11 @@ Petclinic browser verification SHALL exercise Escape cancellation for the `remov
 - **AND** the earlier cancellation has not invoked or corrupted the action lifecycle
 
 ### Requirement: Petclinic time and multiline editor usability qualification
-The Petclinic browser profile SHALL qualify minute-resolution date-time parameter entry, keyboard and pointer clock-trigger operation, and single-ring multiline parameter focus through the public HTMX viewer.
+The Petclinic browser profile SHALL qualify minute-formatted date-time parameter entry with quarter-hour picker choices, keyboard and pointer clock-trigger operation, and single-ring multiline parameter focus through the public HTMX viewer.
 
 #### Scenario: Visit date-time parameter is operated
 - **WHEN** the browser opens the demonstrated visit-booking action and reaches its date-time parameter
-- **THEN** the time field displays and selects at minute precision
+- **THEN** the time field displays without seconds and selects from quarter-hour picker choices
 - **AND** keyboard and pointer activation of its labelled clock trigger opens the time overlay without invoking the action
 
 #### Scenario: Multiline parameter receives keyboard focus
@@ -942,3 +942,25 @@ The Petclinic browser profile SHALL qualify minute-resolution date-time paramete
 #### Scenario: Browser qualification remains clean
 - **WHEN** the time and multiline journeys complete or cancel
 - **THEN** no unexpected mutation, action invocation, focus loss, console error, page error, CSP violation, external request, overlay leak, or horizontal overflow occurs
+
+### Requirement: Petclinic corrected editor and pager presentation
+The Petclinic browser profile SHALL verify user-visible keyboard time selection, one multiline editor boundary, and authoritative paged collection totals through the public HTMX viewer.
+
+#### Scenario: Visit time overlay is keyboard opened
+- **WHEN** the browser tabs from the visit time input to its clock trigger and presses Enter or Space
+- **THEN** the real Vaadin time overlay is visibly open with quarter-hour choices formatted to minutes
+- **AND** no booking mutation occurs until the user invokes the action
+
+#### Scenario: Visit reason has one boundary
+- **WHEN** the multiline visit reason receives keyboard focus
+- **THEN** only the toolkit input-container boundary and focus ring are visible
+- **AND** the slotted internal textarea contributes no nested native border or outline
+
+#### Scenario: Visit pages show authoritative totals
+- **WHEN** the browser navigates first, middle, or final pages of a multi-page Visits collection
+- **THEN** each live range label includes the same authoritative total
+- **AND** its start and end positions match the rows shown on that page
+
+#### Scenario: Corrected journeys remain clean
+- **WHEN** the editor and collection journeys complete or cancel
+- **THEN** no unexpected mutation, invocation, focus loss, console error, page error, CSP violation, external request, overlay leak, or horizontal overflow occurs
diff --git a/openspec/specs/vaadin-collection-grid-adapter/spec.md b/openspec/specs/vaadin-collection-grid-adapter/spec.md
index 0e65514186b..5a4e38ea365 100644
--- a/openspec/specs/vaadin-collection-grid-adapter/spec.md
+++ b/openspec/specs/vaadin-collection-grid-adapter/spec.md
@@ -317,3 +317,22 @@ It SHALL accept only Causeway presentation options whose callbacks trigger compl
 - **THEN** the adapter renders no active sorting affordance
 - **AND** it does not approximate the behavior over bounded items or cached virtual ranges
 
+### Requirement: Bounded pager authoritative total presentation
+The collection-owned bounded pager SHALL append a valid authoritative total count to its current range for both native-table and bounded-Grid presentation.
+Unavailable or invalid totals MUST remain undisclosed rather than being derived from page offsets, returned rows, requested size, or navigation flags.
+
+#### Scenario: Bounded Grid has an authoritative total
+- **WHEN** bounded Grid presentation renders a non-empty normalized window with a non-negative safe-integer total
+- **THEN** the live pager range includes `of <total>`
+- **AND** responsive adapter changes do not alter that count
+
+#### Scenario: Native bounded presentation has an authoritative total
+- **WHEN** the same bounded window renders through native table presentation
+- **THEN** it exposes the same range and total wording
+- **AND** the displayed count remains toolkit-neutral
+
+#### Scenario: Bounded window has no authoritative total
+- **WHEN** total metadata is absent, null, negative, unsafe, or otherwise invalid
+- **THEN** the pager omits the `of` suffix
+- **AND** continues to use authoritative previous and next offsets without inventing collection size
+
diff --git a/openspec/specs/vaadin-semantic-editor-families/spec.md b/openspec/specs/vaadin-semantic-editor-families/spec.md
index 3fb2e09229a..ed3fc3440ad 100644
--- a/openspec/specs/vaadin-semantic-editor-families/spec.md
+++ b/openspec/specs/vaadin-semantic-editor-families/spec.md
@@ -165,21 +165,21 @@ Keyboard use of the trigger MUST preserve Causeway-owned pending value, validati
 - **AND** applications are not required to address Vaadin shadow-DOM structure directly
 
 ### Requirement: Minute-resolution local-time entry
-Each editable qualified Vaadin `LocalTime` or `LocalDateTime` control SHALL present and accept user-entered time at one-minute resolution.
-The adapter MUST use the toolkit's supported step contract and MUST NOT introduce a second temporal parser or mutate authoritative state merely by entering edit mode.
+Each editable qualified Vaadin `LocalTime` or `LocalDateTime` control SHALL present time without seconds or fractional seconds and SHALL use the toolkit-supported 15-minute dropdown interval.
+The adapter MUST use the public step contract, MUST NOT fabricate private dropdown items, and MUST NOT mutate authoritative state merely by entering edit mode.
 
 #### Scenario: User edits a local time
 - **WHEN** a property or action parameter opens an editable qualified `LocalTime` control
 - **THEN** the visible entry omits seconds and fractional seconds
-- **AND** a newly selected or typed valid value is aligned to the minute before the existing codec validates it
+- **AND** the picker offers quarter-hour choices through Vaadin's supported overlay
 
 #### Scenario: User edits a local date-time
 - **WHEN** a property or action parameter opens an editable qualified `LocalDateTime` control
-- **THEN** its integrated time field uses the same one-minute resolution
+- **THEN** its integrated time field uses the same 15-minute dropdown interval
 - **AND** the local date, local time, and absence of browser timezone conversion remain authoritative
 
 #### Scenario: User cancels a precise existing value
-- **WHEN** an authoritative value contains accepted seconds or fractional seconds and the user cancels without committing a changed minute value
+- **WHEN** an authoritative value contains accepted seconds, fractional seconds, or a minute outside a quarter-hour choice and the user cancels without committing a changed picker value
 - **THEN** the authoritative value remains unchanged
 - **AND** no rounded or truncated value is submitted
 
@@ -199,7 +199,7 @@ Keyboard or pointer use of the trigger MUST preserve Causeway-owned pending valu
 
 #### Scenario: Keyboard user opens the time picker
 - **WHEN** the focused clock trigger receives Enter or Space
-- **THEN** the existing Vaadin time overlay opens for minute-resolution selection
+- **THEN** the existing Vaadin time overlay opens with quarter-hour choices formatted to minutes
 - **AND** no property commit, action-parameter commit, submission, or cancellation occurs merely because focus moved to or activated the trigger
 
 #### Scenario: Pointer user opens the time picker
@@ -239,3 +239,41 @@ An editable qualified Vaadin multiline control SHALL display one clear focus ind
 - **WHEN** native toolkit fallback renders a multiline editor
 - **THEN** the application focus-visible outline remains available
 - **AND** focus indication is not suppressed by the Vaadin-specific rule
+
+### Requirement: Visibly presented keyboard time overlay
+Enter or Space activation of an editable qualified time-picker clock trigger SHALL synchronously present the current Vaadin time overlay while processing the trusted keyboard event.
+The qualification contract MUST verify visible overlay presentation and MUST NOT treat the `opened` property alone as sufficient evidence.
+
+#### Scenario: User opens time choices with Enter
+- **WHEN** keyboard focus is on the current clock trigger and the user presses Enter
+- **THEN** focus returns to the associated time input and the Vaadin overlay is visibly presented
+- **AND** quarter-hour choices formatted to minutes are available without committing or cancelling the Causeway interaction
+
+#### Scenario: User opens time choices with Space
+- **WHEN** keyboard focus is on the current clock trigger and the user presses Space
+- **THEN** the same visible overlay is presented synchronously
+- **AND** page scrolling, action invocation, property save, and prompt submission do not occur
+
+#### Scenario: Trigger generation is obsolete
+- **WHEN** a trigger or picker has disconnected or been superseded before activation
+- **THEN** it does not open an obsolete overlay or move focus
+- **AND** the current editor generation remains authoritative
+
+#### Scenario: Overlay state is not visibly presented
+- **WHEN** keyboard activation changes picker state but the pinned Vaadin overlay is not `:popover-open` with visible geometry
+- **THEN** browser qualification fails
+- **AND** the adapter does not report keyboard time selection as operable
+
+### Requirement: Toolkit-owned multiline boundary
+Vaadin multiline editors SHALL exclude their slotted internal textarea from application-native textarea border, padding, sizing, and focus-outline selectors.
+The toolkit SHALL remain responsible for the one visible editor boundary and focus ring.
+
+#### Scenario: Vaadin multiline editor receives focus
+- **WHEN** a qualified multiline property or action parameter receives keyboard focus
+- **THEN** the Vaadin input container presents one visible boundary and focus ring
+- **AND** its internal `slot="textarea"` element has no application-native border or outline
+
+#### Scenario: Native multiline fallback receives focus
+- **WHEN** toolkit policy or family fallback renders an unslotted native textarea
+- **THEN** application-native border, padding, sizing, resizing, and focus indication remain available
+- **AND** the Vaadin-specific exclusion does not suppress native usability


(causeway) 03/04: fix(webcomponents): correct picker textarea and pager presentation

Posted by da...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

danhaywood pushed a commit to branch htmx
in repository https://gitbox.apache.org/repos/asf/causeway.git

commit 5d46a3bd7b5e2562b14862e904c1f719dd713fe1
Author: Dan Haywood <da...@haywood-associates.co.uk>
AuthorDate: Mon Aug 31 23:13:29 2026 +0200

    fix(webcomponents): correct picker textarea and pager presentation
---
 .../design.md                                      | 17 ++++++
 .../proposal.md                                    |  5 +-
 .../specs/domain-web-components/spec.md            | 25 +++++++-
 .../generic-htmx-web-component-viewer/spec.md      |  2 +-
 .../specs/vaadin-semantic-editor-families/spec.md  | 27 ++++++++-
 .../tasks.md                                       | 31 +++++-----
 viewers/webcomponents/foundation/USAGE.adoc        |  5 +-
 .../foundation/src/collection-element.mjs          |  5 +-
 .../webcomponents/foundation/src/field-widget.mjs  | 12 ++--
 viewers/webcomponents/foundation/src/theme.css     | 12 ++--
 .../test/collection-grid-responsive.test.mjs       | 47 ++++++++++++++-
 .../foundation/test/component-styles.test.mjs      |  3 +-
 .../foundation/test/field-widget.test.mjs          | 37 +++++++++---
 .../foundation/vaadin-fields/audit.mjs             | 63 +++++++++++---------
 .../petclinic/PetClinicHtmxPlaywrightTest.java     | 68 +++++++++++++++++-----
 15 files changed, 269 insertions(+), 90 deletions(-)

diff --git a/openspec/changes/fix-time-picker-multiline-and-collection-pagination/design.md b/openspec/changes/fix-time-picker-multiline-and-collection-pagination/design.md
index bda5ec86cb3..e789fd97411 100644
--- a/openspec/changes/fix-time-picker-multiline-and-collection-pagination/design.md
+++ b/openspec/changes/fix-time-picker-multiline-and-collection-pagination/design.md
@@ -4,6 +4,10 @@ The shared picker bridge currently moves focus from a time input to Vaadin's sha
 That delay made a state-based Playwright assertion pass but separates overlay creation from the trusted keyboard event and permits asynchronous prompt replacement to supersede the target.
 A robust browser contract must verify the actual Vaadin overlay is visibly presented, not merely that a Boolean briefly becomes true.
 
+The prior change configured `step=60` to remove seconds and milliseconds.
+Pinned Vaadin 25.2.8 deliberately generates no dropdown items when `step` is below 15 minutes, so `opened=true` can coexist with a closed zero-size overlay.
+The supported minimum dropdown interval is therefore 900 seconds.
+
 The application theme treats `textarea:not([slot="input"])` as native.
 Vaadin Text Area uses an internal `<textarea slot="textarea">`, so the selector applies native border, padding, sizing, and focus rules inside the toolkit control.
 The desired outer orange boundary in the screenshot is Vaadin's own focus ring; the inner textarea boundary is the leaked application-native styling.
@@ -15,6 +19,7 @@ The pager currently renders only `Items rangeStart–rangeEnd`, even when a safe
 
 **Goals:**
 
+- Use Vaadin's supported 15-minute dropdown interval while keeping minute-format presentation without seconds.
 - Make Enter and Space on a focused clock affordance synchronously present the real time overlay.
 - Verify the overlay itself is open and visible in a real browser.
 - Leave exactly the toolkit-owned multiline boundary and focus ring around Vaadin Text Area.
@@ -24,12 +29,23 @@ The pager currently renders only `Items rangeStart–rangeEnd`, even when a safe
 **Non-Goals:**
 
 - Do not replace Vaadin's time overlay or implement a Causeway-owned time list.
+- Do not promise arbitrary one-minute picker choices that pinned Vaadin cannot present.
 - Do not remove focus indication or borders from native fallback textareas.
 - Do not infer collection totals from offsets, page size, navigation flags, or loaded rows.
 - Do not change GraphQL collection window metadata or requests.
 
 ## Decisions
 
+### Use the supported quarter-hour dropdown step
+
+Editable qualified `LocalTime` and `LocalDateTime` controls will use `step=900`.
+This is Vaadin's minimum supported interval for generated dropdown items and yields visible quarter-hour choices formatted to minutes without seconds.
+Read-only controls retain `step=0.001` so authoritative seconds and milliseconds remain displayable, and cancellation continues to preserve the original authoritative value.
+
+Keeping `step=60` was rejected because Vaadin's `__generateDropdownList` intentionally returns no items below 900 seconds.
+Depending on private `_dropdownItems` was rejected because it would bypass the pinned public component contract.
+A custom Causeway-owned minute list was rejected by the selected scope.
+
 ### Open the picker synchronously during keyboard activation
 
 The qualified toggle will consume Enter and Space and, during the trusted key event, return focus to the picker input and invoke the pinned public `open()` method.
@@ -67,6 +83,7 @@ This applies equally to native bounded tables and bounded Grid mode because the
 
 ## Risks / Trade-offs
 
+- [Quarter-hour choices are coarser than arbitrary minute entry] → Document the Vaadin limitation explicitly and retain minute-formatted read-only values and authoritative cancellation semantics.
 - [Synchronous focus transfer could alter keyboard focus expectations] → Preserve the explicit toggle Tab stop, open only on Enter or Space, and verify reverse Tab and prompt containment separately.
 - [Vaadin changes its overlay implementation] → Keep the browser assertion inside the pinned adapter audit so dependency drift fails qualification.
 - [Changing textarea selectors could affect other slotted controls] → Scope only the textarea selector and verify native fallback and Vaadin multiline controls independently.
diff --git a/openspec/changes/fix-time-picker-multiline-and-collection-pagination/proposal.md b/openspec/changes/fix-time-picker-multiline-and-collection-pagination/proposal.md
index e10bbb484ce..86666d61264 100644
--- a/openspec/changes/fix-time-picker-multiline-and-collection-pagination/proposal.md
+++ b/openspec/changes/fix-time-picker-multiline-and-collection-pagination/proposal.md
@@ -5,6 +5,7 @@ Keyboard activation can set time-picker state without presenting a usable visibl
 
 ## What Changes
 
+- Configure editable Vaadin time and date-time controls with the toolkit's minimum supported dropdown interval of 15 minutes while retaining minute-formatted values without seconds.
 - Open the real Vaadin time-picker overlay synchronously from Enter or Space activation while the trusted keyboard event is active, and verify visible popover presentation rather than only the `opened` property.
 - Preserve the clock trigger's Tab and reverse-Tab accessibility without using delayed activation that can lose browser activation context or target a superseded control.
 - Restrict application native textarea styling to unslotted native controls so Vaadin's internal textarea receives only toolkit-owned boundaries and focus indication.
@@ -19,8 +20,8 @@ None.
 
 ### Modified Capabilities
 
-- `vaadin-semantic-editor-families`: Require trusted keyboard activation to present a visible time overlay and prevent native textarea chrome from crossing into Vaadin multiline internals.
-- `domain-web-components`: Require accessible time-picker activation and bounded collection range labels to reflect available authoritative totals.
+- `vaadin-semantic-editor-families`: Use the supported 15-minute dropdown interval, require trusted keyboard activation to present a visible time overlay, and prevent native textarea chrome from crossing into Vaadin multiline internals.
+- `domain-web-components`: Require quarter-hour picker choices with minute-format presentation, accessible time-picker activation, and bounded collection range labels that reflect available authoritative totals.
 - `vaadin-collection-grid-adapter`: Require bounded pager labels to include safe authoritative totals without inventing unavailable counts.
 - `generic-htmx-web-component-viewer`: Qualify the corrected time overlay, multiline boundary, and paged Petclinic collection labels in the executable browser profile.
 
diff --git a/openspec/changes/fix-time-picker-multiline-and-collection-pagination/specs/domain-web-components/spec.md b/openspec/changes/fix-time-picker-multiline-and-collection-pagination/specs/domain-web-components/spec.md
index 954b01eafea..77e680f602a 100644
--- a/openspec/changes/fix-time-picker-multiline-and-collection-pagination/specs/domain-web-components/spec.md
+++ b/openspec/changes/fix-time-picker-multiline-and-collection-pagination/specs/domain-web-components/spec.md
@@ -1,3 +1,24 @@
+## MODIFIED Requirements
+
+### Requirement: Shared minute-precision temporal entry
+Editable Web Component properties and action parameters backed by qualified Vaadin `LocalTime` or `LocalDateTime` controls SHALL present time without seconds or fractional seconds and SHALL use Vaadin's supported 15-minute dropdown interval.
+The behavior SHALL remain consistent across direct property editing, inline prompts, modal prompts, and sidebar prompts.
+
+#### Scenario: Property time enters editing
+- **WHEN** a qualified `LocalTime` or `LocalDateTime` property enters editing
+- **THEN** its visible time entry omits seconds and fractional seconds
+- **AND** its picker offers quarter-hour choices
+
+#### Scenario: Action time parameter is prepared
+- **WHEN** a qualified `LocalTime` or `LocalDateTime` action parameter is prepared in any supported prompt style
+- **THEN** its visible time entry uses the same 15-minute dropdown interval
+- **AND** no default, validation, confirmation, or invocation semantics change
+
+#### Scenario: Precise value remains authoritative without commit
+- **WHEN** an accepted authoritative value contains seconds, fractional seconds, or a minute outside a quarter-hour choice and the interaction is only opened or cancelled
+- **THEN** the authoritative value retains its accepted precision
+- **AND** no rounded value is submitted
+
 ## ADDED Requirements
 
 ### Requirement: User-visible time-picker keyboard activation
@@ -6,12 +27,12 @@ Focus transfer and overlay activation MUST preserve Causeway-owned pending value
 
 #### Scenario: Property clock is keyboard activated
 - **WHEN** a user presses Enter or Space on an editable property's current clock trigger
-- **THEN** the visible minute-resolution time overlay opens
+- **THEN** the visible time overlay opens with quarter-hour choices formatted to minutes
 - **AND** the property is not saved merely by opening the overlay
 
 #### Scenario: Parameter clock is keyboard activated
 - **WHEN** a user presses Enter or Space on an editable action parameter's current clock trigger
-- **THEN** the visible minute-resolution time overlay opens within the prompt lifecycle
+- **THEN** the visible time overlay opens with quarter-hour choices within the prompt lifecycle
 - **AND** the action is not invoked merely by opening the overlay
 
 ### Requirement: Authoritative bounded collection range totals
diff --git a/openspec/changes/fix-time-picker-multiline-and-collection-pagination/specs/generic-htmx-web-component-viewer/spec.md b/openspec/changes/fix-time-picker-multiline-and-collection-pagination/specs/generic-htmx-web-component-viewer/spec.md
index 5eeb4ee57f8..5d8c0ec3d2e 100644
--- a/openspec/changes/fix-time-picker-multiline-and-collection-pagination/specs/generic-htmx-web-component-viewer/spec.md
+++ b/openspec/changes/fix-time-picker-multiline-and-collection-pagination/specs/generic-htmx-web-component-viewer/spec.md
@@ -5,7 +5,7 @@ The Petclinic browser profile SHALL verify user-visible keyboard time selection,
 
 #### Scenario: Visit time overlay is keyboard opened
 - **WHEN** the browser tabs from the visit time input to its clock trigger and presses Enter or Space
-- **THEN** the real Vaadin time overlay is visibly open with minute-resolution choices
+- **THEN** the real Vaadin time overlay is visibly open with quarter-hour choices formatted to minutes
 - **AND** no booking mutation occurs until the user invokes the action
 
 #### Scenario: Visit reason has one boundary
diff --git a/openspec/changes/fix-time-picker-multiline-and-collection-pagination/specs/vaadin-semantic-editor-families/spec.md b/openspec/changes/fix-time-picker-multiline-and-collection-pagination/specs/vaadin-semantic-editor-families/spec.md
index 1c1909d423e..0f11ea4010e 100644
--- a/openspec/changes/fix-time-picker-multiline-and-collection-pagination/specs/vaadin-semantic-editor-families/spec.md
+++ b/openspec/changes/fix-time-picker-multiline-and-collection-pagination/specs/vaadin-semantic-editor-families/spec.md
@@ -1,3 +1,28 @@
+## MODIFIED Requirements
+
+### Requirement: Minute-resolution local-time entry
+Each editable qualified Vaadin `LocalTime` or `LocalDateTime` control SHALL present time without seconds or fractional seconds and SHALL use the toolkit-supported 15-minute dropdown interval.
+The adapter MUST use the public step contract, MUST NOT fabricate private dropdown items, and MUST NOT mutate authoritative state merely by entering edit mode.
+
+#### Scenario: User edits a local time
+- **WHEN** a property or action parameter opens an editable qualified `LocalTime` control
+- **THEN** the visible entry omits seconds and fractional seconds
+- **AND** the picker offers quarter-hour choices through Vaadin's supported overlay
+
+#### Scenario: User edits a local date-time
+- **WHEN** a property or action parameter opens an editable qualified `LocalDateTime` control
+- **THEN** its integrated time field uses the same 15-minute dropdown interval
+- **AND** the local date, local time, and absence of browser timezone conversion remain authoritative
+
+#### Scenario: User cancels a precise existing value
+- **WHEN** an authoritative value contains accepted seconds, fractional seconds, or a minute outside a quarter-hour choice and the user cancels without committing a changed picker value
+- **THEN** the authoritative value remains unchanged
+- **AND** no rounded or truncated value is submitted
+
+#### Scenario: Value is read-only or uses native fallback
+- **WHEN** the value is presented read-only or the Vaadin family is ineligible or disabled
+- **THEN** its established precision and native semantic behavior remain unchanged
+
 ## ADDED Requirements
 
 ### Requirement: Visibly presented keyboard time overlay
@@ -7,7 +32,7 @@ The qualification contract MUST verify visible overlay presentation and MUST NOT
 #### Scenario: User opens time choices with Enter
 - **WHEN** keyboard focus is on the current clock trigger and the user presses Enter
 - **THEN** focus returns to the associated time input and the Vaadin overlay is visibly presented
-- **AND** minute-resolution choices are available without committing or cancelling the Causeway interaction
+- **AND** quarter-hour choices formatted to minutes are available without committing or cancelling the Causeway interaction
 
 #### Scenario: User opens time choices with Space
 - **WHEN** keyboard focus is on the current clock trigger and the user presses Space
diff --git a/openspec/changes/fix-time-picker-multiline-and-collection-pagination/tasks.md b/openspec/changes/fix-time-picker-multiline-and-collection-pagination/tasks.md
index 39b8efb9ab5..2fd2145eaca 100644
--- a/openspec/changes/fix-time-picker-multiline-and-collection-pagination/tasks.md
+++ b/openspec/changes/fix-time-picker-multiline-and-collection-pagination/tasks.md
@@ -1,29 +1,30 @@
 ## 1. Visible keyboard time overlay
 
-- [ ] 1.1 Replace delayed clock-trigger activation with synchronous trusted-event focus transfer and Vaadin `open()` invocation for Enter and Space.
-- [ ] 1.2 Preserve Tab, reverse Tab, disabled/read-only omission, current-generation safety, pointer behavior, and minute resolution.
-- [ ] 1.3 Strengthen foundation unit and Vaadin browser-audit coverage to require an actually visible `:popover-open` time overlay rather than only picker state.
+- [x] 1.1 Configure editable time and date-time pickers with Vaadin's supported 900-second dropdown interval while retaining minute-format presentation and precise read-only values.
+- [x] 1.2 Replace delayed clock-trigger activation with synchronous trusted-event focus transfer and Vaadin `open()` invocation for Enter and Space.
+- [x] 1.3 Preserve Tab, reverse Tab, disabled/read-only omission, current-generation safety, pointer behavior, and authoritative cancellation semantics.
+- [x] 1.4 Strengthen foundation unit and Vaadin browser-audit coverage to require an actually visible `:popover-open` time overlay rather than only picker state.
 
 ## 2. Single multiline boundary
 
-- [ ] 2.1 Restrict application native textarea selectors to unslotted controls so Vaadin's `slot="textarea"` avoids native border, padding, sizing, and focus chrome.
-- [ ] 2.2 Update structural style tests and real-browser assertions for one Vaadin boundary while retaining native fallback textarea presentation.
+- [x] 2.1 Restrict application native textarea selectors to unslotted controls so Vaadin's `slot="textarea"` avoids native border, padding, sizing, and focus chrome.
+- [x] 2.2 Update structural style tests and real-browser assertions for one Vaadin boundary while retaining native fallback textarea presentation.
 
 ## 3. Authoritative collection totals
 
-- [ ] 3.1 Append valid non-negative safe-integer `totalCount` metadata to non-empty bounded pager range labels.
-- [ ] 3.2 Preserve `No items` and unknown-total behavior without deriving counts from offsets, loaded rows, page size, or navigation flags.
-- [ ] 3.3 Extend collection unit and responsive tests across first, middle, final, unavailable-total, empty, and invalid-total windows.
+- [x] 3.1 Append valid non-negative safe-integer `totalCount` metadata to non-empty bounded pager range labels.
+- [x] 3.2 Preserve `No items` and unknown-total behavior without deriving counts from offsets, loaded rows, page size, or navigation flags.
+- [x] 3.3 Extend collection unit and responsive tests across first, middle, final, unavailable-total, empty, and invalid-total windows.
 
 ## 4. Petclinic acceptance and documentation
 
-- [ ] 4.1 Extend Petclinic Playwright coverage to prove Enter and Space visibly open the current visit time overlay without premature booking.
-- [ ] 4.2 Verify the visit reason's slotted textarea has no nested native border or outline while the Vaadin input container remains visibly bounded.
-- [ ] 4.3 Verify paged Visits collection labels show correct ranges and the same authoritative total across pages.
-- [ ] 4.4 Update component usage documentation for visible keyboard time activation, unslotted native textarea styling, and authoritative pager totals.
+- [x] 4.1 Extend Petclinic Playwright coverage to prove Enter and Space visibly open the current visit time overlay without premature booking.
+- [x] 4.2 Verify the visit reason's slotted textarea has no nested native border or outline while the Vaadin input container remains visibly bounded.
+- [x] 4.3 Verify paged Visits collection labels show correct ranges and the same authoritative total across pages.
+- [x] 4.4 Update component usage documentation for visible keyboard time activation, unslotted native textarea styling, and authoritative pager totals.
 
 ## 5. Validation
 
-- [ ] 5.1 Run complete foundation Node and Maven suites plus the Vaadin field browser audit.
-- [ ] 5.2 Run focused Petclinic Playwright acceptance and verify console, page-error, CSP, external-request, overlay, focus, and overflow diagnostics.
-- [ ] 5.3 Run relevant IDE inspections or project compilation, strict OpenSpec validation, and diff checks.
+- [x] 5.1 Run complete foundation Node and Maven suites plus the Vaadin field browser audit.
+- [x] 5.2 Run focused Petclinic Playwright acceptance and verify console, page-error, CSP, external-request, overlay, focus, and overflow diagnostics.
+- [x] 5.3 Run relevant IDE inspections or project compilation, strict OpenSpec validation, and diff checks.
diff --git a/viewers/webcomponents/foundation/USAGE.adoc b/viewers/webcomponents/foundation/USAGE.adoc
index 35f730a5f8e..a8146ab0b85 100644
--- a/viewers/webcomponents/foundation/USAGE.adoc
+++ b/viewers/webcomponents/foundation/USAGE.adoc
@@ -577,6 +577,7 @@ The collection's `rowContexts` property exposes the currently hydrated controlle
 
 When targeted introspection finds the additive rich `window(offset, size)` field, collections use bounded secondary window operations and retain the documented legacy-list fallback.
 `paged="N"` requests bounded windows of `N` rows and renders Causeway-owned previous and next controls when the advertised window permits them.
+A non-empty pager displays `Items <start>–<end> of <total>` when the server advertises a valid authoritative total, and omits the `of` suffix rather than inferring an unavailable count.
 The page size must be an integer from 1 through 100; absent, blank, malformed, fractional, zero, negative, and oversized values provide no override.
 Changing the effective page size retires stale collection work and restarts at offset zero.
 Paging remains available in native table presentation.
@@ -670,11 +671,13 @@ configureCausewayFieldWidgets({families: [], presentation: false});
 `basic` covers reversible text, multiline, protected, Boolean, enum, bounded scalar choice, and URL inputs.
 `numeric` keeps `Long`, `BigInteger`, and `BigDecimal` lexical in text fields while using integer or number controls only for machine numeric codecs.
 `local-temporal` covers `LocalDate`, `LocalTime`, and `LocalDateTime`; values beyond millisecond fractional precision remain native because the picker cannot preserve them.
-Qualified read-only values preserve accepted precision, while editable local-time and local-date-time controls use a sixty-second step for minute-resolution entry through the existing Causeway codec.
+Qualified read-only values preserve accepted precision, while editable local-time and local-date-time controls use Vaadin's minimum supported 900-second dropdown interval and present time without seconds through the existing Causeway codec.
 Local dates use the nonblank document `lang`, then the browser locale, then `en` to format and parse the visible date while preserving the ISO value passed to Causeway codecs and GraphQL.
 Localized numeric input and unambiguous ISO `yyyy-MM-dd` input are accepted, and available locale calendar names and first-weekday settings are applied to the picker.
 In an editable Vaadin date picker, Tab moves from the date input to the labelled calendar button, and Enter or Space opens the calendar without committing or cancelling the Causeway interaction.
 Editable standalone and date-time time inputs expose the equivalent labelled clock button for Tab, reverse Tab, Enter, Space, and pointer use.
+Enter and Space synchronously open the visible Vaadin time overlay while the trusted keyboard event is active.
+Application native-textarea styling applies only to unslotted controls, leaving Vaadin's slotted multiline input with one toolkit-owned boundary and focus ring.
 Read-only and disabled date and time controls do not expose those operable picker buttons.
 Offset, zoned, legacy temporal, Blob, Clob, resource, custom, reference, collection, and unsupported values never become eligible field adapters.
 References retain their separate adapter.
diff --git a/viewers/webcomponents/foundation/src/collection-element.mjs b/viewers/webcomponents/foundation/src/collection-element.mjs
index dd75f14c4b3..c18b7fdb742 100644
--- a/viewers/webcomponents/foundation/src/collection-element.mjs
+++ b/viewers/webcomponents/foundation/src/collection-element.mjs
@@ -738,9 +738,12 @@ export class CausewayCollectionElement extends CausewayContextConsumerElement {
 
   #renderBoundedPager(window) {
     if (!window) return '';
+    const total = Number.isSafeInteger(window.totalCount) && window.totalCount >= 0
+      ? ` of ${window.totalCount}`
+      : '';
     const range = window.rangeStart == null
       ? 'No items'
-      : `Items ${window.rangeStart}–${window.rangeEnd}`;
+      : `Items ${window.rangeStart}–${window.rangeEnd}${total}`;
     return `<nav class="causeway-collection-pager" aria-label="Collection pages">
       <button type="button" data-causeway-grid-previous${window.hasPrevious ? '' : ' disabled aria-disabled="true"'}>Previous</button>
       <span class="causeway-collection-range" aria-live="polite">${range}</span>
diff --git a/viewers/webcomponents/foundation/src/field-widget.mjs b/viewers/webcomponents/foundation/src/field-widget.mjs
index 31fe53348eb..b0640e0171e 100644
--- a/viewers/webcomponents/foundation/src/field-widget.mjs
+++ b/viewers/webcomponents/foundation/src/field-widget.mjs
@@ -384,7 +384,7 @@ export class CausewayFieldEditorElement extends HTMLElement {
       } else {
         if (this.dataset.control === 'select') control.items = this.#items();
         if (['time-picker', 'date-time-picker'].includes(this.dataset.control)) {
-          control.step = viewMode ? 0.001 : 60;
+          control.step = viewMode ? 0.001 : 900;
         }
         control.value = this.dataset.value ?? '';
         if (this.dataset.rows && 'maxRows' in control) {
@@ -592,12 +592,10 @@ function qualifyCausewayPickerTrigger(picker, label, operable, {accessibleSuffix
       if (!['Enter', ' '].includes(event.key)) return;
       event.preventDefault();
       event.stopPropagation();
-      setTimeout(() => {
-        if (trigger.isConnected === false && picker.isConnected === false) return;
-        picker.inputElement?.focus?.();
-        if (typeof picker.open === 'function') picker.open();
-        else picker.opened = true;
-      }, 0);
+      if (trigger.isConnected === false || picker.isConnected === false) return;
+      picker.inputElement?.focus?.();
+      if (typeof picker.open === 'function') picker.open();
+      else picker.opened = true;
     });
     qualifiedPickerTriggers.add(trigger);
   }
diff --git a/viewers/webcomponents/foundation/src/theme.css b/viewers/webcomponents/foundation/src/theme.css
index 7e5881f37a7..d207ab4dee2 100644
--- a/viewers/webcomponents/foundation/src/theme.css
+++ b/viewers/webcomponents/foundation/src/theme.css
@@ -169,7 +169,7 @@ a:hover {
 button,
 input:not([slot="input"]),
 select:not([slot="input"]),
-textarea:not([slot="input"]) {
+textarea:not([slot]) {
   max-width: 100%;
   border: 1px solid var(--causeway-border-strong);
   border-radius: var(--causeway-radius-sm);
@@ -199,7 +199,7 @@ button:disabled,
   opacity: 0.62;
 }
 
-:where(a, button, input:not([slot="input"]), select:not([slot="input"]), textarea:not([slot="input"]), [tabindex]):focus-visible {
+:where(a, button, input:not([slot="input"]), select:not([slot="input"]), textarea:not([slot]), [tabindex]):focus-visible {
   outline: 3px solid var(--causeway-focus);
   outline-offset: 2px;
 }
@@ -210,11 +210,11 @@ cw-field-editor > vaadin-text-area:focus-visible {
 
 input:not([slot="input"]),
 select:not([slot="input"]),
-textarea:not([slot="input"]) {
+textarea:not([slot]) {
   padding: 0.45rem 0.6rem;
 }
 
-textarea:not([slot="input"]) {
+textarea:not([slot]) {
   min-height: 6rem;
   resize: vertical;
 }
@@ -446,7 +446,7 @@ cw-property:last-child .causeway-property {
   min-width: 0;
 }
 
-.causeway-property-editor > :where(input:not([slot="input"]), select:not([slot="input"]), textarea:not([slot="input"])) {
+.causeway-property-editor > :where(input:not([slot="input"]), select:not([slot="input"]), textarea:not([slot])) {
   width: 100%;
 }
 
@@ -692,7 +692,7 @@ cw-interaction-controller:has(.causeway-action-prompt-modal, .causeway-action-pr
   gap: var(--causeway-space-1);
 }
 
-.causeway-action-parameter :where(input:not([slot="input"]), select:not([slot="input"]), textarea:not([slot="input"])) {
+.causeway-action-parameter :where(input:not([slot="input"]), select:not([slot="input"]), textarea:not([slot])) {
   width: 100%;
 }
 
diff --git a/viewers/webcomponents/foundation/test/collection-grid-responsive.test.mjs b/viewers/webcomponents/foundation/test/collection-grid-responsive.test.mjs
index 94c5f7706a0..f888a4bbc2e 100644
--- a/viewers/webcomponents/foundation/test/collection-grid-responsive.test.mjs
+++ b/viewers/webcomponents/foundation/test/collection-grid-responsive.test.mjs
@@ -43,6 +43,7 @@ function row(offset = 0) {
 
 function result(offset = 0, totalCount = 30) {
   const rows = [row(offset)];
+  const knownUpperBound = totalCount ?? 30;
   return {
     descriptor: {id: 'people'},
     data: {window: {rows}},
@@ -55,9 +56,9 @@ function result(offset = 0, totalCount = 30) {
       countAvailable: totalCount != null,
       maximumSize: 20,
       hasPrevious: offset > 0,
-      hasNext: offset + 1 < 30,
+      hasNext: offset + 1 < knownUpperBound,
       previousOffset: offset > 0 ? Math.max(0, offset - 10) : null,
-      nextOffset: offset + 1 < 30 ? offset + 1 : null,
+      nextOffset: offset + 1 < knownUpperBound ? offset + 1 : null,
       rangeStart: offset + 1,
       rangeEnd: offset + 1,
       ordering: 'CONFIGURED'
@@ -138,6 +139,48 @@ test('container observer retains native at 48rem and reuses authoritative rows w
   assert.equal(observers[0].connected, false);
 });
 
+test('bounded pager presents an authoritative total consistently across pages', async () => {
+  configureCausewayGridWidgets({enabled: true});
+  const loads = [];
+  const context = {
+    registerRequirement(_requirement, listener) {
+      listener({status: 'ready', descriptor: {id: 'visits'}, data: {hidden: false}, errors: [], generation: 1});
+      return () => {};
+    },
+    async loadCollection(options) {
+      loads.push(options);
+      return result(options.offset ?? 0, 3);
+    },
+    createHydratedRowContext() {
+      return {disconnect() {}};
+    }
+  };
+  const collection = new CausewayCollectionElement();
+  collection.querySelector = () => ({presentation: null});
+  collection.id = 'visits';
+  collection.columns = [{member: 'name'}];
+  collection.paged = 1;
+  collection.active = true;
+  collection.acceptGridResponsiveState(true);
+  collection.context = context;
+  document.body.appendChild(collection);
+  await waitFor(() => collection.collectionState.status === 'ready');
+  assert.equal(collection.gridQualification.presentation, 'grid-bounded');
+  assert.match(collection.innerHTML, /Items 1–1 of 3/);
+
+  const next = document.createElement('button');
+  next.setAttribute('data-causeway-grid-next', '');
+  collection.appendChild(next);
+  next.dispatchEvent(new Event('click', {bubbles: true}));
+  await waitFor(() => collection.collectionState.window?.offset === 1);
+  assert.match(collection.innerHTML, /Items 2–2 of 3/);
+  next.dispatchEvent(new Event('click', {bubbles: true}));
+  await waitFor(() => collection.collectionState.window?.offset === 2);
+  assert.match(collection.innerHTML, /Items 3–3 of 3/);
+  assert.equal(collection.collectionState.window.hasNext, false);
+  document.body.removeChild(collection);
+});
+
 test('bounded pager uses only normalized offsets and never invents a total', async () => {
   configureCausewayGridWidgets({enabled: true});
   const loads = [];
diff --git a/viewers/webcomponents/foundation/test/component-styles.test.mjs b/viewers/webcomponents/foundation/test/component-styles.test.mjs
index e2c0d145728..6a137b5b971 100644
--- a/viewers/webcomponents/foundation/test/component-styles.test.mjs
+++ b/viewers/webcomponents/foundation/test/component-styles.test.mjs
@@ -101,7 +101,7 @@ test('application native-control chrome does not cross into toolkit-owned slotte
   const theme = await readFile(new URL('../src/theme.css', import.meta.url), 'utf8');
   const nativeInput = 'input:not([slot="input"])';
   const nativeSelect = 'select:not([slot="input"])';
-  const nativeTextarea = 'textarea:not([slot="input"])';
+  const nativeTextarea = 'textarea:not([slot])';
 
   assert.ok(theme.includes(`button,\n${nativeInput},\n${nativeSelect},\n${nativeTextarea} {\n  max-width: 100%;\n  border: 1px solid var(--causeway-border-strong);`));
   assert.ok(theme.includes(`${nativeInput},\n${nativeSelect},\n${nativeTextarea} {\n  padding: 0.45rem 0.6rem;`));
@@ -109,6 +109,7 @@ test('application native-control chrome does not cross into toolkit-owned slotte
   assert.ok(theme.includes(`:where(a, button, ${nativeInput}, ${nativeSelect}, ${nativeTextarea}, [tabindex]):focus-visible`));
   assert.match(theme, /cw-field-editor > vaadin-text-area:focus-visible \{\s+outline: none;/);
   assert.match(CAUSEWAY_COMPONENT_STYLES, /cw-field-editor \{[\s\S]*?--vaadin-focus-ring-color: var\(--causeway-focus, Highlight\);/);
+  assert.doesNotMatch(theme, /textarea:not\(\[slot="input"\]\)/);
   assert.doesNotMatch(theme, /cw-field-editor > textarea:focus-visible/);
   assert.doesNotMatch(theme, /\ninput,\nselect,\ntextarea \{\n  max-width:/);
   assert.doesNotMatch(theme, /:where\(a, button, input, select, textarea, \[tabindex\]\):focus-visible/);
diff --git a/viewers/webcomponents/foundation/test/field-widget.test.mjs b/viewers/webcomponents/foundation/test/field-widget.test.mjs
index 7ca6935badf..f8388003819 100644
--- a/viewers/webcomponents/foundation/test/field-widget.test.mjs
+++ b/viewers/webcomponents/foundation/test/field-widget.test.mjs
@@ -145,17 +145,21 @@ test('local date presentation follows bounded document and browser locales rever
   assert.equal(causewayDatePickerI18n('%%%').formatDate({year: 2026, month: 7, day: 30}), '8/30/2026');
 });
 
-test('calendar trigger gains bounded button semantics and keyboard activation only when operable', async () => {
+test('calendar trigger gains bounded button semantics and synchronous keyboard activation only when operable', () => {
   const trigger = document.createElement('div');
   trigger.setAttribute('aria-hidden', 'true');
+  trigger.isConnected = true;
   let activations = 0;
+  let openCalls = 0;
   trigger.click = () => { activations += 1; };
   const input = document.createElement('input');
   const picker = {
     disabled: false,
     readOnly: false,
     opened: false,
+    isConnected: true,
     inputElement: input,
+    open() { this.opened = true; openCalls += 1; },
     shadowRoot: {querySelector: selector => selector === '[part~="toggle-button"]' ? trigger : null}
   };
   assert.equal(qualifyCausewayCalendarTrigger(picker, 'Last Visit'), trigger);
@@ -185,9 +189,11 @@ test('calendar trigger gains bounded button semantics and keyboard activation on
     keyup.key = key;
     trigger.dispatchEvent(keyup);
     assert.equal(keyup.defaultPrevented, true);
+    assert.equal(picker.opened, true);
+    assert.equal(document.activeElement, input);
+    picker.opened = false;
   }
-  await new Promise(resolve => setTimeout(resolve, 0));
-  assert.equal(picker.opened, true);
+  assert.equal(openCalls, 2);
   assert.equal(activations, 0);
   trigger.click();
   assert.equal(activations, 1);
@@ -195,17 +201,21 @@ test('calendar trigger gains bounded button semantics and keyboard activation on
   assert.equal(qualifyCausewayCalendarTrigger({...picker, disabled: true}, 'Last Visit'), null);
 });
 
-test('time trigger gains bounded button semantics and keyboard and pointer activation only when operable', async () => {
+test('time trigger gains bounded button semantics and synchronous keyboard and pointer activation', () => {
   const trigger = document.createElement('div');
   trigger.setAttribute('aria-hidden', 'true');
+  trigger.isConnected = true;
   let activations = 0;
+  let openCalls = 0;
   trigger.click = () => { activations += 1; };
   const input = document.createElement('input');
   const picker = {
     disabled: false,
     readOnly: false,
     opened: false,
+    isConnected: true,
     inputElement: input,
+    open() { this.opened = true; openCalls += 1; },
     shadowRoot: {querySelector: selector => selector === '[part~="toggle-button"]' ? trigger : null}
   };
   assert.equal(qualifyCausewayTimeTrigger(picker, 'Visit At'), trigger);
@@ -238,18 +248,27 @@ test('time trigger gains bounded button semantics and keyboard and pointer activ
     keyup.key = key;
     trigger.dispatchEvent(keyup);
     assert.equal(keyup.defaultPrevented, true);
+    assert.equal(picker.opened, true);
+    assert.equal(document.activeElement, input);
+    picker.opened = false;
   }
-  await new Promise(resolve => setTimeout(resolve, 0));
-  assert.equal(picker.opened, true);
+  assert.equal(openCalls, 2);
   assert.equal(activations, 0);
   trigger.click();
   assert.equal(activations, 1);
+  trigger.isConnected = false;
+  picker.isConnected = false;
+  const staleKeyup = new Event('keyup', {cancelable: true});
+  staleKeyup.key = 'Enter';
+  trigger.dispatchEvent(staleKeyup);
+  assert.equal(picker.opened, false);
+  assert.equal(openCalls, 2);
   assert.equal(qualifyCausewayTimeTrigger({...picker, readOnly: true}, 'Visit At'), null);
   assert.equal(qualifyCausewayTimeTrigger({...picker, disabled: true}, 'Visit At'), null);
   assert.equal(qualifyCausewayTimeTrigger({...picker, shadowRoot: {querySelector: () => null}}, 'Visit At'), null);
 });
 
-test('editable temporal adapters use minute steps and qualify standalone and composite clock triggers', async () => {
+test('editable temporal adapters use supported quarter-hour dropdown steps and qualify clock triggers', async () => {
   configureCausewayFieldWidgets({
     families: ['local-temporal'],
     moduleUrls: {'local-temporal': fakeLocalTemporalModule}
@@ -264,7 +283,7 @@ test('editable temporal adapters use minute steps and qualify standalone and com
   document.body.appendChild(timeEditor);
   await new Promise(resolve => setTimeout(resolve, 0));
   const timeControl = timeEditor.childNodes[0];
-  assert.equal(timeControl.step, 60);
+  assert.equal(timeControl.step, 900);
   assert.equal(timeControl.value, '13:14:15.123');
   assert.equal(timeControl._trigger.getAttribute('aria-label'), 'Open Visit At time picker');
   assert.equal(timeControl._trigger.hasAttribute('data-causeway-time-trigger'), true);
@@ -279,7 +298,7 @@ test('editable temporal adapters use minute steps and qualify standalone and com
   document.body.appendChild(dateTimeEditor);
   await new Promise(resolve => setTimeout(resolve, 0));
   const dateTimeControl = dateTimeEditor.childNodes[0];
-  assert.equal(dateTimeControl.step, 60);
+  assert.equal(dateTimeControl.step, 900);
   assert.equal(dateTimeControl.value, '2026-08-24T13:14:15.123');
   assert.equal(dateTimeControl._datePicker._trigger.hasAttribute('data-causeway-calendar-trigger'), true);
   assert.equal(dateTimeControl._timePicker._trigger.hasAttribute('data-causeway-time-trigger'), true);
diff --git a/viewers/webcomponents/foundation/vaadin-fields/audit.mjs b/viewers/webcomponents/foundation/vaadin-fields/audit.mjs
index 5d328d171c4..00525dbc08a 100644
--- a/viewers/webcomponents/foundation/vaadin-fields/audit.mjs
+++ b/viewers/webcomponents/foundation/vaadin-fields/audit.mjs
@@ -39,7 +39,7 @@ console.log(JSON.stringify(results, null, 2));
 if (!writePolicy && Object.values(results).some(result => result.cspViolations.length || result.axeViolations.length
     || result.consoleErrors.length || result.pageErrors.length || result.externalRequests.length || result.overflow
     || result.readOnlyState.some(state => state.changed || state.opened || !state.named || !state.described || !state.readOnly)
-    || result.timePickerState.some(state => !state.minuteStep || !state.hasToggle || !state.pointerOperable))) {
+    || result.timePickerState.some(state => !state.quarterHourStep || !state.hasToggle || !state.pointerOperable || !state.visibleOverlay))) {
   process.exitCode = 1;
 }
 
@@ -96,29 +96,34 @@ async function auditFamily(family, controls, hashes) {
       control.blur();
     });
   }
-  const timePickerState = family === 'local-temporal' ? await page.evaluate(() => {
-    const states = [];
-    for (const control of document.querySelectorAll('[id^="control-"]')) {
-      const pickers = control.localName === 'vaadin-time-picker' ? [control] : [];
-      for (const root of [control, control.shadowRoot]) {
-        pickers.push(...(root?.querySelectorAll?.('vaadin-time-picker') ?? []));
-      }
-      for (const picker of new Set(pickers)) {
-        const trigger = picker.shadowRoot?.querySelector('[part~="toggle-button"]');
-        const before = Boolean(picker.opened);
-        trigger?.click();
-        const pointerOperable = !before && Boolean(picker.opened);
-        picker.close?.();
-        states.push({
-          owner: control.id,
-          minuteStep: picker.step === 60,
-          hasToggle: Boolean(trigger),
-          pointerOperable
-        });
-      }
+  const timePickerState = [];
+  if (family === 'local-temporal') {
+    for (const [index, tag] of controls.entries()) {
+      if (!['vaadin-time-picker', 'vaadin-date-time-picker'].includes(tag)) continue;
+      const owner = `control-${index}`;
+      const picker = page.locator(tag === 'vaadin-time-picker' ? `#${owner}` : `#${owner} vaadin-time-picker`);
+      const trigger = picker.locator('[part~="toggle-button"]');
+      const hasToggle = await trigger.count() === 1;
+      await picker.evaluate(control => control.inputElement?.focus());
+      if (hasToggle) await trigger.click();
+      const state = await picker.evaluate(async control => {
+        const overlay = control.$?.overlay ?? control.shadowRoot?.querySelector('vaadin-time-picker-overlay');
+        for (let frame = 0; frame < 10 && !overlay?.matches?.(':popover-open'); frame += 1) {
+          await new Promise(resolve => requestAnimationFrame(resolve));
+        }
+        const bounds = overlay?.getBoundingClientRect?.();
+        return {
+          quarterHourStep: control.step === 900,
+          pointerOperable: Boolean(control.opened),
+          visibleOverlay: Boolean(overlay?.matches?.(':popover-open')
+            && bounds?.width > 0
+            && bounds?.height > 0)
+        };
+      });
+      await picker.evaluate(control => control.close?.());
+      timePickerState.push({owner, hasToggle, ...state});
     }
-    return states;
-  }) : [];
+  }
   const readOnlyState = [];
   for (const [index, tag] of controls.entries()) {
     const locator = page.locator(`#view-${index}`);
@@ -220,9 +225,9 @@ function fixture(family, controls) {
     control.id = 'control-${index}';
     control.required = ${index === 0};
     if ('items' in control) control.items = [{label: 'One', value: 'one'}, {label: 'Two', value: 'two'}];
-    if ('${tag}' === 'vaadin-time-picker' || '${tag}' === 'vaadin-date-time-picker') control.step = 60;
+    if ('${tag}' === 'vaadin-time-picker' || '${tag}' === 'vaadin-date-time-picker') control.step = 900;
     if ('${tag}' === 'vaadin-checkbox') control.checked = true;
-    else if ('value' in control) control.value = ${JSON.stringify(initialValue(family, tag))};
+    else if ('value' in control) control.value = ${JSON.stringify(initialValue(family, tag, true))};
     fixture.append(control);
     await control.updateComplete;
     for (const field of control.querySelectorAll('vaadin-date-picker, vaadin-time-picker')) {
@@ -244,7 +249,7 @@ function fixture(family, controls) {
     if ('items' in view) view.items = [{label: 'One', value: 'one'}, {label: 'Two', value: 'two'}];
     if ('${tag}' === 'vaadin-time-picker' || '${tag}' === 'vaadin-date-time-picker') view.step = 0.001;
     if ('${tag}' === 'vaadin-checkbox') view.checked = true;
-    else if ('value' in view) view.value = ${JSON.stringify(initialValue(family, tag))};
+    else if ('value' in view) view.value = ${JSON.stringify(initialValue(family, tag, false))};
     fixture.append(label, description, view);
     await view.updateComplete;
     const compositeFields = [...view.querySelectorAll('vaadin-date-picker, vaadin-time-picker')];
@@ -268,12 +273,12 @@ await new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(r
 document.body.dataset.ready = 'true';`;
 }
 
-function initialValue(family, tag) {
+function initialValue(family, tag, editable) {
   if (tag === 'vaadin-checkbox') return false;
   if (tag === 'vaadin-select') return 'one';
   if (tag === 'vaadin-date-picker') return '2026-08-24';
-  if (tag === 'vaadin-time-picker') return '13:14:15.123';
-  if (tag === 'vaadin-date-time-picker') return '2026-08-24T13:14:15.123';
+  if (tag === 'vaadin-time-picker') return editable ? '13:15' : '13:14:15.123';
+  if (tag === 'vaadin-date-time-picker') return editable ? '2026-08-24T13:15' : '2026-08-24T13:14:15.123';
   if (family === 'numeric') return '12';
   return tag === 'vaadin-password-field' ? '' : 'initial';
 }
diff --git a/viewers/webcomponents/sample-htmx-petclinic/src/playwright/java/org/apache/causeway/viewer/webcomponents/sample/htmx/petclinic/PetClinicHtmxPlaywrightTest.java b/viewers/webcomponents/sample-htmx-petclinic/src/playwright/java/org/apache/causeway/viewer/webcomponents/sample/htmx/petclinic/PetClinicHtmxPlaywrightTest.java
index 00e00e79520..01becb64f3f 100644
--- a/viewers/webcomponents/sample-htmx-petclinic/src/playwright/java/org/apache/causeway/viewer/webcomponents/sample/htmx/petclinic/PetClinicHtmxPlaywrightTest.java
+++ b/viewers/webcomponents/sample-htmx-petclinic/src/playwright/java/org/apache/causeway/viewer/webcomponents/sample/htmx/petclinic/PetClinicHtmxPlaywrightTest.java
@@ -237,6 +237,8 @@ void routesHomeObjectsHistoryCollectionsAndResponsiveLayout() {
         page.locator("cw-collection[id='petOwners'] [data-causeway-collection-search-clear]").click();
         page.waitForFunction("() => document.querySelector(\"cw-collection[id='petOwners']\")?.collectionState?.window?.totalCount === 4");
         waitForCollectionRows("petOwners", 2);
+        assertThat(page.locator("cw-collection[id='petOwners'] .causeway-collection-range").textContent())
+                .isEqualTo("Items 1–2 of 4");
         page.locator("cw-collection[id='petOwners'] [data-causeway-collection-sort='name']").click();
         page.waitForFunction("() => document.querySelector(\"cw-collection[id='petOwners']\")?.collectionState?.window?.ordering === 'REQUESTED'");
         assertThat(page.locator("cw-collection[id='petOwners'] [data-causeway-collection-sort='name']").getAttribute("aria-label"))
@@ -245,6 +247,8 @@ void routesHomeObjectsHistoryCollectionsAndResponsiveLayout() {
         page.locator("cw-collection[id='petOwners'] [data-causeway-grid-next]").click();
         page.waitForFunction("() => document.querySelector(\"cw-collection[id='petOwners']\")?.collectionState?.window?.offset === 2");
         waitForCollectionRows("petOwners", 2);
+        assertThat(page.locator("cw-collection[id='petOwners'] .causeway-collection-range").textContent())
+                .isEqualTo("Items 3–4 of 4");
         assertThat(page.locator("cw-collection[id='petOwners'] [data-causeway-grid-previous]").isVisible()).isTrue();
         clickObjectLink("Mary Smith");
         waitForRoute("petclinic.PetOwner", "s_owner-mary");
@@ -281,14 +285,30 @@ void routesHomeObjectsHistoryCollectionsAndResponsiveLayout() {
         page.waitForFunction("() => document.querySelector(\"cw-collection[id='pets']\")?.collectionState?.window?.ordering === 'REQUESTED'");
         assertThat(page.locator("cw-collection[id='pets'] cw-action[id='addPet']").isVisible()).isTrue();
         assertThat(page.locator("cw-collection[id='visits']").getAttribute("paged")).isEqualTo("1");
-        assertThat(page.locator("cw-collection[id='visits'] [data-causeway-grid-next]").isVisible()).isTrue();
-        page.locator("cw-collection[id='visits'] [data-causeway-grid-next]").click();
-        page.waitForFunction("() => document.querySelector(\"cw-collection[id='visits']\")?.collectionState?.window?.offset === 1");
-        waitForCollectionRows("visits", 1);
-        assertThat(page.locator("cw-collection[id='visits'] [data-causeway-grid-previous]").isVisible()).isTrue();
-        page.locator("cw-collection[id='visits'] [data-causeway-grid-previous]").click();
-        page.waitForFunction("() => document.querySelector(\"cw-collection[id='visits']\")?.collectionState?.window?.offset === 0");
-        waitForCollectionRows("visits", 1);
+        final var visitTotal = ((Number) page.locator("cw-collection[id='visits']")
+                .evaluate("element => element.collectionState.window.totalCount")).intValue();
+        assertThat(visitTotal).isGreaterThan(1);
+        assertThat(page.locator("cw-collection[id='visits'] .causeway-collection-range").textContent())
+                .isEqualTo("Items 1–1 of " + visitTotal);
+        for (int offset = 1; offset < visitTotal; offset++) {
+            assertThat(page.locator("cw-collection[id='visits'] [data-causeway-grid-next]").isVisible()).isTrue();
+            page.locator("cw-collection[id='visits'] [data-causeway-grid-next]").click();
+            final var expectedOffset = offset;
+            page.waitForFunction("offset => document.querySelector(\"cw-collection[id='visits']\")?.collectionState?.window?.offset === offset", expectedOffset);
+            waitForCollectionRows("visits", 1);
+            assertThat(page.locator("cw-collection[id='visits'] .causeway-collection-range").textContent())
+                    .isEqualTo("Items " + (offset + 1) + "–" + (offset + 1) + " of " + visitTotal);
+        }
+        assertThat(page.locator("cw-collection[id='visits'] [data-causeway-grid-next]").isDisabled()).isTrue();
+        for (int offset = visitTotal - 2; offset >= 0; offset--) {
+            assertThat(page.locator("cw-collection[id='visits'] [data-causeway-grid-previous]").isVisible()).isTrue();
+            page.locator("cw-collection[id='visits'] [data-causeway-grid-previous]").click();
+            final var expectedOffset = offset;
+            page.waitForFunction("offset => document.querySelector(\"cw-collection[id='visits']\")?.collectionState?.window?.offset === offset", expectedOffset);
+            waitForCollectionRows("visits", 1);
+        }
+        assertThat(page.locator("cw-collection[id='visits'] .causeway-collection-range").textContent())
+                .isEqualTo("Items 1–1 of " + visitTotal);
         assertCollectionHeading("pets", "Companion animals", "Pets currently registered to this owner.");
         assertCollectionHeading("visits", "Visit history", "All visits recorded for this owner's pets.");
         final var visitDisabledReason = page.locator("cw-collection[id='visits'] .causeway-visually-hidden");
@@ -784,10 +804,10 @@ void disposableOwnerExercisesCreateAndEveryObjectAction() {
         assertThat(reason.evaluate("element => element.value")).isEqualTo("Routine check-up");
         if (!nativeToolkit()) {
             assertSingleVaadinMultilineFocus(reason);
-            assertThat(visitAt.evaluate("element => element.step")).isEqualTo(60);
+            assertThat(visitAt.evaluate("element => element.step")).isEqualTo(900);
             final var timePicker = visitAt.locator("vaadin-time-picker");
             assertThat(timePicker.count()).isEqualTo(1);
-            assertThat(timePicker.evaluate("element => element.step")).isEqualTo(60);
+            assertThat(timePicker.evaluate("element => element.step")).isEqualTo(900);
             assertThat(String.valueOf(timePicker.evaluate("element => element.inputElement?.value")))
                     .doesNotMatch(".*:[0-9]{2}:[0-9]{2}.*")
                     .doesNotContain(".");
@@ -797,6 +817,17 @@ void disposableOwnerExercisesCreateAndEveryObjectAction() {
             timePicker.evaluate("element => element.inputElement.focus()");
             page.waitForTimeout(750);
             final var mutationsBeforePickerUse = graphQLMutationCount("bookVisit");
+            final var visibleTimeOverlay = """
+                    selector => {
+                      const picker = document.querySelector(selector)?.querySelector('vaadin-time-picker');
+                      const overlay = picker?.shadowRoot?.querySelector('vaadin-time-picker-overlay');
+                      const bounds = overlay?.getBoundingClientRect();
+                      return picker?.opened === true
+                        && overlay?.matches(':popover-open')
+                        && bounds?.width > 0
+                        && bounds?.height > 0;
+                    }
+                    """;
             timePicker.evaluate("element => element.inputElement.focus()");
             assertThat(timePicker.evaluate("element => element.inputElement.matches(':focus')")).isEqualTo(true);
             page.keyboard().press("Tab");
@@ -804,11 +835,16 @@ void disposableOwnerExercisesCreateAndEveryObjectAction() {
                     .as("Tab should move focus from the time input to its clock trigger")
                     .isEqualTo(true);
             timeTrigger.press("Enter");
-            page.waitForFunction("selector => document.querySelector(selector)?.querySelector('vaadin-time-picker')?.opened === true", parameter("visitAt"));
+            page.waitForFunction(visibleTimeOverlay, parameter("visitAt"));
+            timePicker.evaluate("element => element.close()");
+            page.waitForTimeout(50);
+            timeTrigger.focus();
+            timeTrigger.press("Space");
+            page.waitForFunction(visibleTimeOverlay, parameter("visitAt"));
             timePicker.evaluate("element => element.close()");
             page.waitForTimeout(50);
             timeTrigger.click();
-            page.waitForFunction("selector => document.querySelector(selector)?.querySelector('vaadin-time-picker')?.opened === true", parameter("visitAt"));
+            page.waitForFunction(visibleTimeOverlay, parameter("visitAt"));
             assertThat(graphQLMutationCount("bookVisit") - mutationsBeforePickerUse).isZero();
             timePicker.evaluate("element => element.close()");
             page.waitForTimeout(50);
@@ -1337,12 +1373,18 @@ private void assertSingleVaadinMultilineFocus(final Locator editor) {
                 element => {
                   const host = getComputedStyle(element);
                   const field = getComputedStyle(element.shadowRoot.querySelector('[part~="input-field"]'));
-                  return [host.outlineStyle, host.outlineWidth, field.outlineStyle, field.outlineWidth];
+                  const input = getComputedStyle(element.inputElement);
+                  const inputBorder = ['Top', 'Right', 'Bottom', 'Left']
+                    .reduce((total, side) => total + Number.parseFloat(input[`border${side}Width`]), 0);
+                  return [host.outlineStyle, host.outlineWidth, field.outlineStyle, field.outlineWidth,
+                    String(inputBorder), input.outlineStyle];
                 }
                 """);
         assertThat(focusStyles.get(0)).isEqualTo("none");
         assertThat(focusStyles.get(2)).isEqualTo("solid");
         assertThat(Double.parseDouble(focusStyles.get(3).replace("px", ""))).isGreaterThan(0.0);
+        assertThat(Double.parseDouble(focusStyles.get(4))).isZero();
+        assertThat(focusStyles.get(5)).isEqualTo("none");
     }
 
     private void waitForShellResult(final String actionId, final String value) {