Skip to content
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

Tag - Text truncation for overflow fix #2655

Open
wants to merge 17 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 16 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
7 changes: 7 additions & 0 deletions .changeset/brown-wolves-admire.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@hashicorp/design-system-components": minor
---

`Tag` - Truncate any text that is longer than 150px and add a tooltip with the full text when truncation occurs

`Tag` - Added `@tooltipPlacement` argument
73 changes: 56 additions & 17 deletions packages/components/src/components/hds/tag/index.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -2,29 +2,68 @@
Copyright (c) HashiCorp, Inc.
SPDX-License-Identifier: MPL-2.0
}}
<Hds::Text::Body class={{this.classNames}} @tag="span" @size="100" @weight="medium" @color="primary" ...attributes>
<Hds::Text::Body
class={{this.classNames}}
@tag="span"
@size="100"
@weight="medium"
@color="primary"
{{this._setUpObserver}}
...attributes
>
{{#if this.onDismiss}}
<button class="hds-tag__dismiss" type="button" aria-label={{this.ariaLabel}} {{on "click" this.onDismiss}}>
<Hds::Icon class="hds-tag__dismiss-icon" @name="x" @size="16" />
</button>
{{/if}}
{{#if (or @href @route)}}
<Hds::Interactive
class="hds-tag__link"
@current-when={{@current-when}}
@models={{hds-link-to-models @model @models}}
@query={{hds-link-to-query @query}}
@replace={{@replace}}
@route={{@route}}
@isRouteExternal={{@isRouteExternal}}
@href={{@href}}
@isHrefExternal={{@isHrefExternal}}
>
{{this.text}}
</Hds::Interactive>
{{#if this._isTextOverflow}}
<Hds::Interactive
class="hds-tag__link"
@current-when={{@current-when}}
@models={{hds-link-to-models @model @models}}
@query={{hds-link-to-query @query}}
@replace={{@replace}}
@route={{@route}}
@isRouteExternal={{@isRouteExternal}}
@href={{@href}}
@isHrefExternal={{@isHrefExternal}}
{{hds-tooltip this.text options=(hash placement=this.tooltipPlacement)}}
>
<div class="hds-tag__text-container">
{{this.text}}
</div>
</Hds::Interactive>
{{else}}
<Hds::Interactive
class="hds-tag__link"
@current-when={{@current-when}}
@models={{hds-link-to-models @model @models}}
@query={{hds-link-to-query @query}}
@replace={{@replace}}
@route={{@route}}
@isRouteExternal={{@isRouteExternal}}
@href={{@href}}
@isHrefExternal={{@isHrefExternal}}
>
<div class="hds-tag__text-container">
{{this.text}}
</div>
</Hds::Interactive>
{{/if}}
{{else}}
<span class="hds-tag__text">
{{this.text}}
</span>
{{#if this._isTextOverflow}}
<Hds::TooltipButton class="hds-tag__text" @text={{this.text}} @placement={{this.tooltipPlacement}}>
<div class="hds-tag__text-container">
{{this.text}}
</div>
</Hds::TooltipButton>
{{else}}
<span class="hds-tag__text">
<div class="hds-tag__text-container">
{{this.text}}
</div>
</span>
{{/if}}
{{/if}}
</Hds::Text::Body>
50 changes: 50 additions & 0 deletions packages/components/src/components/hds/tag/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,27 +4,73 @@
*/

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;
};
Element: HTMLSpanElement;
}

export default class HdsTag extends Component<HdsTagSignature> {
@tracked private _isTextOverflow!: boolean;
private _observer!: ResizeObserver;

private _setUpObserver = modifier((element: HTMLElement) => {
Copy link
Member

Choose a reason for hiding this comment

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

I like how this aligns with the AdvancedTable observer!

Copy link
Contributor

Choose a reason for hiding this comment

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

all credit to dylan, I followed his pattern :)

// 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}
Expand Down Expand Up @@ -104,4 +150,8 @@ export default class HdsTag extends Component<HdsTagSignature> {

return classes.join(' ');
}

private _isOverflow(el: Element): boolean {
return el.scrollHeight > el.clientHeight;
Copy link
Member

Choose a reason for hiding this comment

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

πŸ™Œ

}
}
17 changes: 17 additions & 0 deletions packages/components/src/components/hds/tag/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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}`;
26 changes: 26 additions & 0 deletions packages/components/src/styles/components/tag.scss
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ $hds-tag-border-radius: 50px;
.hds-tag {
display: inline-flex;
align-items: stretch;
width: fit-content;
dchyun marked this conversation as resolved.
Show resolved Hide resolved
max-width: 100%;
line-height: 1rem; // 16px - override `body-100`
vertical-align: middle;
background-color: var(--token-color-surface-interactive);
Expand All @@ -41,10 +43,19 @@ $hds-tag-border-radius: 50px;
.hds-tag__text,
.hds-tag__link {
flex: 1 0 0;
max-width: 150px;
padding: 3px 10px 5px 10px;
border-radius: inherit;
}

.hds-tag__text-container {
display: -webkit-box;
overflow: hidden;
-webkit-box-orient: vertical;
-webkit-line-clamp: 1;
line-clamp: 1;
}

.hds-tag__dismiss ~ .hds-tag__text,
.hds-tag__dismiss ~ .hds-tag__link {
padding: 3px 8px 5px 6px;
Expand Down Expand Up @@ -76,6 +87,21 @@ $hds-tag-border-radius: 50px;
}
}

.hds-tooltip-button.hds-tag__text {
dchyun marked this conversation as resolved.
Show resolved Hide resolved
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 {
Expand Down
3 changes: 2 additions & 1 deletion showcase/app/routes/components/tag.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 };
}
}
29 changes: 27 additions & 2 deletions showcase/app/templates/components/tag.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -33,12 +33,22 @@
</Shw::Flex>

<Shw::Flex @label="With long text" as |SF|>
<SF.Item {{style width="200px"}}>
<SF.Item>
<Hds::Tag @text="This is a very long text that should go on multiple lines" @onDismiss={{this.noop}} />
</SF.Item>
<SF.Item {{style width="200px"}}>
<SF.Item>
<Hds::Tag @text="This is a very long text that should go on multiple lines" />
</SF.Item>
<SF.Item>
<Hds::Tag
@text="This is a very long text that should go on multiple lines"
@onDismiss={{this.noop}}
@route="components.tag"
/>
</SF.Item>
<SF.Item>
<Hds::Tag @text="This is a very long text that should go on multiple lines" @route="components.tag" />
</SF.Item>
</Shw::Flex>

<Shw::Divider @level={{2}} />
Expand Down Expand Up @@ -119,4 +129,19 @@
{{/let}}
</Shw::Grid>

<Shw::Divider @level={{2}} />

<Shw::Text::H2>Tooltip Placements</Shw::Text::H2>

<Shw::Grid @columns={{3}} as |SG|>
{{#each @model.TOOLTIP_PLACEMENTS as |place|}}
<SG.Item>
<Hds::Tag
@text="{{place}} This is a very long text that should go on multiple lines"
@tooltipPlacement={{place}}
/>
</SG.Item>
{{/each}}
</Shw::Grid>

</section>
31 changes: 30 additions & 1 deletion showcase/tests/integration/components/hds/tag/index-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -25,6 +30,7 @@ module('Integration | Component | hds/tag/index', function (hooks) {
await render(hbs`<Hds::Tag @text="My tag" />`);
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`<Hds::Tag @text="My tag" @onDismiss={{this.NOOP}} />`);
Expand All @@ -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) {
Expand All @@ -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`<Hds::Tag @text="My text tag" @href="/" @color="secondary" id="test-link-tag"/>`
);
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';
Expand All @@ -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';
Expand All @@ -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`
<Hds::Tag @text="My text tag" id="test-tag"/>
`);
assert.dom('.hds-tooltip-button').doesNotExist();
});

test('it should render a tooltip if the text overflows', async function (assert) {
await render(hbs`
<div style="width: 50px;">
dchyun marked this conversation as resolved.
Show resolved Hide resolved
<Hds::Tag @text="This is a very long text that should go on multiple lines" id="test-tag"/>
</div>
`);
await waitFor('.hds-tooltip-button', { timeout: 1000 });
assert.dom('.hds-tooltip-button').exists();
});
});