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/08/26 20:08:17 UTC

(causeway) 02/03: improves multiline property layout

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 8a6ae4cb440ab9c68f38c319f77638dc99e676fb
Author: Dan Haywood <da...@haywood-associates.co.uk>
AuthorDate: Wed Aug 26 21:00:58 2026 +0100

    improves multiline property layout
---
 .../evidence/baseline-and-boundaries.md            | 21 ++++++++
 .../evidence/implementation-and-qualification.md   | 28 +++++++++++
 .../improve-multiline-property-layout/tasks.md     | 24 ++++-----
 viewers/webcomponents/foundation/src/theme.css     | 58 ++++++++++++++++++++++
 .../foundation/test/component-styles.test.mjs      |  9 ++++
 .../petclinic/PetClinicHtmxPlaywrightTest.java     | 26 ++++++++++
 6 files changed, 154 insertions(+), 12 deletions(-)

diff --git a/openspec/changes/improve-multiline-property-layout/evidence/baseline-and-boundaries.md b/openspec/changes/improve-multiline-property-layout/evidence/baseline-and-boundaries.md
new file mode 100644
index 00000000000..34f79ff63c8
--- /dev/null
+++ b/openspec/changes/improve-multiline-property-layout/evidence/baseline-and-boundaries.md
@@ -0,0 +1,21 @@
+# Baseline and boundaries
+
+## Reproduced geometry
+
+The supplied Petclinic screenshot captures the described multiline Notes property before the fix.
+The Notes label begins near x=58 while its value wraps beneath it near x=58 instead of occupying the normal value column.
+The description begins in the value column near x=283, above the actual value.
+The Edit Notes button begins near x=283 and stretches to approximately x=846, consuming almost the full value-column width.
+
+This geometry follows from auto-placement in the existing three-column property grid: the explicitly placed description claims columns two through three, the value falls into the next available first-column cell, and the edit button stretches in the remaining cell.
+
+## Preserved boundaries
+
+- The existing `<causeway-property multiline>` host attribute remains the semantic layout signal.
+- The label, description, output, and edit button remain connected light-DOM nodes with unchanged accessible relationships.
+- GraphQL remains authoritative for property metadata, value, visibility, usability, preparation, validation, and mutation.
+- The public property element, events, test hooks, renderer selection, and native/Vaadin editor policies remain unchanged.
+- Direct associated actions remain independently composed and are not styled as the built-in edit affordance.
+- Petclinic HTML and domain metadata require no change.
+- Strict CSP remains unchanged and no inline style or JavaScript measurement is introduced.
+- The unrelated local `default0RemovePet()` modification in `PetOwner.java` is outside this change and remains unstaged.
diff --git a/openspec/changes/improve-multiline-property-layout/evidence/implementation-and-qualification.md b/openspec/changes/improve-multiline-property-layout/evidence/implementation-and-qualification.md
new file mode 100644
index 00000000000..dcff2d6113d
--- /dev/null
+++ b/openspec/changes/improve-multiline-property-layout/evidence/implementation-and-qualification.md
@@ -0,0 +1,28 @@
+# Implementation and qualification
+
+## Implementation
+
+The standard theme now gives `causeway-property[multiline]` explicit grid placement without changing semantic markup.
+At wide widths the label occupies the first column and first row, its description occupies the first column and second row, and the read-only value occupies the normal second column across those rows.
+The built-in edit button remains in the third column but uses start alignment, a two-rem minimum height, compact padding, and the standard small font size instead of stretching across available space.
+
+The existing 32-rem container breakpoint and 48-rem viewport breakpoint now give multiline properties explicit logical rows.
+The label, description, value, and compact edit affordance remain ordered without overlap, and executable geometry checks verify that document width does not overflow.
+
+No rendering nodes, accessibility ids, GraphQL behavior, interaction state, associated-action composition, toolkit adapter, route, CSP rule, dependency, Petclinic page, or domain metadata changed.
+The unrelated local `PetOwner.java` modification remained untouched and unstaged.
+
+## Qualification
+
+- Focused standard-theme contract: 3 passed, zero failed.
+- Complete foundation Node suite: 172 passed, zero failed.
+- HTMX route-policy Node suite: 5 passed, zero failed.
+- Petclinic Java integration suite: passed.
+- Focused Petclinic geometry and property-interaction browser test: passed.
+- Complete Petclinic Vaadin-default Playwright matrix: 4 passed, zero failed.
+- Complete Petclinic explicit-native Playwright matrix: 4 passed, zero failed.
+- Foundation RAT check: passed.
+- Petclinic RAT check: passed.
+- Ordinary reactor packaging: passed and produced a 53,891-byte Petclinic jar without `BOOT-INF`.
+- Strict OpenSpec validation: passed.
+- Git whitespace validation: passed.
diff --git a/openspec/changes/improve-multiline-property-layout/tasks.md b/openspec/changes/improve-multiline-property-layout/tasks.md
index 466a990f8a7..fdd44bbafa4 100644
--- a/openspec/changes/improve-multiline-property-layout/tasks.md
+++ b/openspec/changes/improve-multiline-property-layout/tasks.md
@@ -1,20 +1,20 @@
 ## 1. Baseline and regression coverage
 
