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();