Skip to content

Commit

Permalink
feat: introduce autoResponsive mode (#8689)
Browse files Browse the repository at this point in the history
This PR introduces an autoResponsive mode to FormLayout. In this mode, the component automatically creates and then dynamically adjusts columns based on the provided columnWidth, maxColumns, and the available space in the container.
  • Loading branch information
vursen authored Feb 20, 2025
1 parent bd7777e commit f8d30f6
Show file tree
Hide file tree
Showing 21 changed files with 718 additions and 20 deletions.
43 changes: 43 additions & 0 deletions packages/form-layout/src/vaadin-form-layout-mixin.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,49 @@ export declare class FormLayoutMixinClass {
*/
responsiveSteps: FormLayoutResponsiveStep[];

/**
* When enabled, the layout automatically creates and adjusts columns based on
* the container's width. Columns have a fixed width defined by `columnWidth`
* and their number increases up to the limit set by `maxColumns`. The layout
* dynamically adjusts the number of columns as the container size changes.
*
* By default, each field is placed on a new row. To group fields in the same row,
* wrap them in `<vaadin-form-row>` or enable the `autoRows` property, which allows
* the layout to fit as many fields as possible in a row before wrapping.
*
* NOTE: In this mode, `responsiveSteps` are ignored.
*
* @attr {boolean} auto-responsive
*/
autoResponsive: boolean;

/**
* When `autoResponsive` is enabled, defines the width of each column.
* The value must be defined in CSS length units, e.g., `100px` or `13em`.
* The default value is `13em`.
*
* @attr {string} column-width
*/
columnWidth: string;

/**
* When `autoResponsive` is enabled, defines the maximum number of columns
* that the layout can create. The layout will create columns up to this
* limit based on the available container width.
*
* @attr {number} max-columns
*/
maxColumns: number;

/**
* When enabled with `autoResponsive`, distributes fields across columns
* by placing each field in the next available column and wrapping to
* the next row when the current row is full.
*
* @attr {boolean} auto-rows
*/
autoRows: boolean;

/**
* Update the layout.
*/
Expand Down
93 changes: 92 additions & 1 deletion packages/form-layout/src/vaadin-form-layout-mixin.js
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,67 @@ export const FormLayoutMixin = (superClass) =>
sync: true,
},

/**
* When enabled, the layout automatically creates and adjusts columns based on
* the container's width. Columns have a fixed width defined by `columnWidth`
* and their number increases up to the limit set by `maxColumns`. The layout
* dynamically adjusts the number of columns as the container size changes.
*
* By default, each field is placed on a new row. To group fields in the same row,
* wrap them in `<vaadin-form-row>` or enable the `autoRows` property, which allows
* the layout to fit as many fields as possible in a row before wrapping.
*
* NOTE: In this mode, `responsiveSteps` are ignored.
*
* @attr {boolean} auto-responsive
*/
autoResponsive: {
type: Boolean,
sync: true,
value: false,
reflectToAttribute: true,
},

/**
* When `autoResponsive` is enabled, defines the width of each column.
* The value must be defined in CSS length units, e.g., `100px` or `13em`.
* The default value is `13em`.
*
* @attr {string} column-width
*/
columnWidth: {
type: String,
sync: true,
value: '13em',
},

/**
* When `autoResponsive` is enabled, defines the maximum number of columns
* that the layout can create. The layout will create columns up to this
* limit based on the available container width.
*
* @attr {number} max-columns
*/
maxColumns: {
type: Number,
sync: true,
value: 10,
},

/**
* When enabled with `autoResponsive`, distributes fields across columns
* by placing each field in the next available column and wrapping to
* the next row when the current row is full.
*
* @attr {boolean} auto-rows
*/
autoRows: {
type: Boolean,
sync: true,
value: false,
reflectToAttribute: true,
},

/**
* Current number of columns in the layout
* @private
Expand All @@ -102,7 +163,11 @@ export const FormLayoutMixin = (superClass) =>
}

static get observers() {
return ['_invokeUpdateLayout(_columnCount, _labelsOnTop)'];
return [
'_invokeUpdateLayout(_columnCount, _labelsOnTop)',
'__columnWidthChanged(columnWidth, autoResponsive)',
'__maxColumnsChanged(maxColumns, autoResponsive)',
];
}

/** @protected */
Expand Down Expand Up @@ -190,6 +255,10 @@ export const FormLayoutMixin = (superClass) =>

/** @private */
_selectResponsiveStep() {
if (this.autoResponsive) {
return;
}

// Iterate through responsiveSteps and choose the step
let selectedStep;
const tmpStyleProp = 'background-position';
Expand Down Expand Up @@ -229,6 +298,10 @@ export const FormLayoutMixin = (superClass) =>
return;
}

if (this.autoResponsive) {
return;
}

/*
The item width formula:
Expand Down Expand Up @@ -327,4 +400,22 @@ export const FormLayoutMixin = (superClass) =>

this.$.layout.style.opacity = '';
}

/** @private */
__columnWidthChanged(columnWidth, autoResponsive) {
if (autoResponsive) {
this.style.setProperty('--_column-width', columnWidth);
} else {
this.style.removeProperty('--_column-width');
}
}

/** @private */
__maxColumnsChanged(maxColumns, autoResponsive) {
if (autoResponsive) {
this.style.setProperty('--_max-columns', maxColumns);
} else {
this.style.removeProperty('--_max-columns');
}
}
};
48 changes: 46 additions & 2 deletions packages/form-layout/src/vaadin-form-layout-styles.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,15 +21,15 @@ export const formLayoutStyles = css`
display: none !important;
}
#layout {
:host(:not([auto-responsive])) #layout {
display: flex;
align-items: baseline; /* default \`stretch\` is not appropriate */
flex-wrap: wrap; /* the items should wrap */
}
#layout ::slotted(*) {
:host(:not([auto-responsive])) #layout ::slotted(*) {
/* Items should neither grow nor shrink. */
flex-grow: 0;
flex-shrink: 0;
Expand All @@ -42,6 +42,50 @@ export const formLayoutStyles = css`
#layout ::slotted(br) {
display: none;
}
:host([auto-responsive]) {
--_column-gap: var(--vaadin-form-layout-column-spacing);
--_max-total-gap-width: calc((var(--_max-columns) - 1) * var(--_column-gap));
--_max-total-col-width: calc(var(--_max-columns) * var(--_column-width));
display: flex;
min-width: var(--_column-width);
}
:host([auto-responsive]) #layout {
display: grid;
grid-template-columns: repeat(auto-fit, var(--_column-width));
justify-items: start;
gap: var(--vaadin-form-layout-row-spacing) var(--_column-gap);
/*
To prevent the layout from exceeding the column limit defined by --_max-columns,
its width needs to be constrained:
1. "width" is used instead of "max-width" because, together with the default "flex: 0 1 auto",
it allows the layout to shrink to its minimum width inside <vaadin-horizontal-layout>, which
wouldn't work otherwise.
2. "width" is used instead of "flex-basis" to make the layout expand to the maximum
number of columns inside <vaadin-overlay>, which creates a new stacking context
without a predefined width.
*/
width: calc(var(--_max-total-col-width) + var(--_max-total-gap-width));
/*
Firefox requires min-width on both :host and #layout to allow the layout
to shrink below the value specified in the CSS width property above.
*/
min-width: inherit;
}
:host([auto-responsive]) #layout ::slotted(*) {
grid-column-start: 1;
}
:host([auto-responsive][auto-rows]) #layout ::slotted(*) {
grid-column-start: auto;
}
`;

export const formItemStyles = css`
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
/* @web/test-runner snapshot v1 */
export const snapshots = {};