-- [ ] 1.1 Record the current described multiline-property geometry and unchanged semantic boundaries.
-- [ ] 1.2 Add foundation style-contract coverage for wide and narrow multiline placement.
-- [ ] 1.3 Add Petclinic browser geometry coverage for the Notes label, description, value, and edit affordance.
+- [x] 1.1 Record the current described multiline-property geometry and unchanged semantic boundaries.
+- [x] 1.2 Add foundation style-contract coverage for wide and narrow multiline placement.
+- [x] 1.3 Add Petclinic browser geometry coverage for the Notes label, description, value, and edit affordance.
 
 ## 2. Multiline property presentation
 
-- [ ] 2.1 Place the multiline label and description in successive rows of the label column at wide widths.
-- [ ] 2.2 Keep the multiline value in the normal value column.
-- [ ] 2.3 Make the built-in multiline edit affordance compact and content-sized.
-- [ ] 2.4 Define logical non-overflowing multiline placement at narrow container and viewport widths.
-- [ ] 2.5 Preserve semantic markup, accessible descriptions, editing, associated actions, and toolkit independence.
+- [x] 2.1 Place the multiline label and description in successive rows of the label column at wide widths.
+- [x] 2.2 Keep the multiline value in the normal value column.
+- [x] 2.3 Make the built-in multiline edit affordance compact and content-sized.
+- [x] 2.4 Define logical non-overflowing multiline placement at narrow container and viewport widths.
+- [x] 2.5 Preserve semantic markup, accessible descriptions, editing, associated actions, and toolkit independence.
 
 ## 3. Qualification
 
-- [ ] 3.1 Run the complete foundation and HTMX route-policy suites.
-- [ ] 3.2 Run focused Petclinic Java integration tests.
-- [ ] 3.3 Run Petclinic Playwright under Vaadin-default and explicit-native toolkit policies.
-- [ ] 3.4 Run RAT, ordinary packaging, strict OpenSpec validation, and whitespace checks.
+- [x] 3.1 Run the complete foundation and HTMX route-policy suites.
+- [x] 3.2 Run focused Petclinic Java integration tests.
+- [x] 3.3 Run Petclinic Playwright under Vaadin-default and explicit-native toolkit policies.
+- [x] 3.4 Run RAT, ordinary packaging, strict OpenSpec validation, and whitespace checks.
diff --git a/viewers/webcomponents/foundation/src/theme.css b/viewers/webcomponents/foundation/src/theme.css
index da4069fa569..5d97badc203 100644
--- a/viewers/webcomponents/foundation/src/theme.css
+++ b/viewers/webcomponents/foundation/src/theme.css
@@ -358,6 +358,30 @@ causeway-property:last-child .causeway-property {
   font-size: var(--causeway-font-size-sm);
 }
 
