🌐 AI搜索 & 代理 主页
Skip to content

Commit 00eb9d9

Browse files
authored
feat(b-form-tags): new tagged input component (#4409)
1 parent 833b028 commit 00eb9d9

File tree

15 files changed

+2643
-0
lines changed

15 files changed

+2643
-0
lines changed

src/components/form-tags/README.md

Lines changed: 710 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
.b-form-tags {
2+
&.focus {
3+
color: $input-focus-color;
4+
background-color: $input-focus-bg;
5+
border-color: $input-focus-border-color;
6+
outline: 0;
7+
@if $enable-shadows {
8+
box-shadow: $input-box-shadow, $input-focus-box-shadow;
9+
} @else {
10+
box-shadow: $input-focus-box-shadow;
11+
}
12+
13+
&.is-valid {
14+
border-color: $form-feedback-valid-color;
15+
box-shadow: 0 0 0 $input-focus-width rgba($form-feedback-valid-color, 0.25);
16+
}
17+
18+
&.is-invalid {
19+
border-color: $form-feedback-invalid-color;
20+
box-shadow: 0 0 0 $input-focus-width rgba($form-feedback-invalid-color, 0.25);
21+
}
22+
}
23+
24+
&.disabled {
25+
background-color: $input-disabled-bg;
26+
}
27+
}
28+
29+
.b-form-tag {
30+
// Override default badge settings
31+
// Due to using text-truncate on the inner content
32+
font-size: 75%;
33+
font-weight: normal;
34+
line-height: $input-line-height;
35+
36+
&.disabled {
37+
opacity: .75;
38+
}
39+
40+
// Override default close button settings
41+
> button.b-form-tag-remove {
42+
color: inherit;
43+
font-size: 125%;
44+
line-height: 1;
45+
float: none;
46+
}
47+
}
48+
49+
.form-control-sm .b-form-tag {
50+
line-height: $input-line-height-sm;
51+
}
52+
53+
.form-control-lg .b-form-tag {
54+
line-height: $input-line-height-lg;
55+
}
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import Vue from '../../utils/vue'
2+
import { getComponentConfig } from '../../utils/config'
3+
import idMixin from '../../mixins/id'
4+
import normalizeSlotMixin from '../../mixins/normalize-slot'
5+
import { BBadge } from '../badge/badge'
6+
import { BButtonClose } from '../button/button-close'
7+
8+
const NAME = 'BFormTag'
9+
10+
export const BFormTag = /*#__PURE__*/ Vue.extend({
11+
name: NAME,
12+
mixins: [idMixin, normalizeSlotMixin],
13+
props: {
14+
variant: {
15+
type: String,
16+
default: () => getComponentConfig(NAME, 'variant')
17+
},
18+
disabled: {
19+
type: Boolean,
20+
default: false
21+
},
22+
title: {
23+
type: String,
24+
default: null
25+
},
26+
pill: {
27+
type: Boolean,
28+
default: false
29+
},
30+
removeLabel: {
31+
type: String,
32+
default: () => getComponentConfig(NAME, 'removeLabel')
33+
},
34+
tag: {
35+
type: String,
36+
default: 'span'
37+
}
38+
},
39+
methods: {
40+
onClick() {
41+
this.$emit('remove')
42+
}
43+
},
44+
render(h) {
45+
const tagId = this.safeId()
46+
let $remove = h()
47+
if (!this.disabled) {
48+
$remove = h(BButtonClose, {
49+
staticClass: 'b-form-tag-remove ml-1',
50+
props: { ariaLabel: this.removeLabel },
51+
attrs: { 'aria-controls': tagId },
52+
on: { click: this.onClick }
53+
})
54+
}
55+
const $tag = h(
56+
'span',
57+
{ staticClass: 'b-form-tag-content flex-grow-1 text-truncate' },
58+
this.normalizeSlot('default') || this.title || [h()]
59+
)
60+
return h(
61+
BBadge,
62+
{
63+
staticClass: 'b-form-tag d-inline-flex align-items-baseline mw-100',
64+
class: { disabled: this.disabled },
65+
attrs: { id: tagId, title: this.title || null },
66+
props: { tag: this.tag, variant: this.variant, pill: this.pill }
67+
},
68+
[$tag, $remove]
69+
)
70+
}
71+
})
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
import { mount } from '@vue/test-utils'
2+
import { BFormTag } from './form-tag'
3+
4+
describe('form-tag', () => {
5+
it('has expected structure', async () => {
6+
const wrapper = mount(BFormTag, {
7+
propsData: {
8+
title: 'foobar'
9+
}
10+
})
11+
12+
expect(wrapper.is('span')).toBe(true)
13+
14+
expect(wrapper.classes()).toContain('b-form-tag')
15+
expect(wrapper.classes()).toContain('badge')
16+
expect(wrapper.classes()).toContain('badge-secondary')
17+
expect(wrapper.attributes('title')).toBe('foobar')
18+
expect(wrapper.text()).toContain('foobar')
19+
20+
const $btn = wrapper.find('button')
21+
expect($btn.exists()).toBe(true)
22+
expect($btn.classes()).toContain('close')
23+
expect($btn.classes()).toContain('b-form-tag-remove')
24+
expect($btn.attributes('aria-label')).toBe('Remove tag')
25+
26+
wrapper.destroy()
27+
})
28+
29+
it('renders custom root element', async () => {
30+
const wrapper = mount(BFormTag, {
31+
propsData: {
32+
title: 'foobar',
33+
tag: 'li'
34+
}
35+
})
36+
37+
expect(wrapper.is('li')).toBe(true)
38+
39+
expect(wrapper.classes()).toContain('b-form-tag')
40+
expect(wrapper.classes()).toContain('badge')
41+
expect(wrapper.classes()).toContain('badge-secondary')
42+
expect(wrapper.attributes('title')).toBe('foobar')
43+
expect(wrapper.text()).toContain('foobar')
44+
45+
const $btn = wrapper.find('button')
46+
expect($btn.exists()).toBe(true)
47+
expect($btn.classes()).toContain('close')
48+
expect($btn.classes()).toContain('b-form-tag-remove')
49+
expect($btn.attributes('aria-label')).toBe('Remove tag')
50+
51+
wrapper.destroy()
52+
})
53+
54+
it('renders default slot', async () => {
55+
const wrapper = mount(BFormTag, {
56+
propsData: {
57+
title: 'foo'
58+
},
59+
slots: {
60+
default: 'bar'
61+
}
62+
})
63+
64+
expect(wrapper.is('span')).toBe(true)
65+
66+
expect(wrapper.classes()).toContain('b-form-tag')
67+
expect(wrapper.classes()).toContain('badge')
68+
expect(wrapper.classes()).toContain('badge-secondary')
69+
expect(wrapper.attributes('title')).toBe('foo')
70+
expect(wrapper.text()).toContain('bar')
71+
expect(wrapper.text()).not.toContain('foo')
72+
73+
const $btn = wrapper.find('button')
74+
expect($btn.exists()).toBe(true)
75+
expect($btn.classes()).toContain('close')
76+
expect($btn.classes()).toContain('b-form-tag-remove')
77+
expect($btn.attributes('aria-label')).toBe('Remove tag')
78+
79+
wrapper.destroy()
80+
})
81+
82+
it('emits remove event when button clicked', async () => {
83+
const wrapper = mount(BFormTag, {
84+
propsData: {
85+
title: 'foobar'
86+
}
87+
})
88+
89+
expect(wrapper.is('span')).toBe(true)
90+
91+
expect(wrapper.classes()).toContain('b-form-tag')
92+
expect(wrapper.classes()).toContain('badge')
93+
expect(wrapper.classes()).toContain('badge-secondary')
94+
expect(wrapper.attributes('title')).toBe('foobar')
95+
expect(wrapper.text()).toContain('foobar')
96+
97+
const $btn = wrapper.find('button')
98+
expect($btn.exists()).toBe(true)
99+
expect($btn.classes()).toContain('close')
100+
expect($btn.classes()).toContain('b-form-tag-remove')
101+
expect($btn.attributes('aria-label')).toBe('Remove tag')
102+
103+
expect(wrapper.emitted('remove')).not.toBeDefined()
104+
105+
$btn.trigger('click')
106+
107+
expect(wrapper.emitted('remove')).toBeDefined()
108+
expect(wrapper.emitted('remove').length).toBe(1)
109+
110+
wrapper.destroy()
111+
})
112+
})

0 commit comments

Comments
 (0)