diff --git a/.changeset/brown-wolves-admire.md b/.changeset/brown-wolves-admire.md new file mode 100644 index 00000000000..cbf448dcf76 --- /dev/null +++ b/.changeset/brown-wolves-admire.md @@ -0,0 +1,7 @@ +--- +"@hashicorp/design-system-components": minor +--- + +`Tag` - Truncate any text that is longer than about 20 characters, and add a tooltip with the full text when truncation occurs + +`Tag` - Added `@tooltipPlacement` argument diff --git a/packages/components/src/components/hds/tag/index.hbs b/packages/components/src/components/hds/tag/index.hbs index 40f56af3ef9..679a5cabfdc 100644 --- a/packages/components/src/components/hds/tag/index.hbs +++ b/packages/components/src/components/hds/tag/index.hbs @@ -2,29 +2,68 @@ Copyright (c) HashiCorp, Inc. SPDX-License-Identifier: MPL-2.0 }} - + {{#if this.onDismiss}} {{/if}} {{#if (or @href @route)}} - - {{this.text}} - + {{#if this._isTextOverflow}} + +
+ {{this.text}} +
+
+ {{else}} + +
+ {{this.text}} +
+
+ {{/if}} {{else}} - - {{this.text}} - + {{#if this._isTextOverflow}} + +
+ {{this.text}} +
+
+ {{else}} + +
+ {{this.text}} +
+
+ {{/if}} {{/if}}
\ No newline at end of file diff --git a/packages/components/src/components/hds/tag/index.ts b/packages/components/src/components/hds/tag/index.ts index 0670d6a31a0..4fe0acc1353 100644 --- a/packages/components/src/components/hds/tag/index.ts +++ b/packages/components/src/components/hds/tag/index.ts @@ -4,20 +4,29 @@ */ import Component from '@glimmer/component'; +import { tracked } from '@glimmer/tracking'; import { assert } from '@ember/debug'; +import { modifier } from 'ember-modifier'; import { HdsTagColorValues } from './types.ts'; import type { HdsTagColors } from './types.ts'; +import { HdsTagTooltipPlacementValues } from './types.ts'; +import type { HdsTagTooltipPlacements } from './types.ts'; import type { HdsInteractiveSignature } from '../interactive/'; export const COLORS: string[] = Object.values(HdsTagColorValues); export const DEFAULT_COLOR = HdsTagColorValues.Primary; +export const TOOLTIP_PLACEMENTS: string[] = Object.values( + HdsTagTooltipPlacementValues +); +export const DEFAULT_TOOLTIP_PLACEMENT = HdsTagTooltipPlacementValues.Top; export interface HdsTagSignature { Args: HdsInteractiveSignature['Args'] & { color?: HdsTagColors; text: string; ariaLabel?: string; + tooltipPlacement: HdsTagTooltipPlacements; // eslint-disable-next-line @typescript-eslint/no-explicit-any onDismiss?: (event: MouseEvent, ...args: any[]) => void; }; @@ -25,6 +34,43 @@ export interface HdsTagSignature { } export default class HdsTag extends Component { + @tracked private _isTextOverflow!: boolean; + private _observer!: ResizeObserver; + + private _setUpObserver = modifier((element: HTMLElement) => { + // Used to detect when text is clipped to one line, and tooltip should be added + this._observer = new ResizeObserver((entries) => { + entries.forEach((entry) => { + this._isTextOverflow = this._isOverflow( + entry.target.querySelector('.hds-tag__text-container')! + ); + }); + }); + this._observer.observe(element); + + return () => { + this._observer.disconnect(); + }; + }); + + /** + * @param tooltioPlacement + * @type {string} + * @default top + * @description The placement property of the tooltip attached to the tag text. + */ + get tooltipPlacement(): HdsTagTooltipPlacements { + const { tooltipPlacement = DEFAULT_TOOLTIP_PLACEMENT } = this.args; + + assert( + '@tooltipPlacement for "Hds::Tag" must have a valid value', + tooltipPlacement == undefined || + TOOLTIP_PLACEMENTS.includes(tooltipPlacement) + ); + + return tooltipPlacement; + } + /** * @param onDismiss * @type {function} @@ -104,4 +150,8 @@ export default class HdsTag extends Component { return classes.join(' '); } + + private _isOverflow(el: Element): boolean { + return el.scrollHeight > el.clientHeight; + } } diff --git a/packages/components/src/components/hds/tag/types.ts b/packages/components/src/components/hds/tag/types.ts index 5749b71ca5c..446d96f852a 100644 --- a/packages/components/src/components/hds/tag/types.ts +++ b/packages/components/src/components/hds/tag/types.ts @@ -8,3 +8,20 @@ export enum HdsTagColorValues { Secondary = 'secondary', } export type HdsTagColors = `${HdsTagColorValues}`; + +export enum HdsTagTooltipPlacementValues { + Top = 'top', + TopStart = 'top-start', + TopEnd = 'top-end', + Right = 'right', + RightStart = 'right-start', + RightEnd = 'right-end', + Bottom = 'bottom', + BottomStart = 'bottom-start', + BottomEnd = 'bottom-end', + Left = 'left', + LeftStart = 'left-start', + LeftEnd = 'left-end', +} + +export type HdsTagTooltipPlacements = `${HdsTagTooltipPlacementValues}`; diff --git a/packages/components/src/styles/components/tag.scss b/packages/components/src/styles/components/tag.scss index c008848330a..c4923587091 100644 --- a/packages/components/src/styles/components/tag.scss +++ b/packages/components/src/styles/components/tag.scss @@ -15,6 +15,8 @@ $hds-tag-border-radius: 50px; .hds-tag { display: inline-flex; align-items: stretch; + width: fit-content; + max-width: 100%; line-height: 1rem; // 16px - override `body-100` vertical-align: middle; background-color: var(--token-color-surface-interactive); @@ -41,12 +43,23 @@ $hds-tag-border-radius: 50px; .hds-tag__text, .hds-tag__link { flex: 1 0 0; + max-width: 166px; // account for excess horizontal padding of text in non-dismissible variant padding: 3px 10px 5px 10px; border-radius: inherit; } +.hds-tag__text-container { + display: -webkit-box; + overflow: hidden; + word-break: break-all; + -webkit-box-orient: vertical; + -webkit-line-clamp: 1; + line-clamp: 1; +} + .hds-tag__dismiss ~ .hds-tag__text, .hds-tag__dismiss ~ .hds-tag__link { + max-width: 160px; padding: 3px 8px 5px 6px; border-top-left-radius: 0; border-bottom-left-radius: 0; @@ -76,6 +89,21 @@ $hds-tag-border-radius: 50px; } } +.hds-tooltip-button.hds-tag__text { + cursor: text; + user-select: text; + + &:focus, + &.mock-focus { + @include hds-focus-ring-basic(); + z-index: 1; // ensures focus is not obscured by adjacent elements + } + + &:focus-visible::before { + box-shadow: none; // override default tooltip button focus styles + } +} + // COLORS (FOR LINK) .hds-tag--color-primary { diff --git a/showcase/app/routes/components/tag.js b/showcase/app/routes/components/tag.js index da64b84ff55..9a625eb363a 100644 --- a/showcase/app/routes/components/tag.js +++ b/showcase/app/routes/components/tag.js @@ -5,11 +5,12 @@ import Route from '@ember/routing/route'; import { COLORS } from '@hashicorp/design-system-components/components/hds/tag'; +import { TOOLTIP_PLACEMENTS } from '@hashicorp/design-system-components/components/hds/tag'; export default class ComponentsTagRoute extends Route { model() { // these are used only for presentation purpose in the showcase const STATES = ['default', 'hover', 'active', 'focus']; - return { COLORS, STATES }; + return { COLORS, TOOLTIP_PLACEMENTS, STATES }; } } diff --git a/showcase/app/templates/components/tag.hbs b/showcase/app/templates/components/tag.hbs index 19441765a63..4e749203a05 100644 --- a/showcase/app/templates/components/tag.hbs +++ b/showcase/app/templates/components/tag.hbs @@ -33,12 +33,22 @@ - + - + + + + + + + @@ -119,4 +129,19 @@ {{/let}} + + + Tooltip Placements + + + {{#each @model.TOOLTIP_PLACEMENTS as |place|}} + + + + {{/each}} + + \ No newline at end of file diff --git a/showcase/tests/integration/components/hds/tag/index-test.js b/showcase/tests/integration/components/hds/tag/index-test.js index 31910e8c603..c34a5a6117f 100644 --- a/showcase/tests/integration/components/hds/tag/index-test.js +++ b/showcase/tests/integration/components/hds/tag/index-test.js @@ -5,7 +5,12 @@ import { module, test } from 'qunit'; import { setupRenderingTest } from 'ember-qunit'; -import { render, resetOnerror, setupOnerror } from '@ember/test-helpers'; +import { + render, + resetOnerror, + setupOnerror, + waitFor, +} from '@ember/test-helpers'; import { hbs } from 'ember-cli-htmlbars'; module('Integration | Component | hds/tag/index', function (hooks) { @@ -25,6 +30,7 @@ module('Integration | Component | hds/tag/index', function (hooks) { await render(hbs``); assert.dom('button.hds-tag__dismiss').doesNotExist(); }); + test('it should render the "dismiss" button if a callback function is passed to the @onDismiss argument', async function (assert) { this.set('NOOP', () => {}); await render(hbs``); @@ -44,6 +50,7 @@ module('Integration | Component | hds/tag/index', function (hooks) { .dom('button.hds-tag__dismiss') .hasAttribute('aria-label', 'Please dismiss My tag'); }); + // COLOR test('it should render the primary color as the default if no @color prop is declared when the text is a link', async function (assert) { @@ -52,12 +59,14 @@ module('Integration | Component | hds/tag/index', function (hooks) { ); assert.dom('#test-link-tag').hasClass('hds-tag--color-primary'); }); + test('it should render the correct CSS color class if the @color prop is declared when the text is a link', async function (assert) { await render( hbs`` ); assert.dom('#test-link-tag').hasClass('hds-tag--color-secondary'); }); + test('it should throw an assertion if an incorrect value for @color is provided when the text is a link', async function (assert) { const errorMessage = '@color for "Hds::Tag" must be one of the following: primary, secondary; received: foo'; @@ -70,6 +79,7 @@ module('Integration | Component | hds/tag/index', function (hooks) { throw new Error(errorMessage); }); }); + test('it should throw an assertion if @color is provided without @href or @route', async function (assert) { const errorMessage = '@color can only be applied to "Hds::Tag" along with either @href or @route'; @@ -82,4 +92,23 @@ module('Integration | Component | hds/tag/index', function (hooks) { throw new Error(errorMessage); }); }); + + // OVERFLOW + + test('it should not render a tooltip if the text does not overflow', async function (assert) { + await render(hbs` + + `); + assert.dom('.hds-tooltip-button').doesNotExist(); + }); + + test('it should render a tooltip if the text overflows', async function (assert) { + await render(hbs` +
+ +
+ `); + await waitFor('.hds-tooltip-button', { timeout: 1000 }); + assert.dom('.hds-tooltip-button').exists(); + }); });