From c1a0952b24cac6e18d84e0f40aefe830f8e91799 Mon Sep 17 00:00:00 2001 From: Serhii Kulykov Date: Fri, 10 Jan 2025 12:55:48 +0200 Subject: [PATCH] refactor: extract board row logic into reusable mixin (#8482) --- packages/board/package.json | 1 + .../board/src/vaadin-board-row-mixin.d.ts | 28 +++ packages/board/src/vaadin-board-row-mixin.js | 230 ++++++++++++++++++ packages/board/src/vaadin-board-row.d.ts | 14 +- packages/board/src/vaadin-board-row.js | 218 +---------------- 5 files changed, 264 insertions(+), 227 deletions(-) create mode 100644 packages/board/src/vaadin-board-row-mixin.d.ts create mode 100644 packages/board/src/vaadin-board-row-mixin.js diff --git a/packages/board/package.json b/packages/board/package.json index dd42570836..5dbbdc92fd 100644 --- a/packages/board/package.json +++ b/packages/board/package.json @@ -38,6 +38,7 @@ "polymer" ], "dependencies": { + "@open-wc/dedupe-mixin": "^1.3.0", "@polymer/polymer": "^3.0.0", "@vaadin/a11y-base": "24.7.0-alpha4", "@vaadin/component-base": "24.7.0-alpha4" diff --git a/packages/board/src/vaadin-board-row-mixin.d.ts b/packages/board/src/vaadin-board-row-mixin.d.ts new file mode 100644 index 0000000000..9a950597aa --- /dev/null +++ b/packages/board/src/vaadin-board-row-mixin.d.ts @@ -0,0 +1,28 @@ +/** + * @license + * Copyright (c) 2000 - 2025 Vaadin Ltd. + * + * This program is available under Vaadin Commercial License and Service Terms. + * + * + * See https://vaadin.com/commercial-license-and-service-terms for the full + * license. + */ +import type { Constructor } from '@open-wc/dedupe-mixin'; +import type { ResizeMixinClass } from '@vaadin/component-base/src/resize-mixin.js'; + +export declare function BoardRowMixin>( + base: T, +): Constructor & Constructor & T; + +export declare class BoardRowMixinClass { + /** + * Redraws the row, if necessary. + * + * In most cases, a board row will redraw itself if your reconfigure it. + * If you dynamically change breakpoints + * `--vaadin-board-width-small` or `--vaadin-board-width-medium`, + * then you need to call this method. + */ + redraw(): void; +} diff --git a/packages/board/src/vaadin-board-row-mixin.js b/packages/board/src/vaadin-board-row-mixin.js new file mode 100644 index 0000000000..4d8b889181 --- /dev/null +++ b/packages/board/src/vaadin-board-row-mixin.js @@ -0,0 +1,230 @@ +/** + * @license + * Copyright (c) 2000 - 2025 Vaadin Ltd. + * + * This program is available under Vaadin Commercial License and Service Terms. + * + * + * See https://vaadin.com/commercial-license-and-service-terms for the full + * license. + */ +import { isElementHidden } from '@vaadin/a11y-base/src/focus-utils.js'; +import { ResizeMixin } from '@vaadin/component-base/src/resize-mixin.js'; + +const CLASSES = { + SMALL: 'small', + MEDIUM: 'medium', + LARGE: 'large', +}; + +/** + * @polymerMixin + * @mixes ResizeMixin + */ +export const BoardRowMixin = (superClass) => + class BoardRowMixinClass extends ResizeMixin(superClass) { + constructor() { + super(); + this._oldWidth = 0; + this._oldBreakpoints = { smallSize: 600, mediumSize: 960 }; + this._oldFlexBasis = []; + } + + /** @protected */ + ready() { + super.ready(); + + this.$.insertionPoint.addEventListener('slotchange', () => this.redraw()); + } + + /** @protected */ + connectedCallback() { + super.connectedCallback(); + this._onResize(); + } + + /** + * Adds styles for board row based on width. + * @private + */ + _addStyleNames(width, breakpoints) { + if (width < breakpoints.smallSize) { + this.classList.add(CLASSES.SMALL); + this.classList.remove(CLASSES.MEDIUM); + this.classList.remove(CLASSES.LARGE); + } else if (width < breakpoints.mediumSize) { + this.classList.remove(CLASSES.SMALL); + this.classList.add(CLASSES.MEDIUM); + this.classList.remove(CLASSES.LARGE); + } else { + this.classList.remove(CLASSES.SMALL); + this.classList.remove(CLASSES.MEDIUM); + this.classList.add(CLASSES.LARGE); + } + } + + /** + * Calculates flex basis based on colSpan, width and breakpoints. + * @param {number} colSpan colspan value of the row + * @param {number} width width of the row in px + * @param {number} colsInRow number of columns in the row + * @param {object} breakpoints object with smallSize and mediumSize number properties, which tells + * where the row should switch rendering size in pixels. + * @private + */ + _calculateFlexBasis(colSpan, width, colsInRow, breakpoints) { + if (width < breakpoints.smallSize) { + colsInRow = 1; + } else if (width < breakpoints.mediumSize && colsInRow === 4) { + colsInRow = 2; + } + let flexBasis = (colSpan / colsInRow) * 100; + flexBasis = flexBasis > 100 ? 100 : flexBasis; + return `${flexBasis}%`; + } + + /** @private */ + _reportError() { + const errorMessage = 'The column configuration is not valid; column count should add up to 3 or 4.'; + console.warn(errorMessage, `check: \r\n${this.outerHTML}`); + } + + /** + * Parses board-cols from DOM. + * If there is not enough space in the row drop board cols. + * @param {!Array} nodes array of nodes + * @return {!Array} array of boardCols + * @private + */ + _parseBoardCols(nodes) { + const boardCols = nodes.map((node) => { + if (node.getAttribute('board-cols')) { + return parseInt(node.getAttribute('board-cols')); + } + return 1; + }); + + let spaceLeft = 4; + let returnBoardCols = []; + nodes.forEach((_node, i) => { + spaceLeft -= boardCols[i]; + }); + + if (spaceLeft < 0) { + this._reportError(); + boardCols.forEach((_node, i) => { + returnBoardCols[i] = 1; + }); + } else { + returnBoardCols = boardCols.slice(0); + } + + return returnBoardCols; + } + + /** + * If there is not enough space in the row. + * Extra items are dropped from DOM. + * @param {!Array} boardCols array of board-cols for every node + * @param {!Array} nodes array of nodes + * @return {!Array} filtered array of nodes + * @private + */ + _removeExtraNodesFromDOM(boardCols, nodes) { + let isErrorReported = false; + let spaceLeft = 4; + const returnNodes = []; + nodes.forEach((node, i) => { + spaceLeft -= boardCols[i]; + if (spaceLeft < 0) { + if (!isErrorReported) { + isErrorReported = true; + this._reportError(); + } + this.removeChild(node); + } else { + returnNodes[i] = node; + } + }); + return returnNodes; + } + + /** + * Redraws the row, if necessary. + * + * In most cases, a board row will redraw itself if your reconfigure it. + * If you dynamically change breakpoints + * --vaadin-board-width-small or --vaadin-board-width-medium, + * then you need to call this method. + */ + redraw() { + this._recalculateFlexBasis(true); + } + + /** + * @protected + * @override + */ + _onResize() { + this._recalculateFlexBasis(false); + } + + /** @private */ + _recalculateFlexBasis(forceResize) { + const width = this.getBoundingClientRect().width; + const breakpoints = this._measureBreakpointsInPx(); + if (forceResize || this._shouldRecalculate(width, breakpoints)) { + const nodes = this.$.insertionPoint.assignedNodes({ flatten: true }); + const filteredNodes = nodes.filter((node) => node.nodeType === Node.ELEMENT_NODE); + this._addStyleNames(width, breakpoints); + const boardCols = this._parseBoardCols(filteredNodes); + const colsInRow = boardCols.reduce((a, b) => a + b, 0); + this._removeExtraNodesFromDOM(boardCols, filteredNodes).forEach((e, i) => { + const newFlexBasis = this._calculateFlexBasis(boardCols[i], width, colsInRow, breakpoints); + if (forceResize || !this._oldFlexBasis[i] || this._oldFlexBasis[i] !== newFlexBasis) { + this._oldFlexBasis[i] = newFlexBasis; + e.style.flexBasis = newFlexBasis; + } + }); + this._oldWidth = width; + this._oldBreakpoints = breakpoints; + } + } + + /** @private */ + _shouldRecalculate(width, breakpoints) { + // Should not recalculate if row is invisible + if (isElementHidden(this)) { + return false; + } + return ( + width !== this._oldWidth || + breakpoints.smallSize !== this._oldBreakpoints.smallSize || + breakpoints.mediumSize !== this._oldBreakpoints.mediumSize + ); + } + + /** + * Measure the breakpoints in pixels. + * + * The breakpoints for `small` and `medium` can be given in any unit: `px`, `em`, `in` etc. + * We need to know them in `px` so that they are comparable with the actual size. + * + * @return {object} object with smallSize and mediumSize number properties, which tells + * where the row should switch rendering size in pixels. + * @private + */ + _measureBreakpointsInPx() { + // Convert minWidth to px units for comparison + const breakpoints = {}; + const tmpStyleProp = 'background-position'; + const smallSize = getComputedStyle(this).getPropertyValue('--small-size'); + const mediumSize = getComputedStyle(this).getPropertyValue('--medium-size'); + this.style.setProperty(tmpStyleProp, smallSize); + breakpoints.smallSize = parseFloat(getComputedStyle(this).getPropertyValue(tmpStyleProp)); + this.style.setProperty(tmpStyleProp, mediumSize); + breakpoints.mediumSize = parseFloat(getComputedStyle(this).getPropertyValue(tmpStyleProp)); + this.style.removeProperty(tmpStyleProp); + return breakpoints; + } + }; diff --git a/packages/board/src/vaadin-board-row.d.ts b/packages/board/src/vaadin-board-row.d.ts index 43a42b25e6..9d4da963cf 100644 --- a/packages/board/src/vaadin-board-row.d.ts +++ b/packages/board/src/vaadin-board-row.d.ts @@ -9,7 +9,7 @@ * license. */ import { ElementMixin } from '@vaadin/component-base/src/element-mixin.js'; -import { ResizeMixin } from '@vaadin/component-base/src/resize-mixin.js'; +import { BoardRowMixin } from './vaadin-board-row-mixin.js'; /** * `` is a web component that together with `` component allows @@ -45,17 +45,7 @@ import { ResizeMixin } from '@vaadin/component-base/src/resize-mixin.js'; * `--vaadin-board-width-small` | Determines the width where mode changes from `small` to `medium` | `600px` * `--vaadin-board-width-medium` | Determines the width where mode changes from `medium` to `large` | `960px` */ -declare class BoardRow extends ResizeMixin(ElementMixin(HTMLElement)) { - /** - * Redraws the row, if necessary. - * - * In most cases, a board row will redraw itself if your reconfigure it. - * If you dynamically change breakpoints - * `--vaadin-board-width-small` or `--vaadin-board-width-medium`, - * then you need to call this method. - */ - redraw(): void; -} +declare class BoardRow extends BoardRowMixin(ElementMixin(HTMLElement)) {} declare global { interface HTMLElementTagNameMap { diff --git a/packages/board/src/vaadin-board-row.js b/packages/board/src/vaadin-board-row.js index a1c30f72c0..b252185a04 100644 --- a/packages/board/src/vaadin-board-row.js +++ b/packages/board/src/vaadin-board-row.js @@ -9,16 +9,9 @@ * license. */ import { html, PolymerElement } from '@polymer/polymer/polymer-element.js'; -import { isElementHidden } from '@vaadin/a11y-base/src/focus-utils.js'; import { defineCustomElement } from '@vaadin/component-base/src/define.js'; import { ElementMixin } from '@vaadin/component-base/src/element-mixin.js'; -import { ResizeMixin } from '@vaadin/component-base/src/resize-mixin.js'; - -const CLASSES = { - SMALL: 'small', - MEDIUM: 'medium', - LARGE: 'large', -}; +import { BoardRowMixin } from './vaadin-board-row-mixin.js'; /** * `` is a web component that together with `` component allows @@ -57,9 +50,9 @@ const CLASSES = { * @customElement * @extends HTMLElement * @mixes ElementMixin - * @mixes ResizeMixin + * @mixes BoardRowMixin */ -class BoardRow extends ResizeMixin(ElementMixin(PolymerElement)) { +class BoardRow extends BoardRowMixin(ElementMixin(PolymerElement)) { static get template() { return html`