snapshots["vaadin-form-layout auto-responsive host default"] =
`<vaadin-form-layout
auto-responsive=""
style="--_column-width: 13em; --_max-columns: 10;"
>
<input placeholder="First name">
<input placeholder="Last name">
<input placeholder="Email">
<input placeholder="Phone">
</vaadin-form-layout>
`;
/* end snapshot vaadin-form-layout auto-responsive host default */

snapshots["vaadin-form-layout auto-responsive host autoRows"] =
`<vaadin-form-layout
auto-responsive=""
auto-rows=""
style="--_column-width: 13em; --_max-columns: 10;"
>
<input placeholder="First name">
<input placeholder="Last name">
<input placeholder="Email">
<input placeholder="Phone">
</vaadin-form-layout>
`;
/* end snapshot vaadin-form-layout auto-responsive host autoRows */

snapshots["vaadin-form-layout auto-responsive host maxColumns"] =
`<vaadin-form-layout
auto-responsive=""
style="--_column-width: 13em; --_max-columns: 3;"
>
<input placeholder="First name">
<input placeholder="Last name">
<input placeholder="Email">
<input placeholder="Phone">
</vaadin-form-layout>
`;
/* end snapshot vaadin-form-layout auto-responsive host maxColumns */

snapshots["vaadin-form-layout auto-responsive host columnWidth"] =
`<vaadin-form-layout
auto-responsive=""
style="--_column-width: 15em; --_max-columns: 10;"
>
<input placeholder="First name">
<input placeholder="Last name">
<input placeholder="Email">
<input placeholder="Phone">
</vaadin-form-layout>
`;
/* end snapshot vaadin-form-layout auto-responsive host columnWidth */

snapshots["vaadin-form-layout auto-responsive shadow default"] =
`<div id="layout">
<slot id="slot">
</slot>
</div>
`;
/* end snapshot vaadin-form-layout auto-responsive shadow default */

47 changes: 47 additions & 0 deletions packages/form-layout/test/dom/form-layout.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { expect } from '@vaadin/chai-plugins';
import { fixtureSync } from '@vaadin/testing-helpers';
import '../../src/vaadin-form-layout.js';

describe('vaadin-form-layout', () => {
let layout;

describe('auto-responsive', () => {
beforeEach(() => {
layout = fixtureSync(`
<vaadin-form-layout auto-responsive>
<input placeholder="First name" />
<input placeholder="Last name" />
<input placeholder="Email" />
<input placeholder="Phone" />
</vaadin-form-layout>
`);
});

describe('host', () => {
it('default', async () => {
await expect(layout).dom.to.equalSnapshot();
});

it('autoRows', async () => {
layout.autoRows = true;
await expect(layout).dom.to.equalSnapshot();
});

it('maxColumns', async () => {
layout.maxColumns = 3;
await expect(layout).dom.to.equalSnapshot();
});

it('columnWidth', async () => {
layout.columnWidth = '15em';
await expect(layout).dom.to.equalSnapshot();
});
});

describe('shadow', () => {
it('default', async () => {
await expect(layout).shadowDom.to.equalSnapshot();
});
});
});
});
Loading

0 comments on commit f8d30f6

Please sign in to comment.