@gitlab/ui 136.3.0 → 136.3.2

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.
Files changed (47) hide show
  1. package/dist/components/base/collapse/collapse.js +3 -4
  2. package/dist/components/base/filtered_search/filtered_search.js +4 -5
  3. package/dist/components/base/filtered_search/filtered_search_token.js +8 -9
  4. package/dist/components/base/filtered_search/filtered_search_token_segment.js +13 -14
  5. package/dist/components/base/form/form_checkbox/form_checkbox.js +14 -15
  6. package/dist/components/base/form/form_input/form_input.js +9 -11
  7. package/dist/components/base/form/form_radio/form_radio.js +14 -15
  8. package/dist/components/base/form/form_textarea/form_textarea.js +13 -16
  9. package/dist/components/base/infinite_scroll/infinite_scroll.js +17 -19
  10. package/dist/components/base/new_dropdowns/listbox/listbox.js +17 -19
  11. package/dist/components/base/sorting/sorting.js +2 -2
  12. package/dist/components/base/tabs/tabs/scrollable_tabs.js +6 -8
  13. package/dist/components/base/toggle/toggle.js +1 -0
  14. package/dist/components/base/token_selector/token_selector.js +13 -15
  15. package/dist/index.css +2 -2
  16. package/dist/index.css.map +1 -1
  17. package/dist/tokens/build/js/tokens.dark.js +34 -17
  18. package/dist/tokens/build/js/tokens.js +34 -17
  19. package/package.json +4 -4
  20. package/src/components/base/collapse/collapse.vue +3 -4
  21. package/src/components/base/filtered_search/filtered_search.vue +4 -5
  22. package/src/components/base/filtered_search/filtered_search_token.vue +8 -9
  23. package/src/components/base/filtered_search/filtered_search_token_segment.vue +10 -11
  24. package/src/components/base/form/form_checkbox/form_checkbox.vue +14 -15
  25. package/src/components/base/form/form_input/form_input.vue +9 -11
  26. package/src/components/base/form/form_radio/form_radio.vue +14 -15
  27. package/src/components/base/form/form_textarea/form_textarea.vue +13 -16
  28. package/src/components/base/infinite_scroll/infinite_scroll.vue +16 -18
  29. package/src/components/base/new_dropdowns/listbox/listbox.vue +17 -19
  30. package/src/components/base/sorting/sorting.vue +3 -2
  31. package/src/components/base/tabs/tabs/scrollable_tabs.vue +7 -9
  32. package/src/components/base/toggle/toggle.scss +90 -22
  33. package/src/components/base/toggle/toggle.vue +1 -0
  34. package/src/components/base/token_selector/token_selector.vue +12 -14
  35. package/src/tokens/build/css/tokens.css +39 -22
  36. package/src/tokens/build/css/tokens.dark.css +39 -22
  37. package/src/tokens/build/figma/mode.dark.json +189 -24
  38. package/src/tokens/build/figma/mode.json +189 -24
  39. package/src/tokens/build/js/tokens.dark.js +33 -16
  40. package/src/tokens/build/js/tokens.js +33 -16
  41. package/src/tokens/build/json/tokens.dark.json +650 -42
  42. package/src/tokens/build/json/tokens.json +650 -42
  43. package/src/tokens/build/scss/_tokens.dark.scss +39 -22
  44. package/src/tokens/build/scss/_tokens.scss +39 -22
  45. package/src/tokens/build/scss/_tokens_custom_properties.scss +17 -0
  46. package/src/tokens/contextual/toggle.tokens.json +198 -33
  47. package/src/tokens/semantic/action.tokens.json +14 -0
@@ -247,18 +247,17 @@ export default {
247
247
  }
248
248
  },
249
249
 
