From 86e3290f9aba3ef013a4f50408eef61d27e5368c Mon Sep 17 00:00:00 2001 From: Matthew Oliveira Date: Thu, 10 Oct 2024 13:29:47 -0400 Subject: [PATCH] [card-group]: Refactor to use subgrid in favor of sameHeight (#12063) ### Related Ticket(s) Closes #11998 ### Description This is a refactoring of card group to make use of CSS subgrid instead of the Javascript `sameHeight` logic on all the pieces of the card group. ### Changelog **Changed** - Use CSS subgrid for layout out the card elements within a card group --- .../components/card-group/_card-group.scss | 64 ++++++- .../styles/scss/components/card/_card.scss | 14 +- .../components/card-group/card-group-item.ts | 12 +- .../src/components/card-group/card-group.ts | 160 +----------------- .../src/components/card/card.ts | 61 ++++--- 5 files changed, 125 insertions(+), 186 deletions(-) diff --git a/packages/styles/scss/components/card-group/_card-group.scss b/packages/styles/scss/components/card-group/_card-group.scss index 1d6ddf92ef2..07b5e161088 100644 --- a/packages/styles/scss/components/card-group/_card-group.scss +++ b/packages/styles/scss/components/card-group/_card-group.scss @@ -56,10 +56,42 @@ } :host(#{$c4d-prefix}-card-group-item) { + display: contents; + .#{$prefix}--card { + display: grid; border: 0; + grid-row: span 10; + grid-template-rows: subgrid; + margin-block-end: $spacing-07; outline: 1px solid $border-tile-01; outline-offset: 0; + + .#{$prefix}--card__wrapper { + display: grid; + justify-content: revert; + grid-row: span 10; + grid-template-rows: subgrid; + + &::before, + &::after { + content: revert; + } + } + + .#{$prefix}--card__content { + display: grid; + grid-row: span 10; + grid-template-rows: subgrid; + row-gap: 0; + } + + .#{$prefix}--card__copy { + display: grid; + grid-row: span 2; + grid-template-rows: subgrid; + row-gap: 0; + } } .#{$prefix}--image { @@ -84,6 +116,30 @@ outline-offset: -1px; } } + + &[grid-mode='narrow'] { + .#{$prefix}--card { + margin-block-end: $spacing-05; + } + } + + &[grid-mode='condensed'] { + .#{$prefix}--card { + margin-block-end: 0; + } + } + + ::slotted(#{$c4d-prefix}-card-footer) { + display: revert; + margin-block-start: revert; + } + + // Selects elements that contain tags which are slotted into the default + // slot. Note that this does not work in Chrome at time of writing. + // @see https://developer.chrome.com/blog/has-m105/#performance_and_limitations + ::slotted(:not([slot]):has(#{$prefix}-tag, #{$c4d-prefix}-tag)) { + grid-row: -1; + } } :host(#{$c4d-prefix}-card-group-item[href='']) { @@ -110,24 +166,24 @@ :host(#{$c4d-prefix}-card-group)[grid-mode='narrow'] { @include breakpoint(sm) { - gap: $spacing-03; + gap: 0 $spacing-03; padding-block-start: $spacing-03; } @include breakpoint(md) { - gap: $spacing-05; + gap: 0 $spacing-05; padding-block-start: $spacing-05; } } :host(#{$c4d-prefix}-card-group)[grid-mode='default'] { @include breakpoint(sm) { - gap: $spacing-07; + gap: 0 $spacing-07; padding-block-start: $spacing-03; } @include breakpoint(md) { - gap: $spacing-07; + gap: 0 $spacing-07; padding-block-start: $spacing-05; } } diff --git a/packages/styles/scss/components/card/_card.scss b/packages/styles/scss/components/card/_card.scss index 0d9c2857e2b..80a200f0ea5 100644 --- a/packages/styles/scss/components/card/_card.scss +++ b/packages/styles/scss/components/card/_card.scss @@ -141,9 +141,13 @@ .#{$prefix}--card__heading, .#{$prefix}--card__copy { - @include content-width; - color: $text-primary; + max-inline-size: to-rem(640px); + padding-inline-end: 10%; + + @include breakpoint(md) { + padding-inline-end: $spacing-07; + } } .#{$prefix}--card__copy:not([hidden]) { @@ -185,6 +189,10 @@ } } + .#{$prefix}--card__pictogram-wrapper { + display: flex; + } + &[pictogram] .#{$prefix}--card { ::slotted(#{$c4d-prefix}-card-heading) { padding-block-start: 0; @@ -417,6 +425,7 @@ :host(#{$c4d-prefix}-card-eyebrow), .#{$prefix}--card__eyebrow { + display: block; @include content-width; @include type-style('label-02'); @@ -597,6 +606,7 @@ :host(#{$c4d-prefix}-card-heading), :host(#{$c4d-prefix}-card-link-heading) { + display: block; @include content-width; color: $text-primary; diff --git a/packages/web-components/src/components/card-group/card-group-item.ts b/packages/web-components/src/components/card-group/card-group-item.ts index 1304a6ca86d..bf557430406 100644 --- a/packages/web-components/src/components/card-group/card-group-item.ts +++ b/packages/web-components/src/components/card-group/card-group-item.ts @@ -12,6 +12,7 @@ import settings from '@carbon/ibmdotcom-utilities/es/utilities/settings/settings import { carbonElement as customElement } from '@carbon/web-components/es/globals/decorators/carbon-element.js'; import C4DCard from '../card/card'; import styles from './card-group.scss'; +import { GRID_MODE } from './defs'; const { stablePrefix: c4dPrefix } = settings; @@ -23,16 +24,17 @@ const { stablePrefix: c4dPrefix } = settings; @customElement(`${c4dPrefix}-card-group-item`) class C4DCardGroupItem extends C4DCard { /** - * `true` if the card group is using border. + * `true` if the card group item is empty. */ @property({ type: Boolean, reflect: true }) - border = false; + empty = false; /** - * `true` if the card group item is empty. + * The inherited grid mode the card group item lives within. + * Condensed (1px) | Narrow (16px) | Default(32px). */ - @property({ type: Boolean, reflect: true }) - empty = false; + @property({ attribute: 'grid-mode', reflect: true }) + gridMode = GRID_MODE.DEFAULT; static get stableSelector() { return `${c4dPrefix}--card-group-item`; diff --git a/packages/web-components/src/components/card-group/card-group.ts b/packages/web-components/src/components/card-group/card-group.ts index c111e335019..996b22cfde3 100644 --- a/packages/web-components/src/components/card-group/card-group.ts +++ b/packages/web-components/src/components/card-group/card-group.ts @@ -10,7 +10,6 @@ import { LitElement, html } from 'lit'; import { property, state } from 'lit/decorators.js'; import settings from '@carbon/ibmdotcom-utilities/es/utilities/settings/settings.js'; -import sameHeight from '@carbon/ibmdotcom-utilities/es/utilities/sameHeight/sameHeight.js'; import { GRID_MODE } from './defs'; import styles from './card-group.scss'; import StableSelectorMixin from '../../globals/mixins/stable-selector'; @@ -20,9 +19,6 @@ export { GRID_MODE }; const { stablePrefix: c4dPrefix } = settings; -// tag constants used for same height calculations -const headingBottomMargin = 64; - /** * Card Group. * @@ -35,55 +31,6 @@ class C4DCardGroup extends StableSelectorMixin(LitElement) { */ private _childItems: any[] = []; - /** - * Array to hold the card-heading elements within child items. - */ - private _childItemHeadings: any[] = []; - - /** - * Array to hold the card-eyebrow elements within child items. - */ - private _childItemEyebrows: any[] = []; - - /** - * Array to hold the tag-group elements within child items. - */ - private _childItemTagGroup: any[] = []; - - /** - * Array to hold the paragraph elements within child items. - */ - private _childItemParagraphs: any[] = []; - - /** - * Array to hold the card-cta-footer elements within child items. - */ - private _childItemFooters: any[] = []; - - /** - * The observer for the resize of the viewport. - */ - private _observerResizeRoot: any | null = null; // TODO: Wait for `.d.ts` update to support `ResizeObserver` - - /** - * Cleans-up and creats the resize observer for the scrolling container. - * - * @param [options] The options. - * @param [options.create] `true` to create the new resize observer. - */ - private _cleanAndCreateObserverResize({ create }: { create?: boolean } = {}) { - if (this._observerResizeRoot) { - this._observerResizeRoot.disconnect(); - this._observerResizeRoot = null; - } - if (create) { - // TODO: Wait for `.d.ts` update to support `ResizeObserver` - // @ts-ignore - this._observerResizeRoot = new ResizeObserver(this._resizeHandler); - this._observerResizeRoot.observe(this.ownerDocument!.documentElement); - } - } - /** * Handler for @slotchange, set the height of all headings to the tallest height. * @@ -98,37 +45,12 @@ class C4DCardGroup extends StableSelectorMixin(LitElement) { ) ); - // retrieve item heading, eyebrows, and footers to set same height + // Retrieve item heading, eyebrows, and footers to set same height. if (this._childItems) { this._childItems.forEach((e) => { if (!e.hasAttribute('href') && this.gridMode === GRID_MODE.CONDENSED) { this.gridMode = GRID_MODE.DEFAULT; } - this._childItemEyebrows.push( - (e as HTMLElement).querySelector( - (this.constructor as typeof C4DCardGroup).selectorItemEyebrow - ) - ); - this._childItemParagraphs.push( - (e as HTMLElement).querySelector( - (this.constructor as typeof C4DCardGroup).selectorItemParagraph - ) - ); - this._childItemTagGroup.push( - (e as HTMLElement).querySelector( - (this.constructor as typeof C4DCardGroup).selectorItemTagGroup - ) - ); - this._childItemHeadings.push( - (e as HTMLElement).querySelector( - (this.constructor as typeof C4DCardGroup).selectorItemHeading - ) - ); - this._childItemFooters.push( - (e as HTMLElement).querySelector( - (this.constructor as typeof C4DCardGroup).selectorItemFooter - ) - ); }); const { customPropertyCardsPerRow } = this @@ -137,70 +59,9 @@ class C4DCardGroup extends StableSelectorMixin(LitElement) { customPropertyCardsPerRow, String(this.cardsPerRow) ); - - if (this.gridMode !== GRID_MODE.NARROW) { - this._resizeHandler(); - } } } - /** - * The observer for the resize of the viewport, calls sameHeight utility function - */ - private _resizeHandler = () => { - if (!this.pictograms) { - window.requestAnimationFrame(() => { - this._setSameHeight(); - }); - } - }; - - private _setSameHeight = () => { - // check if items are not null before using sameHeight - - sameHeight( - this._childItemEyebrows.filter((item) => item !== null), - 'md' - ); - sameHeight( - this._childItemHeadings.filter((item) => item !== null), - 'md' - ); - sameHeight( - this._childItemParagraphs.filter((item) => item !== null), - 'md' - ); - sameHeight( - this._childItemFooters.filter((item) => item !== null), - 'md' - ); - - let tagGroupHeight = 0; - - // get tallest height of tag groups - this._childItemTagGroup.forEach((item) => { - if (item) { - const groupHeight = (item as HTMLElement).offsetHeight; - if (groupHeight > tagGroupHeight) { - tagGroupHeight = groupHeight; - } - } - }); - - this._childItemHeadings.forEach((e) => { - // add tag group height to heading to the cards lacking tag group - if ( - e && - !e.parentElement.hasAttribute('link') && - !e.nextElementSibling?.matches( - (this.constructor as typeof C4DCardGroup).selectorItemTagGroup - ) - ) { - e.style.marginBottom = `${tagGroupHeight + headingBottomMargin}px`; - } - }); - }; - /** * The number of columns per row. Min 2, max 4, default 3. Applies to >=`lg` breakpoint only. */ @@ -243,16 +104,6 @@ class C4DCardGroup extends StableSelectorMixin(LitElement) { @property({ type: Boolean, reflect: true }) pictograms = false; - connectedCallback() { - super.connectedCallback(); - this._cleanAndCreateObserverResize({ create: true }); - } - - disconnectedCallback() { - this._cleanAndCreateObserverResize(); - super.disconnectedCallback(); - } - firstUpdated() { super.connectedCallback(); @@ -263,11 +114,14 @@ class C4DCardGroup extends StableSelectorMixin(LitElement) { ) { this.setAttribute('with-card-in-card', ''); } - this._cleanAndCreateObserverResize({ create: true }); } - updated() { - this._resizeHandler(); + updated(changedProperties) { + super.updated(changedProperties); + + if (changedProperties.has('gridMode')) { + this._childItems.forEach((e) => (e.gridMode = this.gridMode)); + } } render() { diff --git a/packages/web-components/src/components/card/card.ts b/packages/web-components/src/components/card/card.ts index b1e0094df4c..f754de426bf 100644 --- a/packages/web-components/src/components/card/card.ts +++ b/packages/web-components/src/components/card/card.ts @@ -102,9 +102,11 @@ class C4DCard extends CTAMixin(StableSelectorMixin(CDSLink)) { formatVideoCaption: formatVideoCaptionInEffect, } = this; if (ctaType !== CTA_TYPE.VIDEO) { - return html``; + return html`
+ +
`; } - const formatedVideoName = formatVideoCaptionInEffect({ name: videoName }); + const formattedVideoName = formatVideoCaptionInEffect({ name: videoName }); this.dispatchEvent( new CustomEvent( @@ -116,9 +118,11 @@ class C4DCard extends CTAMixin(StableSelectorMixin(CDSLink)) { ) ); return html` - - ${formatedVideoName} - +
+ + ${formattedVideoName} + +
`; } @@ -155,7 +159,11 @@ class C4DCard extends CTAMixin(StableSelectorMixin(CDSLink)) { `; return html` - ${image} +
+ + ${image} + +
`; } @@ -171,12 +179,29 @@ class C4DCard extends CTAMixin(StableSelectorMixin(CDSLink)) { `; } + protected _renderPictogramSlot(placement: PICTOGRAM_PLACEMENT) { + const { _handleSlotChange: handleSlotChange } = this; + return html` +
+ +
+ `; + } + + protected _renderEyebrowSlot() { + return html`
+ +
`; + } + /** * @returns The inner content. */ protected _renderInner() { - const { _handleSlotChange: handleSlotChange, _hasPictogram: hasPictogram } = - this; + const { _hasPictogram: hasPictogram } = this; return html` ${this._renderImage()}
- ${hasPictogram ? '' : html` `} + ${this._renderEyebrowSlot()} ${this.pictogramPlacement === PICTOGRAM_PLACEMENT.TOP - ? html` - - ` + ? this._renderPictogramSlot(PICTOGRAM_PLACEMENT.TOP) : ''} ${this.pictogramPlacement !== PICTOGRAM_PLACEMENT.TOP || !hasPictogram ? this._renderHeading() @@ -202,12 +222,7 @@ class C4DCard extends CTAMixin(StableSelectorMixin(CDSLink)) { ? this._renderCopy() : ''} ${this.pictogramPlacement === PICTOGRAM_PLACEMENT.BOTTOM - ? html` - - ` + ? this._renderPictogramSlot(PICTOGRAM_PLACEMENT.BOTTOM) : ''} ${hasPictogram && this.pictogramPlacement === PICTOGRAM_PLACEMENT.TOP ? this._renderHeading() @@ -215,7 +230,9 @@ class C4DCard extends CTAMixin(StableSelectorMixin(CDSLink)) { ${hasPictogram && this.pictogramPlacement === PICTOGRAM_PLACEMENT.TOP ? this._renderCopy() : ''} - +
`;