srcdev-nuxt-components 9.2.1 → 9.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/app/components/02.molecules/input-copy/CONSUMER-STYLING.md +143 -0
- package/app/components/02.molecules/input-copy/InputCopy.vue +195 -0
- package/app/components/02.molecules/input-copy/stories/InputCopy.stories.ts +110 -0
- package/app/components/02.molecules/input-copy/tests/InputCopy.spec.ts +181 -0
- package/app/components/02.molecules/pricing-card/CONSUMER-STYLING.md +149 -0
- package/app/components/02.molecules/pricing-card/PricingCard.vue +222 -0
- package/app/components/02.molecules/pricing-card/stories/PricingCard.stories.ts +160 -0
- package/app/components/02.molecules/pricing-card/tests/PricingCard.spec.ts +139 -0
- package/package.json +1 -1
- package/.claude/skills/components/input-copy-core.md +0 -66
- package/app/components/05.forms/input-copy/InputCopyCore.vue +0 -132
- package/app/components/05.forms/input-copy/stories/InputCopyCore.stories.ts +0 -89
- package/app/components/05.forms/input-copy/tests/InputCopyCore.spec.ts +0 -212
- package/app/components/05.forms/input-copy/tests/__snapshots__/InputCopyCore.spec.ts.snap +0 -28
|
@@ -1,66 +0,0 @@
|
|
|
1
|
-
# InputCopyCore
|
|
2
|
-
|
|
3
|
-
Readonly input that displays a value with a one-click copy-to-clipboard button. Intended for license keys, API tokens, share URLs, etc.
|
|
4
|
-
|
|
5
|
-
**File**: `app/components/forms/input-copy/InputCopyCore.vue`
|
|
6
|
-
|
|
7
|
-
## Props
|
|
8
|
-
|
|
9
|
-
| Prop | Type | Default | Description |
|
|
10
|
-
|------|------|---------|-------------|
|
|
11
|
-
| `id` | `string` | required | `id` attribute on the `<input>` |
|
|
12
|
-
| `value` | `string` | required | Text to display and copy |
|
|
13
|
-
| `copy-label` | `string` | `"Copy"` | Button label before copying |
|
|
14
|
-
| `copied-label` | `string` | `"Copied!"` | Button label after a successful copy |
|
|
15
|
-
| `feedback-duration` | `number` | `2000` | ms to show the copied state before resetting |
|
|
16
|
-
| `style-class-passthrough` | `string \| string[]` | `[]` | Extra classes on the root element |
|
|
17
|
-
|
|
18
|
-
## Emits
|
|
19
|
-
|
|
20
|
-
| Event | Payload | Description |
|
|
21
|
-
|-------|---------|-------------|
|
|
22
|
-
| `copy` | `value: string` | Fires on successful clipboard write |
|
|
23
|
-
|
|
24
|
-
## Slots
|
|
25
|
-
|
|
26
|
-
| Slot | Description |
|
|
27
|
-
|------|-------------|
|
|
28
|
-
| `icon` | Optional icon shown to the left of the button label (passes through to InputButtonCore `#left`) |
|
|
29
|
-
|
|
30
|
-
## Behaviour
|
|
31
|
-
|
|
32
|
-
- Calls `navigator.clipboard.writeText(value)` on button click
|
|
33
|
-
- On success: adds `.copied` class to root, shows `copiedLabel`, emits `copy`
|
|
34
|
-
- After `feedbackDuration` ms: removes `.copied` class, reverts button label
|
|
35
|
-
- Clipboard failure is caught silently — no `.copied` state is set
|
|
36
|
-
|
|
37
|
-
## CSS classes
|
|
38
|
-
|
|
39
|
-
| Class | Where | Description |
|
|
40
|
-
|-------|-------|-------------|
|
|
41
|
-
| `.input-copy-core` | root | always present |
|
|
42
|
-
| `.copied` | root | present during feedback window |
|
|
43
|
-
| `.input-copy-field` | `<input>` | the readonly text field |
|
|
44
|
-
| `.input-copy-button` | button | extra class added to InputButtonCore root |
|
|
45
|
-
|
|
46
|
-
## Usage
|
|
47
|
-
|
|
48
|
-
```vue
|
|
49
|
-
<InputCopyCore
|
|
50
|
-
id="license-key-input"
|
|
51
|
-
value="sk_live_abc123def456"
|
|
52
|
-
copy-label="Copy key"
|
|
53
|
-
copied-label="Copied!"
|
|
54
|
-
:feedback-duration="2000"
|
|
55
|
-
/>
|
|
56
|
-
```
|
|
57
|
-
|
|
58
|
-
With icon slot (e.g. using a Radix icon):
|
|
59
|
-
|
|
60
|
-
```vue
|
|
61
|
-
<InputCopyCore id="api-key" value="sk_live_abc123def456">
|
|
62
|
-
<template #icon>
|
|
63
|
-
<Icon name="radix-icons:clipboard" class="icon" />
|
|
64
|
-
</template>
|
|
65
|
-
</InputCopyCore>
|
|
66
|
-
```
|
|
@@ -1,132 +0,0 @@
|
|
|
1
|
-
<template>
|
|
2
|
-
<div
|
|
3
|
-
class="input-copy-core"
|
|
4
|
-
data-testid="input-copy-core"
|
|
5
|
-
:class="[elementClasses, { copied: isCopied }]"
|
|
6
|
-
>
|
|
7
|
-
<input
|
|
8
|
-
:id
|
|
9
|
-
:value
|
|
10
|
-
type="text"
|
|
11
|
-
readonly
|
|
12
|
-
aria-readonly="true"
|
|
13
|
-
class="input-copy-field"
|
|
14
|
-
/>
|
|
15
|
-
<InputButtonCore
|
|
16
|
-
type="button"
|
|
17
|
-
variant="inline"
|
|
18
|
-
class="input-copy-button"
|
|
19
|
-
:button-text="isCopied ? copiedLabel : copyLabel"
|
|
20
|
-
:aria-label="isCopied ? copiedLabel : copyLabel"
|
|
21
|
-
@click="handleCopy"
|
|
22
|
-
>
|
|
23
|
-
<template v-if="slots.icon" #left>
|
|
24
|
-
<slot name="icon"></slot>
|
|
25
|
-
</template>
|
|
26
|
-
</InputButtonCore>
|
|
27
|
-
</div>
|
|
28
|
-
</template>
|
|
29
|
-
|
|
30
|
-
<script setup lang="ts">
|
|
31
|
-
interface Props {
|
|
32
|
-
id: string;
|
|
33
|
-
value: string;
|
|
34
|
-
copyLabel?: string;
|
|
35
|
-
copiedLabel?: string;
|
|
36
|
-
feedbackDuration?: number;
|
|
37
|
-
styleClassPassthrough?: string | string[];
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
const props = withDefaults(defineProps<Props>(), {
|
|
41
|
-
copyLabel: "Copy",
|
|
42
|
-
copiedLabel: "Copied!",
|
|
43
|
-
feedbackDuration: 2000,
|
|
44
|
-
styleClassPassthrough: () => [],
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
const emit = defineEmits<{
|
|
48
|
-
copy: [value: string];
|
|
49
|
-
}>();
|
|
50
|
-
|
|
51
|
-
const slots = useSlots();
|
|
52
|
-
const isCopied = ref(false);
|
|
53
|
-
|
|
54
|
-
const handleCopy = async () => {
|
|
55
|
-
try {
|
|
56
|
-
await navigator.clipboard.writeText(props.value);
|
|
57
|
-
isCopied.value = true;
|
|
58
|
-
emit("copy", props.value);
|
|
59
|
-
await useSleep(props.feedbackDuration);
|
|
60
|
-
isCopied.value = false;
|
|
61
|
-
} catch {
|
|
62
|
-
// Clipboard API unavailable — fail silently
|
|
63
|
-
}
|
|
64
|
-
};
|
|
65
|
-
|
|
66
|
-
const { elementClasses, resetElementClasses } = useStyleClassPassthrough(props.styleClassPassthrough);
|
|
67
|
-
|
|
68
|
-
watch(
|
|
69
|
-
() => props.styleClassPassthrough,
|
|
70
|
-
() => {
|
|
71
|
-
resetElementClasses(props.styleClassPassthrough);
|
|
72
|
-
}
|
|
73
|
-
);
|
|
74
|
-
</script>
|
|
75
|
-
|
|
76
|
-
<style lang="css">
|
|
77
|
-
@layer components {
|
|
78
|
-
.input-copy-core {
|
|
79
|
-
display: flex;
|
|
80
|
-
align-items: stretch;
|
|
81
|
-
overflow: hidden;
|
|
82
|
-
border: var(--form-element-border-width) solid var(--theme-border);
|
|
83
|
-
border-radius: var(--form-input-border-radius);
|
|
84
|
-
background-color: var(--theme-input-surface);
|
|
85
|
-
transition: all var(--theme-form-transition-duration) ease-in-out;
|
|
86
|
-
|
|
87
|
-
.input-copy-field {
|
|
88
|
-
all: unset;
|
|
89
|
-
flex-grow: 1;
|
|
90
|
-
padding-block: var(--input-padding-block);
|
|
91
|
-
padding-inline: var(--input-padding-inline);
|
|
92
|
-
font-family: var(--font-family);
|
|
93
|
-
font-size: var(--input-font-size);
|
|
94
|
-
color: var(--theme-input-text-color-normal);
|
|
95
|
-
cursor: default;
|
|
96
|
-
overflow: hidden;
|
|
97
|
-
text-overflow: ellipsis;
|
|
98
|
-
white-space: nowrap;
|
|
99
|
-
min-width: 0;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
.input-copy-button.input-button-core {
|
|
103
|
-
border-radius: 0;
|
|
104
|
-
border-inline-start: var(--form-element-border-width) solid var(--theme-border);
|
|
105
|
-
padding-inline: var(--input-padding-inline);
|
|
106
|
-
min-width: var(--input-min-height);
|
|
107
|
-
background-color: transparent;
|
|
108
|
-
color: var(--theme-text);
|
|
109
|
-
|
|
110
|
-
&:hover {
|
|
111
|
-
background-color: var(--theme-surface);
|
|
112
|
-
color: var(--theme-on-surface);
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
&:focus-visible {
|
|
116
|
-
outline: var(--form-element-outline-width-focus) solid var(--theme-border-focus);
|
|
117
|
-
outline-offset: -4px;
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
&.copied {
|
|
122
|
-
--_copy-success-surface: light-dark(var(--green-01), var(--green-09));
|
|
123
|
-
--_copy-success-text: light-dark(var(--green-08), var(--green-01));
|
|
124
|
-
|
|
125
|
-
.input-copy-button.input-button-core {
|
|
126
|
-
background-color: var(--_copy-success-surface);
|
|
127
|
-
color: var(--_copy-success-text);
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
</style>
|
|
@@ -1,89 +0,0 @@
|
|
|
1
|
-
import InputCopyCore from "../InputCopyCore.vue";
|
|
2
|
-
import type { Meta, StoryObj } from "@nuxtjs/storybook";
|
|
3
|
-
|
|
4
|
-
const meta: Meta<typeof InputCopyCore> = {
|
|
5
|
-
title: "Components/Forms/Input Copy/InputCopyCore",
|
|
6
|
-
component: InputCopyCore,
|
|
7
|
-
argTypes: {
|
|
8
|
-
id: {
|
|
9
|
-
control: "text",
|
|
10
|
-
description: "The id attribute for the input element",
|
|
11
|
-
table: { category: "Content" },
|
|
12
|
-
},
|
|
13
|
-
value: {
|
|
14
|
-
control: "text",
|
|
15
|
-
description: "The text value to display and copy",
|
|
16
|
-
table: { category: "Content" },
|
|
17
|
-
},
|
|
18
|
-
copyLabel: {
|
|
19
|
-
control: "text",
|
|
20
|
-
description: "Button label before copying",
|
|
21
|
-
table: { category: "Behaviour" },
|
|
22
|
-
},
|
|
23
|
-
copiedLabel: {
|
|
24
|
-
control: "text",
|
|
25
|
-
description: "Button label shown after a successful copy",
|
|
26
|
-
table: { category: "Behaviour" },
|
|
27
|
-
},
|
|
28
|
-
feedbackDuration: {
|
|
29
|
-
control: { type: "number", min: 500, max: 5000, step: 500 },
|
|
30
|
-
description: "How long (ms) to show the copied state before resetting",
|
|
31
|
-
table: { category: "Behaviour" },
|
|
32
|
-
},
|
|
33
|
-
styleClassPassthrough: {
|
|
34
|
-
control: "object",
|
|
35
|
-
description: "Additional CSS classes applied to the root element",
|
|
36
|
-
table: { category: "Styling" },
|
|
37
|
-
},
|
|
38
|
-
},
|
|
39
|
-
args: {
|
|
40
|
-
id: "copy-input",
|
|
41
|
-
value: "sk_live_abc123def456",
|
|
42
|
-
copyLabel: "Copy",
|
|
43
|
-
copiedLabel: "Copied!",
|
|
44
|
-
feedbackDuration: 2000,
|
|
45
|
-
styleClassPassthrough: [],
|
|
46
|
-
},
|
|
47
|
-
};
|
|
48
|
-
|
|
49
|
-
export default meta;
|
|
50
|
-
type Story = StoryObj<typeof InputCopyCore>;
|
|
51
|
-
|
|
52
|
-
export const Default: Story = {
|
|
53
|
-
render: (args) => ({
|
|
54
|
-
components: { InputCopyCore },
|
|
55
|
-
setup() {
|
|
56
|
-
return { args };
|
|
57
|
-
},
|
|
58
|
-
template: `<div style="max-width: 420px; padding: 40px;"><InputCopyCore v-bind="args" /></div>`,
|
|
59
|
-
}),
|
|
60
|
-
};
|
|
61
|
-
|
|
62
|
-
export const LongValue: Story = {
|
|
63
|
-
name: "Long value",
|
|
64
|
-
args: {
|
|
65
|
-
value: "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJjdXN0b21lcklkIjoiY3VzdF8wMDEiLCJkb21haW5zIjpbImV4YW1wbGUuY29tIl0sInBsYW4iOiJzaW5nbGUtc2l0ZSIsImlhdCI6MTcwMDAwMDAwMCwiZXhwIjoxNzMxNjI3MjAwfQ",
|
|
66
|
-
},
|
|
67
|
-
render: (args) => ({
|
|
68
|
-
components: { InputCopyCore },
|
|
69
|
-
setup() {
|
|
70
|
-
return { args };
|
|
71
|
-
},
|
|
72
|
-
template: `<div style="max-width: 420px; padding: 40px;"><InputCopyCore v-bind="args" /></div>`,
|
|
73
|
-
}),
|
|
74
|
-
};
|
|
75
|
-
|
|
76
|
-
export const CustomLabels: Story = {
|
|
77
|
-
name: "Custom labels",
|
|
78
|
-
args: {
|
|
79
|
-
copyLabel: "Copy key",
|
|
80
|
-
copiedLabel: "Key copied!",
|
|
81
|
-
},
|
|
82
|
-
render: (args) => ({
|
|
83
|
-
components: { InputCopyCore },
|
|
84
|
-
setup() {
|
|
85
|
-
return { args };
|
|
86
|
-
},
|
|
87
|
-
template: `<div style="max-width: 420px; padding: 40px;"><InputCopyCore v-bind="args" /></div>`,
|
|
88
|
-
}),
|
|
89
|
-
};
|
|
@@ -1,212 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, afterEach, beforeEach, vi } from "vitest";
|
|
2
|
-
import { mountSuspended } from "@nuxt/test-utils/runtime";
|
|
3
|
-
import InputCopyCore from "../InputCopyCore.vue";
|
|
4
|
-
|
|
5
|
-
const DEFAULT_PROPS = {
|
|
6
|
-
id: "copy-input",
|
|
7
|
-
value: "sk_live_abc123def456",
|
|
8
|
-
};
|
|
9
|
-
|
|
10
|
-
const createWrapper = async (props: Record<string, unknown> = {}) => {
|
|
11
|
-
return mountSuspended(InputCopyCore, {
|
|
12
|
-
props: { ...DEFAULT_PROPS, ...props },
|
|
13
|
-
});
|
|
14
|
-
};
|
|
15
|
-
|
|
16
|
-
describe("InputCopyCore", () => {
|
|
17
|
-
let wrapper: Awaited<ReturnType<typeof createWrapper>>;
|
|
18
|
-
|
|
19
|
-
afterEach(() => {
|
|
20
|
-
wrapper?.unmount();
|
|
21
|
-
vi.restoreAllMocks();
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
// ─── Snapshots ───────────────────────────────────────────────────────────
|
|
25
|
-
|
|
26
|
-
describe("Snapshots", () => {
|
|
27
|
-
it("default", async () => {
|
|
28
|
-
wrapper = await createWrapper();
|
|
29
|
-
expect(wrapper.html()).toMatchSnapshot();
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
it("custom labels", async () => {
|
|
33
|
-
wrapper = await createWrapper({ copyLabel: "Copy key", copiedLabel: "Key copied!" });
|
|
34
|
-
expect(wrapper.html()).toMatchSnapshot();
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
it("with styleClassPassthrough", async () => {
|
|
38
|
-
wrapper = await createWrapper({ styleClassPassthrough: ["my-copy-input"] });
|
|
39
|
-
expect(wrapper.html()).toMatchSnapshot();
|
|
40
|
-
});
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
// ─── Rendering ───────────────────────────────────────────────────────────
|
|
44
|
-
|
|
45
|
-
describe("Rendering", () => {
|
|
46
|
-
it("mounts without error", async () => {
|
|
47
|
-
wrapper = await createWrapper();
|
|
48
|
-
expect(wrapper.vm).toBeTruthy();
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
it("root element has input-copy-core class", async () => {
|
|
52
|
-
wrapper = await createWrapper();
|
|
53
|
-
expect(wrapper.classes()).toContain("input-copy-core");
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
it("root element has data-testid attribute", async () => {
|
|
57
|
-
wrapper = await createWrapper();
|
|
58
|
-
expect(wrapper.attributes("data-testid")).toBe("input-copy-core");
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
it("renders a readonly input", async () => {
|
|
62
|
-
wrapper = await createWrapper();
|
|
63
|
-
const input = wrapper.find(".input-copy-field");
|
|
64
|
-
expect(input.exists()).toBe(true);
|
|
65
|
-
expect(input.attributes("readonly")).toBeDefined();
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
it("input has aria-readonly set to true", async () => {
|
|
69
|
-
wrapper = await createWrapper();
|
|
70
|
-
expect(wrapper.find(".input-copy-field").attributes("aria-readonly")).toBe("true");
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
it("input displays the value", async () => {
|
|
74
|
-
wrapper = await createWrapper({ value: "my-license-key" });
|
|
75
|
-
expect(wrapper.find(".input-copy-field").attributes("value")).toBe("my-license-key");
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
it("input has the correct id", async () => {
|
|
79
|
-
wrapper = await createWrapper({ id: "my-input" });
|
|
80
|
-
expect(wrapper.find(".input-copy-field").attributes("id")).toBe("my-input");
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
it("renders the copy button", async () => {
|
|
84
|
-
wrapper = await createWrapper();
|
|
85
|
-
expect(wrapper.find(".input-copy-button").exists()).toBe(true);
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
it("copy button shows copyLabel by default", async () => {
|
|
89
|
-
wrapper = await createWrapper();
|
|
90
|
-
expect(wrapper.find(".input-copy-button").text()).toBe("Copy");
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
it("copy button shows custom copyLabel", async () => {
|
|
94
|
-
wrapper = await createWrapper({ copyLabel: "Copy key" });
|
|
95
|
-
expect(wrapper.find(".input-copy-button").text()).toBe("Copy key");
|
|
96
|
-
});
|
|
97
|
-
});
|
|
98
|
-
|
|
99
|
-
// ─── Props ───────────────────────────────────────────────────────────────
|
|
100
|
-
|
|
101
|
-
describe("Props", () => {
|
|
102
|
-
it("styleClassPassthrough string is applied to root", async () => {
|
|
103
|
-
wrapper = await createWrapper({ styleClassPassthrough: "my-class" });
|
|
104
|
-
expect(wrapper.classes()).toContain("my-class");
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
it("styleClassPassthrough array is applied to root", async () => {
|
|
108
|
-
wrapper = await createWrapper({ styleClassPassthrough: ["class-a", "class-b"] });
|
|
109
|
-
expect(wrapper.classes()).toContain("class-a");
|
|
110
|
-
expect(wrapper.classes()).toContain("class-b");
|
|
111
|
-
});
|
|
112
|
-
|
|
113
|
-
it("updates classes when styleClassPassthrough prop changes", async () => {
|
|
114
|
-
wrapper = await createWrapper({ styleClassPassthrough: ["original"] });
|
|
115
|
-
expect(wrapper.classes()).toContain("original");
|
|
116
|
-
await wrapper.setProps({ styleClassPassthrough: ["updated"] });
|
|
117
|
-
expect(wrapper.classes()).not.toContain("original");
|
|
118
|
-
expect(wrapper.classes()).toContain("updated");
|
|
119
|
-
});
|
|
120
|
-
});
|
|
121
|
-
|
|
122
|
-
// ─── Copy behaviour ──────────────────────────────────────────────────────
|
|
123
|
-
|
|
124
|
-
describe("Copy behaviour", () => {
|
|
125
|
-
beforeEach(() => {
|
|
126
|
-
vi.stubGlobal("navigator", {
|
|
127
|
-
clipboard: {
|
|
128
|
-
writeText: vi.fn().mockResolvedValue(undefined),
|
|
129
|
-
},
|
|
130
|
-
});
|
|
131
|
-
});
|
|
132
|
-
|
|
133
|
-
it("calls navigator.clipboard.writeText with the value on click", async () => {
|
|
134
|
-
wrapper = await createWrapper({ value: "test-key-123" });
|
|
135
|
-
await wrapper.find(".input-copy-button").trigger("click");
|
|
136
|
-
expect(navigator.clipboard.writeText).toHaveBeenCalledWith("test-key-123");
|
|
137
|
-
});
|
|
138
|
-
|
|
139
|
-
it("adds copied class to root after clicking", async () => {
|
|
140
|
-
wrapper = await createWrapper();
|
|
141
|
-
await wrapper.find(".input-copy-button").trigger("click");
|
|
142
|
-
await nextTick();
|
|
143
|
-
expect(wrapper.classes()).toContain("copied");
|
|
144
|
-
});
|
|
145
|
-
|
|
146
|
-
it("button shows copiedLabel after clicking", async () => {
|
|
147
|
-
wrapper = await createWrapper({ copiedLabel: "Done!" });
|
|
148
|
-
await wrapper.find(".input-copy-button").trigger("click");
|
|
149
|
-
await nextTick();
|
|
150
|
-
expect(wrapper.find(".input-copy-button").text()).toBe("Done!");
|
|
151
|
-
});
|
|
152
|
-
|
|
153
|
-
it("button aria-label updates to copiedLabel after clicking", async () => {
|
|
154
|
-
wrapper = await createWrapper({ copiedLabel: "Copied!" });
|
|
155
|
-
await wrapper.find(".input-copy-button").trigger("click");
|
|
156
|
-
await nextTick();
|
|
157
|
-
expect(wrapper.find(".input-copy-button").attributes("aria-label")).toBe("Copied!");
|
|
158
|
-
});
|
|
159
|
-
|
|
160
|
-
it("emits copy event with value on successful copy", async () => {
|
|
161
|
-
wrapper = await createWrapper({ value: "emit-test-key" });
|
|
162
|
-
await wrapper.find(".input-copy-button").trigger("click");
|
|
163
|
-
expect(wrapper.emitted("copy")).toBeTruthy();
|
|
164
|
-
expect(wrapper.emitted("copy")?.[0]).toEqual(["emit-test-key"]);
|
|
165
|
-
});
|
|
166
|
-
|
|
167
|
-
it("removes copied class after feedbackDuration elapses", async () => {
|
|
168
|
-
wrapper = await createWrapper({ feedbackDuration: 1000 });
|
|
169
|
-
await wrapper.find(".input-copy-button").trigger("click");
|
|
170
|
-
await nextTick();
|
|
171
|
-
expect(wrapper.classes()).toContain("copied");
|
|
172
|
-
await vi.advanceTimersByTimeAsync(1000);
|
|
173
|
-
expect(wrapper.classes()).not.toContain("copied");
|
|
174
|
-
});
|
|
175
|
-
|
|
176
|
-
it("does not throw when clipboard is unavailable", async () => {
|
|
177
|
-
vi.stubGlobal("navigator", {
|
|
178
|
-
clipboard: {
|
|
179
|
-
writeText: vi.fn().mockRejectedValue(new Error("Not allowed")),
|
|
180
|
-
},
|
|
181
|
-
});
|
|
182
|
-
wrapper = await createWrapper();
|
|
183
|
-
await expect(wrapper.find(".input-copy-button").trigger("click")).resolves.not.toThrow();
|
|
184
|
-
});
|
|
185
|
-
|
|
186
|
-
it("does not add copied class when clipboard fails", async () => {
|
|
187
|
-
vi.stubGlobal("navigator", {
|
|
188
|
-
clipboard: {
|
|
189
|
-
writeText: vi.fn().mockRejectedValue(new Error("Not allowed")),
|
|
190
|
-
},
|
|
191
|
-
});
|
|
192
|
-
wrapper = await createWrapper();
|
|
193
|
-
await wrapper.find(".input-copy-button").trigger("click");
|
|
194
|
-
await nextTick();
|
|
195
|
-
expect(wrapper.classes()).not.toContain("copied");
|
|
196
|
-
});
|
|
197
|
-
});
|
|
198
|
-
|
|
199
|
-
// ─── Accessibility ───────────────────────────────────────────────────────
|
|
200
|
-
|
|
201
|
-
describe("Accessibility", () => {
|
|
202
|
-
it("copy button has aria-label matching copyLabel by default", async () => {
|
|
203
|
-
wrapper = await createWrapper({ copyLabel: "Copy" });
|
|
204
|
-
expect(wrapper.find(".input-copy-button").attributes("aria-label")).toBe("Copy");
|
|
205
|
-
});
|
|
206
|
-
|
|
207
|
-
it("copy button type is button", async () => {
|
|
208
|
-
wrapper = await createWrapper();
|
|
209
|
-
expect(wrapper.find(".input-copy-button").attributes("type")).toBe("button");
|
|
210
|
-
});
|
|
211
|
-
});
|
|
212
|
-
});
|
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
|
2
|
-
|
|
3
|
-
exports[`InputCopyCore > Snapshots > custom labels 1`] = `
|
|
4
|
-
"<div class="input-copy-core" data-testid="input-copy-core"><input id="copy-input" type="text" readonly="" aria-readonly="true" class="input-copy-field" value="sk_live_abc123def456"><button type="button" aria-disabled="false" data-testid="input-button-core" data-theme="default" class="input-button-core inline input-copy-button" aria-label="Copy key">
|
|
5
|
-
<!--v-if-->
|
|
6
|
-
<!--v-if--><span class="button-text">Copy key</span>
|
|
7
|
-
<!--v-if-->
|
|
8
|
-
<!--v-if-->
|
|
9
|
-
</button></div>"
|
|
10
|
-
`;
|
|
11
|
-
|
|
12
|
-
exports[`InputCopyCore > Snapshots > default 1`] = `
|
|
13
|
-
"<div class="input-copy-core" data-testid="input-copy-core"><input id="copy-input" type="text" readonly="" aria-readonly="true" class="input-copy-field" value="sk_live_abc123def456"><button type="button" aria-disabled="false" data-testid="input-button-core" data-theme="default" class="input-button-core inline input-copy-button" aria-label="Copy">
|
|
14
|
-
<!--v-if-->
|
|
15
|
-
<!--v-if--><span class="button-text">Copy</span>
|
|
16
|
-
<!--v-if-->
|
|
17
|
-
<!--v-if-->
|
|
18
|
-
</button></div>"
|
|
19
|
-
`;
|
|
20
|
-
|
|
21
|
-
exports[`InputCopyCore > Snapshots > with styleClassPassthrough 1`] = `
|
|
22
|
-
"<div class="input-copy-core my-copy-input" data-testid="input-copy-core"><input id="copy-input" type="text" readonly="" aria-readonly="true" class="input-copy-field" value="sk_live_abc123def456"><button type="button" aria-disabled="false" data-testid="input-button-core" data-theme="default" class="input-button-core inline input-copy-button" aria-label="Copy">
|
|
23
|
-
<!--v-if-->
|
|
24
|
-
<!--v-if--><span class="button-text">Copy</span>
|
|
25
|
-
<!--v-if-->
|
|
26
|
-
<!--v-if-->
|
|
27
|
-
</button></div>"
|
|
28
|
-
`;
|