-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(RisCopyableLabel, RisExpandableText): add text utility components
- Loading branch information
1 parent
c93e26d
commit ac7a014
Showing
8 changed files
with
356 additions
and
0 deletions.
There are no files selected for viewing
47 changes: 47 additions & 0 deletions
47
src/components/RisCopyableLabel/RisCopyableLabel.stories.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,47 @@ | ||
import { html } from "@/lib/tags"; | ||
import { Meta, StoryObj } from "@storybook/vue3"; | ||
import RisCopyableLabel from "."; | ||
|
||
const meta: Meta<typeof RisCopyableLabel> = { | ||
component: RisCopyableLabel, | ||
|
||
tags: ["autodocs"], | ||
|
||
args: { | ||
text: "Copy to clipboard", | ||
value: undefined, | ||
name: undefined, | ||
}, | ||
|
||
argTypes: { | ||
value: String, | ||
name: String, | ||
}, | ||
}; | ||
|
||
export default meta; | ||
|
||
export const Default: StoryObj<typeof meta> = { | ||
render: (args) => ({ | ||
components: { RisCopyableLabel }, | ||
setup() { | ||
return { args }; | ||
}, | ||
template: html`<RisCopyableLabel v-bind="args" />`, | ||
}), | ||
}; | ||
|
||
export const CustomValue: StoryObj<typeof meta> = { | ||
args: { | ||
text: 'Copy "test" to clipboard', | ||
value: "test", | ||
}, | ||
|
||
render: (args) => ({ | ||
components: { RisCopyableLabel }, | ||
setup() { | ||
return { args }; | ||
}, | ||
template: html`<RisCopyableLabel v-bind="args" />`, | ||
}), | ||
}; |
51 changes: 51 additions & 0 deletions
51
src/components/RisCopyableLabel/RisCopyableLabel.unit.spec.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,51 @@ | ||
import { userEvent } from "@testing-library/user-event"; | ||
import { render, screen } from "@testing-library/vue"; | ||
import { describe, expect, test, vi } from "vitest"; | ||
import RisCopyableLabel from "."; | ||
|
||
describe("RisCopyableLabel", () => { | ||
test("renders", () => { | ||
render(RisCopyableLabel, { props: { text: "Foo" } }); | ||
expect(screen.getByText("Foo")).toBeInTheDocument(); | ||
}); | ||
|
||
test("renders an accessible label with the default value", () => { | ||
render(RisCopyableLabel, { props: { text: "Foo" } }); | ||
|
||
expect( | ||
screen.getByRole("button", { | ||
name: "Wert in die Zwischenablage kopieren", | ||
}), | ||
).toBeInTheDocument(); | ||
}); | ||
|
||
test("renders an accessible label with a custom value", () => { | ||
render(RisCopyableLabel, { props: { text: "Foo", name: "Bar" } }); | ||
|
||
expect( | ||
screen.getByRole("button", { | ||
name: "Bar in die Zwischenablage kopieren", | ||
}), | ||
).toBeInTheDocument(); | ||
}); | ||
|
||
test("copies the text if no value is provided", async () => { | ||
const user = userEvent.setup(); | ||
const spy = vi.spyOn(navigator.clipboard, "writeText"); | ||
render(RisCopyableLabel, { props: { text: "Foo" } }); | ||
|
||
await user.click(screen.getByRole("button")); | ||
|
||
expect(spy).toHaveBeenCalledWith("Foo"); | ||
}); | ||
|
||
test("copies the value if provided", async () => { | ||
const user = userEvent.setup(); | ||
const spy = vi.spyOn(navigator.clipboard, "writeText"); | ||
render(RisCopyableLabel, { props: { text: "Foo", value: "Bar" } }); | ||
|
||
await user.click(screen.getByRole("button")); | ||
|
||
expect(spy).toHaveBeenCalledWith("Bar"); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,58 @@ | ||
<script setup lang="ts"> | ||
import MdiContentCopy from "~icons/mdi/content-copy"; | ||
import MdiCheck from "~icons/mdi/check"; | ||
import { ref } from "vue"; | ||
const props = withDefaults( | ||
defineProps<{ | ||
/** Visible text. */ | ||
text: string; | ||
/** | ||
* Value that should be copied. If no value is provided, copying will | ||
* copy the `text` by default. | ||
*/ | ||
value?: string; | ||
/** | ||
* Human-readable description of the value that should be copied. This | ||
* will be used to provide an accessible label for the control. | ||
* | ||
* @default "Wert" | ||
*/ | ||
name?: string; | ||
}>(), | ||
{ | ||
value: undefined, | ||
name: "Wert", | ||
}, | ||
); | ||
const copySuccess = ref(false); | ||
async function copy() { | ||
try { | ||
await navigator.clipboard.writeText(props.value ?? props.text); | ||
copySuccess.value = true; | ||
setTimeout(() => { | ||
copySuccess.value = false; | ||
}, 1000); | ||
} catch (err) { | ||
console.error(err); | ||
} | ||
} | ||
</script> | ||
|
||
<template> | ||
<button | ||
:aria-label="`${name} in die Zwischenablage kopieren`" | ||
:title="`${name} in die Zwischenablage kopieren`" | ||
class="ris-link2-regular inline-flex items-center gap-4 text-left" | ||
type="button" | ||
@click="copy()" | ||
> | ||
{{ text }} | ||
<MdiContentCopy v-if="!copySuccess" class="flex-none" /> | ||
<MdiCheck v-else class="flex-none" /> | ||
</button> | ||
</template> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
import RisCopyableLabel from "./RisCopyableLabel.vue"; | ||
|
||
export default RisCopyableLabel; |
49 changes: 49 additions & 0 deletions
49
src/components/RisExpandableText/RisExpandableText.stories.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,49 @@ | ||
import { html } from "@/lib/tags"; | ||
import { Meta, StoryObj } from "@storybook/vue3"; | ||
import RisExpandableText from "."; | ||
|
||
const meta: Meta<typeof RisExpandableText> = { | ||
component: RisExpandableText, | ||
|
||
tags: ["autodocs"], | ||
|
||
args: { | ||
length: 3, | ||
}, | ||
}; | ||
|
||
export default meta; | ||
|
||
export const Default: StoryObj<typeof meta> = { | ||
render: (args) => ({ | ||
components: { RisExpandableText }, | ||
setup() { | ||
return { args }; | ||
}, | ||
template: html`<div class="max-w-320"> | ||
<RisExpandableText v-bind="args"> | ||
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod | ||
tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim | ||
veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea | ||
commodo consequat. Duis aute irure dolor in reprehenderit in voluptate | ||
velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint | ||
occaecat cupidatat non proident, sunt in culpa qui officia deserunt | ||
mollit anim id est laborum. | ||
</RisExpandableText> | ||
</div>`, | ||
}), | ||
}; | ||
|
||
export const TooShortToExpand: StoryObj<typeof meta> = { | ||
render: (args) => ({ | ||
components: { RisExpandableText }, | ||
setup() { | ||
return { args }; | ||
}, | ||
template: html`<div class="max-w-320"> | ||
<RisExpandableText v-bind="args"> | ||
This text is so short, it doesn't need to be truncated. | ||
</RisExpandableText> | ||
</div>`, | ||
}), | ||
}; |
87 changes: 87 additions & 0 deletions
87
src/components/RisExpandableText/RisExpandableText.unit.spec.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,87 @@ | ||
import { userEvent } from "@testing-library/user-event"; | ||
import { render, screen } from "@testing-library/vue"; | ||
import { describe, expect, test, vi } from "vitest"; | ||
import RisExpandableText from "."; | ||
|
||
describe("RisExpandableText", () => { | ||
test("renders the text", () => { | ||
render(RisExpandableText, { slots: { default: "Test" } }); | ||
expect(screen.getByText("Test")).toBeInTheDocument(); | ||
}); | ||
|
||
test("renders an expand button", async () => { | ||
// Need to mock these properties as JSDOM doesn't implement layout so they would always be 0 | ||
vi.spyOn(HTMLElement.prototype, "scrollHeight", "get").mockReturnValue(100); | ||
vi.spyOn(HTMLElement.prototype, "clientHeight", "get").mockReturnValue(50); | ||
render(RisExpandableText, { slots: { default: "Test" } }); | ||
|
||
await vi.waitFor(() => { | ||
expect( | ||
screen.getByRole("button", { name: "Mehr anzeigen" }), | ||
).toBeInTheDocument(); | ||
}); | ||
}); | ||
|
||
test("expands the text", async () => { | ||
// Need to mock these properties as JSDOM doesn't implement layout so they would always be 0 | ||
vi.spyOn(HTMLElement.prototype, "scrollHeight", "get").mockReturnValue(100); | ||
vi.spyOn(HTMLElement.prototype, "clientHeight", "get").mockReturnValue(50); | ||
const user = userEvent.setup(); | ||
render(RisExpandableText, { | ||
slots: { default: "Test" }, | ||
props: { expanded: false }, | ||
}); | ||
|
||
await vi.waitFor(() => screen.getByRole("button")); | ||
|
||
await user.click(screen.getByRole("button")); | ||
|
||
expect(screen.getByRole("button")).toHaveAttribute("aria-expanded", "true"); | ||
}); | ||
|
||
test("collapses the text", async () => { | ||
// Need to mock these properties as JSDOM doesn't implement layout so they would always be 0 | ||
vi.spyOn(HTMLElement.prototype, "scrollHeight", "get").mockReturnValue(100); | ||
vi.spyOn(HTMLElement.prototype, "clientHeight", "get").mockReturnValue(50); | ||
const user = userEvent.setup(); | ||
render(RisExpandableText, { | ||
slots: { default: "Test" }, | ||
props: { expanded: true }, | ||
}); | ||
|
||
await vi.waitFor(() => screen.getByRole("button")); | ||
|
||
await user.click(screen.getByRole("button")); | ||
|
||
expect(screen.getByRole("button")).toHaveAttribute( | ||
"aria-expanded", | ||
"false", | ||
); | ||
}); | ||
|
||
test("renders a collapse button", async () => { | ||
// Need to mock these properties as JSDOM doesn't implement layout so they would always be 0 | ||
vi.spyOn(HTMLElement.prototype, "scrollHeight", "get").mockReturnValue(100); | ||
vi.spyOn(HTMLElement.prototype, "clientHeight", "get").mockReturnValue(50); | ||
render(RisExpandableText, { | ||
props: { expanded: true }, | ||
slots: { default: "Test" }, | ||
}); | ||
|
||
await vi.waitFor(() => { | ||
expect( | ||
screen.getByRole("button", { name: "Weniger anzeigen" }), | ||
).toBeInTheDocument(); | ||
}); | ||
}); | ||
|
||
test("does not render the expand/collapse button if the text is not truncated", async () => { | ||
// Need to mock these properties as JSDOM doesn't implement layout so they would always be 0 | ||
// Need to mock these properties as JSDOM doesn't implement layout so they would always be 0 | ||
vi.spyOn(HTMLElement.prototype, "scrollHeight", "get").mockReturnValue(100); | ||
vi.spyOn(HTMLElement.prototype, "clientHeight", "get").mockReturnValue(100); | ||
render(RisExpandableText, { slots: { default: "Test" } }); | ||
|
||
expect(screen.queryByRole("button")).not.toBeInTheDocument(); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,58 @@ | ||
<script setup lang="ts"> | ||
import { ref, useTemplateRef, watchEffect, useId } from "vue"; | ||
const { length = 3 } = defineProps<{ | ||
/** | ||
* Specifies the maximum number of visible lines. | ||
* @default 3 | ||
*/ | ||
length?: number; | ||
}>(); | ||
const expanded = defineModel<boolean>("expanded", { default: false }); | ||
const canExpand = ref(false); | ||
const textContent = useTemplateRef("textContent"); | ||
const textId = useId(); | ||
watchEffect(() => { | ||
if (textContent.value instanceof HTMLDivElement) { | ||
canExpand.value = | ||
textContent.value.scrollHeight > textContent.value.clientHeight; | ||
} | ||
}); | ||
</script> | ||
|
||
<template> | ||
<div> | ||
<div | ||
:id="textId" | ||
ref="textContent" | ||
:class="{ [$style.truncate]: !expanded }" | ||
> | ||
<slot /> | ||
</div> | ||
|
||
<button | ||
v-if="canExpand" | ||
class="ris-link1-regular" | ||
:aria-controls="textId" | ||
:aria-expanded="expanded" | ||
@click="expanded = !expanded" | ||
> | ||
<template v-if="expanded">Weniger anzeigen</template> | ||
<template v-if="!expanded">Mehr anzeigen</template> | ||
</button> | ||
</div> | ||
</template> | ||
|
||
<style module> | ||
.truncate { | ||
overflow: hidden; | ||
display: -webkit-box; | ||
-webkit-box-orient: vertical; | ||
-webkit-line-clamp: v-bind(length); | ||
} | ||
</style> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
import RisExpandableText from "./RisExpandableText.vue"; | ||
|
||
export default RisExpandableText; |