Skip to content

Commit

Permalink
feat: add middle and end slots to horizontal layout (#8515)
Browse files Browse the repository at this point in the history
  • Loading branch information
web-padawan authored Feb 13, 2025
1 parent a14ed5e commit 1470d9c
Show file tree
Hide file tree
Showing 18 changed files with 542 additions and 5 deletions.
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>
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/**
* @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';

export declare function HorizontalLayoutMixin<T extends Constructor<HTMLElement>>(base: T): T;
85 changes: 85 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,85 @@
/**
* @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 { SlotObserver } from '@vaadin/component-base/src/slot-observer.js';

/**
* @polymerMixin
*/
export const HorizontalLayoutMixin = (superClass) =>
class extends 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');
}

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);
});

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

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

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

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.__updateAttributes(currentNodes, 'middle', true, true);

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

/** @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);
}
}
});
}
};
16 changes: 16 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,6 +27,22 @@ export const baseStyles = 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;
}
`;

// Layout improvements are part of a feature for Flow users where children that have been configured to use full size
Expand Down
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

0 comments on commit 1470d9c

Please sign in to comment.