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/29 10:01:07 UTC

(causeway) 06/07: fix(webcomponents): remove duplicate field borders

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 723f85f814b4aec96e27302c17891bdda338ba66
Author: Dan Haywood <da...@haywood-associates.co.uk>
AuthorDate: Sat Aug 29 11:54:25 2026 +0200

    fix(webcomponents): remove duplicate field borders
---
 .../changes/remove-field-double-borders/design.md  |  4 +--
 .../remove-field-double-borders/proposal.md        |  2 +-
 .../changes/remove-field-double-borders/tasks.md   | 14 +++++-----
 viewers/webcomponents/foundation/src/theme.css     | 24 ++++++++---------
 .../foundation/test/component-styles.test.mjs      | 14 ++++++++++
 .../petclinic/PetClinicHtmxPlaywrightTest.java     | 31 ++++++++++++++++++++++
 6 files changed, 67 insertions(+), 22 deletions(-)

diff --git a/openspec/changes/remove-field-double-borders/design.md b/openspec/changes/remove-field-double-borders/design.md
index 57b3d9054e0..e6d10c13d7d 100644
--- a/openspec/changes/remove-field-double-borders/design.md
+++ b/openspec/changes/remove-field-double-borders/design.md
@@ -20,9 +20,9 @@ Vaadin simultaneously renders its own read-only input container, producing the s
 
 ## Decisions
 
-### Exclude toolkit slotted inputs from native-control theme rules
+### Scope application native-control rules away from toolkit internals
 
-The global theme selectors will exclude controls carrying `slot="input"` wherever it applies border, sizing, focus, padding, or textarea resizing.
+The application-defined global theme selectors will not target controls carrying `slot="input"` wherever they apply border, sizing, focus, padding, or textarea resizing.
 The slot marker is the stable integration boundary already emitted by Vaadin field controls, and the exclusion allows Vaadin's own shadow styles to reset and present that internal input correctly.
 
 Editing generated Vaadin assets is rejected because they are deterministic reviewed outputs and regeneration would overwrite the change.
diff --git a/openspec/changes/remove-field-double-borders/proposal.md b/openspec/changes/remove-field-double-borders/proposal.md
index 8ade0f8a58d..b468cfb8831 100644
--- a/openspec/changes/remove-field-double-borders/proposal.md
+++ b/openspec/changes/remove-field-double-borders/proposal.md
@@ -5,7 +5,7 @@ The duplicate boundary is visually distracting and makes one semantic field appe
 
 ## What Changes
 
-- Prevent global native input, select, and textarea rules from styling toolkit-owned slotted field internals.
+- Scope application-defined native input, select, and textarea rules away from toolkit-owned slotted field internals.
 - Preserve one toolkit-owned read-only field boundary, genuine read-only semantics, visible focus, and native control styling outside toolkit fields.
 - Add regression coverage for the stylesheet boundary and representative Petclinic field presentation.
 
diff --git a/openspec/changes/remove-field-double-borders/tasks.md b/openspec/changes/remove-field-double-borders/tasks.md
index a8affc13ef7..6ef955e02da 100644
--- a/openspec/changes/remove-field-double-borders/tasks.md
+++ b/openspec/changes/remove-field-double-borders/tasks.md
@@ -1,15 +1,15 @@
 ## 1. Theme boundary
 
-- [ ] 1.1 Exclude toolkit-owned `slot="input"` controls from every applicable global native input, select, textarea, focus, padding, and resize selector.
-- [ ] 1.2 Preserve the established global theme rules for ordinary native and fallback controls.
+- [x] 1.1 Scope every applicable application-defined native input, select, textarea, focus, padding, and resize selector away from toolkit-owned `slot="input"` controls.
+- [x] 1.2 Preserve the established global theme rules for ordinary native and fallback controls.
 
 ## 2. Regression coverage
 
-- [ ] 2.1 Add foundation stylesheet tests proving toolkit slotted inputs are excluded and ordinary native controls remain styled.
-- [ ] 2.2 Add Petclinic browser coverage proving representative read-only text fields expose only one visible bordered rectangle.
-- [ ] 2.3 Confirm focus, accessibility, responsive, native-toolkit, and forced-colors behavior remains covered by existing acceptance checks.
+- [x] 2.1 Add foundation stylesheet tests proving toolkit slotted inputs are excluded and ordinary native controls remain styled.
+- [x] 2.2 Add Petclinic browser coverage proving representative read-only text fields expose only one visible bordered rectangle.
+- [x] 2.3 Confirm focus, accessibility, responsive, native-toolkit, and forced-colors behavior remains covered by existing acceptance checks.
 
 ## 3. Validation and archive readiness
 
-- [ ] 3.1 Run foundation verification and Petclinic integration and Playwright acceptance suites.
-- [ ] 3.2 Run strict OpenSpec validation, formatting, license, diff, and working-tree checks.
+- [x] 3.1 Run foundation verification and Petclinic integration and Playwright acceptance suites.
+- [x] 3.2 Run strict OpenSpec validation, formatting, license, diff, and working-tree checks.
diff --git a/viewers/webcomponents/foundation/src/theme.css b/viewers/webcomponents/foundation/src/theme.css
index 575c67278a9..23a59da2914 100644
--- a/viewers/webcomponents/foundation/src/theme.css
+++ b/viewers/webcomponents/foundation/src/theme.css
@@ -163,9 +163,9 @@ a:hover {
 }
 
 button,
