diff --git a/packages/styles/scss/components/filter-panel/_filter-panel.scss b/packages/styles/scss/components/filter-panel/_filter-panel.scss index 9fea2ef116d..e22d65d8ea7 100644 --- a/packages/styles/scss/components/filter-panel/_filter-panel.scss +++ b/packages/styles/scss/components/filter-panel/_filter-panel.scss @@ -1,5 +1,5 @@ /** - * Copyright IBM Corp. 2016, 2022 + * Copyright IBM Corp. 2016, 2023 * * This source code is licensed under the Apache-2.0 license found in the * LICENSE file in the root directory of this source tree. @@ -237,18 +237,7 @@ } :host(#{$dds-prefix}-filter-panel-heading) { - @include carbon--type-style('heading-01'); - @include carbon--breakpoint('sm') { - margin-left: $spacing-05; - margin-bottom: $spacing-05; - } - @include carbon--breakpoint('lg') { - margin: 0; - } - - @include carbon--breakpoint-down('lg') { - display: none; - } + display: none; } :host(#{$dds-prefix}-filter-group:first-of-type) { diff --git a/packages/web-components/src/components/filter-panel/filter-panel-composite.ts b/packages/web-components/src/components/filter-panel/filter-panel-composite.ts index 4faeab8eb37..fd92a060673 100644 --- a/packages/web-components/src/components/filter-panel/filter-panel-composite.ts +++ b/packages/web-components/src/components/filter-panel/filter-panel-composite.ts @@ -7,15 +7,20 @@ * LICENSE file in the root directory of this source tree. */ -import { html, LitElement, property, state, TemplateResult } from 'lit-element'; +import { + html, + LitElement, + property, + state, + TemplateResult, + query, +} from 'lit-element'; import settings from 'carbon-components/es/globals/js/settings.js'; import Filter from '../../internal/vendor/@carbon/web-components/icons/filter/16.js'; import HostListenerMixin from '../../internal/vendor/@carbon/web-components/globals/mixins/host-listener.js'; -import { baseFontSize, breakpoints } from '@carbon/layout'; import './filter-group'; import './filter-panel'; import './filter-panel-modal'; -import { unsafeHTML } from 'lit-html/directives/unsafe-html.js'; import HostListener from '../../internal/vendor/@carbon/web-components/globals/decorators/host-listener.js'; import ddsSettings from '../../internal/vendor/@carbon/ibmdotcom-utilities/utilities/settings/settings'; import StableSelectorMixin from '../../globals/mixins/stable-selector'; @@ -26,20 +31,23 @@ import DDSFilterPanelCheckbox from './filter-panel-checkbox'; import DDSFilterPanelInputSelect from './filter-panel-input-select'; import DDSFilterPanelInputSelectItem from './filter-panel-input-select-item'; import { carbonElement as customElement } from '../../internal/vendor/@carbon/web-components/globals/decorators/carbon-element.js'; +import MediaQueryMixin, { + MQBreakpoints, + MQDirs, +} from '../../component-mixins/media-query/media-query'; const { prefix } = settings; const { stablePrefix: ddsPrefix } = ddsSettings; -const breakpoint = parseFloat(breakpoints.lg.width) * baseFontSize; - /** * Filter panel composite * * @element dds-filter-panel-composite */ @customElement(`${ddsPrefix}-filter-panel-composite`) -class DDSFilterPanelComposite extends HostListenerMixin( - StableSelectorMixin(LitElement) +class DDSFilterPanelComposite extends MediaQueryMixin( + HostListenerMixin(StableSelectorMixin(LitElement)), + { [MQBreakpoints.LG]: MQDirs.MAX } ) { /** * Host listener for handling the state change when a input select item is selected. @@ -54,7 +62,7 @@ class DDSFilterPanelComposite extends HostListenerMixin( const { stableSelector } = DDSFilterPanelInputSelectItem; this._focusElement = `${stableSelector}[value="${value}"]`; - // remove the DDSInputSelect (header) value from list to add an inner child instead + // Remove the DDSInputSelect (header) value from list to add an inner child instead. this._selectedValues = this._selectedValues.filter( (e) => e !== headerValue ); @@ -65,7 +73,7 @@ class DDSFilterPanelComposite extends HostListenerMixin( } if (!this._selectedValues.includes(value)) { - this._selectedValues.push(value); + this._selectedValues = [...this._selectedValues, value]; } if (lastValue && this._selectedValues.includes(lastValue)) { @@ -73,7 +81,6 @@ class DDSFilterPanelComposite extends HostListenerMixin( (e) => e !== lastValue ); } - this.renderStatus(); }; /** @@ -86,7 +93,7 @@ class DDSFilterPanelComposite extends HostListenerMixin( protected _handleCheckboxStateChange = (event: CustomEvent) => { const { value } = event.detail; - // toggle checkbox in filter panel modal + // Toggle checkbox in filter panel modal. this.querySelectorAll(`${ddsPrefix}-filter-panel-checkbox`).forEach((e) => { if (e.getAttribute('value') === value) { e.toggleAttribute('checked'); @@ -108,14 +115,12 @@ class DDSFilterPanelComposite extends HostListenerMixin( } }); - // toggle value in list + // Toggle value in list. if (!this._selectedValues.includes(value)) { - this._selectedValues.push(value); + this._selectedValues = [...this._selectedValues, value]; } else { this._selectedValues = this._selectedValues.filter((e) => e !== value); } - - this.renderStatus(); }; /** @@ -133,7 +138,10 @@ class DDSFilterPanelComposite extends HostListenerMixin( if (match !== -1) { this._filterGroupsAllRevealed[match].value = event.detail.value; } else { - this._filterGroupsAllRevealed.push(event.detail); + this._filterGroupsAllRevealed = [ + ...this._filterGroupsAllRevealed, + event.detail, + ]; } }; @@ -158,10 +166,10 @@ class DDSFilterPanelComposite extends HostListenerMixin( const { stableSelector } = DDSFilterPanelInputSelect; this._focusElement = `${stableSelector}[header-value="${headerValue}"]`; - // toggle checkbox in filter panel modal + // Toggle checkbox in filter panel modal. this.querySelectorAll(`${ddsPrefix}-filter-panel-input-select`).forEach( (e) => { - // capture the element counterpart in Filter Panel Modal + // Capture the element counterpart in Filter Panel Modal. if (e.getAttribute('header-value') === headerValue) { const currentGroup = e.closest(`${ddsPrefix}-filter-group-item`); currentGroup?.setAttribute('open', ''); @@ -184,20 +192,19 @@ class DDSFilterPanelComposite extends HostListenerMixin( } ); - // toggle value in list + // Toggle value in list. if (!this._selectedValues.includes(headerValue)) { - this._selectedValues.push(headerValue); + this._selectedValues = [...this._selectedValues, headerValue]; } else { this._selectedValues = this._selectedValues.filter( (e) => e !== headerValue ); } - this.renderStatus(); }; @HostListener('document:eventHeadingChange') protected _handleHeadingChange = () => { - this.renderStatus(); + this._setHeadingFromSlot(this._headingSlot); }; /** @@ -215,7 +222,7 @@ class DDSFilterPanelComposite extends HostListenerMixin( protected _handleClearSelection = () => { this._selectedValues = []; - // handles clear when clearing from the static filter panel modal + // Handles clear when clearing from the static filter panel modal. this._contents.forEach((group) => { group .querySelectorAll(`${ddsPrefix}-filter-panel-checkbox`) @@ -254,10 +261,11 @@ class DDSFilterPanelComposite extends HostListenerMixin( e.removeAttribute('selected'); e.removeAttribute('is-open'); }); - - this.renderStatus(); }; + @query('slot[name=heading]') + _headingSlot; + /** * `true` to open the locale modal. */ @@ -280,10 +288,10 @@ class DDSFilterPanelComposite extends HostListenerMixin( _contents: any[] = []; /** - * sets the array for the filter button title + * Sets the heading text, to be composed with the current count. */ @property() - _title: any[] = []; + _heading: string = ''; /** * sets the selected values into an array @@ -291,20 +299,14 @@ class DDSFilterPanelComposite extends HostListenerMixin( @property() _selectedValues: string[] = []; - /** - * sets the filter button title - */ - @property() - _filterButtonTitle: string = ''; - /** * stores which filter groups have revealed filters */ @property() _filterGroupsAllRevealed: { id: string; value: boolean }[] = []; - @property() - _isMobile: boolean = window.innerWidth < breakpoint; + @state() + _isMobile = this.carbonBreakpoints.lg.matches; /** * An element to set focus to on render. @@ -312,11 +314,9 @@ class DDSFilterPanelComposite extends HostListenerMixin( @state() _focusElement: string | null = null; - @HostListener('window:resize') - // @ts-ignore: The decorator refers to this method but TS thinks this method is not referred to - private _handleWindowResize = (): void => { - this._isMobile = window.innerWidth < breakpoint; - }; + protected mediaQueryCallbackMaxLG() { + this._isMobile = this.carbonBreakpoints.lg.matches; + } protected async _querySelectorMobile(id: string): Promise { return this.querySelector(id); @@ -348,16 +348,16 @@ class DDSFilterPanelComposite extends HostListenerMixin( * @param event The event. */ protected _handleSlotChange({ target }: Event) { - const contents = (this._contents = (target as HTMLSlotElement) + this._contents = (target as HTMLSlotElement) .assignedNodes() .filter( (node) => node.nodeType !== Node.TEXT_NODE || node!.textContent!.trim() - )); + ); // Calculate initial this._selectedValues. Look at the first node, which is // expected to be . - if (contents[0] instanceof Element) { + if (this._contents[0] instanceof Element) { const items = Array.from( - contents[0].querySelectorAll( + this._contents[0].querySelectorAll( `${ddsPrefix}-filter-panel-checkbox[checked], ${ddsPrefix}-filter-panel-input-select[selected], ${ddsPrefix}-filter-panel-input-select-item[selected]` @@ -365,72 +365,97 @@ class DDSFilterPanelComposite extends HostListenerMixin( ); this._selectedValues = items .map((item) => { - return item.getAttribute('value') ?? ''; + return ( + item.getAttribute('value') ?? + item.getAttribute('header-value') ?? + '' + ); }) .filter((item) => !!item); } } - protected renderStatus() { - this._filterButtonTitle = ` - ${this._title[0].innerText}${ - this._selectedValues.length > 0 ? ` (${this._selectedValues.length})` : '' - } - `; + protected _getComposedHeadingFilterCount() { + const filterCount = + this._selectedValues.length > 0 + ? ` (${this._selectedValues.length})` + : ''; + return `${this._heading}${filterCount}`; } /** - * Handles `slotchange` event. + * Handles `slotchange` event for the heading slot. * * @param event The event. */ - protected _handleTitleSlotChange({ target }: Event) { - this._title = (target as HTMLSlotElement) + protected _handleHeadingSlotChange({ target }: Event) { + this._setHeadingFromSlot(target as HTMLSlotElement); + } + + protected _setHeadingFromSlot(slot: HTMLSlotElement) { + // Clean slate. + this._heading = ''; + + // Work through candidate headings, ultimately we're only interested in the + // first non-empty node. + const candidateHeadings = slot .assignedNodes() .filter( (node) => node.nodeType !== Node.TEXT_NODE || node!.textContent!.trim() ); - this._filterButtonTitle = this._title[0].innerText; + + // If we found something, lets retain it's textContent as the heading. + if (candidateHeadings.length > 0) { + this._heading = candidateHeadings[0].textContent + ? candidateHeadings[0].textContent.trim() + : ''; + } } /** * Renders original content into the modal and listens for changes to this - * content to then be stored in `this._title` and `this._content`. + * content to then be stored in `this._heading` and `this._contents`. */ protected _renderModal = (): TemplateResult => html` - `; + protected _renderMobile = (): TemplateResult => html` + + ${this._renderModal()} + `; + /** * Renders copies of slotted elements into the desktop presentation. */ protected _renderDesktop = (): TemplateResult => html` - ${this._title.map((e) => { - return html` ${unsafeHTML((e as HTMLElement).outerHTML)} `; - })} - ${this._contents.map((e) => { - return html` ${unsafeHTML((e as HTMLElement).outerHTML)} `; - })} + `; render() { + // Note that the contents, intended to be + // are never shown as is. The text contents + // are composed, using this._getComposedHeadingFilterCount(), together with + // the current filter count, and passed as an attribute to + // and . return html` - - ${this._renderModal()} ${this._renderDesktop()} + + ${this._isMobile ? this._renderMobile() : this._renderDesktop()} `; } diff --git a/packages/web-components/src/components/filter-panel/filter-panel-input-select-item.ts b/packages/web-components/src/components/filter-panel/filter-panel-input-select-item.ts index 83f745eb1e4..4c29ceb73a9 100644 --- a/packages/web-components/src/components/filter-panel/filter-panel-input-select-item.ts +++ b/packages/web-components/src/components/filter-panel/filter-panel-input-select-item.ts @@ -40,6 +40,12 @@ class DDSFilterPanelInputSelectItem extends FocusMixin( @property({ type: Boolean, reflect: true }) selected = false; + /** + * Property for setting the value to a string. + */ + @property() + value: string = ''; + /** * Handles `slotchange` event. * diff --git a/packages/web-components/tests/e2e-storybook/cypress/integration/filter-panel/filter-panel.e2e.js b/packages/web-components/tests/e2e-storybook/cypress/integration/filter-panel/filter-panel.e2e.js index e4d77042082..b79aec02bf7 100644 --- a/packages/web-components/tests/e2e-storybook/cypress/integration/filter-panel/filter-panel.e2e.js +++ b/packages/web-components/tests/e2e-storybook/cypress/integration/filter-panel/filter-panel.e2e.js @@ -54,7 +54,6 @@ describe('dds-filter-panel | (desktop)', () => { // Check box on desktop cy.visit(_path) .get(_selector) - .shadow() .find('dds-filter-group-item') .first() .click() @@ -86,7 +85,6 @@ describe('dds-filter-panel | (desktop)', () => { // Check box on desktop cy.visit(_path) .get(_selector) - .shadow() .find('dds-filter-group-item') .eq(1) .click() @@ -117,7 +115,6 @@ describe('dds-filter-panel | (desktop)', () => { `${_path}&knob-Filter%20cutoff=1&knob-Max%20filters=1&knob-Number%20of%20selected%20items=0` ) .get(_selector) - .shadow() .find('dds-filter-group-item') .first() .as('filterGroupItem') @@ -132,7 +129,6 @@ describe('dds-filter-panel | (desktop)', () => { `${_path}&knob-Max%20filters=${filterCount}&knob-Number%20of%20selected%20items=0` ) .get(_selector) - .shadow() .find('dds-filter-group-item') .first() .click() @@ -145,7 +141,6 @@ describe('dds-filter-panel | (desktop)', () => { cy.visit(`${_path}&knob-View%20all%20text=${customText}`) .get(_selector) - .shadow() .find('dds-filter-group-item') .first() .click() @@ -157,7 +152,6 @@ describe('dds-filter-panel | (desktop)', () => { it('should re-hide excess elements when filter groups are closed and reopened', () => { cy.visit(_path) .get(_selector) - .shadow() .find('dds-filter-group-item') .first() .as('filterGroupItem') @@ -183,7 +177,6 @@ describe('dds-filter-panel | (desktop)', () => { it('should not re-hide elements when an element that would be hidden has been selected', () => { cy.visit(_path) .get(_selector) - .shadow() .find('dds-filter-group-item') .first() .as('filterGroupItem') @@ -241,7 +234,6 @@ describe('dds-filter-panel | (mobile)', () => { // Switch to desktop and verify box is checked cy.viewport(..._viewportWidths['desktop']) .get(_selector) - .shadow() .find('dds-filter-group-item') .first() .find('dds-filter-panel-checkbox') @@ -270,7 +262,6 @@ describe('dds-filter-panel | (mobile)', () => { // Switch to desktop and verify box is checked cy.viewport(..._viewportWidths['desktop']) .get(_selector) - .shadow() .find('dds-filter-group-item') .eq(1) .find('dds-filter-panel-input-select') diff --git a/packages/web-components/tests/snapshots/dds-filter-panel-composite.md b/packages/web-components/tests/snapshots/dds-filter-panel-composite.md index 2d27acb8cb7..081e63c370e 100644 --- a/packages/web-components/tests/snapshots/dds-filter-panel-composite.md +++ b/packages/web-components/tests/snapshots/dds-filter-panel-composite.md @@ -5,6 +5,8 @@ #### `should render with minimum attributes` ``` + +