@gitlab/ui 78.17.0 → 78.19.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 +14 -0
- package/dist/components/base/form/form_textarea/form_textarea.js +55 -1
- package/dist/components/base/new_dropdowns/disclosure/mock_data.js +11 -11
- package/dist/tokens/css/tokens.css +1 -1
- package/dist/tokens/css/tokens.dark.css +1 -1
- package/dist/tokens/js/tokens.dark.js +1 -1
- package/dist/tokens/js/tokens.js +1 -1
- package/dist/tokens/scss/_tokens.dark.scss +1 -1
- package/dist/tokens/scss/_tokens.scss +1 -1
- package/package.json +3 -3
- package/src/components/base/form/form_textarea/form_textarea.spec.js +88 -5
- package/src/components/base/form/form_textarea/form_textarea.stories.js +30 -5
- package/src/components/base/form/form_textarea/form_textarea.vue +99 -4
- package/src/components/base/new_dropdowns/disclosure/disclosure_dropdown_item.spec.js +3 -3
- package/src/components/base/new_dropdowns/disclosure/mock_data.js +12 -12
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,17 @@
|
|
|
1
|
+
# [78.19.0](https://gitlab.com/gitlab-org/gitlab-ui/compare/v78.18.0...v78.19.0) (2024-04-22)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Features
|
|
5
|
+
|
|
6
|
+
* **GlFormTextarea:** add support for character count ([1b359aa](https://gitlab.com/gitlab-org/gitlab-ui/commit/1b359aabb514831bddaeca108132427ecde4ae64))
|
|
7
|
+
|
|
8
|
+
# [78.18.0](https://gitlab.com/gitlab-org/gitlab-ui/compare/v78.17.0...v78.18.0) (2024-04-19)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Features
|
|
12
|
+
|
|
13
|
+
* **GlDisclosureDropdown:** Reorder dropdown items in example ([7145be6](https://gitlab.com/gitlab-org/gitlab-ui/commit/7145be686e16ae286e1189071a15e58403aaa635))
|
|
14
|
+
|
|
1
15
|
# [78.17.0](https://gitlab.com/gitlab-org/gitlab-ui/compare/v78.16.0...v78.17.0) (2024-04-19)
|
|
2
16
|
|
|
3
17
|
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { BFormTextarea } from 'bootstrap-vue/esm/index.js';
|
|
2
|
+
import debounce from 'lodash/debounce';
|
|
3
|
+
import uniqueId from 'lodash/uniqueId';
|
|
2
4
|
import __vue_normalize__ from 'vue-runtime-helpers/dist/normalize-component.js';
|
|
3
5
|
|
|
4
6
|
const model = {
|
|
@@ -29,8 +31,23 @@ var script = {
|
|
|
29
31
|
type: Boolean,
|
|
30
32
|
required: false,
|
|
31
33
|
default: false
|
|
34
|
+
},
|
|
35
|
+
/**
|
|
36
|
+
* Max character count for the textarea.
|
|
37
|
+
*/
|
|
38
|
+
characterCount: {
|
|
39
|
+
type: Number,
|
|
40
|
+
required: false,
|
|
41
|
+
default: null
|
|
32
42
|
}
|
|
33
43
|
},
|
|
44
|
+
data() {
|
|
45
|
+
return {
|
|
46
|
+
characterCountId: uniqueId('form-textarea-character-count-'),
|
|
47
|
+
remainingCharacterCount: this.initialRemainingCharacterCount(),
|
|
48
|
+
remainingCharacterCountSrOnly: this.initialRemainingCharacterCount()
|
|
49
|
+
};
|
|
50
|
+
},
|
|
34
51
|
computed: {
|
|
35
52
|
listeners() {
|
|
36
53
|
var _this = this;
|
|
@@ -62,13 +79,50 @@ var script = {
|
|
|
62
79
|
},
|
|
63
80
|
keypressEvent() {
|
|
64
81
|
return this.submitOnEnter ? 'keyup' : null;
|
|
82
|
+
},
|
|
83
|
+
isCharacterCountOverLimit() {
|
|
84
|
+
return this.remainingCharacterCount < 0;
|
|
85
|
+
},
|
|
86
|
+
characterCountTextClass() {
|
|
87
|
+
return this.isCharacterCountOverLimit ? 'gl-text-red-500' : 'gl-text-gray-500';
|
|
88
|
+
},
|
|
89
|
+
showCharacterCount() {
|
|
90
|
+
return this.characterCount !== null;
|
|
91
|
+
},
|
|
92
|
+
bFormTextareaProps() {
|
|
93
|
+
return {
|
|
94
|
+
...this.$attrs,
|
|
95
|
+
class: 'gl-form-input gl-form-textarea',
|
|
96
|
+
noResize: this.noResize,
|
|
97
|
+
value: this.value
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
},
|
|
101
|
+
watch: {
|
|
102
|
+
value(newValue) {
|
|
103
|
+
if (!this.showCharacterCount) {
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
this.remainingCharacterCount = this.characterCount - newValue.length;
|
|
107
|
+
this.debouncedUpdateRemainingCharacterCountSrOnly(newValue);
|
|
65
108
|
}
|
|
66
109
|
},
|
|
110
|
+
created() {
|
|
111
|
+
// Debounce updating the remaining character count for a second so
|
|
112
|
+
// screen readers announce the remaining text after the text in the textarea.
|
|
113
|
+
this.debouncedUpdateRemainingCharacterCountSrOnly = debounce(this.updateRemainingCharacterCountSrOnly, 1000);
|
|
114
|
+
},
|
|
67
115
|
methods: {
|
|
68
116
|
handleKeyPress(e) {
|
|
69
117
|
if (e.keyCode === 13 && (e.metaKey || e.ctrlKey)) {
|
|
70
118
|
this.$emit('submit');
|
|
71
119
|
}
|
|
120
|
+
},
|
|
121
|
+
updateRemainingCharacterCountSrOnly(newValue) {
|
|
122
|
+
this.remainingCharacterCountSrOnly = this.characterCount - newValue.length;
|
|
123
|
+
},
|
|
124
|
+
initialRemainingCharacterCount() {
|
|
125
|
+
return this.characterCount - this.value.length;
|
|
72
126
|
}
|
|
73
127
|
}
|
|
74
128
|
};
|
|
@@ -77,7 +131,7 @@ var script = {
|
|
|
77
131
|
const __vue_script__ = script;
|
|
78
132
|
|
|
79
133
|
/* template */
|
|
80
|
-
var __vue_render__ = function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('b-form-textarea',_vm._g(_vm._b({
|
|
134
|
+
var __vue_render__ = function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return (_vm.showCharacterCount)?_c('div',[_c('b-form-textarea',_vm._g(_vm._b({attrs:{"aria-describedby":_vm.characterCountId},nativeOn:_vm._d({},[_vm.keypressEvent,function($event){return _vm.handleKeyPress.apply(null, arguments)}])},'b-form-textarea',_vm.bFormTextareaProps,false),_vm.listeners)),_vm._v(" "),_c('small',{class:['form-text', _vm.characterCountTextClass],attrs:{"aria-hidden":"true"}},[(_vm.isCharacterCountOverLimit)?_vm._t("character-count-over-limit-text",null,{"count":Math.abs(_vm.remainingCharacterCount)}):_vm._t("character-count-text",null,{"count":_vm.remainingCharacterCount})],2),_vm._v(" "),_c('div',{staticClass:"gl-sr-only",attrs:{"id":_vm.characterCountId,"aria-live":"polite","data-testid":"character-count-text-sr-only"}},[(_vm.isCharacterCountOverLimit)?_vm._t("character-count-over-limit-text",null,{"count":Math.abs(_vm.remainingCharacterCount)}):_vm._t("character-count-text",null,{"count":_vm.remainingCharacterCountSrOnly})],2)],1):_c('b-form-textarea',_vm._g(_vm._b({nativeOn:_vm._d({},[_vm.keypressEvent,function($event){return _vm.handleKeyPress.apply(null, arguments)}])},'b-form-textarea',_vm.bFormTextareaProps,false),_vm.listeners))};
|
|
81
135
|
var __vue_staticRenderFns__ = [];
|
|
82
136
|
|
|
83
137
|
/* style */
|
|
@@ -6,17 +6,6 @@ const mockItems = [{
|
|
|
6
6
|
rel: 'nofollow',
|
|
7
7
|
'data-method': 'put'
|
|
8
8
|
}
|
|
9
|
-
}, {
|
|
10
|
-
text: 'Close merge request',
|
|
11
|
-
action: () => {
|
|
12
|
-
// eslint-disable-next-line no-console
|
|
13
|
-
console.log('CLOSED');
|
|
14
|
-
},
|
|
15
|
-
extraAttrs: {
|
|
16
|
-
class: 'gl-text-red-500!',
|
|
17
|
-
rel: 'nofollow',
|
|
18
|
-
'data-method': 'put'
|
|
19
|
-
}
|
|
20
9
|
}, {
|
|
21
10
|
text: 'Create new',
|
|
22
11
|
href: 'https://gitlab.com/gitlab-org/gitlab/-/merge_requests/new',
|
|
@@ -30,6 +19,17 @@ const mockItems = [{
|
|
|
30
19
|
extraAttrs: {
|
|
31
20
|
'data-uuid': '1234'
|
|
32
21
|
}
|
|
22
|
+
}, {
|
|
23
|
+
text: 'Close merge request',
|
|
24
|
+
action: () => {
|
|
25
|
+
// eslint-disable-next-line no-console
|
|
26
|
+
console.log('CLOSED');
|
|
27
|
+
},
|
|
28
|
+
extraAttrs: {
|
|
29
|
+
class: 'gl-text-red-500!',
|
|
30
|
+
rel: 'nofollow',
|
|
31
|
+
'data-method': 'put'
|
|
32
|
+
}
|
|
33
33
|
}];
|
|
34
34
|
const mockItemsCustomItem = [{
|
|
35
35
|
text: 'Assigned to you',
|
package/dist/tokens/js/tokens.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gitlab/ui",
|
|
3
|
-
"version": "78.
|
|
3
|
+
"version": "78.19.0",
|
|
4
4
|
"description": "GitLab UI Components",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -131,14 +131,14 @@
|
|
|
131
131
|
"babel-jest": "29.0.1",
|
|
132
132
|
"babel-loader": "^8.0.5",
|
|
133
133
|
"bootstrap": "4.6.2",
|
|
134
|
-
"cypress": "13.
|
|
134
|
+
"cypress": "13.8.0",
|
|
135
135
|
"cypress-axe": "^1.4.0",
|
|
136
136
|
"cypress-real-events": "^1.11.0",
|
|
137
137
|
"dompurify": "^3.0.0",
|
|
138
138
|
"emoji-regex": "^10.0.0",
|
|
139
139
|
"eslint": "8.57.0",
|
|
140
140
|
"eslint-import-resolver-jest": "3.0.2",
|
|
141
|
-
"eslint-plugin-cypress": "2.15.
|
|
141
|
+
"eslint-plugin-cypress": "2.15.2",
|
|
142
142
|
"eslint-plugin-storybook": "0.8.0",
|
|
143
143
|
"glob": "10.3.3",
|
|
144
144
|
"identity-obj-proxy": "^3.0.0",
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { mount } from '@vue/test-utils';
|
|
2
|
+
import lodashDebounce from 'lodash/debounce';
|
|
2
3
|
import GlFormTextarea from './form_textarea.vue';
|
|
3
4
|
|
|
5
|
+
jest.mock('lodash/debounce', () => jest.fn((fn) => fn));
|
|
6
|
+
|
|
4
7
|
const modelEvent = GlFormTextarea.model.event;
|
|
5
8
|
const newValue = 'foo';
|
|
6
9
|
|
|
@@ -10,6 +13,25 @@ describe('GlFormTextArea', () => {
|
|
|
10
13
|
const createComponent = (propsData = {}) => {
|
|
11
14
|
wrapper = mount(GlFormTextarea, {
|
|
12
15
|
propsData,
|
|
16
|
+
scopedSlots: {
|
|
17
|
+
'character-count-text': function characterCountText({ count }) {
|
|
18
|
+
return count === 1 ? `${count} character remaining` : `${count} characters remaining`;
|
|
19
|
+
},
|
|
20
|
+
'character-count-over-limit-text': function characterCountOverLimitText({ count }) {
|
|
21
|
+
return count === 1 ? `${count} character over limit` : `${count} characters over limit`;
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
});
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const findTextarea = () => wrapper.find('textarea');
|
|
28
|
+
|
|
29
|
+
const itUpdatesDebouncedScreenReaderText = (expectedText) => {
|
|
30
|
+
it('updates debounced screen reader text', () => {
|
|
31
|
+
expect(lodashDebounce).toHaveBeenCalledWith(expect.any(Function), 1000);
|
|
32
|
+
expect(wrapper.find('[data-testid="character-count-text-sr-only"]').text()).toBe(
|
|
33
|
+
expectedText
|
|
34
|
+
);
|
|
13
35
|
});
|
|
14
36
|
};
|
|
15
37
|
|
|
@@ -20,7 +42,7 @@ describe('GlFormTextArea', () => {
|
|
|
20
42
|
});
|
|
21
43
|
|
|
22
44
|
it(`sets the textarea's value`, () => {
|
|
23
|
-
expect(
|
|
45
|
+
expect(findTextarea().element.value).toBe('initial');
|
|
24
46
|
});
|
|
25
47
|
|
|
26
48
|
describe('when the value prop changes', () => {
|
|
@@ -30,7 +52,7 @@ describe('GlFormTextArea', () => {
|
|
|
30
52
|
});
|
|
31
53
|
|
|
32
54
|
it(`updates the textarea's value`, () => {
|
|
33
|
-
expect(
|
|
55
|
+
expect(findTextarea().element.value).toBe(newValue);
|
|
34
56
|
});
|
|
35
57
|
});
|
|
36
58
|
});
|
|
@@ -39,7 +61,7 @@ describe('GlFormTextArea', () => {
|
|
|
39
61
|
beforeEach(() => {
|
|
40
62
|
createComponent();
|
|
41
63
|
|
|
42
|
-
|
|
64
|
+
findTextarea().setValue(newValue);
|
|
43
65
|
});
|
|
44
66
|
|
|
45
67
|
it('synchronously emits update event', () => {
|
|
@@ -59,7 +81,7 @@ describe('GlFormTextArea', () => {
|
|
|
59
81
|
|
|
60
82
|
createComponent({ debounce });
|
|
61
83
|
|
|
62
|
-
|
|
84
|
+
findTextarea().setValue(newValue);
|
|
63
85
|
});
|
|
64
86
|
|
|
65
87
|
it('synchronously emits an update event', () => {
|
|
@@ -82,7 +104,7 @@ describe('GlFormTextArea', () => {
|
|
|
82
104
|
beforeEach(() => {
|
|
83
105
|
createComponent({ lazy: true });
|
|
84
106
|
|
|
85
|
-
|
|
107
|
+
findTextarea().setValue(newValue);
|
|
86
108
|
});
|
|
87
109
|
|
|
88
110
|
it('synchronously emits an update event', () => {
|
|
@@ -119,4 +141,65 @@ describe('GlFormTextArea', () => {
|
|
|
119
141
|
expect(wrapper.emitted('submit')).toEqual([[]]);
|
|
120
142
|
});
|
|
121
143
|
});
|
|
144
|
+
|
|
145
|
+
describe('when `characterCount` prop is set', () => {
|
|
146
|
+
const characterCount = 10;
|
|
147
|
+
|
|
148
|
+
describe('when textarea character count is under the max character count', () => {
|
|
149
|
+
const textareaCharacterCount = 5;
|
|
150
|
+
const expectedText = `${characterCount - textareaCharacterCount} characters remaining`;
|
|
151
|
+
|
|
152
|
+
beforeEach(() => {
|
|
153
|
+
createComponent({
|
|
154
|
+
value: 'a'.repeat(textareaCharacterCount),
|
|
155
|
+
characterCount,
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it('displays remaining characters', () => {
|
|
160
|
+
expect(wrapper.text()).toContain(expectedText);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
itUpdatesDebouncedScreenReaderText(expectedText);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
describe('when textarea character count is over the max character count', () => {
|
|
167
|
+
const textareaCharacterCount = 15;
|
|
168
|
+
const expectedText = `${textareaCharacterCount - characterCount} characters over limit`;
|
|
169
|
+
|
|
170
|
+
beforeEach(() => {
|
|
171
|
+
createComponent({
|
|
172
|
+
value: 'a'.repeat(textareaCharacterCount),
|
|
173
|
+
characterCount,
|
|
174
|
+
});
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it('displays number of characters over', () => {
|
|
178
|
+
expect(wrapper.text()).toContain(expectedText);
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
itUpdatesDebouncedScreenReaderText(expectedText);
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
describe('when textarea value is updated', () => {
|
|
185
|
+
const textareaCharacterCount = 5;
|
|
186
|
+
const newTextareaCharacterCount = textareaCharacterCount + 3;
|
|
187
|
+
const expectedText = `${characterCount - newTextareaCharacterCount} characters remaining`;
|
|
188
|
+
|
|
189
|
+
beforeEach(() => {
|
|
190
|
+
createComponent({
|
|
191
|
+
value: 'a'.repeat(textareaCharacterCount),
|
|
192
|
+
characterCount,
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
wrapper.setProps({ value: 'a'.repeat(newTextareaCharacterCount) });
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it('updates character count text', () => {
|
|
199
|
+
expect(wrapper.text()).toContain(expectedText);
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
itUpdatesDebouncedScreenReaderText(expectedText);
|
|
203
|
+
});
|
|
204
|
+
});
|
|
122
205
|
});
|
|
@@ -3,32 +3,57 @@ import readme from './form_textarea.md';
|
|
|
3
3
|
|
|
4
4
|
const template = `
|
|
5
5
|
<gl-form-textarea
|
|
6
|
-
|
|
6
|
+
:value="value"
|
|
7
7
|
:placeholder="placeholder"
|
|
8
8
|
:rows="5"
|
|
9
9
|
:no-resize="noResize"
|
|
10
|
-
|
|
10
|
+
:character-count="characterCount"
|
|
11
|
+
@input="onInput"
|
|
12
|
+
>
|
|
13
|
+
<template #character-count-text="{ count }">{{ characterCountText(count) }}</template>
|
|
14
|
+
<template #character-count-over-limit-text="{ count }">{{ characterCountOverLimitText(count) }}</template>
|
|
15
|
+
</gl-form-textarea>
|
|
11
16
|
`;
|
|
12
17
|
|
|
13
18
|
const generateProps = ({
|
|
14
|
-
|
|
19
|
+
value = 'We take inspiration from other companies, and we always go for the boring solutions. Just like the rest of our work, we continually adjust our values and strive always to make them better. We used to have more values, but it was difficult to remember them all, so we condensed them and gave sub-values and created an acronym. Everyone is welcome to suggest improvements.',
|
|
15
20
|
placeholder = 'hello',
|
|
16
21
|
noResize = GlFormTextarea.props.noResize.default,
|
|
22
|
+
characterCount = null,
|
|
17
23
|
} = {}) => ({
|
|
18
|
-
|
|
24
|
+
value,
|
|
19
25
|
placeholder,
|
|
20
26
|
noResize,
|
|
27
|
+
characterCount,
|
|
21
28
|
});
|
|
22
29
|
|
|
23
|
-
const Template = (args) => ({
|
|
30
|
+
const Template = (args, { updateArgs }) => ({
|
|
24
31
|
components: { GlFormTextarea },
|
|
25
32
|
props: Object.keys(args),
|
|
33
|
+
methods: {
|
|
34
|
+
onInput(value) {
|
|
35
|
+
updateArgs({ ...args, value });
|
|
36
|
+
},
|
|
37
|
+
characterCountText(count) {
|
|
38
|
+
return count === 1 ? `${count} character remaining` : `${count} characters remaining`;
|
|
39
|
+
},
|
|
40
|
+
characterCountOverLimitText(count) {
|
|
41
|
+
return count === 1 ? `${count} character over limit` : `${count} characters over limit`;
|
|
42
|
+
},
|
|
43
|
+
},
|
|
26
44
|
template,
|
|
27
45
|
});
|
|
28
46
|
|
|
29
47
|
export const Default = Template.bind({});
|
|
30
48
|
Default.args = generateProps();
|
|
31
49
|
|
|
50
|
+
export const WithCharacterCount = Template.bind({});
|
|
51
|
+
WithCharacterCount.args = generateProps({
|
|
52
|
+
value: '',
|
|
53
|
+
placeholder: 'hello',
|
|
54
|
+
characterCount: 100,
|
|
55
|
+
});
|
|
56
|
+
|
|
32
57
|
export default {
|
|
33
58
|
title: 'base/form/form-textarea',
|
|
34
59
|
component: GlFormTextarea,
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
<script>
|
|
2
2
|
import { BFormTextarea } from 'bootstrap-vue';
|
|
3
|
+
import debounce from 'lodash/debounce';
|
|
4
|
+
import uniqueId from 'lodash/uniqueId';
|
|
3
5
|
|
|
4
6
|
const model = {
|
|
5
7
|
prop: 'value',
|
|
@@ -31,6 +33,21 @@ export default {
|
|
|
31
33
|
required: false,
|
|
32
34
|
default: false,
|
|
33
35
|
},
|
|
36
|
+
/**
|
|
37
|
+
* Max character count for the textarea.
|
|
38
|
+
*/
|
|
39
|
+
characterCount: {
|
|
40
|
+
type: Number,
|
|
41
|
+
required: false,
|
|
42
|
+
default: null,
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
data() {
|
|
46
|
+
return {
|
|
47
|
+
characterCountId: uniqueId('form-textarea-character-count-'),
|
|
48
|
+
remainingCharacterCount: this.initialRemainingCharacterCount(),
|
|
49
|
+
remainingCharacterCountSrOnly: this.initialRemainingCharacterCount(),
|
|
50
|
+
};
|
|
34
51
|
},
|
|
35
52
|
computed: {
|
|
36
53
|
listeners() {
|
|
@@ -57,6 +74,41 @@ export default {
|
|
|
57
74
|
keypressEvent() {
|
|
58
75
|
return this.submitOnEnter ? 'keyup' : null;
|
|
59
76
|
},
|
|
77
|
+
isCharacterCountOverLimit() {
|
|
78
|
+
return this.remainingCharacterCount < 0;
|
|
79
|
+
},
|
|
80
|
+
characterCountTextClass() {
|
|
81
|
+
return this.isCharacterCountOverLimit ? 'gl-text-red-500' : 'gl-text-gray-500';
|
|
82
|
+
},
|
|
83
|
+
showCharacterCount() {
|
|
84
|
+
return this.characterCount !== null;
|
|
85
|
+
},
|
|
86
|
+
bFormTextareaProps() {
|
|
87
|
+
return {
|
|
88
|
+
...this.$attrs,
|
|
89
|
+
class: 'gl-form-input gl-form-textarea',
|
|
90
|
+
noResize: this.noResize,
|
|
91
|
+
value: this.value,
|
|
92
|
+
};
|
|
93
|
+
},
|
|
94
|
+
},
|
|
95
|
+
watch: {
|
|
96
|
+
value(newValue) {
|
|
97
|
+
if (!this.showCharacterCount) {
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
this.remainingCharacterCount = this.characterCount - newValue.length;
|
|
102
|
+
this.debouncedUpdateRemainingCharacterCountSrOnly(newValue);
|
|
103
|
+
},
|
|
104
|
+
},
|
|
105
|
+
created() {
|
|
106
|
+
// Debounce updating the remaining character count for a second so
|
|
107
|
+
// screen readers announce the remaining text after the text in the textarea.
|
|
108
|
+
this.debouncedUpdateRemainingCharacterCountSrOnly = debounce(
|
|
109
|
+
this.updateRemainingCharacterCountSrOnly,
|
|
110
|
+
1000
|
|
111
|
+
);
|
|
60
112
|
},
|
|
61
113
|
methods: {
|
|
62
114
|
handleKeyPress(e) {
|
|
@@ -64,16 +116,59 @@ export default {
|
|
|
64
116
|
this.$emit('submit');
|
|
65
117
|
}
|
|
66
118
|
},
|
|
119
|
+
updateRemainingCharacterCountSrOnly(newValue) {
|
|
120
|
+
this.remainingCharacterCountSrOnly = this.characterCount - newValue.length;
|
|
121
|
+
},
|
|
122
|
+
initialRemainingCharacterCount() {
|
|
123
|
+
return this.characterCount - this.value.length;
|
|
124
|
+
},
|
|
67
125
|
},
|
|
68
126
|
};
|
|
69
127
|
</script>
|
|
70
128
|
|
|
71
129
|
<template>
|
|
130
|
+
<div v-if="showCharacterCount">
|
|
131
|
+
<b-form-textarea
|
|
132
|
+
:aria-describedby="characterCountId"
|
|
133
|
+
v-bind="bFormTextareaProps"
|
|
134
|
+
v-on="listeners"
|
|
135
|
+
@[keypressEvent].native="handleKeyPress"
|
|
136
|
+
/>
|
|
137
|
+
<small :class="['form-text', characterCountTextClass]" aria-hidden="true">
|
|
138
|
+
<!--
|
|
139
|
+
@slot Internationalized over character count text. Example: `<template #character-count-over-limit-text="{ count }">{{ n__('%d character over limit', '%d characters over limit', count) }}</template>`
|
|
140
|
+
@binding {number} count
|
|
141
|
+
-->
|
|
142
|
+
<slot
|
|
143
|
+
v-if="isCharacterCountOverLimit"
|
|
144
|
+
name="character-count-over-limit-text"
|
|
145
|
+
:count="Math.abs(remainingCharacterCount)"
|
|
146
|
+
></slot>
|
|
147
|
+
<!--
|
|
148
|
+
@slot Internationalized character count text. Example: `<template #character-count-text="{ count }">{{ n__('%d character remaining', '%d characters remaining', count) }}</template>`
|
|
149
|
+
@binding {number} count
|
|
150
|
+
-->
|
|
151
|
+
|
|
152
|
+
<slot v-else name="character-count-text" :count="remainingCharacterCount"></slot>
|
|
153
|
+
</small>
|
|
154
|
+
<div
|
|
155
|
+
:id="characterCountId"
|
|
156
|
+
class="gl-sr-only"
|
|
157
|
+
aria-live="polite"
|
|
158
|
+
data-testid="character-count-text-sr-only"
|
|
159
|
+
>
|
|
160
|
+
<slot
|
|
161
|
+
v-if="isCharacterCountOverLimit"
|
|
162
|
+
name="character-count-over-limit-text"
|
|
163
|
+
:count="Math.abs(remainingCharacterCount)"
|
|
164
|
+
></slot>
|
|
165
|
+
|
|
166
|
+
<slot v-else name="character-count-text" :count="remainingCharacterCountSrOnly"></slot>
|
|
167
|
+
</div>
|
|
168
|
+
</div>
|
|
72
169
|
<b-form-textarea
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
v-bind="$attrs"
|
|
76
|
-
:value="value"
|
|
170
|
+
v-else
|
|
171
|
+
v-bind="bFormTextareaProps"
|
|
77
172
|
v-on="listeners"
|
|
78
173
|
@[keypressEvent].native="handleKeyPress"
|
|
79
174
|
/>
|
|
@@ -51,7 +51,7 @@ describe('GlDisclosureDropdownItem', () => {
|
|
|
51
51
|
describe.each`
|
|
52
52
|
prop | mockItem
|
|
53
53
|
${'href'} | ${0}
|
|
54
|
-
${'to'} | ${
|
|
54
|
+
${'to'} | ${2}
|
|
55
55
|
`('when item has a `$prop`', ({ prop, mockItem }) => {
|
|
56
56
|
const item = mockItems[mockItem];
|
|
57
57
|
|
|
@@ -102,7 +102,7 @@ describe('GlDisclosureDropdownItem', () => {
|
|
|
102
102
|
});
|
|
103
103
|
|
|
104
104
|
describe('when item has an `action`', () => {
|
|
105
|
-
const item = mockItems[
|
|
105
|
+
const item = mockItems[3];
|
|
106
106
|
const action = jest.spyOn(item, 'action');
|
|
107
107
|
|
|
108
108
|
beforeEach(() => {
|
|
@@ -184,7 +184,7 @@ describe('GlDisclosureDropdownItem', () => {
|
|
|
184
184
|
beforeEach(() => {
|
|
185
185
|
buildWrapper({
|
|
186
186
|
item: {
|
|
187
|
-
...mockItems[
|
|
187
|
+
...mockItems[3],
|
|
188
188
|
extraAttrs,
|
|
189
189
|
},
|
|
190
190
|
});
|
|
@@ -8,18 +8,6 @@ export const mockItems = [
|
|
|
8
8
|
'data-method': 'put',
|
|
9
9
|
},
|
|
10
10
|
},
|
|
11
|
-
{
|
|
12
|
-
text: 'Close merge request',
|
|
13
|
-
action: () => {
|
|
14
|
-
// eslint-disable-next-line no-console
|
|
15
|
-
console.log('CLOSED');
|
|
16
|
-
},
|
|
17
|
-
extraAttrs: {
|
|
18
|
-
class: 'gl-text-red-500!',
|
|
19
|
-
rel: 'nofollow',
|
|
20
|
-
'data-method': 'put',
|
|
21
|
-
},
|
|
22
|
-
},
|
|
23
11
|
{
|
|
24
12
|
text: 'Create new',
|
|
25
13
|
href: 'https://gitlab.com/gitlab-org/gitlab/-/merge_requests/new',
|
|
@@ -35,6 +23,18 @@ export const mockItems = [
|
|
|
35
23
|
'data-uuid': '1234',
|
|
36
24
|
},
|
|
37
25
|
},
|
|
26
|
+
{
|
|
27
|
+
text: 'Close merge request',
|
|
28
|
+
action: () => {
|
|
29
|
+
// eslint-disable-next-line no-console
|
|
30
|
+
console.log('CLOSED');
|
|
31
|
+
},
|
|
32
|
+
extraAttrs: {
|
|
33
|
+
class: 'gl-text-red-500!',
|
|
34
|
+
rel: 'nofollow',
|
|
35
|
+
'data-method': 'put',
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
38
|
];
|
|
39
39
|
|
|
40
40
|
export const mockItemsCustomItem = [
|