-input,
-select,
-textarea {
+input:not([slot="input"]),
+select:not([slot="input"]),
+textarea:not([slot="input"]) {
   max-width: 100%;
   border: 1px solid var(--causeway-border-strong);
   border-radius: var(--causeway-radius-sm);
@@ -174,8 +174,8 @@ textarea {
 }
 
 button,
-select,
-input:not([type="checkbox"]):not([type="radio"]) {
+select:not([slot="input"]),
+input:not([slot="input"]):not([type="checkbox"]):not([type="radio"]) {
   min-height: var(--causeway-control-height);
 }
 
@@ -195,18 +195,18 @@ button:disabled,
   opacity: 0.62;
 }
 
-:where(a, button, input, select, textarea, [tabindex]):focus-visible {
+:where(a, button, input:not([slot="input"]), select:not([slot="input"]), textarea:not([slot="input"]), [tabindex]):focus-visible {
   outline: 3px solid var(--causeway-focus);
   outline-offset: 2px;
 }
 
-input,
-select,
-textarea {
+input:not([slot="input"]),
+select:not([slot="input"]),
+textarea:not([slot="input"]) {
   padding: 0.45rem 0.6rem;
 }
 
-textarea {
+textarea:not([slot="input"]) {
   min-height: 6rem;
   resize: vertical;
 }
@@ -429,7 +429,7 @@ cw-property:last-child .causeway-property {
   min-width: 0;
 }
 
-.causeway-property-editor > :where(input, select, textarea) {
+.causeway-property-editor > :where(input:not([slot="input"]), select:not([slot="input"]), textarea:not([slot="input"])) {
   width: 100%;
 }
 
@@ -638,7 +638,7 @@ cw-interaction-controller:has(.causeway-action-prompt)::before {
   gap: var(--causeway-space-1);
 }
 
-.causeway-action-parameter :where(input, select, textarea) {
+.causeway-action-parameter :where(input:not([slot="input"]), select:not([slot="input"]), textarea:not([slot="input"])) {
   width: 100%;
 }
 
diff --git a/viewers/webcomponents/foundation/test/component-styles.test.mjs b/viewers/webcomponents/foundation/test/component-styles.test.mjs
index f9eca1661b7..a4906d8218f 100644
--- a/viewers/webcomponents/foundation/test/component-styles.test.mjs
+++ b/viewers/webcomponents/foundation/test/component-styles.test.mjs
@@ -56,6 +56,20 @@ test('disabled reasons use label-owned tooltips and string values align to logic
   assert.match(theme, /\.causeway-property-value-string \{\s+text-align: start;/);
 });
 
+test('application native-control chrome does not cross into toolkit-owned slotted field inputs', async () => {
+  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"])';
+
+  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;`));
+  assert.ok(theme.includes(`${nativeTextarea} {\n  min-height: 6rem;\n  resize: vertical;`));
+  assert.ok(theme.includes(`:where(a, button, ${nativeInput}, ${nativeSelect}, ${nativeTextarea}, [tabindex]):focus-visible`));
+  assert.doesNotMatch(theme, /\ninput,\nselect,\ntextarea \{\n  max-width:/);
+  assert.doesNotMatch(theme, /:where\(a, button, input, select, textarea, \[tabindex\]\):focus-visible/);
+});
+
 test('direct member associations wrap in semantic source and keyboard order', () => {
   assert.match(CAUSEWAY_COMPONENT_STYLES, /cw-property\[data-causeway-action-group\]/);
   assert.match(CAUSEWAY_COMPONENT_STYLES, /cw-collection\[data-causeway-action-group\]/);
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 33463d565da..544ad578641 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
@@ -234,6 +234,8 @@ void routesHomeObjectsHistoryCollectionsAndResponsiveLayout() {
         assertThat(page.locator("cw-collection[id='visits'] > cw-action[id='bookVisit']").count())
                 .isEqualTo(1);
         assertDefaultOrNativeMemberPresentation("name", "delete");
+        assertSingleToolkitFieldBoundary("name");
+        assertSingleToolkitFieldBoundary("telephoneNumber");
         assertThat(page.locator("cw-collection[id='pets']")
                 .evaluate("element => [...element.children].filter(child => child.localName === 'cw-action').map(child => child.getAttribute('id')).join(',')"))
                 .isEqualTo("addPet,removePet");
@@ -883,6 +885,35 @@ private void waitForPromptError(final String messagePart) {
         alert.waitFor();
     }
 
+    @SuppressWarnings("unchecked")
+    private void assertSingleToolkitFieldBoundary(final String member) {
+        final var property = page.locator("cw-property[id='" + member + "']");
+        if (nativeToolkit()) {
+            assertThat(property.locator("cw-field-editor").count()).isZero();
+            return;
+        }
+        page.waitForFunction("member => document.querySelector(`cw-property[id='${member}'] cw-field-editor[data-mode='view'] > [data-causeway-field-view]`)?.shadowRoot?.querySelector('[part~=input-field]')", member);
+        final var borderWidths = (List<Number>) property.evaluate("""
+                element => {
+                  const control = element.querySelector('cw-field-editor[data-mode="view"] > [data-causeway-field-view]');
+                  const container = control.shadowRoot.querySelector('[part~="input-field"]');
+                  const input = control.querySelector('[slot="input"]');
+                  const borderWidth = candidate => {
+                    const style = getComputedStyle(candidate);
+                    return ['Top', 'Right', 'Bottom', 'Left']
+                      .reduce((total, side) => total + Number.parseFloat(style[`border${side}Width`]), 0);
+                  };
+                  return [borderWidth(container), borderWidth(input)];
+                }
+                """);
+        assertThat(borderWidths.get(0).doubleValue())
+                .as("toolkit field boundary for %s", member)
+                .isGreaterThan(0.0);
+        assertThat(borderWidths.get(1).doubleValue())
+                .as("nested native input boundary for %s", member)
+                .isZero();
+    }
+
     @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 [...]