Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add middle and end slots to horizontal layout #8515

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
74 changes: 74 additions & 0 deletions dev/horizontal-layout.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Horizontal layout</title>
<script type="module" src="./common.js"></script>

<script type="module">
import '@vaadin/horizontal-layout';
</script>
</head>

<body>
<vaadin-horizontal-layout style="border: solid 1px blue; width: 600px;" theme="spacing wrap">
<div style="background: #90ee90; width: 100px">Start</div>
<div style="background: #90ee90; width: 100px">Start</div>
<div slot="middle" style="background: #ffd700;">Middle</div>
<div slot="end" style="background: #f08080; width: 100px">End</div>
</vaadin-horizontal-layout>

<br />

<vaadin-horizontal-layout style="border: solid 1px blue; width: 350px;" theme="spacing wrap">
<div style="background: #90ee90; width: 100px">Start</div>
<div style="background: #90ee90; width: 100px">Start</div>
<div slot="middle" style="background: #ffd700;">Middle</div>
<div slot="end" style="background: #f08080; width: 100px">End</div>
</vaadin-horizontal-layout>

<br />

<vaadin-horizontal-layout style="border: solid 1px blue; width: 350px;" theme="spacing wrap">
<div style="background: #90ee90; width: 200px">Start</div>
<div style="background: #90ee90; width: 100px">Start</div>
<div slot="middle" style="background: #ffd700;">Middle</div>
<div slot="end" style="background: #f08080; width: 100px">End</div>
</vaadin-horizontal-layout>

<br />

<vaadin-horizontal-layout style="border: solid 1px blue; width: 400px;" theme="spacing wrap">
<div style="background: #90ee90; width: 100px">Start</div>
<div style="background: #90ee90; width: 100px">Start</div>
<div slot="middle" style="background: #ffd700;">Middle</div>
</vaadin-horizontal-layout>

<br />

<vaadin-horizontal-layout style="border: solid 1px blue; width: 250px;" theme="spacing wrap">
<div style="background: #90ee90; width: 100px">Start</div>
<div style="background: #90ee90; width: 100px">Start</div>
<div slot="middle" style="background: #ffd700;">Middle</div>
</vaadin-horizontal-layout>


<br />

<vaadin-horizontal-layout style="border: solid 1px blue; width: 400px;" theme="spacing wrap">
<div slot="middle" style="background: #ffd700;">Middle</div>
<div slot="end" style="background: #f08080; width: 100px">End</div>
<div slot="end" style="background: #f08080; width: 100px">End</div>
</vaadin-horizontal-layout>

<br />

<vaadin-horizontal-layout style="border: solid 1px blue; width: 250px;" theme="spacing wrap">
<div slot="middle" style="background: #ffd700;">Middle</div>
<div slot="end" style="background: #f08080; width: 100px">End</div>
<div slot="end" style="background: #f08080; width: 100px">End</div>
</vaadin-horizontal-layout>
</body>
</html>
11 changes: 11 additions & 0 deletions packages/horizontal-layout/src/vaadin-horizontal-layout-mixin.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
/**
* @license
* Copyright (c) 2017 - 2025 Vaadin Ltd.
* This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
*/
import type { Constructor } from '@open-wc/dedupe-mixin';
import type { ResizeMixinClass } from '@vaadin/component-base/src/resize-mixin.js';

export declare function HorizontalLayoutMixin<T extends Constructor<HTMLElement>>(
base: T,
): Constructor<ResizeMixinClass> & T;
117 changes: 117 additions & 0 deletions packages/horizontal-layout/src/vaadin-horizontal-layout-mixin.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
/**
* @license
* Copyright (c) 2017 - 2025 Vaadin Ltd.
* This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
*/
import { isEmptyTextNode } from '@vaadin/component-base/src/dom-utils.js';
import { ResizeMixin } from '@vaadin/component-base/src/resize-mixin.js';
import { SlotObserver } from '@vaadin/component-base/src/slot-observer.js';

