Skip to content

Commit 82aca36

Browse files
AndreiBaicu26abaicu
andauthored
Truncated element (#4125)
* feat: 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 <[email protected]>
1 parent f89ea42 commit 82aca36

File tree

17 files changed

+574
-3
lines changed

17 files changed

+574
-3
lines changed

.circleci/config.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ executors:
1010
parameters:
1111
current_golden_images_hash:
1212
type: string
13-
default: c70f3ed57c4e3536313872fd3d23510caa801a44
13+
default: a3a63503e5584396e8b34ec0b02e8a1a26ebc1ae
1414
wireit_cache_name:
1515
type: string
1616
default: wireit

CONTRIBUTING.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ For e.g: Be descriptive after the /, like `john-doe/123-fix-bug`.
8484

8585
### Commitlint
8686

87-
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:
87+
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:
8888

8989
```bash
9090
type(scope?): subject #scope is optional, but should reference the package you are updating

tools/bundle/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,7 @@
131131
"@spectrum-web-components/tooltip": "^0.41.2",
132132
"@spectrum-web-components/top-nav": "^0.41.2",
133133
"@spectrum-web-components/tray": "^0.41.2",
134+
"@spectrum-web-components/truncated": "^0.0.1",
134135
"@spectrum-web-components/underlay": "^0.41.2"
135136
},
136137
"types": "./src/index.d.ts",

tools/bundle/tsconfig.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@
6565
{ "path": "../../packages/tags" },
6666
{ "path": "../../packages/textfield" },
6767
{ "path": "../theme" },
68+
{ "path": "../truncated" },
6869
{ "path": "../../packages/thumbnail" },
6970
{ "path": "../../packages/toast" },
7071
{ "path": "../../packages/tooltip" },

tools/truncated/.npmignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
stories
2+
test

tools/truncated/README.md

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
## Description
2+
3+
`<sp-truncated>` renders a line of text, truncating it if it overflows its container. When overflowing, a tooltip is automatically created
4+
that renders the entire non-truncated content.
5+
6+
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
7+
names and email addresses.
8+
9+
### Usage
10+
11+
[![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)
12+
[![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)
13+
14+
```
15+
yarn add @spectrum-web-components/truncated
16+
```
17+
18+
Import the side effectful registration of `<sp-truncated>` via:
19+
20+
```
21+
import '@spectrum-web-components/truncated/sp-truncated.js';
22+
```
23+
24+
When looking to leverage the `Truncated` base class as a type and/or for extension purposes, do so via:
25+
26+
```
27+
import { Truncated } from '@spectrum-web-components/truncated';
28+
```
29+
30+
## Example
31+
32+
```html
33+
<sp-truncated>
34+
This will truncate into a tooltip if there isn't enough space for it.
35+
</sp-truncated>
36+
```
37+
38+
### With specific overflow content
39+
40+
By default, tooltip text will be extracted from overflowing content. To provide your own overflow content, slot it into "overflow":
41+
42+
```html
43+
<sp-truncated placement="right">
44+
This is the inline content
45+
<span slot="overflow">
46+
And this overflow content will go into the tooltip, on the right
47+
</span>
48+
</sp-truncated>
49+
```

tools/truncated/exports.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"./src/*": "./src/*.js",
3+
"./sp-truncated": "./sp-truncated.js",
4+
"./sp-truncated.js": "./sp-truncated.js"
5+
}

tools/truncated/package.json

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
{
2+
"name": "@spectrum-web-components/truncated",
3+
"version": "0.0.1",
4+
"publishConfig": {
5+
"access": "public"
6+
},
7+
"description": "Web component implementation of a Spectrum design Truncated",
8+
"license": "Apache-2.0",
9+
"repository": {
10+
"type": "git",
11+
"url": "https://github.com/adobe/spectrum-web-components.git",
12+
"directory": "tools/truncated"
13+
},
14+
"author": "",
15+
"homepage": "https://adobe.github.io/spectrum-web-components/components/truncated",
16+
"bugs": {
17+
"url": "https://github.com/adobe/spectrum-web-components/issues"
18+
},
19+
"main": "src/index.js",
20+
"module": "src/index.js",
21+
"type": "module",
22+
"exports": {
23+
".": {
24+
"development": "./src/index.dev.js",
25+
"default": "./src/index.js"
26+
},
27+
"./package.json": "./package.json",
28+
"./src/Truncated.js": {
29+
"development": "./src/Truncated.dev.js",
30+
"default": "./src/Truncated.js"
31+
},
32+
"./src/index.js": {
33+
"development": "./src/index.dev.js",
34+
"default": "./src/index.js"
35+
},
36+
"./src/truncated.css.js": "./src/truncated.css.js",
37+
"./sp-truncated": "./sp-truncated.js",
38+
"./sp-truncated.js": {
39+
"development": "./sp-truncated.dev.js",
40+
"default": "./sp-truncated.js"
41+
}
42+
},
43+
"scripts": {
44+
"test": "echo \"Error: run tests from mono-repo root.\" && exit 1"
45+
},
46+
"files": [
47+
"**/*.d.ts",
48+
"**/*.js",
49+
"**/*.js.map",
50+
"custom-elements.json",
51+
"!stories/",
52+
"!test/"
53+
],
54+
"keywords": [
55+
"spectrum css",
56+
"web components",
57+
"lit-element",
58+
"lit-html"
59+
],
60+
"dependencies": {
61+
"@spectrum-web-components/base": "^0.41.0",
62+
"@spectrum-web-components/overlay": "^0.41.0",
63+
"@spectrum-web-components/styles": "^0.41.0",
64+
"@spectrum-web-components/tooltip": "^0.41.0"
65+
},
66+
"types": "./src/index.d.ts",
67+
"customElements": "custom-elements.json",
68+
"sideEffects": [
69+
"./sp-*.js",
70+
"./**/*.dev.js"
71+
]
72+
}

tools/truncated/sp-truncated.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
/*
2+
Copyright 2024 Adobe. All rights reserved.
3+
This file is licensed to you under the Apache License, Version 2.0 (the "License");
4+
you may not use this file except in compliance with the License. You may obtain a copy
5+
of the License at http://www.apache.org/licenses/LICENSE-2.0
6+
7+
Unless required by applicable law or agreed to in writing, software distributed under
8+
the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
9+
OF ANY KIND, either express or implied. See the License for the specific language
10+
governing permissions and limitations under the License.
11+
*/
12+
13+
import { Truncated } from './src/Truncated.js';
14+
15+
customElements.define('sp-truncated', Truncated);
16+
17+
declare global {
18+
interface HTMLElementTagNameMap {
19+
'sp-truncated': Truncated;
20+
}
21+
}

tools/truncated/src/Truncated.ts

Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
1+
/*
2+
Copyright 2021 Adobe. All rights reserved.
3+
This file is licensed to you under the Apache License, Version 2.0 (the "License");
4+
you may not use this file except in compliance with the License. You may obtain a copy
5+
of the License at http://www.apache.org/licenses/LICENSE-2.0
6+
7+
Unless required by applicable law or agreed to in writing, software distributed under
8+
the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
9+
OF ANY KIND, either express or implied. See the License for the specific language
10+
governing permissions and limitations under the License.
11+
*/
12+
13+
import {
14+
CSSResultArray,
15+
html,
16+
PropertyValues,
17+
SpectrumElement,
18+
TemplateResult,
19+
} from '@spectrum-web-components/base';
20+
import type { Overlay, Placement } from '@spectrum-web-components/overlay';
21+
import '@spectrum-web-components/overlay/sp-overlay.js';
22+
import '@spectrum-web-components/tooltip/sp-tooltip.js';
23+
import {
24+
property,
25+
query,
26+
queryAssignedElements,
27+
queryAssignedNodes,
28+
state,
29+
} from '@spectrum-web-components/base/src/decorators.js';
30+
31+
import styles from './truncated.css.js';
32+
33+
/**
34+
* @element sp-truncated
35+
*/
36+
export class Truncated extends SpectrumElement {
37+
public static override get styles(): CSSResultArray {
38+
return [styles];
39+
}
40+
41+
/**
42+
* @type {"top" | "top-start" | "top-end" | "right" | "right-start" | "right-end" | "bottom" | "bottom-start" | "bottom-end" | "left" | "left-start" | "left-end"}
43+
*/
44+
@property()
45+
placement: Placement = 'top-start';
46+
47+
/*
48+
* @type {String}
49+
* @attr success-message
50+
* @description The message to display when the text is copied to the clipboard after clicking on the truncated text
51+
*/
52+
@property({ type: String, attribute: 'success-message' })
53+
successMessage = 'Copied to clipboard';
54+
55+
@state()
56+
hasCopied = false;
57+
58+
@state()
59+
private fullText = '';
60+
61+
@state()
62+
private overflowing = false;
63+
64+
@query('#content')
65+
private content!: HTMLElement;
66+
67+
@query('#overlay')
68+
private overlayEl?: Overlay;
69+
70+
@queryAssignedNodes({ flatten: true })
71+
private slottedContent!: Node[];
72+
73+
// elements instead of nodes because, according to spec,
74+
// flattened assignedNodes will return a slot's *children* if there are no assigned nodes.
75+
// ¯\_(ツ)_/¯
76+
@queryAssignedElements({ slot: 'overflow', flatten: true })
77+
private slottedOverflow!: HTMLElement[];
78+
79+
get hasCustomOverflow(): boolean {
80+
return this.slottedOverflow.length > 0;
81+
}
82+
83+
private resizeObserver = new ResizeObserver(() => {
84+
this.measureOverflow();
85+
});
86+
87+
private mutationObserver = new MutationObserver(() => {
88+
this.copyText();
89+
});
90+
91+
override render(): TemplateResult {
92+
/* eslint-disable lit-a11y/click-events-have-key-events */
93+
return html`
94+
<span id="content" @click=${this.handleClick}>
95+
<slot></slot>
96+
</span>
97+
${this.renderTooltip()}
98+
`;
99+
/* eslint-enable lit-a11y/click-events-have-key-events */
100+
}
101+
102+
private renderTooltip(): TemplateResult | undefined {
103+
if (!this.overflowing) {
104+
return html`
105+
<slot
106+
name="overflow"
107+
style="display: none"
108+
@slotchange=${this.handleOverflowSlotchange}
109+
></slot>
110+
`;
111+
}
112+
return html`
113+
<sp-overlay
114+
id="overlay"
115+
.triggerElement=${this as HTMLElement}
116+
.triggerInteraction=${'hover'}
117+
type="hint"
118+
placement=${this.placement}
119+
>
120+
<sp-tooltip name="tooltip">
121+
${!this.hasCopied
122+
? html`
123+
<slot
124+
name="overflow"
125+
@slotchange=${this.handleOverflowSlotchange}
126+
>
127+
${this.fullText}
128+
</slot>
129+
`
130+
: this.successMessage}
131+
</sp-tooltip>
132+
</sp-overlay>
133+
`;
134+
}
135+
136+
protected override firstUpdated(
137+
_changedProperties: PropertyValues<this>
138+
): void {
139+
this.resizeObserver.observe(this);
140+
this.resizeObserver.observe(this.content);
141+
this.copyText();
142+
this.measureOverflow();
143+
}
144+
145+
protected override updated(changedProperties: PropertyValues<this>): void {
146+
super.updated(changedProperties);
147+
if (
148+
changedProperties.has('hasCopied') &&
149+
this.hasCopied &&
150+
this.overlayEl
151+
) {
152+
// we know overlayEl exists because it couldn't copy the text otherwise
153+
this.overlayEl.open = true;
154+
}
155+
}
156+
157+
private handleOverflowSlotchange(): void {
158+
this.mutationObserver.disconnect();
159+
if (!this.hasCustomOverflow) {
160+
/* c8 ignore next 5 */
161+
this.mutationObserver.observe(this.content, {
162+
subtree: true,
163+
childList: true,
164+
characterData: true,
165+
});
166+
}
167+
}
168+
169+
private handleClick(): void {
170+
if (!this.overflowing) return;
171+
172+
const textToCopy = this.slottedContent
173+
.map((node) => node.textContent ?? '')
174+
.join('')
175+
.trim();
176+
navigator.clipboard.writeText(textToCopy);
177+
this.hasCopied = true;
178+
setTimeout(() => {
179+
this.hasCopied = false;
180+
}, 6000);
181+
}
182+
183+
private measureOverflow(): void {
184+
// Add 1 because Safari sometimes rounds by 1px, breaking the calculation otherwise
185+
this.overflowing = this.content.offsetWidth > this.clientWidth + 1;
186+
}
187+
188+
// Copies just the textContent of slotted nodes into the tooltip to avoid duplicating the user's DOM
189+
private copyText(): void {
190+
if (this.hasCustomOverflow) return;
191+
this.fullText = this.slottedContent
192+
.map((node) => node.textContent ?? '')
193+
.join('');
194+
}
195+
}

0 commit comments

Comments
 (0)