@gitlab/ui 132.2.1 → 133.0.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.
@@ -352,57 +352,6 @@ export default {
352
352
  noWheel() {
353
353
  return this.type === 'number';
354
354
  },
355
- selectionStart: {
356
- // Expose selectionStart for formatters, etc
357
- cache: false,
358
- get() {
359
- return this.$refs.input.selectionStart;
360
- },
361
- set(val) {
362
- this.$refs.input.selectionStart = val;
363
- },
364
- },
365
- selectionEnd: {
366
- // Expose selectionEnd for formatters, etc
367
- cache: false,
368
- get() {
369
- return this.$refs.input.selectionEnd;
370
- },
371
- set(val) {
372
- this.$refs.input.selectionEnd = val;
373
- },
374
- },
375
- selectionDirection: {
376
- // Expose selectionDirection for formatters, etc
377
- cache: false,
378
- get() {
379
- return this.$refs.input.selectionDirection;
380
- },
381
- set(val) {
382
- this.$refs.input.selectionDirection = val;
383
- },
384
- },
385
- validity: {
386
- // Expose validity property
387
- cache: false,
388
- get() {
389
- return this.$refs.input.validity;
390
- },
391
- },
392
- validationMessage: {
393
- // Expose validationMessage property
394
- cache: false,
395
- get() {
396
- return this.$refs.input.validationMessage;
397
- },
398
- },
399
- willValidate: {
400
- // Expose willValidate property
401
- cache: false,
402
- get() {
403
- return this.$refs.input.willValidate;
404
- },
405
- },
406
355
  },