/**
* @polymerMixin
* @mixes ResizeMixin
*/
export const HorizontalLayoutMixin = (superClass) =>
class extends ResizeMixin(superClass) {
/** @protected */
ready() {
super.ready();

const startSlot = this.shadowRoot.querySelector('slot:not([name])');
this.__startSlotObserver = new SlotObserver(startSlot, ({ currentNodes, removedNodes }) => {
if (removedNodes.length) {
this.__clearAttribute(removedNodes, 'last-start-child');
this.__clearAttribute(removedNodes, 'first-in-row');
}

const children = currentNodes.filter((node) => node.nodeType === Node.ELEMENT_NODE);
this.__updateAttributes(children, 'start', false, true);

const nodes = currentNodes.filter((node) => !isEmptyTextNode(node));
this.toggleAttribute('has-start', nodes.length > 0);

this.__updateRowState();
});

const endSlot = this.shadowRoot.querySelector('[name="end"]');
this.__endSlotObserver = new SlotObserver(endSlot, ({ currentNodes, removedNodes }) => {
if (removedNodes.length) {
this.__clearAttribute(removedNodes, 'first-end-child');
this.__clearAttribute(removedNodes, 'first-in-row');
}

this.__updateAttributes(currentNodes, 'end', true, false);

this.toggleAttribute('has-end', currentNodes.length > 0);

this.__updateRowState();
});

const middleSlot = this.shadowRoot.querySelector('[name="middle"]');
this.__middleSlotObserver = new SlotObserver(middleSlot, ({ currentNodes, removedNodes }) => {
if (removedNodes.length) {
this.__clearAttribute(removedNodes, 'first-middle-child');
this.__clearAttribute(removedNodes, 'last-middle-child');
this.__clearAttribute(removedNodes, 'first-in-row');
}

this.__updateAttributes(currentNodes, 'middle', true, true);

this.toggleAttribute('has-middle', currentNodes.length > 0);

this.__updateRowState();
});
}

/**
* @protected
* @override
*/
_onResize() {
this.__updateRowState();
}

/** @private */
__clearAttribute(nodes, attr) {
const el = nodes.find((node) => node.nodeType === Node.ELEMENT_NODE && node.hasAttribute(attr));
if (el) {
el.removeAttribute(attr);
}
}

/** @private */
__updateAttributes(nodes, slot, setFirst, setLast) {
nodes.forEach((child, idx) => {
if (setFirst) {
const attr = `first-${slot}-child`;
if (idx === 0) {
child.setAttribute(attr, '');
} else if (child.hasAttribute(attr)) {
child.removeAttribute(attr);
}
}

if (setLast) {
const attr = `last-${slot}-child`;
if (idx === nodes.length - 1) {
child.setAttribute(attr, '');
} else if (child.hasAttribute(attr)) {
child.removeAttribute(attr);
}
}
});
}

/** @private */
__updateRowState() {
let offset = 0;
for (const child of this.children) {
if (child.offsetTop > offset) {
offset = child.offsetTop;
child.setAttribute('first-in-row', '');
} else {
child.removeAttribute('first-in-row');
}
}
}
};
20 changes: 20 additions & 0 deletions packages/horizontal-layout/src/vaadin-horizontal-layout-styles.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,4 +27,24 @@ export const horizontalLayoutStyles = css`
:host([theme~='spacing']) {
gap: 1em;
}

:host([has-end]:not([has-middle])) ::slotted([last-start-child]) {
margin-inline-end: auto;
}

::slotted([first-middle-child]) {
margin-inline-start: auto;
}

::slotted([last-middle-child]) {
margin-inline-end: auto;
}

:host([has-start]:not([has-middle])) ::slotted([first-end-child]) {
margin-inline-start: auto;
}

::slotted([slot='end'][first-in-row]) {
margin-inline-start: auto;
}
`;
13 changes: 12 additions & 1 deletion packages/horizontal-layout/src/vaadin-horizontal-layout.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
*/
import { ElementMixin } from '@vaadin/component-base/src/element-mixin.js';
import { ThemableMixin } from '@vaadin/vaadin-themable-mixin/vaadin-themable-mixin.js';
import { HorizontalLayoutMixin } from './vaadin-horizontal-layout-mixin.js';

