Skip to content

Commit

Permalink
fix(truncated): add truncated package (#4163)
Browse files Browse the repository at this point in the history
* fix: add truncated element

* fix: typo

* docs: add truncated docs

* fix: fix lint

* fix: add pr comments

* test: improve coverage

* ci: update hash

---------

Co-authored-by: abaicu <abaicu@adobe.com>
  • Loading branch information
AndreiBaicu26 and abaicu authored Mar 8, 2024
1 parent cc0de6e commit 4ba0480
Show file tree
Hide file tree
Showing 17 changed files with 574 additions and 3 deletions.
2 changes: 1 addition & 1 deletion .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ executors:
parameters:
current_golden_images_hash:
type: string
default: c70f3ed57c4e3536313872fd3d23510caa801a44
default: a3a63503e5584396e8b34ec0b02e8a1a26ebc1ae
wireit_cache_name:
type: string
default: wireit
Expand Down
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ For e.g: Be descriptive after the /, like `john-doe/123-fix-bug`.

### Commitlint

We use [Commitlint](https://github.com/conventional-changelog/commitlint/#what-is-commitlint) to help manage the semantic versions across the various packages in this library. Please be sure that you take this into concideration when submitting PRs to this repositiory. Generally, your commits should look like the following:
We use [Commitlint](https://github.com/conventional-changelog/commitlint/#what-is-commitlint) to help manage the semantic versions across the various packages in this library. Please be sure that you take this into consideration when submitting PRs to this repository. Generally, your commits should look like the following:

```bash
type(scope?): subject #scope is optional, but should reference the package you are updating
Expand Down
1 change: 1 addition & 0 deletions tools/bundle/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,7 @@
"@spectrum-web-components/tooltip": "^0.41.2",
"@spectrum-web-components/top-nav": "^0.41.2",
"@spectrum-web-components/tray": "^0.41.2",
"@spectrum-web-components/truncated": "^0.0.1",
"@spectrum-web-components/underlay": "^0.41.2"
},
"types": "./src/index.d.ts",
Expand Down
1 change: 1 addition & 0 deletions tools/bundle/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@
{ "path": "../../packages/tags" },
{ "path": "../../packages/textfield" },
{ "path": "../theme" },
{ "path": "../truncated" },
{ "path": "../../packages/thumbnail" },
{ "path": "../../packages/toast" },
{ "path": "../../packages/tooltip" },
Expand Down
2 changes: 2 additions & 0 deletions tools/truncated/.npmignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
stories
test
49 changes: 49 additions & 0 deletions tools/truncated/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
## Description

`<sp-truncated>` renders a line of text, truncating it if it overflows its container. When overflowing, a tooltip is automatically created
that renders the entire non-truncated content.

It is used like a `<span>` to contain potentially-long strings that are important for users to see, even when in small containers, like full
names and email addresses.

### Usage

[![See it on NPM!](https://img.shields.io/npm/v/@spectrum-web-components/truncated?style=for-the-badge)](https://www.npmjs.com/package/@spectrum-web-components/truncated)
[![How big is this package in your project?](https://img.shields.io/bundlephobia/minzip/@spectrum-web-components/truncated?style=for-the-badge)](https://bundlephobia.com/result?p=@spectrum-web-components/truncated)

```
yarn add @spectrum-web-components/truncated
```

Import the side effectful registration of `<sp-truncated>` via:

```
import '@spectrum-web-components/truncated/sp-truncated.js';
```

When looking to leverage the `Truncated` base class as a type and/or for extension purposes, do so via:

```
import { Truncated } from '@spectrum-web-components/truncated';
```

## Example

```html
<sp-truncated>
This will truncate into a tooltip if there isn't enough space for it.
</sp-truncated>
```

### With specific overflow content

By default, tooltip text will be extracted from overflowing content. To provide your own overflow content, slot it into "overflow":

```html
<sp-truncated placement="right">
This is the inline content
<span slot="overflow">
And this overflow content will go into the tooltip, on the right
</span>
</sp-truncated>
```
5 changes: 5 additions & 0 deletions tools/truncated/exports.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"./src/*": "./src/*.js",
"./sp-truncated": "./sp-truncated.js",
"./sp-truncated.js": "./sp-truncated.js"
}
72 changes: 72 additions & 0 deletions tools/truncated/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
{
"name": "@spectrum-web-components/truncated",
"version": "0.0.1",
"publishConfig": {
"access": "public"
},
"description": "Web component implementation of a Spectrum design Truncated",
"license": "Apache-2.0",
"repository": {
"type": "git",
"url": "https://github.com/adobe/spectrum-web-components.git",
"directory": "tools/truncated"
},
"author": "",
"homepage": "https://adobe.github.io/spectrum-web-components/components/truncated",
"bugs": {
"url": "https://github.com/adobe/spectrum-web-components/issues"
},
"main": "src/index.js",
"module": "src/index.js",
"type": "module",
"exports": {
".": {
"development": "./src/index.dev.js",
"default": "./src/index.js"
},
"./package.json": "./package.json",
"./src/Truncated.js": {
"development": "./src/Truncated.dev.js",
"default": "./src/Truncated.js"
},
"./src/index.js": {
"development": "./src/index.dev.js",
"default": "./src/index.js"
},
"./src/truncated.css.js": "./src/truncated.css.js",
"./sp-truncated": "./sp-truncated.js",
"./sp-truncated.js": {
"development": "./sp-truncated.dev.js",
"default": "./sp-truncated.js"
}
},
"scripts": {
"test": "echo \"Error: run tests from mono-repo root.\" && exit 1"
},
"files": [
"**/*.d.ts",
"**/*.js",
"**/*.js.map",
"custom-elements.json",
"!stories/",
"!test/"
],
"keywords": [
"spectrum css",
"web components",
"lit-element",
"lit-html"
],
"dependencies": {
"@spectrum-web-components/base": "^0.41.0",
"@spectrum-web-components/overlay": "^0.41.0",
"@spectrum-web-components/styles": "^0.41.0",
"@spectrum-web-components/tooltip": "^0.41.0"
},
"types": "./src/index.d.ts",
"customElements": "custom-elements.json",
"sideEffects": [
"./sp-*.js",
"./**/*.dev.js"
]
}
21 changes: 21 additions & 0 deletions tools/truncated/sp-truncated.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/*
Copyright 2024 Adobe. All rights reserved.
This file is licensed to you under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. You may obtain a copy
of the License at http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software distributed under
the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
OF ANY KIND, either express or implied. See the License for the specific language
governing permissions and limitations under the License.
*/

import { Truncated } from './src/Truncated.js';

customElements.define('sp-truncated', Truncated);

declare global {
interface HTMLElementTagNameMap {
'sp-truncated': Truncated;
}
}
195 changes: 195 additions & 0 deletions tools/truncated/src/Truncated.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
/*
Copyright 2021 Adobe. All rights reserved.
This file is licensed to you under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. You may obtain a copy
of the License at http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software distributed under
the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
OF ANY KIND, either express or implied. See the License for the specific language
governing permissions and limitations under the License.
*/

import {
CSSResultArray,
html,
PropertyValues,
SpectrumElement,
TemplateResult,
} from '@spectrum-web-components/base';
import type { Overlay, Placement } from '@spectrum-web-components/overlay';
import '@spectrum-web-components/overlay/sp-overlay.js';
import '@spectrum-web-components/tooltip/sp-tooltip.js';
import {
property,
query,
queryAssignedElements,
queryAssignedNodes,
state,
} from '@spectrum-web-components/base/src/decorators.js';

import styles from './truncated.css.js';

/**
* @element sp-truncated
*/
export class Truncated extends SpectrumElement {
public static override get styles(): CSSResultArray {
return [styles];
}

/**
* @type {"top" | "top-start" | "top-end" | "right" | "right-start" | "right-end" | "bottom" | "bottom-start" | "bottom-end" | "left" | "left-start" | "left-end"}
*/
@property()
placement: Placement = 'top-start';

/*
* @type {String}
* @attr success-message
* @description The message to display when the text is copied to the clipboard after clicking on the truncated text
*/
@property({ type: String, attribute: 'success-message' })
successMessage = 'Copied to clipboard';

@state()
hasCopied = false;

@state()
private fullText = '';

@state()
private overflowing = false;

@query('#content')
private content!: HTMLElement;

@query('#overlay')
private overlayEl?: Overlay;

@queryAssignedNodes({ flatten: true })
private slottedContent!: Node[];

// elements instead of nodes because, according to spec,
// flattened assignedNodes will return a slot's *children* if there are no assigned nodes.
// ¯\_(ツ)_/¯
@queryAssignedElements({ slot: 'overflow', flatten: true })
private slottedOverflow!: HTMLElement[];

get hasCustomOverflow(): boolean {
return this.slottedOverflow.length > 0;
}

private resizeObserver = new ResizeObserver(() => {
this.measureOverflow();
});

private mutationObserver = new MutationObserver(() => {
this.copyText();
});

override render(): TemplateResult {
/* eslint-disable lit-a11y/click-events-have-key-events */
return html`
<span id="content" @click=${this.handleClick}>
<slot></slot>
</span>
${this.renderTooltip()}
`;
/* eslint-enable lit-a11y/click-events-have-key-events */
}

private renderTooltip(): TemplateResult | undefined {
if (!this.overflowing) {
return html`
<slot
name="overflow"
style="display: none"
@slotchange=${this.handleOverflowSlotchange}
></slot>
`;
}
return html`
<sp-overlay
id="overlay"
.triggerElement=${this as HTMLElement}
.triggerInteraction=${'hover'}
type="hint"
placement=${this.placement}
>
<sp-tooltip name="tooltip">
${!this.hasCopied
? html`
<slot
name="overflow"
@slotchange=${this.handleOverflowSlotchange}
>
${this.fullText}
</slot>
`
: this.successMessage}
</sp-tooltip>
</sp-overlay>
`;
}

protected override firstUpdated(
_changedProperties: PropertyValues<this>
): void {
this.resizeObserver.observe(this);
this.resizeObserver.observe(this.content);
this.copyText();
this.measureOverflow();
}

protected override updated(changedProperties: PropertyValues<this>): void {
super.updated(changedProperties);
if (
changedProperties.has('hasCopied') &&
this.hasCopied &&
this.overlayEl
) {
// we know overlayEl exists because it couldn't copy the text otherwise
this.overlayEl.open = true;
}
}

private handleOverflowSlotchange(): void {
this.mutationObserver.disconnect();
if (!this.hasCustomOverflow) {
/* c8 ignore next 5 */
this.mutationObserver.observe(this.content, {
subtree: true,
childList: true,
characterData: true,
});
}
}

private handleClick(): void {
if (!this.overflowing) return;

const textToCopy = this.slottedContent
.map((node) => node.textContent ?? '')
.join('')
.trim();
navigator.clipboard.writeText(textToCopy);
this.hasCopied = true;
setTimeout(() => {
this.hasCopied = false;
}, 6000);
}

private measureOverflow(): void {
// Add 1 because Safari sometimes rounds by 1px, breaking the calculation otherwise
this.overflowing = this.content.offsetWidth > this.clientWidth + 1;
}

// Copies just the textContent of slotted nodes into the tooltip to avoid duplicating the user's DOM
private copyText(): void {
if (this.hasCustomOverflow) return;
this.fullText = this.slottedContent
.map((node) => node.textContent ?? '')
.join('');
}
}
12 changes: 12 additions & 0 deletions tools/truncated/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/*
Copyright 2024 Adobe. All rights reserved.
This file is licensed to you under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. You may obtain a copy
of the License at http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software distributed under
the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
OF ANY KIND, either express or implied. See the License for the specific language
governing permissions and limitations under the License.
*/
export * from './Truncated.js';
Loading

0 comments on commit 4ba0480

Please sign in to comment.