@aleph-alpha/ui-library 1.16.1 → 1.17.0
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/dist/system/index.d.ts +585 -402
- package/dist/system/lib.js +13060 -12822
- package/package.json +2 -2
- package/src/components/UiChip/UiChip.stories.ts +239 -0
- package/src/components/UiChip/UiChip.vue +128 -0
- package/src/components/UiChip/__tests__/UiChip.test.ts +102 -0
- package/src/components/UiChip/index.ts +2 -0
- package/src/components/UiChip/types.ts +50 -0
- package/src/components/UiDropdownMenu/UiDropdownMenu.stories.ts +259 -1
- package/src/components/UiField/UiField.stories.ts +589 -249
- package/src/components/UiField/UiField.vue +87 -2
- package/src/components/UiField/UiFieldDescription.vue +22 -1
- package/src/components/UiField/UiFieldLabel.vue +2 -0
- package/src/components/UiField/UiFieldLabelInfo.vue +22 -0
- package/src/components/UiField/index.ts +1 -0
- package/src/components/UiField/keys.ts +5 -0
- package/src/components/UiField/types.ts +86 -1
- package/src/components/UiSelect/__tests__/UiSelectTrigger.test.ts +47 -2
- package/src/components/UiToggle/UiToggle.stories.ts +54 -1
- package/src/components/UiToggle/__tests__/UiToggle.test.ts +15 -0
- package/src/components/UiToggle/types.ts +1 -1
- package/src/components/UiToggleGroup/__tests__/UiToggleGroup.test.ts +21 -0
- package/src/components/UiToggleGroup/types.ts +2 -2
- package/src/components/core/button/index.ts +5 -0
- package/src/components/core/field/FieldLabel.vue +1 -1
- package/src/components/core/field/index.ts +5 -5
- package/src/components/core/select/SelectTrigger.vue +1 -1
- package/src/components/core/toggle/Toggle.vue +3 -3
- package/src/components/core/toggle/index.ts +6 -3
- package/src/components/core/toggle-group/index.ts +6 -3
- package/src/components/index.ts +1 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aleph-alpha/ui-library",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.17.0",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/system/lib.js",
|
|
@@ -71,8 +71,8 @@
|
|
|
71
71
|
"vitest": "^3.0.0",
|
|
72
72
|
"vue-tsc": "^2.2.12",
|
|
73
73
|
"wait-on": "9.0.3",
|
|
74
|
-
"@aleph-alpha/prettier-config-frontend": "0.4.0",
|
|
75
74
|
"@aleph-alpha/eslint-config-frontend": "0.5.0",
|
|
75
|
+
"@aleph-alpha/prettier-config-frontend": "0.4.0",
|
|
76
76
|
"@aleph-alpha/tsconfig-frontend": "0.5.0"
|
|
77
77
|
},
|
|
78
78
|
"peerDependencies": {
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/vue3-vite';
|
|
2
|
+
import { ref } from 'vue';
|
|
3
|
+
import UiChip from './UiChip.vue';
|
|
4
|
+
import { UiIcon } from '../UiIcon';
|
|
5
|
+
|
|
6
|
+
const TagIcon = {
|
|
7
|
+
components: { UiIcon },
|
|
8
|
+
template: '<UiIcon name="tag" :size="14" />',
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
const InfoIcon = {
|
|
12
|
+
components: { UiIcon },
|
|
13
|
+
template: '<UiIcon name="info" :size="14" />',
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const meta: Meta<typeof UiChip> = {
|
|
17
|
+
title: 'Components/UiChip',
|
|
18
|
+
component: UiChip,
|
|
19
|
+
tags: ['autodocs'],
|
|
20
|
+
argTypes: {
|
|
21
|
+
variant: {
|
|
22
|
+
control: 'select',
|
|
23
|
+
options: ['static', 'selectable', 'removable'],
|
|
24
|
+
description: 'Behavior variant of the chip',
|
|
25
|
+
},
|
|
26
|
+
selectable: {
|
|
27
|
+
control: 'boolean',
|
|
28
|
+
description: 'Enables selection for removable chips',
|
|
29
|
+
},
|
|
30
|
+
disabled: {
|
|
31
|
+
control: 'boolean',
|
|
32
|
+
description: 'Disables interactions',
|
|
33
|
+
},
|
|
34
|
+
startIcon: {
|
|
35
|
+
control: false,
|
|
36
|
+
description: 'Optional leading icon',
|
|
37
|
+
},
|
|
38
|
+
removeLabel: {
|
|
39
|
+
control: 'text',
|
|
40
|
+
description: 'Accessible label for remove button',
|
|
41
|
+
},
|
|
42
|
+
modelValue: {
|
|
43
|
+
control: 'boolean',
|
|
44
|
+
description: 'Selection state for interactive chips',
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
args: {
|
|
48
|
+
variant: 'static',
|
|
49
|
+
selectable: false,
|
|
50
|
+
disabled: false,
|
|
51
|
+
removeLabel: 'Remove chip',
|
|
52
|
+
modelValue: false,
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
export default meta;
|
|
57
|
+
|
|
58
|
+
type Story = StoryObj<typeof UiChip>;
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Static chips are non-interactive and work as labels/tags.
|
|
62
|
+
*/
|
|
63
|
+
export const Static: Story = {
|
|
64
|
+
render: () => ({
|
|
65
|
+
components: { UiChip },
|
|
66
|
+
setup() {
|
|
67
|
+
return { TagIcon };
|
|
68
|
+
},
|
|
69
|
+
template: `
|
|
70
|
+
<div class="flex flex-wrap items-center gap-3">
|
|
71
|
+
<UiChip variant="static">Data Analysis</UiChip>
|
|
72
|
+
<UiChip variant="static" :start-icon="TagIcon">With icon</UiChip>
|
|
73
|
+
<UiChip variant="static" disabled>Disabled</UiChip>
|
|
74
|
+
</div>
|
|
75
|
+
`,
|
|
76
|
+
}),
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Selectable chips support hover, focus, selected and disabled states.
|
|
81
|
+
*/
|
|
82
|
+
export const Selectable: Story = {
|
|
83
|
+
render: () => ({
|
|
84
|
+
components: { UiChip },
|
|
85
|
+
setup() {
|
|
86
|
+
const isSelected = ref(false);
|
|
87
|
+
const isActive = ref(true);
|
|
88
|
+
return { isSelected, isActive, InfoIcon };
|
|
89
|
+
},
|
|
90
|
+
template: `
|
|
91
|
+
<div class="flex flex-wrap items-center gap-3">
|
|
92
|
+
<UiChip v-model="isSelected" variant="selectable">Default</UiChip>
|
|
93
|
+
<UiChip v-model="isActive" variant="selectable" :start-icon="InfoIcon">Active</UiChip>
|
|
94
|
+
<UiChip variant="selectable" disabled>Disabled</UiChip>
|
|
95
|
+
</div>
|
|
96
|
+
`,
|
|
97
|
+
}),
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Removable chips include a dismiss action and can also be selectable.
|
|
102
|
+
*/
|
|
103
|
+
export const Removable: Story = {
|
|
104
|
+
render: () => ({
|
|
105
|
+
components: { UiChip },
|
|
106
|
+
setup() {
|
|
107
|
+
const selectedRemovable = ref(true);
|
|
108
|
+
const chips = ref(['Data Analysis', 'Report Generation']);
|
|
109
|
+
|
|
110
|
+
function removeChip(label: string) {
|
|
111
|
+
chips.value = chips.value.filter((chip) => chip !== label);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return { chips, selectedRemovable, removeChip, TagIcon };
|
|
115
|
+
},
|
|
116
|
+
template: `
|
|
117
|
+
<div class="flex flex-wrap items-center gap-3">
|
|
118
|
+
<UiChip
|
|
119
|
+
v-for="chip in chips"
|
|
120
|
+
:key="chip"
|
|
121
|
+
variant="removable"
|
|
122
|
+
:start-icon="TagIcon"
|
|
123
|
+
@remove="removeChip(chip)"
|
|
124
|
+
>
|
|
125
|
+
{{ chip }}
|
|
126
|
+
</UiChip>
|
|
127
|
+
|
|
128
|
+
<UiChip
|
|
129
|
+
v-model="selectedRemovable"
|
|
130
|
+
variant="removable"
|
|
131
|
+
selectable
|
|
132
|
+
:start-icon="TagIcon"
|
|
133
|
+
>
|
|
134
|
+
Selectable removable
|
|
135
|
+
</UiChip>
|
|
136
|
+
</div>
|
|
137
|
+
`,
|
|
138
|
+
}),
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Chips with and without a leading icon.
|
|
143
|
+
*/
|
|
144
|
+
export const WithAndWithoutLeadingIcon: Story = {
|
|
145
|
+
render: () => ({
|
|
146
|
+
components: { UiChip },
|
|
147
|
+
setup() {
|
|
148
|
+
const selectableWithoutIcon = ref(true);
|
|
149
|
+
const selectableWithIcon = ref(false);
|
|
150
|
+
return { TagIcon, selectableWithoutIcon, selectableWithIcon };
|
|
151
|
+
},
|
|
152
|
+
template: `
|
|
153
|
+
<div class="flex flex-wrap items-center gap-3">
|
|
154
|
+
<UiChip variant="static">No icon</UiChip>
|
|
155
|
+
<UiChip variant="static" :start-icon="TagIcon">Leading icon</UiChip>
|
|
156
|
+
<UiChip v-model="selectableWithoutIcon" variant="selectable">Selectable no icon</UiChip>
|
|
157
|
+
<UiChip v-model="selectableWithIcon" variant="selectable" :start-icon="TagIcon">
|
|
158
|
+
Selectable with icon
|
|
159
|
+
</UiChip>
|
|
160
|
+
</div>
|
|
161
|
+
`,
|
|
162
|
+
}),
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Group usage with keyboard-focusable interactive chips.
|
|
167
|
+
* Focus order is left-to-right, top-to-bottom across interactive chips and remove buttons.
|
|
168
|
+
*/
|
|
169
|
+
export const Group: Story = {
|
|
170
|
+
render: () => ({
|
|
171
|
+
components: { UiChip },
|
|
172
|
+
setup() {
|
|
173
|
+
const firstSelected = ref(true);
|
|
174
|
+
const secondSelected = ref(false);
|
|
175
|
+
const removableSelected = ref(true);
|
|
176
|
+
|
|
177
|
+
return { firstSelected, secondSelected, removableSelected, TagIcon, InfoIcon };
|
|
178
|
+
},
|
|
179
|
+
template: `
|
|
180
|
+
<div class="flex max-w-[620px] flex-col gap-3">
|
|
181
|
+
<p class="text-sm text-content-on-surface-muted">
|
|
182
|
+
Interactive group example inspired by the Figma chips group. Use Tab to verify focus order.
|
|
183
|
+
</p>
|
|
184
|
+
|
|
185
|
+
<div class="flex flex-wrap items-center gap-2">
|
|
186
|
+
<UiChip v-model="firstSelected" variant="selectable" :start-icon="TagIcon">
|
|
187
|
+
Data Analysis
|
|
188
|
+
</UiChip>
|
|
189
|
+
|
|
190
|
+
<UiChip v-model="secondSelected" variant="selectable">
|
|
191
|
+
Report Generation
|
|
192
|
+
</UiChip>
|
|
193
|
+
|
|
194
|
+
<UiChip variant="removable" :start-icon="InfoIcon">Workflow Automation</UiChip>
|
|
195
|
+
|
|
196
|
+
<UiChip v-model="removableSelected" variant="removable" selectable>
|
|
197
|
+
Add
|
|
198
|
+
</UiChip>
|
|
199
|
+
</div>
|
|
200
|
+
</div>
|
|
201
|
+
`,
|
|
202
|
+
}),
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Focus order for interactive chips:
|
|
207
|
+
* selectable chip -> removable chip action -> removable chip dismiss action.
|
|
208
|
+
*/
|
|
209
|
+
export const FocusOrderInteractive: Story = {
|
|
210
|
+
render: () => ({
|
|
211
|
+
components: { UiChip },
|
|
212
|
+
setup() {
|
|
213
|
+
const first = ref(false);
|
|
214
|
+
const second = ref(true);
|
|
215
|
+
return { first, second, TagIcon };
|
|
216
|
+
},
|
|
217
|
+
template: `
|
|
218
|
+
<div class="flex max-w-[620px] flex-col gap-3">
|
|
219
|
+
<p class="text-sm text-content-on-surface-muted">
|
|
220
|
+
Press Tab to verify focus order from left to right and top to bottom.
|
|
221
|
+
</p>
|
|
222
|
+
|
|
223
|
+
<div class="flex flex-wrap items-center gap-2">
|
|
224
|
+
<UiChip v-model="first" variant="selectable" :start-icon="TagIcon">
|
|
225
|
+
Focus 1
|
|
226
|
+
</UiChip>
|
|
227
|
+
|
|
228
|
+
<UiChip v-model="second" variant="removable" selectable :start-icon="TagIcon">
|
|
229
|
+
Focus 2 and 3
|
|
230
|
+
</UiChip>
|
|
231
|
+
|
|
232
|
+
<UiChip variant="removable" :start-icon="TagIcon">
|
|
233
|
+
Focus 4
|
|
234
|
+
</UiChip>
|
|
235
|
+
</div>
|
|
236
|
+
</div>
|
|
237
|
+
`,
|
|
238
|
+
}),
|
|
239
|
+
};
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed } from 'vue';
|
|
3
|
+
import { cn } from '@/lib/utils';
|
|
4
|
+
import { UiIcon } from '@/components/UiIcon';
|
|
5
|
+
import type { UiChipProps } from './types';
|
|
6
|
+
|
|
7
|
+
defineOptions({
|
|
8
|
+
name: 'UiChip',
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
const props = withDefaults(defineProps<UiChipProps>(), {
|
|
12
|
+
variant: 'static',
|
|
13
|
+
selectable: false,
|
|
14
|
+
disabled: false,
|
|
15
|
+
removeLabel: 'Remove chip',
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
const emits = defineEmits<{
|
|
19
|
+
(e: 'remove'): void;
|
|
20
|
+
}>();
|
|
21
|
+
|
|
22
|
+
const modelValue = defineModel<boolean>({ default: false });
|
|
23
|
+
|
|
24
|
+
const isRemovable = computed(() => props.variant === 'removable');
|
|
25
|
+
const isSelectable = computed(
|
|
26
|
+
() => props.variant === 'selectable' || (props.variant === 'removable' && props.selectable),
|
|
27
|
+
);
|
|
28
|
+
const isSelected = computed(() => isSelectable.value && modelValue.value);
|
|
29
|
+
|
|
30
|
+
function toggleSelected() {
|
|
31
|
+
if (!isSelectable.value || props.disabled) {
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
modelValue.value = !modelValue.value;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function removeChip(event: MouseEvent) {
|
|
39
|
+
event.stopPropagation();
|
|
40
|
+
if (props.disabled) {
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
emits('remove');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function handleRemovablePrimaryAction() {
|
|
47
|
+
if (isSelectable.value) {
|
|
48
|
+
toggleSelected();
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const chipBaseClass = computed(() =>
|
|
53
|
+
cn(
|
|
54
|
+
'inline-flex items-center rounded-full border border-border-button-outlined bg-background-surface-secondary text-content-on-accent-soft text-xs font-medium transition-[color,background-color,border-color]',
|
|
55
|
+
'data-[selected=true]:bg-background-accent-soft data-[selected=true]:border-accent-default',
|
|
56
|
+
'data-[selected=true]:hover:bg-background-accent-soft data-[selected=true]:hover:border-accent-default',
|
|
57
|
+
props.disabled && 'opacity-50',
|
|
58
|
+
),
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
const standaloneChipClass = computed(() =>
|
|
62
|
+
cn(
|
|
63
|
+
chipBaseClass.value,
|
|
64
|
+
'h-7 gap-1.5 px-3',
|
|
65
|
+
isSelectable.value &&
|
|
66
|
+
'cursor-pointer hover:bg-background-surface-secondary hover:border-border-button-outlined focus-visible:outline-none focus-visible:border-border-surface-focus-ring disabled:pointer-events-none',
|
|
67
|
+
),
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
const removableChipClass = computed(() =>
|
|
71
|
+
cn(
|
|
72
|
+
chipBaseClass.value,
|
|
73
|
+
'h-7 gap-0.5 pl-3 pr-1',
|
|
74
|
+
'focus-within:border-border-surface-focus-ring',
|
|
75
|
+
),
|
|
76
|
+
);
|
|
77
|
+
</script>
|
|
78
|
+
|
|
79
|
+
<template>
|
|
80
|
+
<span
|
|
81
|
+
v-if="!isRemovable && !isSelectable"
|
|
82
|
+
data-slot="chip"
|
|
83
|
+
:aria-disabled="props.disabled ? 'true' : undefined"
|
|
84
|
+
:class="standaloneChipClass"
|
|
85
|
+
>
|
|
86
|
+
<component :is="props.startIcon" v-if="props.startIcon" class="size-3.5 shrink-0" />
|
|
87
|
+
<slot />
|
|
88
|
+
</span>
|
|
89
|
+
|
|
90
|
+
<button
|
|
91
|
+
v-else-if="!isRemovable"
|
|
92
|
+
data-slot="chip"
|
|
93
|
+
:data-selected="isSelected"
|
|
94
|
+
:disabled="props.disabled"
|
|
95
|
+
:aria-pressed="isSelected ? 'true' : 'false'"
|
|
96
|
+
type="button"
|
|
97
|
+
:class="standaloneChipClass"
|
|
98
|
+
@click="toggleSelected"
|
|
99
|
+
>
|
|
100
|
+
<component :is="props.startIcon" v-if="props.startIcon" class="size-3.5 shrink-0" />
|
|
101
|
+
<slot />
|
|
102
|
+
</button>
|
|
103
|
+
|
|
104
|
+
<div v-else data-slot="chip" :data-selected="isSelected" :class="removableChipClass">
|
|
105
|
+
<button
|
|
106
|
+
type="button"
|
|
107
|
+
class="inline-flex min-w-0 items-center gap-1.5 rounded-full outline-none"
|
|
108
|
+
:class="isSelectable ? 'cursor-pointer' : 'cursor-default'"
|
|
109
|
+
:disabled="props.disabled"
|
|
110
|
+
:aria-pressed="isSelectable ? (isSelected ? 'true' : 'false') : undefined"
|
|
111
|
+
@click="handleRemovablePrimaryAction"
|
|
112
|
+
>
|
|
113
|
+
<component :is="props.startIcon" v-if="props.startIcon" class="size-3.5 shrink-0" />
|
|
114
|
+
<slot />
|
|
115
|
+
</button>
|
|
116
|
+
|
|
117
|
+
<button
|
|
118
|
+
data-slot="chip-remove"
|
|
119
|
+
type="button"
|
|
120
|
+
class="inline-flex size-5 items-center justify-center rounded-full outline-none hover:bg-hover-default focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none"
|
|
121
|
+
:aria-label="props.removeLabel"
|
|
122
|
+
:disabled="props.disabled"
|
|
123
|
+
@click="removeChip"
|
|
124
|
+
>
|
|
125
|
+
<UiIcon name="x" :size="12" class="text-content-on-surface-primary" />
|
|
126
|
+
</button>
|
|
127
|
+
</div>
|
|
128
|
+
</template>
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { render } from '@testing-library/vue';
|
|
2
|
+
import userEvent from '@testing-library/user-event';
|
|
3
|
+
import { describe, expect, test } from 'vitest';
|
|
4
|
+
import { MockIcon } from '@/__tests__/test-utils';
|
|
5
|
+
import UiChip from '../UiChip.vue';
|
|
6
|
+
|
|
7
|
+
describe('UiChip', () => {
|
|
8
|
+
test('renders static chip content', () => {
|
|
9
|
+
const { getByText } = render(UiChip, {
|
|
10
|
+
props: { variant: 'static' },
|
|
11
|
+
slots: { default: 'Data Analysis' },
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
expect(getByText('Data Analysis')).toBeInTheDocument();
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
test('renders start icon when provided', () => {
|
|
18
|
+
const { getByTestId } = render(UiChip, {
|
|
19
|
+
props: {
|
|
20
|
+
variant: 'static',
|
|
21
|
+
startIcon: MockIcon,
|
|
22
|
+
},
|
|
23
|
+
slots: { default: 'Chip' },
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
expect(getByTestId('mock-icon')).toBeInTheDocument();
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test('toggles selected state for selectable chips', async () => {
|
|
30
|
+
const user = userEvent.setup();
|
|
31
|
+
const { getByRole } = render(UiChip, {
|
|
32
|
+
props: { variant: 'selectable' },
|
|
33
|
+
slots: { default: 'Selectable chip' },
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
const chipButton = getByRole('button');
|
|
37
|
+
|
|
38
|
+
expect(chipButton).toHaveAttribute('aria-pressed', 'false');
|
|
39
|
+
await user.click(chipButton);
|
|
40
|
+
expect(chipButton).toHaveAttribute('aria-pressed', 'true');
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test('renders remove button for removable chips', () => {
|
|
44
|
+
const { getByRole } = render(UiChip, {
|
|
45
|
+
props: { variant: 'removable' },
|
|
46
|
+
slots: { default: 'Removable chip' },
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
expect(getByRole('button', { name: 'Removable chip' })).toBeInTheDocument();
|
|
50
|
+
expect(getByRole('button', { name: 'Remove chip' })).toBeInTheDocument();
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test('tabs focus from chip body to remove button for removable chips', async () => {
|
|
54
|
+
const user = userEvent.setup();
|
|
55
|
+
const { getByRole } = render(UiChip, {
|
|
56
|
+
props: { variant: 'removable' },
|
|
57
|
+
slots: { default: 'Removable chip' },
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
const chipBodyButton = getByRole('button', { name: 'Removable chip' });
|
|
61
|
+
const removeButton = getByRole('button', { name: 'Remove chip' });
|
|
62
|
+
|
|
63
|
+
await user.tab();
|
|
64
|
+
expect(chipBodyButton).toHaveFocus();
|
|
65
|
+
|
|
66
|
+
await user.tab();
|
|
67
|
+
expect(removeButton).toHaveFocus();
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test('emits remove when remove button is clicked', async () => {
|
|
71
|
+
const user = userEvent.setup();
|
|
72
|
+
const { getByRole, emitted } = render(UiChip, {
|
|
73
|
+
props: { variant: 'removable' },
|
|
74
|
+
slots: { default: 'Removable chip' },
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
await user.click(getByRole('button', { name: 'Remove chip' }));
|
|
78
|
+
|
|
79
|
+
expect(emitted('remove')).toHaveLength(1);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test('disables interactions when disabled', async () => {
|
|
83
|
+
const user = userEvent.setup();
|
|
84
|
+
const { getByRole, emitted } = render(UiChip, {
|
|
85
|
+
props: {
|
|
86
|
+
variant: 'removable',
|
|
87
|
+
selectable: true,
|
|
88
|
+
disabled: true,
|
|
89
|
+
},
|
|
90
|
+
slots: { default: 'Disabled chip' },
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
const selectableButton = getByRole('button', { name: 'Disabled chip' });
|
|
94
|
+
const removeButton = getByRole('button', { name: 'Remove chip' });
|
|
95
|
+
|
|
96
|
+
expect(selectableButton).toBeDisabled();
|
|
97
|
+
expect(removeButton).toBeDisabled();
|
|
98
|
+
|
|
99
|
+
await user.click(removeButton);
|
|
100
|
+
expect(emitted('remove')).toBeUndefined();
|
|
101
|
+
});
|
|
102
|
+
});
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { Component } from 'vue';
|
|
2
|
+
|
|
3
|
+
export type UiChipVariant = 'static' | 'selectable' | 'removable';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Compact element used to represent a choice, tag, filter, or action.
|
|
7
|
+
* Static and selectable chips share the same base styling, while removable chips
|
|
8
|
+
* expose an additional dismiss action.
|
|
9
|
+
* @category Form Inputs
|
|
10
|
+
* @useCases filters, selected options, tags, quick actions
|
|
11
|
+
* @keywords chip, tag, filter, removable, selectable, dismiss
|
|
12
|
+
* @related UiBadge, UiTagsInput, UiToggle
|
|
13
|
+
*/
|
|
14
|
+
export interface UiChipProps {
|
|
15
|
+
/**
|
|
16
|
+
* Chip behavior variant.
|
|
17
|
+
* @default 'static'
|
|
18
|
+
*/
|
|
19
|
+
variant?: UiChipVariant;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Enables selection for the removable variant.
|
|
23
|
+
* Has no effect for `static` and `selectable` variants.
|
|
24
|
+
* @default false
|
|
25
|
+
*/
|
|
26
|
+
selectable?: boolean;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Controlled selected state for interactive chips.
|
|
30
|
+
* Use with `v-model`.
|
|
31
|
+
*/
|
|
32
|
+
modelValue?: boolean;
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Whether the chip is disabled.
|
|
36
|
+
* @default false
|
|
37
|
+
*/
|
|
38
|
+
disabled?: boolean;
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Optional icon component displayed before the label.
|
|
42
|
+
*/
|
|
43
|
+
startIcon?: Component;
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Accessible label for the remove button.
|
|
47
|
+
* @default 'Remove chip'
|
|
48
|
+
*/
|
|
49
|
+
removeLabel?: string;
|
|
50
|
+
}
|