407
356
  watch: {
408
357
  value(newValue) {
@@ -510,21 +459,14 @@ export default {
510
459
  };
511
460
  // Only debounce the value update when a value greater than `0`
512
461
  // is set and we are not in lazy mode or this is a forced update
513
- const debounce = this.computedDebounce;
514
- if (debounce > 0 && !lazy && !force) {
515
- this.$_inputDebounceTimer = setTimeout(doUpdate, debounce);
462
+ if (this.computedDebounce > 0 && !lazy && !force) {
463
+ this.$_inputDebounceTimer = setTimeout(doUpdate, this.computedDebounce);
516
464
  } else {
517
465
  // Immediately update the v-model
518
466
  doUpdate();
519
467
  }
520
468
  },
521
469
  onInput(event) {
522
- // `event.target.composing` is set by Vue
523
- // https://github.com/vuejs/vue/blob/dev/src/platforms/web/runtime/directives/model.js
524
- // TODO: Is this needed now with the latest Vue?
525
- if (event.target.composing) {
526
- return;
527
- }
528
470
  const { value } = event.target;
529
471
  const formattedValue = this.formatValue(value, event);
530
472
  // Exit when the `formatter` function strictly returned `false`
@@ -1,24 +1,27 @@
1
1
  <script>
2
- import { uniqueId } from 'lodash-es';
3
- import { BFormTextarea } from '../../../../vendor/bootstrap-vue/src/components/form-textarea/form-textarea';
2
+ import { uniqueId, isBoolean, toInteger, toString } from 'lodash-es';
3
+ import { toFloat } from '../../../../utils/number_utils';
4
+ import { isVisible, stopEvent } from '../../../../utils/utils';
5
+ import { VBVisible } from '../../../../vendor/bootstrap-vue/src/directives/visible/visible';
4
6
  import GlFormCharacterCount from '../form_character_count/form_character_count.vue';
5
7
 
6
- const model = {
7
- prop: 'value',
8
- event: 'input',
9
- };
10
-
11
8
  export default {
12
9
  name: 'GlFormTextarea',
13
10
  components: {
14
- BFormTextarea,
15
11
  GlFormCharacterCount,
16
12
  },
13
+ directives: {
14
+ 'b-visible': VBVisible,
15
+ },
17
16
  inheritAttrs: false,
18
- model,
17
+ model: {
18
+ prop: 'value',
19
+ event: 'input',
20
+ },
19
21
  props: {
20
- // This prop is needed to map the v-model correctly
21
- // https://alligator.io/vuejs/add-v-model-support/
22
+ /**
23
+ * The current value of the textarea.
24
+ */
22
25
  value: {
23
26
  type: String,
24
27
  required: false,
@@ -64,32 +67,229 @@ export default {
64
67
  required: false,
65
68
  default: 4,
66
69
  },
70
+ /**
71
+ * Used to set the `id` attribute on the rendered content.
72
+ */
73
+ id: {
74
+ type: String,
75
+ required: false,
76
+ default: undefined,
77
+ },
78
+ /**
79
+ * When set to `true`, attempts to auto-focus the control when it is mounted.
80
+ */
81
+ autofocus: {
82
+ type: Boolean,
83
+ required: false,
84
+ default: false,
85
+ },
86
+ /**
87
+ * When set to `true`, disables the component's functionality.
88
+ */
89
+ disabled: {
90
+ type: Boolean,
91
+ required: false,
92
+ default: false,
93
+ },
94
+ /**
95
+ * ID of the form that the form control belongs to. Sets the `form` attribute on the control.
96
+ */
97
+ form: {
98
+ type: String,
99
+ required: false,
100
+ default: undefined,
101
+ },
102
+ /**
103
+ * Sets the value of the `name` attribute on the form control.
104
+ */
105
+ name: {
106
+ type: String,
107
+ required: false,
108
+ default: undefined,
109
+ },
110
+ /**
111
+ * Adds the `required` attribute to the form control.
112
+ */
113
+ required: {
114
+ type: Boolean,
115
+ required: false,
116
+ default: false,
117
+ },
118
+ /**
119
+ * Controls the validation state appearance of the component.
120
+ * `true` for valid, `false` for invalid, or `null` for no validation state.
121
+ */
122
+ state: {
123
+ type: Boolean,
124
+ required: false,
125
+ default: null,
126
+ },
127
+ /**
128
+ * Optional value to set for the 'aria-invalid' attribute.
129
+ */
130
+ ariaInvalid: {
131
+ type: [Boolean, String],
132
+ required: false,
133
+ default: false,
134
+ },
135
+ /**
136
+ * Sets the 'autocomplete' attribute value on the form control.
137
+ */
138
+ autocomplete: {
139
+ type: String,
140
+ required: false,
141
+ default: undefined,
142
+ },
143
+ /**
144
+ * When set to a number of milliseconds greater than zero, will debounce the user input.
145
+ */
146
+ debounce: {
147
+ type: [Number, String],
148
+ required: false,
149
+ default: 0,
150
+ },
151
+ /**
152
+ * Reference to a function for formatting the input.
153
+ */
154
+ formatter: {
155
+ type: Function,
156
+ required: false,
157
+ default: undefined,
158
+ },
159
+ /**
160
+ * Sets the `placeholder` attribute value on the form control.
161
+ */
162
+ placeholder: {
163
+ type: String,
164
+ required: false,
165
+ default: undefined,
166
+ },
167
+ /**
168
+ * Sets the `readonly` attribute on the form control.
169
+ */
170
+ readonly: {
171
+ type: Boolean,
172
+ required: false,
173
+ default: false,
174
+ },
175
+ /**
176
+ * Set the size of the component's appearance. 'sm' or 'lg'. Defaults to medium size when omitted.
177
+ */
178
+ size: {
179
+ type: String,
180
+ required: false,
181
+ default: undefined,
182
+ },
183
+ /**
184
+ * The maximum number of rows to show. When set, enables auto-height.
185
+ */
186
+ maxRows: {
187
+ type: [Number, String],
188
+ required: false,
189
+ default: undefined,
190
+ },
67
191
  },
68
192
  data() {
69
193
  return {
194
+ localValue: toString(this.value),
195
+ vModelValue: this.value,
196
+ localId: null,
197
+ heightInPx: null,
70
198
  characterCountTextId: uniqueId('form-textarea-character-count-'),
71
199
  };
72
200
  },
73
201
  computed: {
74
- listeners() {
202
+ computedId() {
203
+ return this.id || this.localId;
204
+ },
205
+ computedState() {
206
+ return isBoolean(this.state) ? this.state : null;
207
+ },
208
+ stateClass() {
209
+ if (this.computedState === true) return 'is-valid';
210
+ if (this.computedState === false) return 'is-invalid';
211
+ return null;
212
+ },
213
+ computedAriaInvalid() {
214
+ const { ariaInvalid } = this;
215
+ if (ariaInvalid === true || ariaInvalid === 'true' || ariaInvalid === '') {
216
+ return 'true';
217
+ }
218
+ return this.computedState === false ? 'true' : ariaInvalid;
219
+ },
220
+ sizeClass() {
221
+ return this.size ? `form-control-${this.size}` : null;
222
+ },
223
+ computedDebounce() {
224
+ return Math.max(toInteger(this.debounce), 0);
225
+ },
226
+ hasFormatter() {
227
+ return typeof this.formatter === 'function';
228
+ },
229
+ computedMinRows() {
230
+ // Ensure rows is at least 2 and positive (2 is the native textarea value)
231
+ // A value of 1 can cause issues in some browsers, and most browsers
232
+ // only support 2 as the smallest value
233
+ return Math.max(toInteger(this.rows), 2);
234
+ },
235
+ computedMaxRows() {
236
+ return Math.max(this.computedMinRows, toInteger(this.maxRows));
237
+ },
238
+ computedRows() {
239
+ // This is used to set the attribute 'rows' on the textarea
240
+ // If auto-height is enabled, then we return `null` as we use CSS to control height
241
+ return this.computedMinRows === this.computedMaxRows ? this.computedMinRows : null;
242
+ },
243
+ computedStyle() {
244
+ const styles = {
245
+ // Setting `noResize` to true will disable the ability for the user to
246
+ // manually resize the textarea. We also disable when in auto height mode
247
+ resize: !this.computedRows || this.noResize ? 'none' : null,
248
+ };
249
+ if (!this.computedRows) {
250
+ // Conditionally set the computed CSS height when auto rows/height is enabled
251
+ // We avoid setting the style to `null`, which can override user manual resize handle
252
+ styles.height = this.heightInPx;
253
+ // We always add a vertical scrollbar to the textarea when auto-height is
254
+ // enabled so that the computed height calculation returns a stable value
255
+ styles.overflowY = 'scroll';
256
+ }
257
+ return styles;
258
+ },
259
+ computedAttrs() {
260
+ const { disabled, required, readonly } = this;
261
+
262
+ return {
263
+ ...this.$attrs,
264
+ id: this.computedId,
265
+ name: this.name || null,
266
+ form: this.form || null,
267
+ disabled,
268
+ placeholder: this.placeholder || null,
269
+ required,
270
+ autocomplete: this.autocomplete || null,
271
+ readonly,
272
+ rows: this.computedRows,
273
+ 'aria-required': required ? 'true' : null,
274
+ 'aria-invalid': this.computedAriaInvalid,
275
+ };
276
+ },
277
+ computedClass() {
278
+ return [
279
+ 'gl-form-input',
280
+ 'gl-form-textarea',
281
+ 'form-control',
282
+ this.textareaClasses,
283
+ this.sizeClass,
284
+ this.stateClass,
285
+ ];
286
+ },
287
+ computedListeners() {
75
288
  return {
76
289
  ...this.$listeners,
77
- // Swap purpose of input and update events from underlying BFormTextarea.
78
- // See https://gitlab.com/gitlab-org/gitlab-ui/-/issues/631.
79
- input: (...args) => {
80
- /**
81
- * Emitted to update the v-model
82
- */
83
- this.$emit('update', ...args);
84
- },
85
- update: (...args) => {
86
- /**
87
- * Triggered by user interaction.
88
- * Emitted after any formatting (not including 'trim' or 'number' props).
89
- * Useful for getting the currently entered value when the 'debounce' or 'lazy' props are set.
90
- */
91
- this.$emit(model.event, ...args);
92
- },
290
+ input: this.onInput,
291
+ change: this.onChange,
292
+ blur: this.onBlur,
93
293
  };
94
294
  },
95
295
  keypressEvent() {
@@ -98,34 +298,230 @@ export default {
98
298
  showCharacterCount() {
99
299
  return this.characterCountLimit !== null;
100
300
  },
101
- bFormTextareaProps() {
102
- return {
103
- ...this.$attrs,
104
- class: ['gl-form-input gl-form-textarea', this.textareaClasses],
105
- noResize: this.noResize,
106
- value: this.value,
107
- rows: this.rows,
108
- };
301
+ },
302
+ watch: {
303
+ value(newValue) {
304
+ const stringifyValue = toString(newValue);
305
+ if (stringifyValue !== this.localValue || newValue !== this.vModelValue) {
306
+ this.clearDebounce();
307
+ this.localValue = stringifyValue;
308
+ this.vModelValue = newValue;
309
+ }
109
310
  },
311
+ localValue() {
312
+ this.setHeight();
313
+ },
314
+ },
315
+ created() {
316
+ this.$_inputDebounceTimer = null;
317
+ },
318
+ mounted() {
319
+ this.handleAutofocus();
320
+ this.setHeight();
321
+ this.$nextTick(() => {
322
+ this.localId = uniqueId('gl-form-textarea-');
323
+ });
324
+ },
325
+ beforeDestroy() {
326
+ this.clearDebounce();
110
327
  },
111
328
  methods: {
329
+ focus() {
330
+ if (!this.disabled) {
331
+ this.$refs.input?.focus();
332
+ }
333
+ },
334
+ blur() {
335
+ if (!this.disabled) {
336
+ this.$refs.input?.blur();
337
+ }
338
+ },
339
+ clearDebounce() {
340
+ clearTimeout(this.$_inputDebounceTimer);
341
+ this.$_inputDebounceTimer = null;
342
+ },
343
+ formatValue(value, event) {
344
+ let newValue = toString(value);
345
+ if (this.hasFormatter) {
346
+ newValue = this.formatter(newValue, event);
347
+ }
348
+ return newValue;
349
+ },
350
+ updateValue(value, force = false) {
351
+ this.clearDebounce();
352
+
353
+ const doUpdate = () => {
354
+ if (value !== this.vModelValue) {
355
+ this.vModelValue = value;
356
+ /**
357
+ * Triggered by user interaction.
358
+ * Emitted after any formatting (not including 'trim' or 'number' props).
359
+ * Useful for getting the currently entered value when the 'debounce'is set.
360
+ *
361
+ * @event input
362
+ */
363
+ this.$emit('input', value);
364
+ } else if (this.hasFormatter) {
365
+ const { input } = this.$refs;
366
+ if (input && value !== input.value) {
367
+ input.value = value;
368
+ }
369
+ }
370
+ };
371
+
372
+ if (this.computedDebounce > 0 && !force) {
373
+ this.$_inputDebounceTimer = setTimeout(doUpdate, this.computedDebounce);
374
+ } else {
375
+ doUpdate();
376
+ }
377
+ },
378
+ onInput(event) {
379
+ const { value } = event.target;
380
+ const formattedValue = this.formatValue(value, event);
381
+ if (formattedValue === false || event.defaultPrevented) {
382
+ stopEvent(event, { propagation: false });
383
+ return;
384
+ }
385
+ this.localValue = formattedValue;
386
+ this.updateValue(formattedValue);
387
+ /**
388
+ * The `input` and `update` events are swapped
389
+ * see https://gitlab.com/gitlab-org/gitlab-ui/-/merge_requests/1628.
390
+ *
391
+ * @event update
392
+ */
393
+ this.$emit('update', formattedValue);
394
+ },
395
+ onChange(event) {
396
+ const { value } = event.target;
397
+ const formattedValue = this.formatValue(value, event);
398
+ if (formattedValue === false || event.defaultPrevented) {
399
+ stopEvent(event, { propagation: false });
400
+ return;
401
+ }
402
+ this.localValue = formattedValue;
403
+ this.updateValue(formattedValue, true);
404
+ /**
405
+ * Change event triggered by user interaction.
406
+ * Emitted after any formatting (not including 'trim' or 'number' props)
407
+ * and after the v-model is updated. The `input` and `update` events are swapped
408
+ * see https://gitlab.com/gitlab-org/gitlab-ui/-/merge_requests/1628.
409
+ *
410
+ * @event change
411
+ */
412
+ this.$emit('change', formattedValue);
413
+ },
414
+ onBlur(event) {
415
+ const { value } = event.target;
416
+ const formattedValue = this.formatValue(value, event);
417
+ if (formattedValue !== false) {
418
+ this.localValue = toString(formattedValue);
419
+ this.updateValue(formattedValue, true);
420
+ }
421
+ /**
422
+ * Emitted after the textarea loses focus
423
+ *
424
+ * @event blur
425
+ */
426
+ this.$emit('blur', event);
427
+ },
428
+ handleAutofocus() {
429
+ this.$nextTick(() => {
430
+ window.requestAnimationFrame(() => {
431
+ if (this.autofocus && isVisible(this.$refs.input)) {
432
+ this.focus();
433
+ }
434
+ });
435
+ });
436
+ },
112
437
  handleKeyPress(e) {
113
- if (e.keyCode === 13 && (e.metaKey || e.ctrlKey)) {
438
+ if (this.submitOnEnter && e.keyCode === 13 && (e.metaKey || e.ctrlKey)) {
439
+ /**
440
+ * Emitted after enter is pressed in textarea
441
+ *
442
+ * @event submit
443
+ */
114
444
  this.$emit('submit');
115
445
  }
116
446
  },
447
+ visibleCallback(visible) {
448
+ if (visible) {
449
+ this.$nextTick(this.setHeight);
450
+ }
451
+ },
452
+ setHeight() {
453
+ this.$nextTick(() => {
454
+ window.requestAnimationFrame(() => {
455
+ this.heightInPx = this.computeHeight();
456
+ });
457
+ });
458
+ },
459
+ computeHeight() {
460
+ if (this.computedRows !== null) {
461
+ return null;
462
+ }
463
+
464
+ const el = this.$refs.input;
465
+
466
+ if (!el || !isVisible(el)) {
467
+ return null;
468
+ }
469
+
470
+ const computedStyle = getComputedStyle(el);
471
+ const lineHeight = toFloat(computedStyle.lineHeight, 1);
472
+ const border =
473
+ toFloat(computedStyle.borderTopWidth, 0) + toFloat(computedStyle.borderBottomWidth, 0);
474
+ const padding =
475
+ toFloat(computedStyle.paddingTop, 0) + toFloat(computedStyle.paddingBottom, 0);
476
+ const offset = border + padding;
477
+ const minHeight = lineHeight * this.computedMinRows + offset;
478
+
479
+ const oldHeight = el.style.height || computedStyle.height;
480
+ el.style.height = 'auto';
481
+ const { scrollHeight } = el;
482
+ el.style.height = oldHeight;
483
+
484
+ const contentRows = Math.max((scrollHeight - padding) / lineHeight, 2);
485
+ const rows = Math.min(Math.max(contentRows, this.computedMinRows), this.computedMaxRows);
486
+ const height = Math.max(Math.ceil(rows * lineHeight + offset), minHeight);
487
+
488
+ return `${height}px`;
489
+ },
490
+ select(...args) {
491
+ this.$refs.input.select(...args);
492
+ },
493
+ setSelectionRange(...args) {
494
+ this.$refs.input.setSelectionRange(...args);
495
+ },
496
+ setRangeText(...args) {
497
+ this.$refs.input.setRangeText(...args);
498
+ },
499
+ setCustomValidity(...args) {
500
+ return this.$refs.input.setCustomValidity(...args);
501
+ },
502
+ checkValidity(...args) {
503
+ return this.$refs.input.checkValidity(...args);
504
+ },
505
+ reportValidity(...args) {
506
+ return this.$refs.input.reportValidity(...args);
507
+ },
117
508
  },
118
509
  };
119
510
  </script>
120
511
 
121
512
  <template>
122
513
  <div v-if="showCharacterCount">
123
- <b-form-textarea
124
- v-bind="bFormTextareaProps"
514
+ <textarea
515
+ ref="input"
516
+ v-b-visible.640="visibleCallback"
517
+ :value="localValue"
518
+ :class="computedClass"
519
+ :style="computedStyle"
520
+ v-bind="computedAttrs"
125
521
  :aria-describedby="characterCountTextId"
126
- v-on="listeners"
127
- @[keypressEvent].native="handleKeyPress"
128
- />
522
+ v-on="computedListeners"
523
+ @keyup.enter="handleKeyPress"
524
+ ></textarea>
129
525
  <gl-form-character-count
130
526
  :value="value"
131
527
  :limit="characterCountLimit"
@@ -133,7 +529,13 @@ export default {
133
529
  >
134
530
  <template #over-limit-text="{ count }">
135
531
  <!--
136
- @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>`
532
+ @slot Internationalized over character count text.
533
+ Example:
534
+ ```
535
+ <template #character-count-over-limit-text="{ count }">
536
+ {{ n__('%d character over limit.', '%d characters over limit.', count) }}
537
+ </template>
538
+ ```
137
539
  @binding {number} count
138
540
  -->
139
541
  <slot name="character-count-over-limit-text" :count="count"></slot>
@@ -141,7 +543,13 @@ export default {
141
543
 
142
544
  <template #remaining-count-text="{ count }">
143
545
  <!--
144
- @slot Internationalized character count text. Example: `<template #remaining-character-count-text="{ count }">{{ n__('%d character remaining.', '%d characters remaining.', count) }}</template>`
546
+ @slot Internationalized character count text.
547
+ Example:
548
+ ```
549
+ <template #remaining-character-count-text="{ count }">
550
+ {{ n__('%d character remaining.', '%d characters remaining.', count) }}
551
+ </template>
552
+ ```
145
553
  @binding {number} count
146
554
  -->
147
555
 
@@ -149,10 +557,15 @@ export default {
149
557
  ></template>
150
558
  </gl-form-character-count>
151
559
  </div>
152
- <b-form-textarea
560
+ <textarea
153
561
  v-else
154
- v-bind="bFormTextareaProps"
155
- v-on="listeners"
156
- @[keypressEvent].native="handleKeyPress"
157
- />
562
+ ref="input"
563
+ v-b-visible.640="visibleCallback"
564
+ :value="localValue"
565
+ :class="computedClass"
566
+ :style="computedStyle"
567
+ v-bind="computedAttrs"
568
+ v-on="computedListeners"
569
+ @keyup.enter="handleKeyPress"
570
+ ></textarea>
158
571
  </template>
@@ -85,7 +85,7 @@ export default {
85
85
  >
86
86
  <span class="gl-new-dropdown-item-content">
87
87
  <gl-icon
88
- name="mobile-issue-close"
88
+ name="check"
89
89
  data-testid="dropdown-item-checkbox"
90
90
  :class="[
91
91
  'gl-new-dropdown-item-check-icon',
@@ -18,7 +18,6 @@ export const NAME_FORM_SELECT = 'BFormSelect'
18
18
  export const NAME_FORM_SELECT_OPTION = 'BFormSelectOption'
19
19
  export const NAME_FORM_SELECT_OPTION_GROUP = 'BFormSelectOptionGroup'
20
20
  export const NAME_FORM_TEXT = 'BFormText'
21
- export const NAME_FORM_TEXTAREA = 'BFormTextarea'
22
21
  export const NAME_FORM_VALID_FEEDBACK = 'BFormValidFeedback'
23
22
  export const NAME_LINK = 'BLink'
24
23
  export const NAME_MODAL = 'BModal'