Skip to content

Commit cb6acc0

Browse files
committed
InputCopyCore - component added
1 parent 0d9131d commit cb6acc0

4 files changed

Lines changed: 461 additions & 0 deletions

File tree

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
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-input-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-input-border);
105+
padding-inline: var(--input-padding-inline);
106+
min-width: var(--input-min-height);
107+
background-color: var(--theme-button-secondary-surface);
108+
color: var(--theme-button-secondary-text);
109+
110+
&:hover {
111+
background-color: var(--theme-button-primary-surface);
112+
color: var(--theme-button-primary-text);
113+
}
114+
115+
&:focus-visible {
116+
outline: var(--form-element-outline-width-focus) solid var(--theme-input-outline-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>
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
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+
};

0 commit comments

Comments
 (0)