diff --git a/src/components/RisCopyableLabel/RisCopyableLabel.stories.ts b/src/components/RisCopyableLabel/RisCopyableLabel.stories.ts new file mode 100644 index 0000000..47f4699 --- /dev/null +++ b/src/components/RisCopyableLabel/RisCopyableLabel.stories.ts @@ -0,0 +1,47 @@ +import { html } from "@/lib/tags"; +import { Meta, StoryObj } from "@storybook/vue3"; +import RisCopyableLabel from "."; + +const meta: Meta = { + 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 = { + render: (args) => ({ + components: { RisCopyableLabel }, + setup() { + return { args }; + }, + template: html``, + }), +}; + +export const CustomValue: StoryObj = { + args: { + text: 'Copy "test" to clipboard', + value: "test", + }, + + render: (args) => ({ + components: { RisCopyableLabel }, + setup() { + return { args }; + }, + template: html``, + }), +}; diff --git a/src/components/RisCopyableLabel/RisCopyableLabel.unit.spec.ts b/src/components/RisCopyableLabel/RisCopyableLabel.unit.spec.ts new file mode 100644 index 0000000..b05b830 --- /dev/null +++ b/src/components/RisCopyableLabel/RisCopyableLabel.unit.spec.ts @@ -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"); + }); +}); diff --git a/src/components/RisCopyableLabel/RisCopyableLabel.vue b/src/components/RisCopyableLabel/RisCopyableLabel.vue new file mode 100644 index 0000000..e1dbb1a --- /dev/null +++ b/src/components/RisCopyableLabel/RisCopyableLabel.vue @@ -0,0 +1,58 @@ + + + diff --git a/src/components/RisCopyableLabel/index.ts b/src/components/RisCopyableLabel/index.ts new file mode 100644 index 0000000..9117998 --- /dev/null +++ b/src/components/RisCopyableLabel/index.ts @@ -0,0 +1,3 @@ +import RisCopyableLabel from "./RisCopyableLabel.vue"; + +export default RisCopyableLabel; diff --git a/src/components/RisExpandableText/RisExpandableText.stories.ts b/src/components/RisExpandableText/RisExpandableText.stories.ts new file mode 100644 index 0000000..bc4d87f --- /dev/null +++ b/src/components/RisExpandableText/RisExpandableText.stories.ts @@ -0,0 +1,49 @@ +import { html } from "@/lib/tags"; +import { Meta, StoryObj } from "@storybook/vue3"; +import RisExpandableText from "."; + +const meta: Meta = { + component: RisExpandableText, + + tags: ["autodocs"], + + args: { + length: 3, + }, +}; + +export default meta; + +export const Default: StoryObj = { + render: (args) => ({ + components: { RisExpandableText }, + setup() { + return { args }; + }, + template: html`
+ + 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. + +
`, + }), +}; + +export const TooShortToExpand: StoryObj = { + render: (args) => ({ + components: { RisExpandableText }, + setup() { + return { args }; + }, + template: html`
+ + This text is so short, it doesn't need to be truncated. + +
`, + }), +}; diff --git a/src/components/RisExpandableText/RisExpandableText.unit.spec.ts b/src/components/RisExpandableText/RisExpandableText.unit.spec.ts new file mode 100644 index 0000000..e1a8fb8 --- /dev/null +++ b/src/components/RisExpandableText/RisExpandableText.unit.spec.ts @@ -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(); + }); +}); diff --git a/src/components/RisExpandableText/RisExpandableText.vue b/src/components/RisExpandableText/RisExpandableText.vue new file mode 100644 index 0000000..507cdd0 --- /dev/null +++ b/src/components/RisExpandableText/RisExpandableText.vue @@ -0,0 +1,58 @@ + + + + + diff --git a/src/components/RisExpandableText/index.ts b/src/components/RisExpandableText/index.ts new file mode 100644 index 0000000..00d3eda --- /dev/null +++ b/src/components/RisExpandableText/index.ts @@ -0,0 +1,3 @@ +import RisExpandableText from "./RisExpandableText.vue"; + +export default RisExpandableText;