Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion packages/components-dev/form-field/module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -45,9 +46,12 @@ import { DevFileFlatNode, DevFileNode } from '../tree/module';
FormFieldOverviewExample,
FormFieldWithoutBordersExample,
FormFieldWithPrefixAndSuffixExample,
FormFieldHorizontalExample
FormFieldHorizontalExample,
FormFieldsetWithButtonExample
],
template: `
<form-fieldset-with-button-example />
<hr />
<form-field-horizontal-example />
<hr />
<form-field-overview-example />
Expand Down
24 changes: 24 additions & 0 deletions packages/components/form-field/e2e.playwright-spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
});
}
});
});
10 changes: 10 additions & 0 deletions packages/components/form-field/fieldset.scss
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down