@gitlab/ui 49.0.3 → 49.2.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/CHANGELOG.md +15 -0
- package/dist/components/base/badge/badge.js +14 -2
- package/dist/components/base/new_dropdowns/listbox/listbox.js +41 -2
- package/dist/components/base/search_box_by_type/search_box_by_type.js +10 -1
- package/dist/utility_classes.css +1 -1
- package/dist/utility_classes.css.map +1 -1
- package/dist/utils/constants.js +5 -1
- package/package.json +1 -1
- package/src/components/base/badge/badge.spec.js +10 -5
- package/src/components/base/badge/badge.stories.js +29 -9
- package/src/components/base/badge/badge.vue +24 -2
- package/src/components/base/new_dropdowns/listbox/listbox.md +6 -0
- package/src/components/base/new_dropdowns/listbox/listbox.spec.js +58 -0
- package/src/components/base/new_dropdowns/listbox/listbox.stories.js +24 -2
- package/src/components/base/new_dropdowns/listbox/listbox.vue +53 -3
- package/src/components/base/search_box_by_type/search_box_by_type.spec.js +18 -0
- package/src/components/base/search_box_by_type/search_box_by_type.vue +12 -0
- package/src/scss/utilities.scss +16 -0
- package/src/scss/utility-mixins/box-shadow.scss +4 -0
- package/src/utils/constants.js +5 -0
package/dist/utils/constants.js
CHANGED
|
@@ -35,6 +35,10 @@ const badgeVariantOptions = {
|
|
|
35
35
|
danger: 'danger',
|
|
36
36
|
tier: 'tier'
|
|
37
37
|
};
|
|
38
|
+
const badgeIconSizeOptions = {
|
|
39
|
+
sm: 12,
|
|
40
|
+
md: 16
|
|
41
|
+
};
|
|
38
42
|
const variantCssColorMap = {
|
|
39
43
|
muted: 'gl-text-gray-500',
|
|
40
44
|
neutral: 'gl-text-blue-100',
|
|
@@ -248,4 +252,4 @@ const loadingIconSizes = {
|
|
|
248
252
|
'xl (64x64)': 'xl'
|
|
249
253
|
};
|
|
250
254
|
|
|
251
|
-
export { COMMA, LEFT_MOUSE_BUTTON, alertVariantIconMap, alertVariantOptions, alignOptions, avatarShapeOptions, avatarSizeOptions, avatarsInlineSizeOptions, badgeForButtonOptions, badgeSizeOptions, badgeVariantOptions, bannerVariants, buttonCategoryOptions, buttonSizeOptions, buttonSizeOptionsMap, buttonVariantOptions, colorThemes, columnOptions, defaultDateFormat, drawerVariants, dropdownVariantOptions, focusableTags, formInputSizes, formStateOptions, glThemes, iconSizeOptions, keyboard, labelColorOptions, labelSizeOptions, loadingIconSizes, maxZIndex, modalButtonDefaults, modalSizeOptions, popoverPlacements, resizeDebounceTime, tabsButtonDefaults, targetOptions, toggleLabelPosition, tokenVariants, tooltipActionEvents, tooltipDelay, tooltipPlacements, triggerVariantOptions, truncateOptions, variantCssColorMap, variantOptions, variantOptionsWithNoDefault, viewModeOptions };
|
|
255
|
+
export { COMMA, LEFT_MOUSE_BUTTON, alertVariantIconMap, alertVariantOptions, alignOptions, avatarShapeOptions, avatarSizeOptions, avatarsInlineSizeOptions, badgeForButtonOptions, badgeIconSizeOptions, badgeSizeOptions, badgeVariantOptions, bannerVariants, buttonCategoryOptions, buttonSizeOptions, buttonSizeOptionsMap, buttonVariantOptions, colorThemes, columnOptions, defaultDateFormat, drawerVariants, dropdownVariantOptions, focusableTags, formInputSizes, formStateOptions, glThemes, iconSizeOptions, keyboard, labelColorOptions, labelSizeOptions, loadingIconSizes, maxZIndex, modalButtonDefaults, modalSizeOptions, popoverPlacements, resizeDebounceTime, tabsButtonDefaults, targetOptions, toggleLabelPosition, tokenVariants, tooltipActionEvents, tooltipDelay, tooltipPlacements, triggerVariantOptions, truncateOptions, variantCssColorMap, variantOptions, variantOptionsWithNoDefault, viewModeOptions };
|
package/package.json
CHANGED
|
@@ -17,13 +17,14 @@ describe('badge', () => {
|
|
|
17
17
|
|
|
18
18
|
describe('with "icon" prop', () => {
|
|
19
19
|
describe.each`
|
|
20
|
-
scenario
|
|
21
|
-
${'icon-only'}
|
|
22
|
-
${'icon and slot'} | ${true} | ${'warning'} | ${undefined}
|
|
23
|
-
|
|
20
|
+
scenario | hasSlot | iconName | iconSize | expectedIconSize | expectedRole
|
|
21
|
+
${'icon-only'} | ${false} | ${'warning'} | ${'md'} | ${16} | ${'img'}
|
|
22
|
+
${'16px icon and slot'} | ${true} | ${'warning'} | ${'md'} | ${16} | ${undefined}
|
|
23
|
+
${'12px icon and slot'} | ${true} | ${'warning'} | ${'sm'} | ${12} | ${undefined}
|
|
24
|
+
`('with $scenario', ({ iconName, iconSize, expectedIconSize, hasSlot, expectedRole }) => {
|
|
24
25
|
beforeEach(() => {
|
|
25
26
|
const slots = hasSlot ? { default: 'slot-content' } : undefined;
|
|
26
|
-
createComponent({ propsData: { icon: iconName }, slots });
|
|
27
|
+
createComponent({ propsData: { icon: iconName, iconSize }, slots });
|
|
27
28
|
});
|
|
28
29
|
|
|
29
30
|
it(`sets badge "role" attribute to ${expectedRole}`, () => {
|
|
@@ -42,6 +43,10 @@ describe('badge', () => {
|
|
|
42
43
|
|
|
43
44
|
expect(icon.classes('gl-mr-2')).toBe(hasSlot);
|
|
44
45
|
});
|
|
46
|
+
|
|
47
|
+
it('with correct size', () => {
|
|
48
|
+
expect(findIcon().props('size')).toBe(expectedIconSize);
|
|
49
|
+
});
|
|
45
50
|
});
|
|
46
51
|
});
|
|
47
52
|
});
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import iconSpriteInfo from '@gitlab/svgs/dist/icons.json';
|
|
2
2
|
import { GlBadge } from '../../../index';
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
badgeSizeOptions,
|
|
5
|
+
badgeVariantOptions,
|
|
6
|
+
badgeIconSizeOptions,
|
|
7
|
+
} from '../../../utils/constants';
|
|
4
8
|
import { disableControls } from '../../../utils/stories_utils';
|
|
5
9
|
import readme from './badge.md';
|
|
6
10
|
|
|
@@ -10,6 +14,7 @@ const template = `
|
|
|
10
14
|
:variant="variant"
|
|
11
15
|
:size="size"
|
|
12
16
|
:icon="icon"
|
|
17
|
+
:iconSize="iconSize"
|
|
13
18
|
>{{ content }}</gl-badge>
|
|
14
19
|
`;
|
|
15
20
|
|
|
@@ -21,12 +26,14 @@ const generateProps = ({
|
|
|
21
26
|
href = '',
|
|
22
27
|
content = 'TestBadge',
|
|
23
28
|
icon = '',
|
|
29
|
+
iconSize = defaultValue('iconSize'),
|
|
24
30
|
} = {}) => ({
|
|
25
31
|
variant,
|
|
26
32
|
size,
|
|
27
33
|
href,
|
|
28
34
|
content,
|
|
29
35
|
icon,
|
|
36
|
+
iconSize,
|
|
30
37
|
});
|
|
31
38
|
|
|
32
39
|
const Template = (args, { argTypes }) => ({
|
|
@@ -53,6 +60,7 @@ export const Variants = (args, { argTypes }) => ({
|
|
|
53
60
|
:variant="variant"
|
|
54
61
|
:size="size"
|
|
55
62
|
:icon="icon"
|
|
63
|
+
:iconSize="iconSize"
|
|
56
64
|
class="gl-mr-3"
|
|
57
65
|
>{{ variant }}</gl-badge>
|
|
58
66
|
</div>
|
|
@@ -76,6 +84,7 @@ export const Actionable = (args, { argTypes }) => ({
|
|
|
76
84
|
:variant="variant"
|
|
77
85
|
:size="size"
|
|
78
86
|
:icon="icon"
|
|
87
|
+
:iconSize="iconSize"
|
|
79
88
|
class="gl-mr-3"
|
|
80
89
|
>{{ variant }}</gl-badge>
|
|
81
90
|
</div>
|
|
@@ -103,6 +112,7 @@ export const Sizes = (args, { argTypes }) => ({
|
|
|
103
112
|
:variant="variant"
|
|
104
113
|
:size="size"
|
|
105
114
|
:icon="icon"
|
|
115
|
+
:iconSize="iconSize"
|
|
106
116
|
class="gl-mr-3"
|
|
107
117
|
>{{ size }}</gl-badge>
|
|
108
118
|
</div>
|
|
@@ -115,6 +125,18 @@ Sizes.args = generateProps({
|
|
|
115
125
|
Sizes.argTypes = disableControls(['content', 'size']);
|
|
116
126
|
|
|
117
127
|
export const BadgeIcon = (args, { argTypes }) => ({
|
|
128
|
+
components: { GlBadge },
|
|
129
|
+
props: Object.keys(argTypes),
|
|
130
|
+
template: `
|
|
131
|
+
<div class="gl-display-flex gl-gap-3">
|
|
132
|
+
<gl-badge variant="tier" icon="license">16px icon</gl-badge>
|
|
133
|
+
<gl-badge variant="tier" icon="license-sm" iconSize="sm">12px icon</gl-badge>
|
|
134
|
+
</div>
|
|
135
|
+
`,
|
|
136
|
+
});
|
|
137
|
+
BadgeIcon.argTypes = disableControls(['content', 'iconSize']);
|
|
138
|
+
|
|
139
|
+
export const IconOnly = (args, { argTypes }) => ({
|
|
118
140
|
components: { GlBadge },
|
|
119
141
|
props: Object.keys(argTypes),
|
|
120
142
|
template: `
|
|
@@ -124,20 +146,14 @@ export const BadgeIcon = (args, { argTypes }) => ({
|
|
|
124
146
|
:variant="variant"
|
|
125
147
|
:size="size"
|
|
126
148
|
:icon="icon"
|
|
127
|
-
|
|
128
|
-
<gl-badge
|
|
129
|
-
:href="href"
|
|
130
|
-
:variant="variant"
|
|
131
|
-
:size="size"
|
|
132
|
-
:icon="icon"
|
|
149
|
+
:iconSize="iconSize"
|
|
133
150
|
/>
|
|
134
151
|
</div>
|
|
135
152
|
`,
|
|
136
153
|
});
|
|
137
|
-
|
|
154
|
+
IconOnly.args = generateProps({
|
|
138
155
|
variant: badgeVariantOptions.success,
|
|
139
156
|
icon: 'calendar',
|
|
140
|
-
content: 'Badge icon',
|
|
141
157
|
});
|
|
142
158
|
|
|
143
159
|
export default {
|
|
@@ -164,5 +180,9 @@ export default {
|
|
|
164
180
|
options: ['', ...iconSpriteInfo.icons],
|
|
165
181
|
control: 'select',
|
|
166
182
|
},
|
|
183
|
+
iconSize: {
|
|
184
|
+
options: Object.keys(badgeIconSizeOptions),
|
|
185
|
+
control: 'select',
|
|
186
|
+
},
|
|
167
187
|
},
|
|
168
188
|
};
|
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
<!-- eslint-disable vue/multi-word-component-names -->
|
|
2
2
|
<script>
|
|
3
3
|
import { BBadge } from 'bootstrap-vue';
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
badgeSizeOptions,
|
|
6
|
+
badgeVariantOptions,
|
|
7
|
+
badgeIconSizeOptions,
|
|
8
|
+
} from '../../../utils/constants';
|
|
5
9
|
import GlIcon from '../icon/icon.vue';
|
|
6
10
|
|
|
7
11
|
export default {
|
|
@@ -41,6 +45,15 @@ export default {
|
|
|
41
45
|
required: false,
|
|
42
46
|
default: null,
|
|
43
47
|
},
|
|
48
|
+
/**
|
|
49
|
+
* The size of the icon 16 or 12
|
|
50
|
+
*/
|
|
51
|
+
iconSize: {
|
|
52
|
+
type: String,
|
|
53
|
+
default: 'md',
|
|
54
|
+
validator: (value) => Object.keys(badgeIconSizeOptions).includes(value),
|
|
55
|
+
required: false,
|
|
56
|
+
},
|
|
44
57
|
},
|
|
45
58
|
computed: {
|
|
46
59
|
hasIconOnly() {
|
|
@@ -50,13 +63,22 @@ export default {
|
|
|
50
63
|
role() {
|
|
51
64
|
return this.hasIconOnly ? 'img' : undefined;
|
|
52
65
|
},
|
|
66
|
+
iconSizeComputed() {
|
|
67
|
+
return badgeIconSizeOptions[this.iconSize];
|
|
68
|
+
},
|
|
53
69
|
},
|
|
54
70
|
};
|
|
55
71
|
</script>
|
|
56
72
|
|
|
57
73
|
<template>
|
|
58
74
|
<b-badge v-bind="$attrs" :variant="variant" :class="['gl-badge', size]" :role="role" pill>
|
|
59
|
-
<gl-icon
|
|
75
|
+
<gl-icon
|
|
76
|
+
v-if="icon"
|
|
77
|
+
class="gl-badge-icon"
|
|
78
|
+
:size="iconSizeComputed"
|
|
79
|
+
:class="{ 'gl-mr-2': !hasIconOnly }"
|
|
80
|
+
:name="icon"
|
|
81
|
+
/>
|
|
60
82
|
<!-- @slot The badge content to display. -->
|
|
61
83
|
<slot></slot>
|
|
62
84
|
</b-badge>
|
|
@@ -49,6 +49,12 @@ Alternatively, you can set `selected` property to the array of selected items
|
|
|
49
49
|
`value` properties (for multi-select) or to the selected item `value` property for a single-select.
|
|
50
50
|
On selection the listbox will emit the `select` event with the selected values.
|
|
51
51
|
|
|
52
|
+
### Resetting the selection
|
|
53
|
+
|
|
54
|
+
`GlListbox` can render a reset button if the `headerText` and `resetButtonLabel` props are provided.
|
|
55
|
+
When clicking on the reset button, a `reset` event is emitted. It is the consumer's responsibility
|
|
56
|
+
to listen to that event and to update the model as needed.
|
|
57
|
+
|
|
52
58
|
### Setting listbox options
|
|
53
59
|
|
|
54
60
|
Use the `items` prop to provide options to the listbox. Each item can be
|
|
@@ -35,6 +35,7 @@ describe('GlListbox', () => {
|
|
|
35
35
|
const findNoResultsText = () => wrapper.find("[data-testid='listbox-no-results-text']");
|
|
36
36
|
const findLoadingIcon = () => wrapper.find("[data-testid='listbox-search-loader']");
|
|
37
37
|
const findSRNumberOfResultsText = () => wrapper.find("[data-testid='listbox-number-of-results']");
|
|
38
|
+
const findResetButton = () => wrapper.find("[data-testid='listbox-reset-button']");
|
|
38
39
|
|
|
39
40
|
describe('toggle text', () => {
|
|
40
41
|
describe.each`
|
|
@@ -377,4 +378,61 @@ describe('GlListbox', () => {
|
|
|
377
378
|
});
|
|
378
379
|
});
|
|
379
380
|
});
|
|
381
|
+
|
|
382
|
+
describe('with a reset action', () => {
|
|
383
|
+
it('throws when enabling the reset action without a header', () => {
|
|
384
|
+
expect(() => {
|
|
385
|
+
buildWrapper({ resetButtonLabel: 'Unassign' });
|
|
386
|
+
}).toThrow(
|
|
387
|
+
'The reset button cannot be rendered without a header. Either provide a header via the headerText prop, or do not provide the resetButtonLabel prop.'
|
|
388
|
+
);
|
|
389
|
+
expect(wrapper).toHaveLoggedVueErrors();
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
it('shows the reset button if the label is provided and the selection is not empty', () => {
|
|
393
|
+
buildWrapper({
|
|
394
|
+
headerText: 'Select assignee',
|
|
395
|
+
resetButtonLabel: 'Unassign',
|
|
396
|
+
selected: mockOptions[1].value,
|
|
397
|
+
items: mockOptions,
|
|
398
|
+
});
|
|
399
|
+
const button = findResetButton();
|
|
400
|
+
|
|
401
|
+
expect(button.exists()).toBe(true);
|
|
402
|
+
expect(button.text()).toBe('Unassign');
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
it.each`
|
|
406
|
+
description | props
|
|
407
|
+
${'multi-select'} | ${{ multiple: true, selected: [] }}
|
|
408
|
+
${'single-select'} | ${{ multiple: false, selected: null }}
|
|
409
|
+
`('hides the button if the selection is empty in $description mode', ({ props }) => {
|
|
410
|
+
buildWrapper({
|
|
411
|
+
headerText: 'Select assignee',
|
|
412
|
+
resetButtonLabel: 'Unassign',
|
|
413
|
+
items: mockOptions,
|
|
414
|
+
...props,
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
expect(findResetButton().exists()).toBe(false);
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
it('on click, emits the reset event and calls closeAndFocus()', () => {
|
|
421
|
+
buildWrapper({
|
|
422
|
+
headerText: 'Select assignee',
|
|
423
|
+
resetButtonLabel: 'Unassign',
|
|
424
|
+
selected: mockOptions[1].value,
|
|
425
|
+
items: mockOptions,
|
|
426
|
+
});
|
|
427
|
+
jest.spyOn(wrapper.vm, 'closeAndFocus');
|
|
428
|
+
|
|
429
|
+
expect(wrapper.emitted('reset')).toBe(undefined);
|
|
430
|
+
expect(wrapper.vm.closeAndFocus).not.toHaveBeenCalled();
|
|
431
|
+
|
|
432
|
+
findResetButton().trigger('click');
|
|
433
|
+
|
|
434
|
+
expect(wrapper.emitted('reset')).toHaveLength(1);
|
|
435
|
+
expect(wrapper.vm.closeAndFocus).toHaveBeenCalled();
|
|
436
|
+
});
|
|
437
|
+
});
|
|
380
438
|
});
|
|
@@ -39,6 +39,7 @@ const generateProps = ({
|
|
|
39
39
|
isCheckCentered = defaultValue('isCheckCentered'),
|
|
40
40
|
toggleAriaLabelledBy,
|
|
41
41
|
listAriaLabelledBy,
|
|
42
|
+
resetButtonLabel = defaultValue('resetButtonLabel'),
|
|
42
43
|
startOpened = true,
|
|
43
44
|
} = {}) => ({
|
|
44
45
|
items,
|
|
@@ -60,6 +61,7 @@ const generateProps = ({
|
|
|
60
61
|
isCheckCentered,
|
|
61
62
|
toggleAriaLabelledBy,
|
|
62
63
|
listAriaLabelledBy,
|
|
64
|
+
resetButtonLabel,
|
|
63
65
|
startOpened,
|
|
64
66
|
});
|
|
65
67
|
|
|
@@ -84,6 +86,7 @@ const makeBindings = (overrides = {}) =>
|
|
|
84
86
|
':is-check-centered': 'isCheckCentered',
|
|
85
87
|
':toggle-aria-labelled-by': 'toggleAriaLabelledBy',
|
|
86
88
|
':list-aria-labelled-by': 'listAriaLabelledBy',
|
|
89
|
+
':reset-button-label': 'resetButtonLabel',
|
|
87
90
|
...overrides,
|
|
88
91
|
})
|
|
89
92
|
.map(([key, value]) => `${key}="${value}"`)
|
|
@@ -153,8 +156,12 @@ export const HeaderAndFooter = (args, { argTypes }) => ({
|
|
|
153
156
|
selectItem(index) {
|
|
154
157
|
this.selected.push(mockOptions[index].value);
|
|
155
158
|
},
|
|
159
|
+
onReset() {
|
|
160
|
+
this.selected = [];
|
|
161
|
+
},
|
|
156
162
|
},
|
|
157
|
-
template: template(
|
|
163
|
+
template: template(
|
|
164
|
+
`
|
|
158
165
|
<template #footer>
|
|
159
166
|
<div class="gl-border-t-solid gl-border-t-1 gl-border-t-gray-100 gl-display-flex gl-justify-content-center gl-p-3">
|
|
160
167
|
<gl-button-group :vertical="false">
|
|
@@ -164,11 +171,18 @@ export const HeaderAndFooter = (args, { argTypes }) => ({
|
|
|
164
171
|
</gl-button-group>
|
|
165
172
|
</div>
|
|
166
173
|
</template>
|
|
167
|
-
|
|
174
|
+
`,
|
|
175
|
+
{
|
|
176
|
+
bindingOverrides: {
|
|
177
|
+
'@reset': 'onReset',
|
|
178
|
+
},
|
|
179
|
+
}
|
|
180
|
+
),
|
|
168
181
|
});
|
|
169
182
|
HeaderAndFooter.args = generateProps({
|
|
170
183
|
toggleText: 'Header and Footer',
|
|
171
184
|
headerText: 'Assign to department',
|
|
185
|
+
resetButtonLabel: 'Unassign',
|
|
172
186
|
multiple: true,
|
|
173
187
|
});
|
|
174
188
|
HeaderAndFooter.decorators = [makeContainer({ height: '370px' })];
|
|
@@ -197,6 +211,11 @@ export const CustomListItem = (args, { argTypes }) => ({
|
|
|
197
211
|
: this.items.find(({ value }) => value === this.selected[0]).text;
|
|
198
212
|
},
|
|
199
213
|
},
|
|
214
|
+
methods: {
|
|
215
|
+
onReset() {
|
|
216
|
+
this.selected = [];
|
|
217
|
+
},
|
|
218
|
+
},
|
|
200
219
|
template: template(
|
|
201
220
|
`<template #list-item="{ item }">
|
|
202
221
|
<span class="gl-display-flex gl-align-items-center">
|
|
@@ -211,6 +230,7 @@ export const CustomListItem = (args, { argTypes }) => ({
|
|
|
211
230
|
{
|
|
212
231
|
bindingOverrides: {
|
|
213
232
|
':toggle-text': 'customToggleText',
|
|
233
|
+
'@reset': 'onReset',
|
|
214
234
|
},
|
|
215
235
|
}
|
|
216
236
|
),
|
|
@@ -229,6 +249,8 @@ CustomListItem.args = generateProps({
|
|
|
229
249
|
],
|
|
230
250
|
multiple: true,
|
|
231
251
|
isCheckCentered: true,
|
|
252
|
+
headerText: 'Select assignees',
|
|
253
|
+
resetButtonLabel: 'Unassign',
|
|
232
254
|
});
|
|
233
255
|
CustomListItem.decorators = [makeContainer({ height: '200px' })];
|
|
234
256
|
|
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
buttonSizeOptions,
|
|
16
16
|
dropdownVariantOptions,
|
|
17
17
|
} from '../../../../utils/constants';
|
|
18
|
+
import GlButton from '../../button/button.vue';
|
|
18
19
|
import GlLoadingIcon from '../../loading_icon/loading_icon.vue';
|
|
19
20
|
import GlSearchBoxByType from '../../search_box_by_type/search_box_by_type.vue';
|
|
20
21
|
import GlBaseDropdown from '../base_dropdown/base_dropdown.vue';
|
|
@@ -37,6 +38,7 @@ export default {
|
|
|
37
38
|
GlBaseDropdown,
|
|
38
39
|
GlListboxItem,
|
|
39
40
|
GlListboxGroup,
|
|
41
|
+
GlButton,
|
|
40
42
|
GlSearchBoxByType,
|
|
41
43
|
GlLoadingIcon,
|
|
42
44
|
},
|
|
@@ -219,6 +221,17 @@ export default {
|
|
|
219
221
|
required: false,
|
|
220
222
|
default: 'No results found',
|
|
221
223
|
},
|
|
224
|
+
/**
|
|
225
|
+
* The reset button's label, to be rendered in the header. If this is omitted, the button is not
|
|
226
|
+
* rendered.
|
|
227
|
+
* The reset button requires a header to be set, so this prop should be used in conjunction with
|
|
228
|
+
* headerText.
|
|
229
|
+
*/
|
|
230
|
+
resetButtonLabel: {
|
|
231
|
+
type: String,
|
|
232
|
+
required: false,
|
|
233
|
+
default: '',
|
|
234
|
+
},
|
|
222
235
|
},
|
|
223
236
|
data() {
|
|
224
237
|
return {
|
|
@@ -266,6 +279,15 @@ export default {
|
|
|
266
279
|
headerId() {
|
|
267
280
|
return this.headerText && uniqueId('listbox-header-');
|
|
268
281
|
},
|
|
282
|
+
showResetButton() {
|
|
283
|
+
if (!this.resetButtonLabel) {
|
|
284
|
+
return false;
|
|
285
|
+
}
|
|
286
|
+
if (this.multiple) {
|
|
287
|
+
return this.selected.length > 0;
|
|
288
|
+
}
|
|
289
|
+
return Boolean(this.selected);
|
|
290
|
+
},
|
|
269
291
|
},
|
|
270
292
|
watch: {
|
|
271
293
|
selected: {
|
|
@@ -282,6 +304,13 @@ export default {
|
|
|
282
304
|
},
|
|
283
305
|
},
|
|
284
306
|
},
|
|
307
|
+
mounted() {
|
|
308
|
+
if (process.env.NODE_ENV !== 'production' && this.resetButtonLabel && !this.headerText) {
|
|
309
|
+
throw new Error(
|
|
310
|
+
'The reset button cannot be rendered without a header. Either provide a header via the headerText prop, or do not provide the resetButtonLabel prop.'
|
|
311
|
+
);
|
|
312
|
+
}
|
|
313
|
+
},
|
|
285
314
|
methods: {
|
|
286
315
|
open() {
|
|
287
316
|
this.$refs.baseDropdown.open();
|
|
@@ -392,7 +421,7 @@ export default {
|
|
|
392
421
|
this.$emit('select', value);
|
|
393
422
|
}
|
|
394
423
|
|
|
395
|
-
this
|
|
424
|
+
this.closeAndFocus();
|
|
396
425
|
},
|
|
397
426
|
onMultiSelect(value, isSelected) {
|
|
398
427
|
if (isSelected) {
|
|
@@ -413,6 +442,18 @@ export default {
|
|
|
413
442
|
*/
|
|
414
443
|
this.$emit('search', searchTerm);
|
|
415
444
|
},
|
|
445
|
+
onResetButtonClicked() {
|
|
446
|
+
/**
|
|
447
|
+
* Emitted when the reset button is clicked
|
|
448
|
+
*
|
|
449
|
+
* @event reset
|
|
450
|
+
*/
|
|
451
|
+
this.$emit('reset');
|
|
452
|
+
this.closeAndFocus();
|
|
453
|
+
},
|
|
454
|
+
closeAndFocus() {
|
|
455
|
+
this.$refs.baseDropdown.closeAndFocus();
|
|
456
|
+
},
|
|
416
457
|
isOption,
|
|
417
458
|
},
|
|
418
459
|
};
|
|
@@ -440,16 +481,25 @@ export default {
|
|
|
440
481
|
>
|
|
441
482
|
<div
|
|
442
483
|
v-if="headerText"
|
|
443
|
-
class="gl-display-flex gl-align-items-center gl-
|
|
484
|
+
class="gl-display-flex gl-align-items-center gl-px-3 gl-py-2! gl-min-h-8"
|
|
444
485
|
:class="$options.HEADER_ITEMS_BORDER_CLASSES"
|
|
445
486
|
>
|
|
446
487
|
<div
|
|
447
488
|
:id="headerId"
|
|
448
|
-
class="gl-flex-grow-1 gl-font-weight-bold gl-font-sm"
|
|
489
|
+
class="gl-flex-grow-1 gl-font-weight-bold gl-font-sm gl-pr-2"
|
|
449
490
|
data-testid="listbox-header-text"
|
|
450
491
|
>
|
|
451
492
|
{{ headerText }}
|
|
452
493
|
</div>
|
|
494
|
+
<gl-button
|
|
495
|
+
v-if="showResetButton"
|
|
496
|
+
category="tertiary"
|
|
497
|
+
class="gl-focus-inset-border-2-blue-400! gl-flex-shrink-0 gl-font-sm! gl-px-2! gl-py-2!"
|
|
498
|
+
data-testid="listbox-reset-button"
|
|
499
|
+
@click="onResetButtonClicked"
|
|
500
|
+
>
|
|
501
|
+
{{ resetButtonLabel }}
|
|
502
|
+
</gl-button>
|
|
453
503
|
</div>
|
|
454
504
|
|
|
455
505
|
<div v-if="searchable" :class="$options.HEADER_ITEMS_BORDER_CLASSES">
|
|
@@ -42,6 +42,24 @@ describe('search box by type component', () => {
|
|
|
42
42
|
|
|
43
43
|
expect(wrapper.emitted('input')).toEqual([['']]);
|
|
44
44
|
});
|
|
45
|
+
|
|
46
|
+
it('emits clearButtonFocus when focused', () => {
|
|
47
|
+
findClearIcon().vm.$emit('focus', { stopPropagation: jest.fn() });
|
|
48
|
+
|
|
49
|
+
expect(wrapper.emitted('clearButtonFocus')).toHaveLength(1);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('emits clearButtonBlur when blur', () => {
|
|
53
|
+
findClearIcon().vm.$emit('blur', { stopPropagation: jest.fn() });
|
|
54
|
+
|
|
55
|
+
expect(wrapper.emitted('clearButtonBlur')).toHaveLength(1);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('emits clearButtonRelease when released', () => {
|
|
59
|
+
findClearIcon().vm.$emit('release', { stopPropagation: jest.fn() });
|
|
60
|
+
|
|
61
|
+
expect(wrapper.emitted('clearButtonRelease')).toHaveLength(1);
|
|
62
|
+
});
|
|
45
63
|
});
|
|
46
64
|
|
|
47
65
|
describe('v-model', () => {
|
|
@@ -93,6 +93,15 @@ export default {
|
|
|
93
93
|
focusInput() {
|
|
94
94
|
this.$refs.input.$el.focus();
|
|
95
95
|
},
|
|
96
|
+
onClearButtonFocus() {
|
|
97
|
+
this.$emit('clearButtonFocus');
|
|
98
|
+
},
|
|
99
|
+
onClearButtonBlur() {
|
|
100
|
+
this.$emit('clearButtonBlur');
|
|
101
|
+
},
|
|
102
|
+
onClearButtonRelease() {
|
|
103
|
+
this.$emit('clearButtonRelease');
|
|
104
|
+
},
|
|
96
105
|
},
|
|
97
106
|
};
|
|
98
107
|
</script>
|
|
@@ -116,6 +125,9 @@ export default {
|
|
|
116
125
|
:tooltip-container="tooltipContainer"
|
|
117
126
|
class="gl-search-box-by-type-clear gl-clear-icon-button"
|
|
118
127
|
@click.stop="clearInput"
|
|
128
|
+
@focus="onClearButtonFocus"
|
|
129
|
+
@blur="onClearButtonBlur"
|
|
130
|
+
@release="onClearButtonRelease"
|
|
119
131
|
/>
|
|
120
132
|
</div>
|
|
121
133
|
</div>
|
package/src/scss/utilities.scss
CHANGED
|
@@ -1898,6 +1898,22 @@
|
|
|
1898
1898
|
box-shadow: inset 0 0 0 $gl-border-size-1 $green-600 !important
|
|
1899
1899
|
}
|
|
1900
1900
|
|
|
1901
|
+
.gl-inset-border-2-blue-400 {
|
|
1902
|
+
box-shadow: inset 0 0 0 $gl-border-size-2 $blue-400
|
|
1903
|
+
}
|
|
1904
|
+
|
|
1905
|
+
.gl-focus-inset-border-2-blue-400:focus {
|
|
1906
|
+
box-shadow: inset 0 0 0 $gl-border-size-2 $blue-400
|
|
1907
|
+
}
|
|
1908
|
+
|
|
1909
|
+
.gl-inset-border-2-blue-400\! {
|
|
1910
|
+
box-shadow: inset 0 0 0 $gl-border-size-2 $blue-400 !important
|
|
1911
|
+
}
|
|
1912
|
+
|
|
1913
|
+
.gl-focus-inset-border-2-blue-400\!:focus {
|
|
1914
|
+
box-shadow: inset 0 0 0 $gl-border-size-2 $blue-400 !important
|
|
1915
|
+
}
|
|
1916
|
+
|
|
1901
1917
|
.gl-inset-border-2-green-400 {
|
|
1902
1918
|
box-shadow: inset 0 0 0 $gl-border-size-2 $green-400
|
|
1903
1919
|
}
|
|
@@ -93,6 +93,10 @@
|
|
|
93
93
|
box-shadow: inset 0 0 0 $gl-border-size-1 $green-600;
|
|
94
94
|
}
|
|
95
95
|
|
|
96
|
+
@mixin gl-inset-border-2-blue-400($focus: true) {
|
|
97
|
+
box-shadow: inset 0 0 0 $gl-border-size-2 $blue-400;
|
|
98
|
+
}
|
|
99
|
+
|
|
96
100
|
@mixin gl-inset-border-2-green-400 {
|
|
97
101
|
box-shadow: inset 0 0 0 $gl-border-size-2 $green-400;
|
|
98
102
|
}
|
package/src/utils/constants.js
CHANGED