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.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/src/components/cxl-filter-header.js b/packages/cxl-ui/src/components/cxl-filter-header.js new file mode 100644 index 000000000..5c1049355 --- /dev/null +++ b/packages/cxl-ui/src/components/cxl-filter-header.js @@ -0,0 +1,94 @@ +/* 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/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..dbaaf3047 --- /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, i) => 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/yarn.lock b/yarn.lock index 4883c32ba..1dde77bd5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2353,11 +2353,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", "@lit-labs/ssr-dom-shim@^1.1.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.3" + resolved "https://registry.yarnpkg.com/@lit/reactive-element/-/reactive-element-1.6.3.tgz#25b4eece2592132845d303e091bad9b04cdcfe03" + integrity sha512-QuTgnG52Poic7uM1AN5yJ09QMe0O28e10XzSvWDz02TJiiKee4stsiownEIadWm8nYzyDAyT+gKzUoZmiWQtsQ== + dependencies: + "@lit-labs/ssr-dom-shim" "^1.0.0" + "@lit/reactive-element@^1.3.0", "@lit/reactive-element@^1.6.0": version "1.6.2" resolved "https://registry.yarnpkg.com/@lit/reactive-element/-/reactive-element-1.6.2.tgz#c256690f82f2d7d0ffb0b1cdf68dcb1ec86cea28"