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
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
# InputCopy — Consumer Styling Guide
|
|
2
|
+
|
|
3
|
+
## Public token API
|
|
4
|
+
|
|
5
|
+
All `--input-copy-*` tokens are the stable override surface. Set them at any scope (global, page,
|
|
6
|
+
or instance) without touching the component itself.
|
|
7
|
+
|
|
8
|
+
| Token | Default | Controls |
|
|
9
|
+
|---|---|---|
|
|
10
|
+
| `--input-copy-input-bg` | `var(--slate-00)` | Input field background colour |
|
|
11
|
+
| `--input-copy-input-border` | `1px solid var(--slate-03)` | Input field border |
|
|
12
|
+
| `--input-copy-input-border-radius` | `0.4rem` | Input field corner rounding |
|
|
13
|
+
| `--input-copy-input-padding` | `0.8rem 1rem` | Input field padding |
|
|
14
|
+
| `--input-copy-input-text-color` | `#333` | Input text colour |
|
|
15
|
+
| `--input-copy-input-placeholder-color` | `#999` | Placeholder text colour |
|
|
16
|
+
| `--input-copy-input-font-size` | `0.95rem` | Input field font size |
|
|
17
|
+
| `--input-copy-button-bg` | `var(--theme-button-primary-surface)` | Copy button background |
|
|
18
|
+
| `--input-copy-button-bg-hover` | `var(--theme-button-primary-surface-hover)` | Copy button background on hover |
|
|
19
|
+
| `--input-copy-button-text-color` | `var(--theme-button-primary-text)` | Copy button text colour |
|
|
20
|
+
| `--input-copy-button-padding` | `0.8rem 1.2rem` | Copy button padding |
|
|
21
|
+
| `--input-copy-button-border-radius` | `0.4rem` | Copy button corner rounding |
|
|
22
|
+
| `--input-copy-button-gap` | `0.6rem` | Gap between icon and text in button |
|
|
23
|
+
| `--input-copy-description-color` | `#666` | Description text colour |
|
|
24
|
+
| `--input-copy-description-font-size` | `0.85rem` | Description font size |
|
|
25
|
+
| `--input-copy-description-margin` | `0.6rem 0 0 0` | Description margin |
|
|
26
|
+
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
## Global theming — app-level CSS file
|
|
30
|
+
|
|
31
|
+
Create `assets/styles/setup/07.components/input-copy.css` in the consuming app and set tokens on
|
|
32
|
+
`:root`. These values apply to every `InputCopy` instance across the site.
|
|
33
|
+
|
|
34
|
+
```css
|
|
35
|
+
/* assets/styles/setup/07.components/input-copy.css */
|
|
36
|
+
:root {
|
|
37
|
+
--input-copy-input-border-radius: 0.6rem;
|
|
38
|
+
--input-copy-input-padding: 1rem 1.2rem;
|
|
39
|
+
--input-copy-button-border-radius: 0.6rem;
|
|
40
|
+
--input-copy-button-gap: 0.8rem;
|
|
41
|
+
--input-copy-description-color: var(--slate-06);
|
|
42
|
+
}
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
## Per-instance overrides via inline styles
|
|
48
|
+
|
|
49
|
+
Override tokens directly on a single InputCopy instance:
|
|
50
|
+
|
|
51
|
+
```vue
|
|
52
|
+
<InputCopy
|
|
53
|
+
value="sk_live_abc123"
|
|
54
|
+
label="API key"
|
|
55
|
+
style="
|
|
56
|
+
--input-copy-input-border-radius: 0.8rem;
|
|
57
|
+
--input-copy-button-bg: var(--brand-success);
|
|
58
|
+
"
|
|
59
|
+
/>
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
---
|
|
63
|
+
|
|
64
|
+
## Page-scoped overrides
|
|
65
|
+
|
|
66
|
+
When input-copy appears in a specific page context (e.g., checkout success page), scope overrides
|
|
67
|
+
under the page wrapper:
|
|
68
|
+
|
|
69
|
+
```css
|
|
70
|
+
/* In the consuming page's unscoped <style> block */
|
|
71
|
+
.success-page {
|
|
72
|
+
.input-copy {
|
|
73
|
+
--input-copy-input-bg: var(--brand-surface-light);
|
|
74
|
+
--input-copy-input-border: 2px solid var(--brand-primary);
|
|
75
|
+
--input-copy-button-bg: var(--brand-primary);
|
|
76
|
+
--input-copy-button-bg-hover: var(--brand-primary-hover);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
---
|
|
82
|
+
|
|
83
|
+
## License key display pattern
|
|
84
|
+
|
|
85
|
+
Typical usage on a success page or dashboard to display a license key:
|
|
86
|
+
|
|
87
|
+
```vue
|
|
88
|
+
<InputCopy
|
|
89
|
+
:value="licenseKey"
|
|
90
|
+
label="License key"
|
|
91
|
+
description="Your license key is ready to use. Copy it and add to your component."
|
|
92
|
+
buttonText="Copy"
|
|
93
|
+
copiedText="Copied!"
|
|
94
|
+
@copied="showFeedback"
|
|
95
|
+
/>
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
```css
|
|
99
|
+
.success-page {
|
|
100
|
+
.input-copy {
|
|
101
|
+
max-width: 500px;
|
|
102
|
+
|
|
103
|
+
--input-copy-input-bg: #f9f9f9;
|
|
104
|
+
--input-copy-input-border: 1px solid var(--slate-02);
|
|
105
|
+
--input-copy-input-text-color: var(--slate-09);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
---
|
|
111
|
+
|
|
112
|
+
## Readonly input styling
|
|
113
|
+
|
|
114
|
+
The input field is always readonly to prevent accidental edits. Style it distinctly if needed:
|
|
115
|
+
|
|
116
|
+
```css
|
|
117
|
+
.input-copy {
|
|
118
|
+
.input-copy__input {
|
|
119
|
+
background-color: var(--slate-00);
|
|
120
|
+
color: var(--slate-09);
|
|
121
|
+
cursor: default;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
---
|
|
127
|
+
|
|
128
|
+
## Button text visibility on small screens
|
|
129
|
+
|
|
130
|
+
By default, the button text is hidden on screens < 480px. Override for always-visible text:
|
|
131
|
+
|
|
132
|
+
```css
|
|
133
|
+
.input-copy__button-text {
|
|
134
|
+
display: inline !important;
|
|
135
|
+
}
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
Or show only the icon on mobile and text on desktop (default behavior):
|
|
139
|
+
|
|
140
|
+
```css
|
|
141
|
+
/* Default: button shows only icon on mobile, icon + text on desktop (480px+) */
|
|
142
|
+
/* No override needed; this is the built-in behavior */
|
|
143
|
+
```
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="input-copy" :class="elementClasses">
|
|
3
|
+
<div class="input-copy__wrapper">
|
|
4
|
+
<input
|
|
5
|
+
type="text"
|
|
6
|
+
class="input-copy__input"
|
|
7
|
+
:value="value"
|
|
8
|
+
:aria-label="ariaLabel || label"
|
|
9
|
+
readonly
|
|
10
|
+
/>
|
|
11
|
+
<button
|
|
12
|
+
type="button"
|
|
13
|
+
class="input-copy__button"
|
|
14
|
+
:aria-label="`Copy ${ariaLabel || label || 'value'} to clipboard`"
|
|
15
|
+
:disabled="copied || isDisabled"
|
|
16
|
+
@click="handleCopy"
|
|
17
|
+
>
|
|
18
|
+
<Icon name="lucide:copy" class="input-copy__icon" />
|
|
19
|
+
<span class="input-copy__button-text">{{ copied ? copiedText : buttonText }}</span>
|
|
20
|
+
</button>
|
|
21
|
+
</div>
|
|
22
|
+
<p v-if="description" class="input-copy__description">{{ description }}</p>
|
|
23
|
+
</div>
|
|
24
|
+
</template>
|
|
25
|
+
|
|
26
|
+
<script setup lang="ts">
|
|
27
|
+
interface Props {
|
|
28
|
+
value: string;
|
|
29
|
+
label?: string;
|
|
30
|
+
ariaLabel?: string;
|
|
31
|
+
description?: string;
|
|
32
|
+
buttonText?: string;
|
|
33
|
+
copiedText?: string;
|
|
34
|
+
copiedDuration?: number;
|
|
35
|
+
isDisabled?: boolean;
|
|
36
|
+
styleClassPassthrough?: string | string[];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const props = withDefaults(defineProps<Props>(), {
|
|
40
|
+
label: undefined,
|
|
41
|
+
ariaLabel: undefined,
|
|
42
|
+
description: undefined,
|
|
43
|
+
buttonText: "Copy",
|
|
44
|
+
copiedText: "Copied!",
|
|
45
|
+
copiedDuration: 2000,
|
|
46
|
+
isDisabled: false,
|
|
47
|
+
styleClassPassthrough: () => [],
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
const emit = defineEmits<{
|
|
51
|
+
copy: [value: string];
|
|
52
|
+
copied: [value: string];
|
|
53
|
+
}>();
|
|
54
|
+
|
|
55
|
+
const copied = ref(false);
|
|
56
|
+
let copyTimeoutId: ReturnType<typeof setTimeout> | null = null;
|
|
57
|
+
|
|
58
|
+
const { elementClasses, resetElementClasses } = useStyleClassPassthrough(props.styleClassPassthrough);
|
|
59
|
+
|
|
60
|
+
const handleCopy = async () => {
|
|
61
|
+
try {
|
|
62
|
+
await navigator.clipboard.writeText(props.value);
|
|
63
|
+
copied.value = true;
|
|
64
|
+
emit("copy", props.value);
|
|
65
|
+
emit("copied", props.value);
|
|
66
|
+
|
|
67
|
+
if (copyTimeoutId) {
|
|
68
|
+
clearTimeout(copyTimeoutId);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
copyTimeoutId = setTimeout(() => {
|
|
72
|
+
copied.value = false;
|
|
73
|
+
copyTimeoutId = null;
|
|
74
|
+
}, props.copiedDuration);
|
|
75
|
+
} catch (err) {
|
|
76
|
+
console.error("Failed to copy to clipboard:", err);
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
watch(
|
|
81
|
+
() => props.styleClassPassthrough,
|
|
82
|
+
() => {
|
|
83
|
+
resetElementClasses(props.styleClassPassthrough);
|
|
84
|
+
},
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
onUnmounted(() => {
|
|
88
|
+
if (copyTimeoutId) {
|
|
89
|
+
clearTimeout(copyTimeoutId);
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
</script>
|
|
93
|
+
|
|
94
|
+
<style lang="css">
|
|
95
|
+
@layer components {
|
|
96
|
+
.input-copy {
|
|
97
|
+
--_input-bg: var(--input-copy-input-bg, var(--slate-00));
|
|
98
|
+
--_input-border: var(--input-copy-input-border, 1px solid var(--slate-03));
|
|
99
|
+
--_input-border-radius: var(--input-copy-input-border-radius, 0.4rem);
|
|
100
|
+
--_input-padding: var(--input-copy-input-padding, 0.8rem 1rem);
|
|
101
|
+
--_input-text-color: var(--input-copy-input-text-color, #333);
|
|
102
|
+
--_input-placeholder-color: var(--input-copy-input-placeholder-color, #999);
|
|
103
|
+
--_input-font-size: var(--input-copy-input-font-size, 0.95rem);
|
|
104
|
+
|
|
105
|
+
--_button-bg: var(--input-copy-button-bg, var(--theme-button-primary-surface));
|
|
106
|
+
--_button-bg-hover: var(--input-copy-button-bg-hover, var(--theme-button-primary-surface-hover));
|
|
107
|
+
--_button-text-color: var(--input-copy-button-text-color, var(--theme-button-primary-text));
|
|
108
|
+
--_button-padding: var(--input-copy-button-padding, 0.8rem 1.2rem);
|
|
109
|
+
--_button-border-radius: var(--input-copy-button-border-radius, 0.4rem);
|
|
110
|
+
--_button-gap: var(--input-copy-button-gap, 0.6rem);
|
|
111
|
+
|
|
112
|
+
--_description-color: var(--input-copy-description-color, #666);
|
|
113
|
+
--_description-font-size: var(--input-copy-description-font-size, 0.85rem);
|
|
114
|
+
--_description-margin: var(--input-copy-description-margin, 0.6rem 0 0 0);
|
|
115
|
+
|
|
116
|
+
display: flex;
|
|
117
|
+
flex-direction: column;
|
|
118
|
+
gap: 0.8rem;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
.input-copy__wrapper {
|
|
122
|
+
display: flex;
|
|
123
|
+
gap: 0.8rem;
|
|
124
|
+
align-items: stretch;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
.input-copy__input {
|
|
128
|
+
flex: 1;
|
|
129
|
+
min-width: 0;
|
|
130
|
+
|
|
131
|
+
padding: var(--_input-padding);
|
|
132
|
+
background-color: var(--_input-bg);
|
|
133
|
+
border: var(--_input-border);
|
|
134
|
+
border-radius: var(--_input-border-radius);
|
|
135
|
+
font-size: var(--_input-font-size);
|
|
136
|
+
color: var(--_input-text-color);
|
|
137
|
+
font-family: inherit;
|
|
138
|
+
|
|
139
|
+
&::placeholder {
|
|
140
|
+
color: var(--_input-placeholder-color);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
&:focus {
|
|
144
|
+
outline: 2px solid var(--_input-border-radius);
|
|
145
|
+
outline-offset: 2px;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
.input-copy__button {
|
|
150
|
+
display: flex;
|
|
151
|
+
align-items: center;
|
|
152
|
+
gap: var(--_button-gap);
|
|
153
|
+
padding: var(--_button-padding);
|
|
154
|
+
|
|
155
|
+
background-color: var(--_button-bg);
|
|
156
|
+
color: var(--_button-text-color);
|
|
157
|
+
border: none;
|
|
158
|
+
border-radius: var(--_button-border-radius);
|
|
159
|
+
font-size: var(--_input-font-size);
|
|
160
|
+
font-weight: 500;
|
|
161
|
+
cursor: pointer;
|
|
162
|
+
white-space: nowrap;
|
|
163
|
+
transition: background-color 0.2s ease;
|
|
164
|
+
|
|
165
|
+
&:hover:not(:disabled) {
|
|
166
|
+
background-color: var(--_button-bg-hover);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
&:disabled {
|
|
170
|
+
opacity: 0.6;
|
|
171
|
+
cursor: not-allowed;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
.input-copy__icon {
|
|
175
|
+
width: 1.2em;
|
|
176
|
+
height: 1.2em;
|
|
177
|
+
flex-shrink: 0;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
.input-copy__button-text {
|
|
181
|
+
display: none;
|
|
182
|
+
|
|
183
|
+
@media (min-width: 480px) {
|
|
184
|
+
display: inline;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
.input-copy__description {
|
|
190
|
+
margin: var(--_description-margin);
|
|
191
|
+
font-size: var(--_description-font-size);
|
|
192
|
+
color: var(--_description-color);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
</style>
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@nuxtjs/storybook";
|
|
2
|
+
import InputCopy from "../InputCopy.vue";
|
|
3
|
+
|
|
4
|
+
const meta: Meta<typeof InputCopy> = {
|
|
5
|
+
title: "Molecules/Input Copy",
|
|
6
|
+
component: InputCopy,
|
|
7
|
+
tags: ["autodocs"],
|
|
8
|
+
argTypes: {
|
|
9
|
+
value: { control: "text" },
|
|
10
|
+
label: { control: "text" },
|
|
11
|
+
description: { control: "text" },
|
|
12
|
+
buttonText: { control: "text" },
|
|
13
|
+
copiedText: { control: "text" },
|
|
14
|
+
copiedDuration: { control: "number" },
|
|
15
|
+
isDisabled: { control: "boolean" },
|
|
16
|
+
},
|
|
17
|
+
args: {
|
|
18
|
+
value: "sk_live_abc123defgh456xyz789uvw",
|
|
19
|
+
label: "License key",
|
|
20
|
+
description: "Your license key is ready to use. Copy it to embed in your application.",
|
|
21
|
+
buttonText: "Copy",
|
|
22
|
+
copiedText: "Copied!",
|
|
23
|
+
copiedDuration: 2000,
|
|
24
|
+
isDisabled: false,
|
|
25
|
+
},
|
|
26
|
+
parameters: {
|
|
27
|
+
docs: {
|
|
28
|
+
description: {
|
|
29
|
+
component:
|
|
30
|
+
"A readonly input field with a copy-to-clipboard button. Displays a value that users can quickly copy to their clipboard. " +
|
|
31
|
+
"Shows feedback ('Copied!') for a brief duration after successful copy. " +
|
|
32
|
+
"Emits `copy` and `copied` events. Useful for license keys, API tokens, or any copyable identifier.",
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export default meta;
|
|
39
|
+
type Story = StoryObj<typeof meta>;
|
|
40
|
+
|
|
41
|
+
const storyWrapper = (args: Record<string, unknown>) => ({
|
|
42
|
+
components: { InputCopy },
|
|
43
|
+
setup() {
|
|
44
|
+
const handleCopy = (value: string) => {
|
|
45
|
+
console.log(`Copied: ${value}`);
|
|
46
|
+
};
|
|
47
|
+
const handleCopied = (value: string) => {
|
|
48
|
+
console.log(`Copied event fired: ${value}`);
|
|
49
|
+
};
|
|
50
|
+
return { args, handleCopy, handleCopied };
|
|
51
|
+
},
|
|
52
|
+
template: `
|
|
53
|
+
<div style="padding: 2rem; background: #f5f5f5; border-radius: 0.5rem; max-width: 600px;">
|
|
54
|
+
<InputCopy v-bind="args" @copy="handleCopy" @copied="handleCopied" />
|
|
55
|
+
</div>
|
|
56
|
+
`,
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
export const Default: Story = {
|
|
60
|
+
render: (args) => storyWrapper(args),
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
export const LicenseKey: Story = {
|
|
64
|
+
args: {
|
|
65
|
+
value: "gmh_lic_single_site_abc123def456ghi789",
|
|
66
|
+
label: "License key",
|
|
67
|
+
description: "Your GuideMyHair license key. Keep this safe and use it to embed the widget.",
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
export const APIToken: Story = {
|
|
72
|
+
args: {
|
|
73
|
+
value: "ghp_16C7e42F292c6912E7710c838347Ae178B4a",
|
|
74
|
+
label: "API token",
|
|
75
|
+
description: "Your API token for authentication. Do not share this token.",
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
export const ShortValue: Story = {
|
|
80
|
+
args: {
|
|
81
|
+
value: "ABC-123-XYZ",
|
|
82
|
+
label: "Invite code",
|
|
83
|
+
description: "Share this invite code with your team members.",
|
|
84
|
+
},
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
export const Disabled: Story = {
|
|
88
|
+
args: {
|
|
89
|
+
value: "Disabled input",
|
|
90
|
+
label: "License key",
|
|
91
|
+
isDisabled: true,
|
|
92
|
+
description: "This input is disabled and cannot be copied.",
|
|
93
|
+
},
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
export const NoDescription: Story = {
|
|
97
|
+
args: {
|
|
98
|
+
value: "sk_test_51234567890abcdef",
|
|
99
|
+
label: "Secret key",
|
|
100
|
+
description: undefined,
|
|
101
|
+
},
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
export const LongValue: Story = {
|
|
105
|
+
args: {
|
|
106
|
+
value: "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.POdHyP_DQkT3WTZhu9qLWSwVe8ZlHfHEPBOzzVI3vFA",
|
|
107
|
+
label: "JWT token",
|
|
108
|
+
description: "Your JWT authentication token. This is a long-lived token used for API access.",
|
|
109
|
+
},
|
|
110
|
+
};
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
2
|
+
import { mount, flushPromises } from "@vue/test-utils";
|
|
3
|
+
import InputCopy from "../InputCopy.vue";
|
|
4
|
+
|
|
5
|
+
describe("InputCopy", () => {
|
|
6
|
+
beforeEach(() => {
|
|
7
|
+
vi.clearAllMocks();
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
it("renders readonly input with value", () => {
|
|
11
|
+
const wrapper = mount(InputCopy, {
|
|
12
|
+
props: {
|
|
13
|
+
value: "test-key-123",
|
|
14
|
+
label: "Test key",
|
|
15
|
+
},
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
const input = wrapper.find("input");
|
|
19
|
+
expect(input.element.value).toBe("test-key-123");
|
|
20
|
+
expect(input.element.readOnly).toBe(true);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("displays description when provided", () => {
|
|
24
|
+
const wrapper = mount(InputCopy, {
|
|
25
|
+
props: {
|
|
26
|
+
value: "test-key",
|
|
27
|
+
description: "This is your license key.",
|
|
28
|
+
},
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
expect(wrapper.text()).toContain("This is your license key.");
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("does not display description when not provided", () => {
|
|
35
|
+
const wrapper = mount(InputCopy, {
|
|
36
|
+
props: {
|
|
37
|
+
value: "test-key",
|
|
38
|
+
description: undefined,
|
|
39
|
+
},
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
const description = wrapper.find(".input-copy__description");
|
|
43
|
+
expect(description.exists()).toBe(false);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("renders copy button with default text", () => {
|
|
47
|
+
const wrapper = mount(InputCopy, {
|
|
48
|
+
props: {
|
|
49
|
+
value: "test-key",
|
|
50
|
+
buttonText: "Copy",
|
|
51
|
+
},
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
const button = wrapper.find("button");
|
|
55
|
+
expect(button.text()).toContain("Copy");
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("renders copy button with custom text", () => {
|
|
59
|
+
const wrapper = mount(InputCopy, {
|
|
60
|
+
props: {
|
|
61
|
+
value: "test-key",
|
|
62
|
+
buttonText: "Copy to clipboard",
|
|
63
|
+
},
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
const button = wrapper.find("button");
|
|
67
|
+
expect(button.text()).toContain("Copy to clipboard");
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("disables button when isDisabled is true", () => {
|
|
71
|
+
const wrapper = mount(InputCopy, {
|
|
72
|
+
props: {
|
|
73
|
+
value: "test-key",
|
|
74
|
+
isDisabled: true,
|
|
75
|
+
},
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
const button = wrapper.find("button");
|
|
79
|
+
expect(button.attributes("disabled")).toBeDefined();
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it("button is not disabled when isDisabled is false", () => {
|
|
83
|
+
const wrapper = mount(InputCopy, {
|
|
84
|
+
props: {
|
|
85
|
+
value: "test-key",
|
|
86
|
+
isDisabled: false,
|
|
87
|
+
},
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
const button = wrapper.find("button");
|
|
91
|
+
expect(button.attributes("disabled")).toBeUndefined();
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("has proper aria-label on input from label prop", () => {
|
|
95
|
+
const wrapper = mount(InputCopy, {
|
|
96
|
+
props: {
|
|
97
|
+
value: "test-key",
|
|
98
|
+
label: "License key",
|
|
99
|
+
},
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
const input = wrapper.find("input");
|
|
103
|
+
expect(input.attributes("aria-label")).toBe("License key");
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it("has proper aria-label on input from ariaLabel prop (takes precedence)", () => {
|
|
107
|
+
const wrapper = mount(InputCopy, {
|
|
108
|
+
props: {
|
|
109
|
+
value: "test-key",
|
|
110
|
+
label: "License key",
|
|
111
|
+
ariaLabel: "Unique license identifier",
|
|
112
|
+
},
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
const input = wrapper.find("input");
|
|
116
|
+
expect(input.attributes("aria-label")).toBe("Unique license identifier");
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it("has proper aria-label on button", () => {
|
|
120
|
+
const wrapper = mount(InputCopy, {
|
|
121
|
+
props: {
|
|
122
|
+
value: "test-key",
|
|
123
|
+
label: "API token",
|
|
124
|
+
},
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
const button = wrapper.find("button");
|
|
128
|
+
expect(button.attributes("aria-label")).toContain("Copy");
|
|
129
|
+
expect(button.attributes("aria-label")).toContain("API token");
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it("has icon element in button", () => {
|
|
133
|
+
const wrapper = mount(InputCopy, {
|
|
134
|
+
props: {
|
|
135
|
+
value: "test-key",
|
|
136
|
+
},
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
const icon = wrapper.find(".input-copy__icon");
|
|
140
|
+
expect(icon.exists()).toBe(true);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it("input wrapper contains both input and button", () => {
|
|
144
|
+
const wrapper = mount(InputCopy, {
|
|
145
|
+
props: {
|
|
146
|
+
value: "test-key",
|
|
147
|
+
},
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
const wrapper_el = wrapper.find(".input-copy__wrapper");
|
|
151
|
+
expect(wrapper_el.exists()).toBe(true);
|
|
152
|
+
|
|
153
|
+
const input = wrapper_el.find("input");
|
|
154
|
+
const button = wrapper_el.find("button");
|
|
155
|
+
|
|
156
|
+
expect(input.exists()).toBe(true);
|
|
157
|
+
expect(button.exists()).toBe(true);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it("renders with correct component classes", () => {
|
|
161
|
+
const wrapper = mount(InputCopy, {
|
|
162
|
+
props: {
|
|
163
|
+
value: "test-key",
|
|
164
|
+
},
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
expect(wrapper.classes()).toContain("input-copy");
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it("applies styleClassPassthrough classes", () => {
|
|
171
|
+
const wrapper = mount(InputCopy, {
|
|
172
|
+
props: {
|
|
173
|
+
value: "test-key",
|
|
174
|
+
styleClassPassthrough: ["custom-class", "another-class"],
|
|
175
|
+
},
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
expect(wrapper.classes()).toContain("custom-class");
|
|
179
|
+
expect(wrapper.classes()).toContain("another-class");
|
|
180
|
+
});
|
|
181
|
+
});
|