250
- replaceToken(newType) {
250
+ async replaceToken(newType) {
251
251
  const newTokenConfig = this.availableTokens.find(({ type }) => type === newType);
252
252
 
253
253
  if (newTokenConfig === this.config) {
254
- this.$nextTick(() => {
255
- /**
256
- * Emitted when this term token will lose its focus.
257
- *
258
- * @event deactivate
259
- */
260
- this.$emit('deactivate');
261
- });
254
+ await this.$nextTick();
255
+ /**
256
+ * Emitted when this term token will lose its focus.
257
+ *
258
+ * @event deactivate
259
+ */
260
+ this.$emit('deactivate');
262
261
  return;
263
262
  }
264
263
 
@@ -316,20 +316,19 @@ export default {
316
316
  );
317
317
  },
318
318
 
319
- activate() {
319
+ async activate() {
320
320
  this.fallbackValue = this.value;
321
321
 
322
- this.$nextTick(() => {
323
- const { input } = this.$refs;
324
- if (input) {
325
- input.focus();
326
- input.scrollIntoView({ block: 'nearest', inline: 'end' });
327
- this.alignSuggestions(input);
328
- if (this.cursorPosition === 'start') {
329
- input?.setSelectionRange(0, 0);
330
- }
322
+ await this.$nextTick();
323
+ const { input } = this.$refs;
324
+ if (input) {
325
+ input.focus();
326
+ input.scrollIntoView({ block: 'nearest', inline: 'end' });
327
+ this.alignSuggestions(input);
328
+ if (this.cursorPosition === 'start') {
329
+ input?.setSelectionRange(0, 0);
331
330
  }
332
- });
331
+ }
333
332
  },
334
333
 
335
334
  deactivate() {
@@ -221,7 +221,7 @@ export default {
221
221
  }
222
222
  }
223
223
  },
224
- handleChange({ target: { checked, indeterminate } }) {
224
+ async handleChange({ target: { checked, indeterminate } }) {
225
225
  const { value, uncheckedValue } = this;
226
226
 
227
227
  // Update `computedLocalChecked`
@@ -241,26 +241,25 @@ export default {
241
241
  this.computedLocalChecked = localChecked;
242
242
 
243
243
  // Fire events in a `$nextTick()` to ensure the `v-model` is updated
244
- this.$nextTick(() => {
244
+ await this.$nextTick();
245
+ /**
246
+ * Emitted when selected value(s) is changed due to user interaction.
247
+ *
248
+ * @event change
249
+ */
250
+ this.$emit('change', localChecked);
251
+
252
+ // If this is a child of a group, we emit a change event on it as well
253
+ if (this.isGroup) {
245
254
  /**
246
255
  * Emitted when selected value(s) is changed due to user interaction.
247
256
  *
248
257
  * @event change
249
258
  */
250
- this.$emit('change', localChecked);
251
-
252
- // If this is a child of a group, we emit a change event on it as well
253
- if (this.isGroup) {
254
- /**
255
- * Emitted when selected value(s) is changed due to user interaction.
256
- *
257
- * @event change
258
- */
259
- this.group.$emit('change', localChecked);
260
- }
259
+ this.group.$emit('change', localChecked);
260
+ }
261
261
 
262
- this.$emit('indeterminate', indeterminate);
263
- });
262
+ this.$emit('indeterminate', indeterminate);
264
263
  },
265
264
  setIndeterminate(state) {
266
265
  // Indeterminate only supported in single checkbox mode
@@ -373,14 +373,13 @@ export default {
373
373
  // Create private non-reactive props
374
374
  this.$_inputDebounceTimer = null;
375
375
  },
376
- mounted() {
376
+ async mounted() {
377
377
  this.setWheelStopper(this.noWheel);
378
378
  this.handleAutofocus();
379
- this.$nextTick(() => {
380
- // Update DOM with auto-generated ID after mount
381
- // to prevent SSR hydration errors
382
- this.localId = uniqueId('gl-form-input-');
383
- });
379
+ await this.$nextTick();
380
+ // Update DOM with auto-generated ID after mount
381
+ // to prevent SSR hydration errors
382
+ this.localId = uniqueId('gl-form-input-');
384
383
  },
385
384
  deactivated() {
386
385
  // Turn off listeners when keep-alive component deactivated
@@ -534,11 +533,10 @@ export default {
534
533
  stopEvent(event, { propagation: false });
535
534
  this.blur();
536
535
  },
537
- handleAutofocus() {
538
- this.$nextTick(() => {
539
- window.requestAnimationFrame(() => {
540
- if (this.autofocus && isVisible(this.$refs.input)) this.focus();
541
- });
536
+ async handleAutofocus() {
537
+ await this.$nextTick();
538
+ window.requestAnimationFrame(() => {
539
+ if (this.autofocus && isVisible(this.$refs.input)) this.focus();
542
540
  });
543
541
  },
544
542
  select(...args) {
@@ -171,33 +171,32 @@ export default {
171
171
  this.$emit('input', newValue);
172
172
  }
173
173
  },
174
- handleChange() {
174
+ async handleChange() {
175
175
  const { value } = this;
176
176
 
177
177
  // Update `computedLocalChecked`
178
178
  this.computedLocalChecked = value;
179
179
 
180
180
  // Fire events in a `$nextTick()` to ensure the `v-model` is updated
181
- this.$nextTick(() => {
181
+ await this.$nextTick();
182
+ /**
183
+ * Emitted when the selected value is changed due to user interaction
184
+ *
185
+ * @event change
186
+ * @type {boolean} current selected value of radio group
187
+ */
188
+ this.$emit('change', value);
189
+
190
+ // If this is a child of a group, we emit a change event on it as well
191
+ if (this.isGroup) {
182
192
  /**
183
193
  * Emitted when the selected value is changed due to user interaction
184
194
  *
185
195
  * @event change
186
196
  * @type {boolean} current selected value of radio group
187
197
  */
188
- this.$emit('change', value);
189
-
190
- // If this is a child of a group, we emit a change event on it as well
191
- if (this.isGroup) {
192
- /**
193
- * Emitted when the selected value is changed due to user interaction
194
- *
195
- * @event change
196
- * @type {boolean} current selected value of radio group
197
- */
198
- this.group.$emit('change', value);
199
- }
200
- });
198
+ this.group.$emit('change', value);
199
+ }
201
200
  },
202
201
  focus() {
203
202
  if (!this.disabled) {
@@ -315,11 +315,10 @@ export default {
315
315
  created() {
316
316
  this.$_inputDebounceTimer = null;
317
317
  },
318
- mounted() {
318
+ async mounted() {
319
319
  this.handleAutofocus();
320
- this.$nextTick(() => {
321
- this.localId = uniqueId('gl-form-textarea-');
322
- });
320
+ await this.$nextTick();
321
+ this.localId = uniqueId('gl-form-textarea-');
323
322
  },
324
323
  beforeDestroy() {
325
324
  this.clearDebounce();
@@ -424,13 +423,12 @@ export default {
424
423
  */
425
424
  this.$emit('blur', event);
426
425
  },
427
- handleAutofocus() {
428
- this.$nextTick(() => {
429
- window.requestAnimationFrame(() => {
430
- if (this.autofocus && isVisible(this.$refs.input)) {
431
- this.focus();
432
- }
433
- });
426
+ async handleAutofocus() {
427
+ await this.$nextTick();
428
+ window.requestAnimationFrame(() => {
429
+ if (this.autofocus && isVisible(this.$refs.input)) {
430
+ this.focus();
431
+ }
434
432
  });
435
433
  },
436
434
  handleKeyPress(e) {
@@ -443,11 +441,10 @@ export default {
443
441
  this.$emit('submit');
444
442
  }
445
443
  },
446
- setHeight() {
447
- this.$nextTick(() => {
448
- window.requestAnimationFrame(() => {
449
- this.heightInPx = this.computeHeight();
450
- });
444
+ async setHeight() {
445
+ await this.$nextTick();
446
+ window.requestAnimationFrame(() => {
447
+ this.heightInPx = this.computeHeight();
451
448
  });
452
449
  },
453
450
  computeHeight() {
@@ -51,39 +51,37 @@ export default {
51
51
  },
52
52
  },
53
53
  watch: {
54
- fetchedItems(newVal, oldVal) {
54
+ async fetchedItems(newVal, oldVal) {
55
55
  // Re-adjust scroll to the current item if more items are added
56
56
  if (newVal > oldVal) {
57
57
  const { scrollHeight, scrollTop } = this.$refs.infiniteContainer;
58
58
  // Only when scrolled to the top
59
59
  if (scrollHeight !== 0 && scrollTop === 0) {
60
60
  // Wait until the DOM is fully updated to adjust scroll
61
- this.$nextTick(() => {
62
- const { scrollHeight: newScrollHeight } = this.$refs.infiniteContainer;
61
+ await this.$nextTick();
62
+ const { scrollHeight: newScrollHeight } = this.$refs.infiniteContainer;
63
63
 
64
- // New scrollTop is the new height, minus the old height
65
- // minus a small space to allow the user to trigger a scroll once more
66
- let top = newScrollHeight - scrollHeight - adjustScrollGap;
64
+ // New scrollTop is the new height, minus the old height
65
+ // minus a small space to allow the user to trigger a scroll once more
66
+ let top = newScrollHeight - scrollHeight - adjustScrollGap;
67
67
 
68
- // Never adjust to 0, or a new event may be be triggered
69
- if (top < 1) {
70
- top = 1;
71
- }
68
+ // Never adjust to 0, or a new event may be be triggered
69
+ if (top < 1) {
70
+ top = 1;
71
+ }
72
72
 
73
- this.scrollTo({ top });
74
- });
73
+ this.scrollTo({ top });
75
74
  }
76
75
  }
77
76
  },
78
77
  },
79
78
 
80
- mounted() {
79
+ async mounted() {
81
80
  // Scroll to bottom for reverse effect
82
- this.$nextTick(() => {
83
- if (this.$listeners.topReached && !this.$listeners.bottomReached) {
84
- this.scrollDown();
85
- }
86
- });
81
+ await this.$nextTick();
82
+ if (this.$listeners.topReached && !this.$listeners.bottomReached) {
83
+ this.scrollDown();
84
+ }
87
85
  },
88
86
 
89
87
  methods: {
@@ -610,22 +610,21 @@ export default {
610
610
  },
611
611
  },
612
612
  items: {
613
- handler() {
614
- this.$nextTick(() => {
615
- /* Every time the list of items changes (on search),
616
- * the observed elements are recreated, thus we need to start obesrving them again */
617
- this.observeScroll();
613
+ async handler() {
614
+ await this.$nextTick();
615
+ /* Every time the list of items changes (on search),
616
+ * the observed elements are recreated, thus we need to start obesrving them again */
617
+ this.observeScroll();
618
618
 
619
- /**
620
- * Every time the list of items changes, and there is a search string,
621
- * we want to visually highlight the first enabled item
622
- */
623
- if (this.searchHasOptions) {
624
- this.nextFocusedItemIndex = this.getFirstEnabledIndex();
625
- } else {
626
- this.nextFocusedItemIndex = null;
627
- }
628
- });
619
+ /**
620
+ * Every time the list of items changes, and there is a search string,
621
+ * we want to visually highlight the first enabled item
622
+ */
623
+ if (this.searchHasOptions) {
624
+ this.nextFocusedItemIndex = this.getFirstEnabledIndex();
625
+ } else {
626
+ this.nextFocusedItemIndex = null;
627
+ }
629
628
  },
630
629
  },
631
630
  ...(process.env.NODE_ENV !== 'production'
@@ -831,7 +830,7 @@ export default {
831
830
 
832
831
  this.focusItem(nextIndex, elements);
833
832
  },
834
- focusItem(index, elements, keepSearchFocused = false) {
833
+ async focusItem(index, elements, keepSearchFocused = false) {
835
834
  this.nextFocusedItemIndex = index;
836
835
 
837
836
  // Always update the activeItemId when focus changes
@@ -847,9 +846,8 @@ export default {
847
846
  elements[index]?.focus();
848
847
  }
849
848
 
850
- this.$nextTick(() => {
851
- this.scrollActiveItemIntoView();
852
- });
849
+ await this.$nextTick();
850
+ this.scrollActiveItemIntoView();
853
851
  },
854
852
  focusSearchInput() {
855
853
  this.$refs.searchBox.focusInput();
@@ -96,7 +96,7 @@ export default {
96
96
  block: {
97
97
  type: Boolean,
98
98
  required: false,
99
- default: false,
99
+ default: true,
100
100
  },
101
101
  },
102
102
  data() {
@@ -171,12 +171,13 @@ export default {
171
171
  </script>
172
172
 
173
173
  <template>
174
- <gl-button-group class="gl-sorting">
174
+ <gl-button-group class="gl-sorting gl-flex">
175
175
  <gl-collapsible-listbox
176
176
  :toggle-text="text"
177
177
  :items="sortOptions"
178
178
  :selected="sortBy"
179
179
  :toggle-class="dropdownToggleClass"
180
+ class="gl-w-full"
180
181
  :class="dropdownClass"
181
182
  placement="bottom-end"
182
183
  :block="block"
@@ -72,25 +72,23 @@ export default {
72
72
  .reduce((acc, key) => Object.assign(acc, { [key]: this.$attrs[key] }), {});
73
73
  },
74
74
  },
75
- mounted() {
76
- this.$nextTick(() => {
77
- this.navScrollWidth = this.getScrollWidth();
78
- });
79
-
75
+ async mounted() {
80
76
  this.handleNavScroll = debounce((e) => {
81
77
  this.scrollLeft = e.target.scrollLeft;
82
78
  }, 100);
83
79
 
84
80
  this.getNavContainer().addEventListener('scroll', this.handleNavScroll);
81
+
82
+ await this.$nextTick();
83
+ this.navScrollWidth = this.getScrollWidth();
85
84
  },
86
85
  beforeDestroy() {
87
86
  this.getNavContainer().removeEventListener('scroll', this.handleNavScroll);
88
87
  },
89
- updated() {
88
+ async updated() {
90
89
  // Whenever tabs are added or removed we need to recalculate the reactive scrollWidth
91
- this.$nextTick(() => {
92
- this.navScrollWidth = this.getScrollWidth();
93
- });
90
+ await this.$nextTick();
91
+ this.navScrollWidth = this.getScrollWidth();
94
92
  },
95
93
  methods: {
96
94
  handleResize({ contentRect: { width } }) {
@@ -71,7 +71,8 @@ $toggle-height: 2.5 * $grid-size;
71
71
  width: $toggle-width;
72
72
  height: $toggle-height;
73
73
  user-select: none;
74
- box-shadow: inset 0 0 0 $gl-border-size-1 var(--gl-action-neutral-border-color-default);
74
+ box-shadow: inset 0 0 0 $gl-border-size-1
75
+ var(--gl-toggle-switch-track-border-color-unchecked-default);
75
76
  @apply gl-rounded-pill;
76
77
  @apply gl-leading-normal;
77
78
  transition:
@@ -93,23 +94,33 @@ $toggle-height: 2.5 * $grid-size;
93
94
  &:hover {
94
95
  color: var(--gl-toggle-switch-icon-color-unchecked-hover);
95
96
  background-color: var(--gl-toggle-switch-track-color-unchecked-hover);
96
- box-shadow: inset 0 0 0 $gl-border-size-1 var(--gl-action-neutral-border-color-hover);
97
-
98
- .toggle-icon::before {
99
- transform: scale(0.875);
100
- }
97
+ box-shadow: inset 0 0 0 $gl-border-size-1
98
+ var(--gl-toggle-switch-track-border-color-unchecked-hover);
101
99
  }
102
100
 
103
101
  &:focus-visible {
104
102
  color: var(--gl-toggle-switch-icon-color-unchecked-focus);
105
103
  background-color: var(--gl-toggle-switch-track-color-unchecked-focus);
106
- @include gl-focus($color: var(--gl-action-neutral-border-color-focus), $important: true);
104
+ @include gl-focus(
105
+ $color: var(--gl-toggle-switch-track-border-color-unchecked-focus),
106
+ $important: true
107
+ );
107
108
  }
108
109
 
109
110
  &:active {
110
111
  color: var(--gl-toggle-switch-icon-color-unchecked-active);
111
112
  background-color: var(--gl-toggle-switch-track-color-unchecked-active);
112
- @include gl-focus($color: var(--gl-action-neutral-border-color-active), $important: true);
113
+ @include gl-focus(
114
+ $color: var(--gl-toggle-switch-track-border-color-unchecked-active),
115
+ $important: true
116
+ );
117
+ }
118
+
119
+ // The thumb grows on hover and focus so the interaction reads on the thumb as
120
+ // well as on the track.
121
+ &:hover .toggle-icon::before,
122
+ &:focus-visible .toggle-icon::before {
123
+ transform: scale(0.875);
113
124
  }
114
125
 
115
126
  .toggle-loading {
@@ -159,24 +170,32 @@ $toggle-height: 2.5 * $grid-size;
159
170
  &.is-checked {
160
171
  color: var(--gl-toggle-switch-icon-color-checked-default);
161
172
  background-color: var(--gl-toggle-switch-track-color-checked-default);
162
- box-shadow: inset 0 0 0 $gl-border-size-1 var(--gl-action-neutral-border-color-default);
173
+ box-shadow: inset 0 0 0 $gl-border-size-1
174
+ var(--gl-toggle-switch-track-border-color-checked-default);
163
175
 
164
176
  &:hover {
165
177
  color: var(--gl-toggle-switch-icon-color-checked-hover);
166
178
  background-color: var(--gl-toggle-switch-track-color-checked-hover);
167
- box-shadow: inset 0 0 0 $gl-border-size-1 var(--gl-action-neutral-border-color-hover);
179
+ box-shadow: inset 0 0 0 $gl-border-size-1
180
+ var(--gl-toggle-switch-track-border-color-checked-hover);
168
181
  }
169
182
 
170
183
  &:focus-visible {
171
184
  color: var(--gl-toggle-switch-icon-color-checked-focus);
172
185
  background-color: var(--gl-toggle-switch-track-color-checked-focus);
173
- @include gl-focus($color: var(--gl-action-neutral-border-color-focus), $important: true);
186
+ @include gl-focus(
187
+ $color: var(--gl-toggle-switch-track-border-color-checked-focus),
188
+ $important: true
189
+ );
174
190
  }
175
191
 
176
192
  &:active {
177
193
  color: var(--gl-toggle-switch-icon-color-checked-active);
178
194
  background-color: var(--gl-toggle-switch-track-color-checked-active);
179
- @include gl-focus($color: var(--gl-action-neutral-border-color-active), $important: true);
195
+ @include gl-focus(
196
+ $color: var(--gl-toggle-switch-track-border-color-checked-active),
197
+ $important: true
198
+ );
180
199
  }
181
200
 
182
201
  .toggle-icon {
@@ -189,34 +208,83 @@ $toggle-height: 2.5 * $grid-size;
189
208
  }
190
209
  }
191
210
 
192
- &.is-disabled,
193
- &.is-checked.is-disabled {
211
+ // Both disabled blocks must stay last. `.gl-toggle.is-checked` and
212
+ // `.gl-toggle.is-disabled` carry equal specificity, so source order is what
213
+ // lets the disabled styles win over the checked ones.
214
+ &.is-disabled {
194
215
  @apply gl-cursor-not-allowed;
195
- color: var(--gl-action-disabled-background-color);
196
- background-color: var(--gl-action-disabled-background-color);
197
- box-shadow: inset 0 0 0 $gl-border-size-1 var(--gl-action-disabled-border-color);
216
+ background-color: var(--gl-toggle-switch-track-color-unchecked-disabled);
217
+ box-shadow: inset 0 0 0 $gl-border-size-1
218
+ var(--gl-toggle-switch-track-border-color-unchecked-disabled);
198
219
 
199
220
  .toggle-icon {
200
221
  color: var(--gl-toggle-switch-icon-color-disabled);
201
222
  }
202
223
 
203
224
  .toggle-icon::before {
204
- background-color: var(--gl-toggle-switch-background-color-disabled);
225
+ background-color: var(--gl-toggle-switch-background-color-unchecked-disabled);
205
226
  }
206
227
 
207
- &:hover .toggle-icon::before {
228
+ &:hover .toggle-icon::before,
229
+ &:focus-visible .toggle-icon::before {
208
230
  transform: scale(0.75);
209
231
  }
210
232
 
211
- &.is-checked .toggle-icon::before,
212
- &.is-checked:hover .toggle-icon::before {
233
+ &:active,
234
+ &:focus-visible,
235
+ &:focus-visible:active {
236
+ @include gl-focus(
237
+ $color: var(--gl-toggle-switch-track-border-color-unchecked-disabled),
238
+ $important: true
239
+ );
240
+ }
241
+ }
242
+
243
+ &.is-checked.is-disabled {
244
+ background-color: var(--gl-toggle-switch-track-color-checked-disabled);
245
+ box-shadow: inset 0 0 0 $gl-border-size-1
246
+ var(--gl-toggle-switch-track-border-color-checked-disabled);
247
+
248
+ .toggle-icon::before {
249
+ background-color: var(--gl-toggle-switch-background-color-checked-disabled);
250
+ transform: scale(1);
251
+ }
252
+
253
+ &:hover .toggle-icon::before,
254
+ &:focus-visible .toggle-icon::before {
213
255
  transform: scale(1);
214
256
  }
215
257
 
216
258
  &:active,
217
259
  &:focus-visible,
218
260
  &:focus-visible:active {
219
- @include gl-focus($color: var(--gl-action-disabled-border-color), $important: true);
261
+ @include gl-focus(
262
+ $color: var(--gl-toggle-switch-track-border-color-checked-disabled),
263
+ $important: true
264
+ );
265
+ }
266
+ }
267
+
268
+ // A loading toggle renders a spinner in place of the thumb, so the track is
269
+ // the only thing left to look at and its value is still in flight. It gets a
270
+ // flat fill that ignores the checked state, which also keeps the spinner
271
+ // legible in both color modes.
272
+ //
273
+ // `.is-loading` is always accompanied by `.is-disabled`, so this block has to
274
+ // stay after both disabled blocks: the checked pairs tie on specificity and
275
+ // source order is what settles them.
276
+ &.is-loading,
277
+ &.is-checked.is-loading {
278
+ background-color: var(--gl-toggle-switch-track-color-loading);
279
+ box-shadow: inset 0 0 0 $gl-border-size-1 var(--gl-toggle-switch-track-border-color-loading);
280
+
281
+ &:active,
282
+ &:focus-visible,
283
+ &:focus-visible:active {
284
+ @include gl-focus(
285
+ $color: var(--gl-toggle-switch-track-border-color-loading),
286
+ $important: true
287
+ );
220
288
  }
221
289
  }
222
290
  }
@@ -178,6 +178,7 @@ export default {
178
178
  :class="{
179
179
  'is-checked': value,
180
180
  'is-disabled': disabled || isLoading,
181
+ 'is-loading': isLoading,
181
182
  }"
182
183
  class="gl-toggle gl-shrink-0"
183
184
  type="button"
@@ -222,7 +222,7 @@ export default {
222
222
  },
223
223
  },
224
224
  watch: {
225
- inputText(newValue, oldValue) {
225
+ async inputText(newValue, oldValue) {
226
226
  if (newValue !== oldValue) {
227
227
  /**
228
228
  * Fired when user types in the token selector
@@ -242,13 +242,12 @@ export default {
242
242
  // Wait a tick so `text-input` event can be used to validate
243
243
  // the value and change the `allowUserDefinedTokens` and/or
244
244
  // `hideDropdownWithNoItems` props
245
- this.$nextTick(() => {
246
- if (this.hideDropdown) {
247
- this.closeDropdown();
248
- } else if (newValue !== '') {
249
- this.openDropdown();
250
- }
251
- });
245
+ await this.$nextTick();
246
+ if (this.hideDropdown) {
247
+ this.closeDropdown();
248
+ } else if (newValue !== '') {
249
+ this.openDropdown();
250
+ }
252
251
  }
253
252
  },
254
253
  },
@@ -286,7 +285,7 @@ export default {
286
285
  this.triggerTokenFocusNextBackspace = true;
287
286
  }
288
287
  },
289
- handleBlur(event) {
288
+ async handleBlur(event) {
290
289
  /**
291
290
  * Fired when the token selector is blurred
292
291
  *
@@ -300,11 +299,10 @@ export default {
300
299
  // Workaround is to:
301
300
  // 1. Explicitly focus the dropdown menu item button on `mousedown` event. (see './token_selector_dropdown.vue')
302
301
  // 2. Use `nextTick` so `blur` event is fired after the `mousedown` event
303
- this.$nextTick(() => {
304
- if (!event.relatedTarget?.closest?.('.dropdown-item')) {
305
- this.closeDropdown();
306
- }
307
- });
302
+ await this.$nextTick();
303
+ if (!event.relatedTarget?.closest?.('.dropdown-item')) {
304
+ this.closeDropdown();
305
+ }
308
306
  },
309
307
  handleEnter() {
310
308
  if (this.focusedDropdownItem && this.dropdownIsOpen) {