From be528c3785928e7ff612938175f474ebef6e2685 Mon Sep 17 00:00:00 2001 From: Raphael Mattos Date: Thu, 27 Jul 2023 22:38:06 -0300 Subject: [PATCH] feat(cxl-ui): add `cxl-search-filters` and related components --- package.json | 1 + packages/cxl-lumo-styles/src/icons.js | 1 + packages/cxl-ui/package.json | 3 +- .../cxl-ui/scss/cxl-filter-header-item.scss | 29 +++++ packages/cxl-ui/scss/cxl-filter-header.scss | 114 ++++++++++++++++++ .../cxl-ui/scss/cxl-search-filters-item.scss | 24 ++++ .../cxl-ui/scss/cxl-search-filters-panel.scss | 23 ++++ packages/cxl-ui/scss/cxl-search-filters.scss | 13 ++ .../src/components/cxl-filter-header-item.js | 29 +++++ .../src/components/cxl-filter-header.js | 95 +++++++++++++++ .../src/components/cxl-search-filters-item.js | 40 ++++++ .../components/cxl-search-filters-panel.js | 30 +++++ .../src/components/cxl-search-filters.js | 38 ++++++ .../cxl-filter-header.stories.js | 63 ++++++++++ .../cxl-search-filters-panel.stories.js | 30 +++++ .../cxl-search-filters.stories.js | 34 ++++++ yarn.lock | 19 +++ 17 files changed, 585 insertions(+), 1 deletion(-) create mode 100644 packages/cxl-ui/scss/cxl-filter-header-item.scss create mode 100644 packages/cxl-ui/scss/cxl-filter-header.scss create mode 100644 packages/cxl-ui/scss/cxl-search-filters-item.scss create mode 100644 packages/cxl-ui/scss/cxl-search-filters-panel.scss create mode 100644 packages/cxl-ui/scss/cxl-search-filters.scss create mode 100644 packages/cxl-ui/src/components/cxl-filter-header-item.js create mode 100644 packages/cxl-ui/src/components/cxl-filter-header.js create mode 100644 packages/cxl-ui/src/components/cxl-search-filters-item.js create mode 100644 packages/cxl-ui/src/components/cxl-search-filters-panel.js create mode 100644 packages/cxl-ui/src/components/cxl-search-filters.js create mode 100644 packages/storybook/cxl-ui/cxl-search-filters/cxl-filter-header.stories.js create mode 100644 packages/storybook/cxl-ui/cxl-search-filters/cxl-search-filters-panel.stories.js create mode 100644 packages/storybook/cxl-ui/cxl-search-filters/cxl-search-filters.stories.js diff --git a/package.json b/package.json index 0bbf1acba..ab490d5b2 100644 --- a/package.json +++ b/package.json @@ -33,6 +33,7 @@ "@commitlint/cli": "^12.0.1", "@commitlint/config-conventional": "^12.0.1", "@commitlint/config-lerna-scopes": "^12.1.4", + "@lit-labs/observers": "^2.0.0", "@open-wc/testing-helpers": "^0.9.5", "@size-limit/file": "^6.0.3", "@webcomponents/webcomponentsjs": "^2.2.10", diff --git a/packages/cxl-lumo-styles/src/icons.js b/packages/cxl-lumo-styles/src/icons.js index a35fb45d1..e9fd20e07 100644 --- a/packages/cxl-lumo-styles/src/icons.js +++ b/packages/cxl-lumo-styles/src/icons.js @@ -62,6 +62,7 @@ $documentContainer.innerHTML = ` + diff --git a/packages/cxl-ui/package.json b/packages/cxl-ui/package.json index 1a9b361b6..beb6f73ba 100644 --- a/packages/cxl-ui/package.json +++ b/packages/cxl-ui/package.json @@ -10,7 +10,8 @@ "directory": "packages/cxl-ui" }, "dependencies": { - "@conversionxl/cxl-lumo-styles": "^1.6.4" + "@conversionxl/cxl-lumo-styles": "^1.6.4", + "@lit-labs/observers": "^2.0.0" }, "devDependencies": { "@conversionxl/normalize-wheel": "^1.0.1", diff --git a/packages/cxl-ui/scss/cxl-filter-header-item.scss b/packages/cxl-ui/scss/cxl-filter-header-item.scss new file mode 100644 index 000000000..6ea5bb76f --- /dev/null +++ b/packages/cxl-ui/scss/cxl-filter-header-item.scss @@ -0,0 +1,29 @@ +:host { + font-weight: 400; + + vaadin-button { + background-color: transparent; + border-radius: var(--lumo-border-radius-s) var(--lumo-border-radius-s) 0 0 ; + margin-bottom: 0; + + &::part(label) { + color: var(--lumo-shade); + } + } + + .label { + font-weight: 600; + } +} + +:host(.checked) { + color: var(--lumo-contrast); + + vaadin-button, vaadin-button { + background-color: var(--lumo-primary-color); + + &::part(label) { + color: var(--lumo-base-color); + } + } +} diff --git a/packages/cxl-ui/scss/cxl-filter-header.scss b/packages/cxl-ui/scss/cxl-filter-header.scss new file mode 100644 index 000000000..7aa70d834 --- /dev/null +++ b/packages/cxl-ui/scss/cxl-filter-header.scss @@ -0,0 +1,114 @@ +@use "~@conversionxl/cxl-lumo-styles/scss/mq"; + +:host { + display: block; + font-weight: 400; + + .container { + width: 100%; + } + + .tabs { + display: flex; + position: relative; + width: 100%; + + .scroll-control { + position: absolute; + + &:first-child { + left: -2px; + background: linear-gradient(to left, rgba(255, 255, 255, 0), var(--lumo-base-color)); + z-index: 1; + } + + &:last-child { + right: -2px; + background: linear-gradient(to right, rgba(255, 255, 255, 0), var(--lumo-base-color)); + z-index: 1; + } + } + } + + .filters { + display: flex; + border-bottom: 1px solid var(--lumo-shade-20pct); + max-width: 100%; + overflow-x: scroll; + scroll-snap-type: x mandatory; + -webkit-overflow-scrolling: touch; + scroll-behavior: smooth; + + &::-webkit-scrollbar { + display: none; + } + + /* This seems more future-proof than \`overflow: -moz-scrollbars-none\` which is marked obsolete + and is no longer guaranteed to work: + https://developer.mozilla.org/en-US/docs/Web/CSS/overflow#Mozilla_Extensions + */ + @-moz-document url-prefix() { + overflow: hidden; + } + + ::slotted(*) { + scroll-snap-align: start; + scroll-snap-stop: always; + } + + } + + .controls { + display: flex; + flex-direction: row-reverse; + flex-wrap: wrap-reverse; + justify-content: stretch; + padding-top: var(--lumo-space-l); + gap: var(--lumo-space-m); + + .flex-group { + display: flex; + flex-direction: row-reverse; + justify-content: stretch; + width: 100%; + gap: var(--lumo-space-m); + + > ::slotted(*) { + flex: 1; + width: auto !important; + } + + #show-filters { + --lumo-button-size: var(--lumo-size-l); + display: block; + margin: 0; + text-align: start; + flex-basis: 50%; + } + } + + ::slotted([slot="search"]) { + width: 100%; + line-height: var(--lumo-size-l); + } + } + + @media #{mq.$small} { + .controls { + flex-direction: row; + flex-wrap: nowrap; + + .flex-group { + width: auto; + + #show-filters { + display: none; + } + } + + .search { + line-height: normal; + } + } + } +} diff --git a/packages/cxl-ui/scss/cxl-search-filters-item.scss b/packages/cxl-ui/scss/cxl-search-filters-item.scss new file mode 100644 index 000000000..4b0deccba --- /dev/null +++ b/packages/cxl-ui/scss/cxl-search-filters-item.scss @@ -0,0 +1,24 @@ +:host { + display: flex; + justify-content: space-between; + font-size: var(--lumo-font-size-s); + gap: var(--lumo-space-s); + + .count { + color: var(--lumo-shade-60pct); + } + + vaadin-checkbox { + font-size: var(--lumo-font-size-s); + --vaadin-checkbox-size: var(--lumo-font-size-s); + + // Checkbox style can't be matched with current design tokens + &::part(checkbox) { + // border-radius: calc(var(--lumo-border-radius-s) / 2); + // background-color: transparent; + // border: 1px solid var(--lumo-shade-60pct); + margin-right: var(--lumo-space-s); + } + } + +} diff --git a/packages/cxl-ui/scss/cxl-search-filters-panel.scss b/packages/cxl-ui/scss/cxl-search-filters-panel.scss new file mode 100644 index 000000000..776e00b10 --- /dev/null +++ b/packages/cxl-ui/scss/cxl-search-filters-panel.scss @@ -0,0 +1,23 @@ + +:host { + display: block; + min-width: 240px; + + .content { + display: flex; + flex-direction: column; + } + vaadin-accordion-panel { + font-size: var(--lumo-font-size-s); + + &::part(summary-content) { + font-weight: 600; + color: var(--lumo-contrast); + font-size: var(--lumo-font-size-s); + } + + &::part(toggle) { + background-color: unset; + } + } +} diff --git a/packages/cxl-ui/scss/cxl-search-filters.scss b/packages/cxl-ui/scss/cxl-search-filters.scss new file mode 100644 index 000000000..43e6e0e7f --- /dev/null +++ b/packages/cxl-ui/scss/cxl-search-filters.scss @@ -0,0 +1,13 @@ +:host { + display: flex; + flex-direction: column; + align-items: start; + gap: var(--lumo-space-m); + + header { + color: var(--lumo-shade-60pct); + font-size: var(--lumo-font-size-s); + font-weight: 300; + text-transform: uppercase; + } +} diff --git a/packages/cxl-ui/src/components/cxl-filter-header-item.js b/packages/cxl-ui/src/components/cxl-filter-header-item.js new file mode 100644 index 000000000..8068dff54 --- /dev/null +++ b/packages/cxl-ui/src/components/cxl-filter-header-item.js @@ -0,0 +1,29 @@ +/* eslint-disable import/no-extraneous-dependencies */ +import { LitElement, html } from 'lit'; +import { customElement, property } from 'lit/decorators.js'; +import '@vaadin/button'; +import cxlFilterHeaderItemStyles from '../styles/cxl-filter-header-item-css.js'; + +@customElement('cxl-filter-header-item') +export class CXLFilterHeaderItemElement extends LitElement { + static get styles() { + return [cxlFilterHeaderItemStyles]; + } + + @property({ type: String }) label = ''; + + @property({ type: String }) value = ''; + + @property({ type: Number }) count = 0; + + @property({ type: Boolean, reflect: true }) checked = false; + + render () { + return html` + + ${this.label} + (${this.count}) + + ` + } +} diff --git a/packages/cxl-ui/src/components/cxl-filter-header.js b/packages/cxl-ui/src/components/cxl-filter-header.js new file mode 100644 index 000000000..5a74f3749 --- /dev/null +++ b/packages/cxl-ui/src/components/cxl-filter-header.js @@ -0,0 +1,95 @@ +/* eslint-disable import/no-extraneous-dependencies */ +import { LitElement, html } from 'lit'; +import { ResizeController } from '@lit-labs/observers/resize-controller.js'; +import { customElement, state, query } from 'lit/decorators.js'; +import './cxl-vaadin-accordion'; +import '@vaadin/button' +import cxlDashboardFilterHeaderStyles from '../styles/cxl-filter-header-css.js'; + +const supportsScrollEndEvent = 'onscrollend' in window +const isFirefox = document.scrollingElement.scrollLeftMax !== undefined + +@customElement('cxl-filter-header') +export class CXLFilterHeaderElement extends LitElement { + static get styles() { + return [cxlDashboardFilterHeaderStyles]; + } + + @state() tabsWidth = 0 + + @state() tabsNumber = 1 + + @query('.filters') filtersContainer = null + + @query('#filters-slot') filtersSlot = null + + isOverflowing = new ResizeController(this, { + callback: (entries) => { + const entry = entries[0]; + this.showScrollers = entry && entry.borderBoxSize[0].inlineSize < this.tabsWidth; + return this.showScrollers + } + }); + + _checkTabsMaxWidth () { + if (!this.filtersSlot) return 0 + const tabs = this.filtersSlot.assignedElements() + const fullWidth = tabs.map(tab => tab.clientWidth).reduce((total, w) => total + w, 0); + this.tabsWidth = fullWidth + this.tabsNumber = tabs.length + return this.tabsWidth + } + + _scrollForwards () { + // `behavior: 'smooth'` option not being used due to firefox bug + this.filtersContainer.scrollBy({ left: (this.filtersContainer.scrollWidth - this.filtersContainer.clientWidth), behavior: isFirefox ? 'instant' : 'smooth' }) + // Workaround for browsers that don't support scrollend event to update rendering + if (!supportsScrollEndEvent) { + setTimeout(() => { this.requestUpdate() }, 100) + } + } + + _scrollBackwards () { + // `behavior: 'smooth'` option not being used due to firefox bug + this.filtersContainer.scrollBy({ left: -(this.filtersContainer.scrollWidth - this.filtersContainer.clientWidth), behavior: isFirefox ? 'instant' : 'smooth' }) + // Workaround for browsers that don't support scrollend event to update rendering + if (!supportsScrollEndEvent) { + setTimeout(() => { this.requestUpdate() }, 100) + } + } + + get showBackwardScroller () { + return this.isOverflowing.value && this.filtersContainer.scrollLeft > 0 + } + + get showForwardScroller () { + return this.isOverflowing.value && this.filtersContainer.scrollLeft < (this.filtersContainer.scrollWidth - this.filtersContainer.clientWidth) + } + + + render () { + return html` +
+
+ +
{ this.requestUpdate() }}> + +
+ +
+
+ + +
+ + + + + Filters + +
+
+
+ ` + } +} diff --git a/packages/cxl-ui/src/components/cxl-search-filters-item.js b/packages/cxl-ui/src/components/cxl-search-filters-item.js new file mode 100644 index 000000000..81f302e8e --- /dev/null +++ b/packages/cxl-ui/src/components/cxl-search-filters-item.js @@ -0,0 +1,40 @@ +/* eslint-disable import/no-extraneous-dependencies */ +import { LitElement, html } from 'lit'; +import { customElement, property, query } from 'lit/decorators.js'; +import '@vaadin/checkbox'; +import cxlSearchFiltersItemStyles from '../styles/cxl-search-filters-item-css.js'; + +@customElement('cxl-search-filters-item') +export class CXLSearchFiltersItemElement extends LitElement { + static get styles() { + return [cxlSearchFiltersItemStyles]; + } + + @property({ type: String }) label = ''; + + @property({ type: String }) value = ''; + + @property({ type: Number }) count = 0; + + @property({ type: Boolean, reflect: true }) checked = false; + + get checked () { + return this.checkbox?.checked + } + + set checked (value) { + if (this.checkbox) { + this.checkbox.checked = !!value + } + } + + @query('#checkbox') checkbox = null + + + render () { + return html` + + (${this.count}) + ` + } +} diff --git a/packages/cxl-ui/src/components/cxl-search-filters-panel.js b/packages/cxl-ui/src/components/cxl-search-filters-panel.js new file mode 100644 index 000000000..56d7ec950 --- /dev/null +++ b/packages/cxl-ui/src/components/cxl-search-filters-panel.js @@ -0,0 +1,30 @@ +/* eslint-disable import/no-extraneous-dependencies */ +import { LitElement, html } from 'lit'; +import { customElement, property } from 'lit/decorators.js'; +import './cxl-vaadin-accordion'; +import '@vaadin/accordion/vaadin-accordion-panel' +import cxlSearchFilterPanelStyles from '../styles/cxl-search-filters-panel-css.js'; + +@customElement('cxl-search-filters-panel') +export class CXLSearchFilterPanelElement extends LitElement { + static get styles() { + return [cxlSearchFilterPanelStyles]; + } + + @property({ type: Boolean }) opened = ''; + + @property({ type: String }) label = ''; + + @property({ type: String }) value = ''; + + render () { + return html` + +
${this.label}
+
+ +
+
+ ` + } +} diff --git a/packages/cxl-ui/src/components/cxl-search-filters.js b/packages/cxl-ui/src/components/cxl-search-filters.js new file mode 100644 index 000000000..32b168901 --- /dev/null +++ b/packages/cxl-ui/src/components/cxl-search-filters.js @@ -0,0 +1,38 @@ +/* eslint-disable import/no-extraneous-dependencies */ +import { LitElement, html } from 'lit'; +import { customElement, property } from 'lit/decorators.js'; +import './cxl-vaadin-accordion'; +import '@vaadin/button'; +import cxlSearchFiltersStyles from '../styles/cxl-search-filters-css.js'; + +@customElement('cxl-search-filters') +export class CXLSearchFiltersElement extends LitElement { + static get styles() { + return [cxlSearchFiltersStyles]; + } + + @property({ type: String }) label = 'FILTER CONTENTS'; + + @property({ type: Number }) count = 0; + + _resetFWP () { + if (window.FWP) window.FWP.reset() + this.dispatchEvent(new CustomEvent('reset', { bubbles: true, composed: true })) + } + + render () { + return html` +
+ ${this.label} + (${this.count}) +
+ + + + + + Reset filters + + ` + } +} diff --git a/packages/storybook/cxl-ui/cxl-search-filters/cxl-filter-header.stories.js b/packages/storybook/cxl-ui/cxl-search-filters/cxl-filter-header.stories.js new file mode 100644 index 000000000..223492723 --- /dev/null +++ b/packages/storybook/cxl-ui/cxl-search-filters/cxl-filter-header.stories.js @@ -0,0 +1,63 @@ +/* eslint-disable import/no-extraneous-dependencies */ +import { html } from 'lit'; +import '@conversionxl/cxl-ui/src/components/cxl-filter-header.js'; +import '@conversionxl/cxl-ui/src/components/cxl-filter-header-item.js'; + +export default { + title: 'CXL UI/cxl-search-filters', + parameters: { + layout: 'centered', + docs: { + description: { + component: 'CXL Search Filter Header', + }, + }, + }, +} + +const mockMarkAsChecked = (e) => { + const el = e.currentTarget + const parent = el.parentElement + const children = [...parent.children] + + children.forEach(child => { + if (child.nodeName === 'CXL-FILTER-HEADER-ITEM') { + child.classList.remove('checked') + } + }); + el.classList.add('checked') +} + +export const CXLFilterHeader = ({ filters }) => html` + + + + ${filters.map(filter => html``)} + + + +`; + +CXLFilterHeader.args = { + filters: [ + { label: 'All contents', count: '108' }, + { label: 'Deep marketing', count: '32' }, + { label: 'Broad marketing', count: '40' }, + { label: 'Fast marketing', count: '36' }, + ], +} diff --git a/packages/storybook/cxl-ui/cxl-search-filters/cxl-search-filters-panel.stories.js b/packages/storybook/cxl-ui/cxl-search-filters/cxl-search-filters-panel.stories.js new file mode 100644 index 000000000..b08007f04 --- /dev/null +++ b/packages/storybook/cxl-ui/cxl-search-filters/cxl-search-filters-panel.stories.js @@ -0,0 +1,30 @@ +import { html } from 'lit'; +import '@conversionxl/cxl-ui/src/components/cxl-search-filters-panel.js'; +import '@conversionxl/cxl-ui/src/components/cxl-search-filters-item.js'; + +export default { + title: 'CXL UI/cxl-search-filters', + parameters: { + layout: 'centered', + docs: { + description: { + component: 'CXL Search Filters Panels', + }, + }, + }, +} + +export const CXLSearchFiltersPanel = ({ label, filters }) => html` + + ${filters.map(filter => html``)} + +`; + +CXLSearchFiltersPanel.args = { + label: 'Categories', + filters: [ + { label: 'Brand Marketing', count: '10' }, + { label: 'Convertion Optimization', count: '20' }, + { label: 'Digital psychology and persuasion', count: '30' }, + ], +} diff --git a/packages/storybook/cxl-ui/cxl-search-filters/cxl-search-filters.stories.js b/packages/storybook/cxl-ui/cxl-search-filters/cxl-search-filters.stories.js new file mode 100644 index 000000000..6543b605d --- /dev/null +++ b/packages/storybook/cxl-ui/cxl-search-filters/cxl-search-filters.stories.js @@ -0,0 +1,34 @@ +import { html } from 'lit'; +import '@conversionxl/cxl-ui/src/components/cxl-vaadin-accordion'; +import '@conversionxl/cxl-ui/src/components/cxl-search-filters.js'; +import { CXLSearchFiltersPanel } from './cxl-search-filters-panel.stories'; + + +export default { + title: 'CXL UI/cxl-search-filters', + parameters: { + layout: 'centered', + docs: { + description: { + component: 'CXL Search Filters', + }, + }, + }, +} +const tagsPanelArgs = { + label: 'Tags', + filters: [ + { label: 'A' }, + { label: 'B' }, + { label: 'C' }, + { label: 'D' }, + { label: 'E' }, + ] +} + +export const CXLSearchFilters = () => html` + + ${CXLSearchFiltersPanel(CXLSearchFiltersPanel.args)} + ${CXLSearchFiltersPanel(tagsPanelArgs)} + +`; diff --git a/yarn.lock b/yarn.lock index 7e6ab5f82..eb20fae3f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2375,6 +2375,25 @@ npmlog "^4.1.2" write-file-atomic "^2.3.0" +"@lit-labs/observers@^2.0.0": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@lit-labs/observers/-/observers-2.0.0.tgz#b1ab73e43460e97b3910f6be68121bfabf14669a" + integrity sha512-NMbCjJEqp8V9TpTtt8HhzFVymx/WGpTD7iU+FMKSis4u3iBWwu2UYh6KChwFTEPW9xMum70r2IBnaViBINaGTA== + dependencies: + "@lit/reactive-element" "^1.1.0" + +"@lit-labs/ssr-dom-shim@^1.0.0": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@lit-labs/ssr-dom-shim/-/ssr-dom-shim-1.1.1.tgz#64df34e2f12e68e78ac57e571d25ec07fa460ca9" + integrity sha512-kXOeFbfCm4fFf2A3WwVEeQj55tMZa8c8/f9AKHMobQMkzNUfUj+antR3fRPaZJawsa1aZiP/Da3ndpZrwEe4rQ== + +"@lit/reactive-element@^1.1.0": + version "1.6.2" + resolved "https://registry.yarnpkg.com/@lit/reactive-element/-/reactive-element-1.6.2.tgz#c256690f82f2d7d0ffb0b1cdf68dcb1ec86cea28" + integrity sha512-rDfl+QnCYjuIGf5xI2sVJWdYIi56CTCwWa+nidKYX6oIuBYwUbT/vX4qbUDlHiZKJ/3FRNQ/tWJui44p6/stSA== + dependencies: + "@lit-labs/ssr-dom-shim" "^1.0.0" + "@lit/reactive-element@^1.3.0", "@lit/reactive-element@^1.4.0": version "1.4.1" resolved "https://registry.yarnpkg.com/@lit/reactive-element/-/reactive-element-1.4.1.tgz#3f587eec5708692135bc9e94cf396130604979f3"