From 5207df422b01e52e5d2004cc96545f9e055812ae Mon Sep 17 00:00:00 2001 From: Nikita Guryev Date: Tue, 4 Aug 2026 18:18:32 +0300 Subject: [PATCH] fix(form-field): sync heights inside fieldset (#DS-5370) --- packages/components-dev/form-field/module.ts | 6 ++++- .../form-field/e2e.playwright-spec.ts | 24 +++++++++++++++++++ packages/components/form-field/fieldset.scss | 10 ++++++++ 3 files changed, 39 insertions(+), 1 deletion(-) diff --git a/packages/components-dev/form-field/module.ts b/packages/components-dev/form-field/module.ts index 008b8fd72e..a5f54e6757 100644 --- a/packages/components-dev/form-field/module.ts +++ b/packages/components-dev/form-field/module.ts @@ -30,6 +30,7 @@ import { FormFieldWithoutBordersExample, FormFieldWithPrefixAndSuffixExample } from 'packages/docs-examples/components/form-field'; +import { FormFieldsetWithButtonExample } from 'packages/docs-examples/components/forms/form-fieldset-with-button/form-fieldset-with-button-example'; import { DevThemeToggle } from '../theme-toggle'; import { DevFileFlatNode, DevFileNode } from '../tree/module'; @@ -45,9 +46,12 @@ import { DevFileFlatNode, DevFileNode } from '../tree/module'; FormFieldOverviewExample, FormFieldWithoutBordersExample, FormFieldWithPrefixAndSuffixExample, - FormFieldHorizontalExample + FormFieldHorizontalExample, + FormFieldsetWithButtonExample ], template: ` + +

diff --git a/packages/components/form-field/e2e.playwright-spec.ts b/packages/components/form-field/e2e.playwright-spec.ts index 694af95ce7..11966851fd 100644 --- a/packages/components/form-field/e2e.playwright-spec.ts +++ b/packages/components/form-field/e2e.playwright-spec.ts @@ -33,5 +33,29 @@ test.describe('KbqFormFieldModule', () => { await expect(getScreenshotTarget(locator)).toHaveScreenshot('03-light.png'); }); + + // All height tokens involved are whole CSS pixels, so at a 1x device pixel ratio the + // form-field's height (sum of independently-rounded border/padding/line-height values) and + // the button's height (a single token) land on the same integer and a plain boundingBox + // comparison would pass either way. Fractional device pixel ratios force the browser to snap + // each side's boxes to physical pixels independently, which is what actually exposes a + // regression of the merged-border seam. + for (const deviceScaleFactor of [1.5, 2.5]) { + test.describe(`at ${deviceScaleFactor}x device scale`, () => { + test.use({ deviceScaleFactor }); + + test('button and form-field stay flush at the shared border seam', async ({ page }) => { + await page.goto('/E2eFormFieldset'); + const locator = getComponent(page); + + const fieldBox = (await locator.locator('.kbq-form-field__container').boundingBox())!; + const buttonBox = (await locator.locator('button.kbq-button').boundingBox())!; + + expect(fieldBox.height).toBeCloseTo(buttonBox.height, 1); + expect(fieldBox.y).toBeCloseTo(buttonBox.y, 1); + expect(fieldBox.y + fieldBox.height).toBeCloseTo(buttonBox.y + buttonBox.height, 1); + }); + }); + } }); }); diff --git a/packages/components/form-field/fieldset.scss b/packages/components/form-field/fieldset.scss index aac7cd683f..78d9b8691b 100644 --- a/packages/components/form-field/fieldset.scss +++ b/packages/components/form-field/fieldset.scss @@ -20,6 +20,16 @@ } .kbq-form-field { + // Force a single-token height, matching how the button computes its own height, so both + // round to the same pixel value. Without this, form-field's height is the sum of independently + // rounded border/padding/line-height values, which can drift 1px from the button's height after + // a resize-triggered subpixel layout recalculation, breaking the merged border line. + height: var(--kbq-form-field-size-height); + + .kbq-form-field__content { + height: 100%; + } + // set selector specifity to 0 , so focused state will override z-index :where(&.kbq-form-field_invalid, &.kbq-form-field_has-validate-directive.ng-invalid) { z-index: 2;