Skip to content

Card - Add tag argument (HDS-4688) #2787

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Apr 2, 2025
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/wise-stingrays-heal.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@hashicorp/design-system-components": minor
---

`Card` - Add `tag` argument to choose between using a `div` tag (the default) or an `li` tag
14 changes: 11 additions & 3 deletions packages/components/src/components/hds/card/container.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,14 @@
Copyright (c) HashiCorp, Inc.
SPDX-License-Identifier: MPL-2.0
}}
<div class={{this.classNames}} ...attributes>
{{yield}}
</div>
{{!
Dynamically generating an HTML tag in Ember creates a dynamic component class (with the corresponding tagName), while rendering
a plain HTML element requires less computing cycles for Ember (you will notice it doesn't add the `ember-view` class to it).
}}
{{#if (eq this.componentTag "div")}}
<div class={{this.classNames}} ...attributes>{{yield}}</div>
{{else}}
{{#let (element this.componentTag) as |Tag|}}
<Tag class={{this.classNames}} ...attributes>{{yield}}</Tag>
{{/let}}
{{/if}}
69 changes: 25 additions & 44 deletions packages/components/src/components/hds/card/container.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,27 +5,33 @@

import Component from '@glimmer/component';
import { assert } from '@ember/debug';

import {
HdsCardBackgroundValues,
HdsCardLevelValues,
HdsCardOverflowValues,
HdsCardTagValues,
} from './types.ts';

import type {
HdsCardBackground,
HdsCardLevel,
HdsCardOverflow,
HdsCardTag,
} from './types.ts';

export const DEFAULT_LEVEL = HdsCardLevelValues.Base;
export const DEFAULT_BACKGROUND = HdsCardBackgroundValues.NeutralPrimary;
export const DEFAULT_OVERFLOW = HdsCardOverflowValues.Visible;
export const DEFAULT_TAG = HdsCardTagValues.Div;
export const AVAILABLE_LEVELS: string[] = Object.values(HdsCardLevelValues);
export const AVAILABLE_BACKGROUNDS: string[] = Object.values(
HdsCardBackgroundValues
);
export const AVAILABLE_OVERFLOWS: string[] = Object.values(
HdsCardOverflowValues
);
export const AVAILABLE_TAGS: string[] = Object.values(HdsCardTagValues);

export interface HdsCardContainerSignature {
Args: {
Expand All @@ -35,22 +41,16 @@ export interface HdsCardContainerSignature {
background?: HdsCardBackground;
hasBorder?: boolean;
overflow?: HdsCardOverflow;
tag?: HdsCardTag;
};
Blocks: {
default: [];
};
Element: HTMLDivElement;
Element: HTMLElement;
}

export default class HdsCardContainer extends Component<HdsCardContainerSignature> {
/**
* Sets the "elevation" level for the component
* Accepted values: base, mid, high
*
* @param level
* @type {HdsCardLevel}
* @default 'base'
*/
// Sets the "elevation" level for the component
get level(): HdsCardLevel {
const { level = DEFAULT_LEVEL } = this.args;

Expand All @@ -64,13 +64,7 @@ export default class HdsCardContainer extends Component<HdsCardContainerSignatur
return level;
}

/**
* Sets the "elevation" level for the component on ":hover" state
* Accepted values: base, mid, high
*
* @param levelHover
* @type {HdsCardLevel}
*/
// Sets the "elevation" level for the component on ":hover" state
get levelHover(): HdsCardLevel | undefined {
const { levelHover } = this.args;

Expand All @@ -86,13 +80,7 @@ export default class HdsCardContainer extends Component<HdsCardContainerSignatur
return levelHover;
}

/**
* Sets the "elevation" level for the component on ":active" state
* Accepted values: base, mid, high
*
* @param levelActive
* @type {HdsCardLevel}
*/
// Sets the "elevation" level for the component on ":active" state
get levelActive(): HdsCardLevel | undefined {
const { levelActive } = this.args;

Expand All @@ -108,14 +96,7 @@ export default class HdsCardContainer extends Component<HdsCardContainerSignatur
return levelActive;
}

/**
* Sets the background for the component
* Accepted values: neutral-primary, neutral-secondary
*
* @param background
* @type {HdsCardBackground}
* @default 'base'
*/
// Sets the background for the component
get background(): HdsCardBackground {
const { background = DEFAULT_BACKGROUND } = this.args;

Expand All @@ -129,14 +110,7 @@ export default class HdsCardContainer extends Component<HdsCardContainerSignatur
return background;
}

/**
* Sets the level for the card
* Accepted values: visible, hidden
*
* @param overflow
* @type {HdsCardOverflow}
* @default 'visible'
*/
// Sets the level for the card
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@KristinLBradley this comment was not correct (it's actually setting the overflow). Maybe we could remove the comments that are redundant, and leave only the ones that are not obvoius?

get overflow(): HdsCardOverflow {
const { overflow = DEFAULT_OVERFLOW } = this.args;

Expand All @@ -150,11 +124,18 @@ export default class HdsCardContainer extends Component<HdsCardContainerSignatur
return overflow;
}

/**
* Get the class names to apply to the component.
* @method Card#classNames
* @return {string} The "class" attribute to apply to the component.
*/
get componentTag(): HdsCardTag {
const { tag = DEFAULT_TAG } = this.args;

assert(
`@tag for "Hds::Card::Container" must be one of the following: ${AVAILABLE_TAGS.join(', ')}; received: ${tag}`,
AVAILABLE_TAGS.includes(tag)
);

return tag;
}

// Get the class names to apply to the component.
get classNames(): string {
const classes = ['hds-card__container'];

Expand Down
7 changes: 7 additions & 0 deletions packages/components/src/components/hds/card/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,10 @@ export enum HdsCardOverflowValues {
export type HdsCardOverflow =
| HdsCardOverflowValues.Hidden
| HdsCardOverflowValues.Visible;

export enum HdsCardTagValues {
Div = 'div',
Li = 'li',
}

export type HdsCardTag = `${HdsCardTagValues}`;
6 changes: 6 additions & 0 deletions showcase/app/styles/showcase-pages/card.scss
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,10 @@ body.components-card {
border-radius: 8px;
}
}

.shw-component-card-list {
margin: 0;
padding: 0;
list-style-type: none;
}
}
22 changes: 22 additions & 0 deletions showcase/app/templates/components/card.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@
{{/each}}
</Shw::Grid>
</div>

<Shw::Text::H2>Background</Shw::Text::H2>

<div class="shw-component-card-wrapper">
Expand All @@ -71,6 +72,7 @@
</div>

<Shw::Text::H2>Overflow</Shw::Text::H2>

<div class="shw-component-card-wrapper">
<Shw::Grid @columns={{2}} @gap="2rem" {{style width="fit-content"}} as |SG|>
<SG.Item>
Expand All @@ -92,4 +94,24 @@
</Shw::Grid>
</div>

<Shw::Text::H2>Tag</Shw::Text::H2>

<div class="shw-component-card-wrapper">
<Shw::Grid @columns={{2}} @gap="2rem" {{style width="fit-content"}} as |SG|>
<SG.Item @label="Card using default div tag">
<Hds::Card::Container @level="mid" @hasBorder={{true}}>
<Shw::Placeholder @text="div" @width="200" @height="200" @background="transparent" />
</Hds::Card::Container>
</SG.Item>

<SG.Item @label="Card using list item tag">
<ul class="shw-component-card-list">
<Hds::Card::Container @level="mid" @hasBorder={{true}} @tag="li">
<Shw::Placeholder @text="li" @width="200" @height="200" @background="transparent" />
</Hds::Card::Container>
</ul>
</SG.Item>
</Shw::Grid>
</div>

</section>
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,20 @@ module('Integration | Component | hds/card/container', function (hooks) {
.hasClass('hds-card__container--overflow-hidden');
});

// TAG

test(`it should render a div if no @tag prop is declared`, async function (assert) {
await render(hbs`<Hds::Card::Container id="test-card-container" />`);
assert.dom('#test-card-container').hasTagName('div');
});

test(`it should render an li vs. a div if specified in the @tag prop`, async function (assert) {
await render(
hbs`<Hds::Card::Container id="test-card-container" @tag="li" />`
);
assert.dom('#test-card-container').hasTagName('li');
});

// ASSERTIONS

test('it should throw an assertion if an incorrect value for @level is provided', async function (assert) {
Expand Down Expand Up @@ -117,4 +131,18 @@ module('Integration | Component | hds/card/container', function (hooks) {
throw new Error(errorMessage);
});
});

// If a tag other than div or li is passed, it should throw an assertion
test('it should throw an assertion if an incorrect value for @tag is provided', async function (assert) {
const errorMessage =
'@tag for "Hds::Card::Container" must be one of the following: div, li; received: section';
assert.expect(2);
setupOnerror(function (error) {
assert.strictEqual(error.message, `Assertion Failed: ${errorMessage}`);
});
await render(hbs`<Hds::Card::Container @tag="section" />`);
assert.throws(function () {
throw new Error(errorMessage);
});
});
});