/**
* `<vaadin-horizontal-layout>` provides a simple way to horizontally align your HTML elements.
Expand All @@ -26,8 +27,18 @@ import { ThemableMixin } from '@vaadin/vaadin-themable-mixin/vaadin-themable-mix
* `theme="padding"` | Applies the default amount of CSS padding for the host element (specified by the theme)
* `theme="spacing"` | Applies the default amount of CSS margin between items (specified by the theme)
* `theme="wrap"` | Items wrap to the next row when they exceed the layout width
*
* ### Component's slots
*
* The following slots are available to be set:
*
* Slot name | Description
* -------------------|---------------
* no name | Default slot
* `middle` | Slot for the content placed in the middle
* `end` | Slot for the content placed at the end
*/
declare class HorizontalLayout extends ThemableMixin(ElementMixin(HTMLElement)) {}
declare class HorizontalLayout extends HorizontalLayoutMixin(ThemableMixin(ElementMixin(HTMLElement))) {}

declare global {
interface HTMLElementTagNameMap {
Expand Down
20 changes: 18 additions & 2 deletions packages/horizontal-layout/src/vaadin-horizontal-layout.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { html, PolymerElement } from '@polymer/polymer/polymer-element.js';
import { defineCustomElement } from '@vaadin/component-base/src/define.js';
import { ElementMixin } from '@vaadin/component-base/src/element-mixin.js';
import { registerStyles, ThemableMixin } from '@vaadin/vaadin-themable-mixin/vaadin-themable-mixin.js';
import { HorizontalLayoutMixin } from './vaadin-horizontal-layout-mixin.js';
import { horizontalLayoutStyles } from './vaadin-horizontal-layout-styles.js';

registerStyles('vaadin-horizontal-layout', horizontalLayoutStyles, { moduleId: 'vaadin-horizontal-layout-styles' });
Expand All @@ -32,14 +33,29 @@ registerStyles('vaadin-horizontal-layout', horizontalLayoutStyles, { moduleId: '
* `theme="spacing"` | Applies the default amount of CSS margin between items (specified by the theme)
* `theme="wrap"` | Items wrap to the next row when they exceed the layout width
*
* ### Component's slots
*
* The following slots are available to be set:
*
* Slot name | Description
* -------------------|---------------
* no name | Default slot
* `middle` | Slot for the content placed in the middle
* `end` | Slot for the content placed at the end
*
* @customElement
* @extends HTMLElement
* @mixes ThemableMixin
* @mixes ElementMixin
* @mixes HorizontalLayoutMixin
*/
class HorizontalLayout extends ElementMixin(ThemableMixin(PolymerElement)) {
class HorizontalLayout extends HorizontalLayoutMixin(ElementMixin(ThemableMixin(PolymerElement))) {
static get template() {
return html`<slot></slot>`;
return html`
<slot></slot>
<slot name="middle"></slot>
<slot name="end"></slot>
`;
}

static get is() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { defineCustomElement } from '@vaadin/component-base/src/define.js';
import { ElementMixin } from '@vaadin/component-base/src/element-mixin.js';
import { PolylitMixin } from '@vaadin/component-base/src/polylit-mixin.js';
import { ThemableMixin } from '@vaadin/vaadin-themable-mixin/vaadin-themable-mixin.js';
import { HorizontalLayoutMixin } from './vaadin-horizontal-layout-mixin.js';
import { horizontalLayoutStyles } from './vaadin-horizontal-layout-styles.js';

/**
Expand All @@ -19,7 +20,7 @@ import { horizontalLayoutStyles } from './vaadin-horizontal-layout-styles.js';
* There is no ETA regarding specific Vaadin version where it'll land.
* Feel free to try this code in your apps as per Apache 2.0 license.
*/
class HorizontalLayout extends ThemableMixin(ElementMixin(PolylitMixin(LitElement))) {
class HorizontalLayout extends HorizontalLayoutMixin(ThemableMixin(ElementMixin(PolylitMixin(LitElement)))) {
static get is() {
return 'vaadin-horizontal-layout';
}
Expand All @@ -30,7 +31,11 @@ class HorizontalLayout extends ThemableMixin(ElementMixin(PolylitMixin(LitElemen

/** @protected */
render() {
return html`<slot></slot>`;
return html`
<slot></slot>
<slot name="middle"></slot>
<slot name="end"></slot>
`;
}
}

Expand Down
Loading