+:where(causeway-property[multiline]) .causeway-property-label {
+  grid-column: 1;
+  grid-row: 1;
+}
+
+:where(causeway-property[multiline]) .causeway-property-description {
+  grid-column: 1;
+  grid-row: 2;
+}
+
+:where(causeway-property[multiline]) .causeway-property-value {
+  grid-column: 2;
+  grid-row: 1 / span 2;
+}
+
+:where(causeway-property[multiline]) .causeway-property-edit {
+  grid-column: 3;
+  grid-row: 1;
+  justify-self: start;
+  min-height: 2rem;
+  padding: 0.25rem 0.55rem;
+  font-size: var(--causeway-font-size-sm);
+}
+
 .causeway-action-description {
   position: absolute !important;
   width: 1px !important;
@@ -586,6 +610,23 @@ causeway-interaction-controller:has(.causeway-action-prompt)::before {
   .causeway-property-editor {
     grid-column: 1;
   }
+
+  :where(causeway-property[multiline]) .causeway-property-label {
+    grid-row: 1;
+  }
+
+  :where(causeway-property[multiline]) .causeway-property-description {
+    grid-row: 2;
+  }
+
+  :where(causeway-property[multiline]) .causeway-property-value {
+    grid-row: 3;
+  }
+
+  :where(causeway-property[multiline]) .causeway-property-edit {
+    grid-column: 2;
+    grid-row: 3;
+  }
 }
 
 @media (max-width: 48rem) {
@@ -623,6 +664,23 @@ causeway-interaction-controller:has(.causeway-action-prompt)::before {
     grid-column: 1;
   }
 
+  :where(causeway-property[multiline]) .causeway-property-label {
+    grid-row: 1;
+  }
+
+  :where(causeway-property[multiline]) .causeway-property-description {
+    grid-row: 2;
+  }
+
+  :where(causeway-property[multiline]) .causeway-property-value {
+    grid-row: 3;
+  }
+
+  :where(causeway-property[multiline]) .causeway-property-edit {
+    grid-column: 2;
+    grid-row: 3;
+  }
+
   .causeway-action-prompt {
     width: calc(100% - (2 * var(--causeway-space-3)));
     max-height: calc(100vh - (2 * var(--causeway-space-3)));
diff --git a/viewers/webcomponents/foundation/test/component-styles.test.mjs b/viewers/webcomponents/foundation/test/component-styles.test.mjs
index fa2c3bf94e3..152e5041230 100644
--- a/viewers/webcomponents/foundation/test/component-styles.test.mjs
+++ b/viewers/webcomponents/foundation/test/component-styles.test.mjs
@@ -37,3 +37,12 @@ test('direct member associations wrap in semantic source and keyboard order', ()
   assert.match(CAUSEWAY_COMPONENT_STYLES, /flex-wrap: wrap/);
   assert.match(CAUSEWAY_COMPONENT_STYLES, /--causeway-associated-action-gap/);
 });
+
+test('standard theme gives described multiline properties explicit responsive placement', async () => {
+  const theme = await readFile(new URL('../src/theme.css', import.meta.url), 'utf8');
+  assert.match(theme, /:where\(causeway-property\[multiline\]\) \.causeway-property-description \{\s+grid-column: 1;\s+grid-row: 2;/);
+  assert.match(theme, /:where\(causeway-property\[multiline\]\) \.causeway-property-value \{\s+grid-column: 2;\s+grid-row: 1 \/ span 2;/);
+  assert.match(theme, /:where\(causeway-property\[multiline\]\) \.causeway-property-edit \{\s+grid-column: 3;\s+grid-row: 1;\s+justify-self: start;/);
+  assert.match(theme, /@container \(max-width: 32rem\)[\s\S]+:where\(causeway-property\[multiline\]\) \.causeway-property-value \{\s+grid-row: 3;/);
+  assert.match(theme, /@media \(max-width: 48rem\)[\s\S]+:where\(causeway-property\[multiline\]\) \.causeway-property-edit \{\s+grid-column: 2;\s+grid-row: 3;/);
+});
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 5bd769302eb..eab14a8db1c 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
@@ -340,6 +340,7 @@ void propertyEditingAndPromptFocusRemainDeterministic() {
         revealContainingTab(notes, notesEdit);
         assertThat(notes.locator(".causeway-property-label").getAttribute("title"))
                 .isEqualTo("Additional notes about this pet owner.");
+        assertWideMultilinePropertyLayout(notes);
         notesEdit.click();
         final var notesEditor = resolveEditor("causeway-property[member='notes'] [data-causeway-editor='notes']");
         assertThat(notesEditor.evaluate("element => element.localName === 'vaadin-text-area' ? element.maxRows : Number(element.rows)"))
@@ -347,6 +348,10 @@ void propertyEditingAndPromptFocusRemainDeterministic() {
         fillEditor(notesEditor, "First line\nSecond line");
         notes.locator("[data-causeway-action='cancel']").click();
 
+        page.setViewportSize(390, 844);
+        assertNarrowMultilinePropertyLayout(notes);
+        page.setViewportSize(1440, 900);
+
         editProperty("telephoneNumber", "020 7000 1234");
         waitForPropertyValue("telephoneNumber", "020 7000 1234");
         assertFocused("causeway-property[member='telephoneNumber'] [data-causeway-action='edit']");
@@ -684,6 +689,27 @@ private void waitForPromptError(final String messagePart) {
         alert.waitFor();
     }
 
+    @SuppressWarnings("unchecked")
+    private void assertWideMultilinePropertyLayout(final Locator property) {
+        final var geometry = (List<Number>) property.evaluate("element => { const label = element.querySelector('.causeway-property-label').getBoundingClientRect(); const description = element.querySelector('.causeway-property-description').getBoundingClientRect(); const value = element.querySelector('.causeway-property-value').getBoundingClientRect(); const edit = element.querySelector('.causeway-property-edit').getBoundingClientRect(); return [label.left, label.right, label.bottom, des [...]
+        assertThat(Math.abs(geometry.get(3).doubleValue() - geometry.get(0).doubleValue())).isLessThanOrEqualTo(1.0);
+        assertThat(geometry.get(4).doubleValue()).isGreaterThanOrEqualTo(geometry.get(2).doubleValue() - 1.0);
+        assertThat(geometry.get(5).doubleValue()).isGreaterThan(geometry.get(1).doubleValue());
+        assertThat(geometry.get(6).doubleValue()).isLessThanOrEqualTo(geometry.get(4).doubleValue());
+        assertThat(geometry.get(7).doubleValue()).isGreaterThan(geometry.get(5).doubleValue());
+        assertThat(geometry.get(8).doubleValue()).isLessThan(160.0);
+    }
+
+    @SuppressWarnings("unchecked")
+    private void assertNarrowMultilinePropertyLayout(final Locator property) {
+        final var geometry = (List<Number>) property.evaluate("element => { const label = element.querySelector('.causeway-property-label').getBoundingClientRect(); const description = element.querySelector('.causeway-property-description').getBoundingClientRect(); const value = element.querySelector('.causeway-property-value').getBoundingClientRect(); const edit = element.querySelector('.causeway-property-edit').getBoundingClientRect(); return [label.bottom, description.top, description [...]
+        assertThat(geometry.get(1).doubleValue()).isGreaterThanOrEqualTo(geometry.get(0).doubleValue() - 1.0);
+        assertThat(geometry.get(3).doubleValue()).isGreaterThanOrEqualTo(geometry.get(2).doubleValue() - 1.0);
+        assertThat(Math.abs(geometry.get(4).doubleValue() - geometry.get(3).doubleValue())).isLessThanOrEqualTo(1.0);
+        assertThat(geometry.get(5).doubleValue()).isLessThan(160.0);
+        assertThat(geometry.get(6).doubleValue()).isLessThanOrEqualTo(geometry.get(7).doubleValue() + 1.0);
+    }
+
     private void waitForShellResult(final String actionId, final String value) {
         final var result = page.locator("[data-testid='causeway-shell-result']");
         result.waitFor();