@gitlab/ui 124.1.1 → 124.3.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gitlab/ui",
3
- "version": "124.1.1",
3
+ "version": "124.3.0",
4
4
  "description": "GitLab UI Components",
5
5
  "license": "MIT",
6
6
  "main": "dist/index.js",
@@ -105,7 +105,7 @@
105
105
  "@jest/test-sequencer": "30.2.0",
106
106
  "@rollup/plugin-commonjs": "^28.0.9",
107
107
  "@rollup/plugin-node-resolve": "^16.0.3",
108
- "@rollup/plugin-replace": "^6.0.2",
108
+ "@rollup/plugin-replace": "^6.0.3",
109
109
  "@storybook/addon-a11y": "^7.6.20",
110
110
  "@storybook/addon-docs": "^7.6.20",
111
111
  "@storybook/addon-essentials": "^7.6.20",
@@ -137,7 +137,7 @@
137
137
  "cypress-real-events": "^1.15.0",
138
138
  "dompurify": "^3.1.2",
139
139
  "emoji-regex": "^10.6.0",
140
- "esbuild": "^0.25.11",
140
+ "esbuild": "^0.25.12",
141
141
  "gitlab-api-async-iterator": "^1.3.1",
142
142
  "glob": "11.0.3",
143
143
  "globby": "^14.1.0",
@@ -163,7 +163,7 @@
163
163
  "rollup-plugin-string": "^3.0.0",
164
164
  "rollup-plugin-svg": "^2.0.0",
165
165
  "rollup-plugin-vue": "^5.1.9",
166
- "sass": "^1.93.2",
166
+ "sass": "^1.93.3",
167
167
  "sass-loader": "^10.5.2",
168
168
  "sass-true": "^9",
169
169
  "start-server-and-test": "^2.1.2",
@@ -20,7 +20,7 @@ $breadcrumb-max-width: $grid-size * 16;
20
20
  @apply gl-leading-normal;
21
21
  @apply gl-shrink-0;
22
22
 
23
- &:not(:last-child)::after {
23
+ &:not(:last-child, :has(+ .gl-breadcrumb-clipboard-button))::after {
24
24
  color: var(--gl-breadcrumb-separator-color);
25
25
  @apply gl-px-3;
26
26
  content: '/';
@@ -75,3 +75,8 @@ $breadcrumb-max-width: $grid-size * 16;
75
75
  }
76
76
  }
77
77
  }
78
+
79
+ .gl-breadcrumb-clipboard-button {
80
+ @apply gl-leading-normal;
81
+ @apply gl-shrink-0;
82
+ }
@@ -6,6 +6,7 @@ import GlAvatar from '../avatar/avatar.vue';
6
6
  import GlDisclosureDropdown from '../new_dropdowns/disclosure/disclosure_dropdown.vue';
7
7
  import { GlTooltipDirective } from '../../../directives/tooltip/tooltip';
8
8
  import { breadCrumbSizeOptions } from '../../../utils/constants';
9
+ import ClipboardButton from '../../shared_components/clipboard_button/clipboard_button.vue';
9
10
  import GlBreadcrumbItem from './breadcrumb_item.vue';
10
11
 
11
12
  export default {
@@ -14,6 +15,7 @@ export default {
14
15
  GlBreadcrumbItem,
15
16
  GlAvatar,
16
17
  GlDisclosureDropdown,
18
+ ClipboardButton,
17
19
  },
18
20
  directives: {
19
21
  GlTooltip: GlTooltipDirective,
@@ -64,6 +66,30 @@ export default {
64
66
  default: breadCrumbSizeOptions.sm,
65
67
  validator: (value) => Object.keys(breadCrumbSizeOptions).includes(value),
66
68
  },
69
+ /**
70
+ * Copy to clipboard button for breadcrumbs path.
71
+ */
72
+ showClipboardButton: {
73
+ type: Boolean,
74
+ required: false,
75
+ default: false,
76
+ },
77
+ /**
78
+ * Custom path for copy to clipboard button. By default, it resolves to all items text values with `/` separator.
79
+ */
80
+ pathToCopy: {
81
+ type: String,
82
+ required: false,
83
+ default: null,
84
+ },
85
+ /**
86
+ * Custom tooltip text for clipboard button.
87
+ */
88
+ clipboardTooltipText: {
89
+ type: String,
90
+ required: false,
91
+ default: null,
92
+ },
67
93
  },
68
94
  data() {
69
95
  return {
@@ -94,6 +120,12 @@ export default {
94
120
  avatarSize() {
95
121
  return this.size === 'sm' ? 16 : 24;
96
122
  },
123
+ clipboardButtonText() {
124
+ if (this.pathToCopy) return this.pathToCopy;
125
+
126
+ const items = Array.from(this.items, (item) => item.text);
127
+ return items.join('/');
128
+ },
97
129
  },
98
130
  watch: {
99
131
  items: {
@@ -230,16 +262,28 @@ export default {
230
262
  :size="size"
231
263
  :aria-current="getAriaCurrentAttr(index)"
232
264
  :class="[hideItemClass(item), itemClass]"
233
- ><gl-avatar
234
- v-if="item.avatarPath"
235
- :src="item.avatarPath"
236
- :size="avatarSize"
237
- aria-hidden="true"
238
- class="gl-breadcrumb-avatar-tile gl-border gl-mr-2 !gl-rounded-default"
239
- shape="rect"
240
- data-testid="avatar"
241
- /><span class="gl-align-middle">{{ item.text }}</span>
265
+ ><template #default>
266
+ <gl-avatar
267
+ v-if="item.avatarPath"
268
+ :src="item.avatarPath"
269
+ :size="avatarSize"
270
+ aria-hidden="true"
271
+ class="gl-breadcrumb-avatar-tile gl-border gl-mr-2 !gl-rounded-default"
272
+ shape="rect"
273
+ data-testid="avatar"
274
+ /><span class="gl-align-middle">{{ item.text }}</span>
275
+ </template>
242
276
  </gl-breadcrumb-item>
277
+
278
+ <li v-if="showClipboardButton" class="gl-breadcrumb-clipboard-button">
279
+ <clipboard-button
280
+ data-testid="copy-to-clipboard-button"
281
+ class="gl-ml-2"
282
+ :text="clipboardButtonText"
283
+ v-bind="clipboardTooltipText ? { title: clipboardTooltipText } : {}"
284
+ :size="dropdownSize"
285
+ />
286
+ </li>
243
287
  </ol>
244
288
  </nav>
245
289
  </template>
@@ -1,5 +1,125 @@
1
1
  .gl-form-combobox {
2
2
  .gl-form-combobox-inner {
3
3
  max-height: $gl-max-dropdown-max-height;
4
+ position: relative;
5
+ @apply gl-grow;
6
+ @apply gl-overflow-y-auto;
7
+ @apply gl-pl-0;
8
+ @apply gl-mb-0;
9
+ @apply gl-py-2;
10
+ @apply gl-list-none;
11
+ border-radius: var(--gl-dropdown-border-radius);
12
+
13
+ &:focus-visible {
14
+ @apply gl-focus;
15
+ }
16
+
17
+ ul {
18
+ @apply gl-list-none;
19
+ }
20
+
21
+ // Update dropdown items to match new listbox styling exactly
22
+ .gl-dropdown-item {
23
+ @apply gl-cursor-pointer;
24
+ @apply gl-px-2;
25
+ @apply gl-my-1;
26
+
27
+ &:first-child {
28
+ @apply gl-mt-0;
29
+ }
30
+
31
+ &:last-child {
32
+ @apply gl-mb-0;
33
+ }
34
+
35
+ // Target the actual Bootstrap dropdown item element
36
+ .dropdown-item {
37
+ transition: background-color $gl-transition-duration-fast $gl-easing-out-cubic,
38
+ box-shadow $gl-transition-duration-medium $gl-easing-out-cubic;
39
+ @apply gl-rounded-default;
40
+ @apply gl-border-0;
41
+ @apply gl-w-full;
42
+ background-color: var(--gl-dropdown-option-background-color-unselected-default);
43
+ @apply gl-items-center;
44
+ @apply gl-flex;
45
+ @apply gl-text-base;
46
+ @apply gl-font-normal;
47
+ @apply gl-leading-normal;
48
+ @apply gl-px-3;
49
+ @apply gl-py-0;
50
+ position: relative;
51
+ @apply gl-no-underline;
52
+ color: var(--gl-dropdown-option-text-color-default);
53
+ @apply gl-text-left;
54
+ @apply gl-whitespace-normal;
55
+ @include gl-prefers-reduced-motion-transition;
56
+
57
+ .gl-dropdown-item-text-wrapper {
58
+ @apply gl-min-w-0;
59
+ @apply gl-grow;
60
+ @apply gl-py-3;
61
+ }
62
+
63
+ // Hover state
64
+ &:not(.disable-hover):hover {
65
+ color: var(--gl-dropdown-option-text-color-hover);
66
+ background-color: var(--gl-dropdown-option-background-color-unselected-hover);
67
+ }
68
+
69
+ // Focus state (when item receives actual focus)
70
+ &:focus {
71
+ color: var(--gl-dropdown-option-text-color-focus);
72
+ background-color: var(--gl-dropdown-option-background-color-unselected-focus);
73
+ @include gl-focus($inset: true);
74
+ z-index: 1;
75
+ }
76
+
77
+ // Active state (when item is clicked)
78
+ &:active,
79
+ &:focus:active {
80
+ color: var(--gl-dropdown-option-text-color-active);
81
+ background-color: var(--gl-dropdown-option-background-color-unselected-active);
82
+ @include gl-focus($inset: true);
83
+ }
84
+
85
+ // Keyboard navigation highlight (Bootstrap active class when :active prop is true)
86
+ &.active {
87
+ color: var(--gl-dropdown-option-text-color-focus) !important;
88
+ background-color: var(--gl-dropdown-option-background-color-unselected-focus) !important;
89
+ @include gl-focus($inset: true);
90
+ z-index: 1;
91
+ }
92
+
93
+ // Also handle the is-focused class for compatibility
94
+ &.is-focused {
95
+ color: var(--gl-dropdown-option-text-color-focus) !important;
96
+ background-color: var(--gl-dropdown-option-background-color-unselected-focus) !important;
97
+ @include gl-focus($inset: true);
98
+ z-index: 1;
99
+ }
100
+
101
+ &:focus-visible {
102
+ outline: none;
103
+ }
104
+ }
105
+
106
+ // Selected state (if needed for multi-select scenarios)
107
+ &[aria-selected="true"] .dropdown-item {
108
+ background-color: var(--gl-dropdown-option-background-color-selected-default);
109
+
110
+ &:hover {
111
+ background-color: var(--gl-dropdown-option-background-color-selected-hover);
112
+ }
113
+
114
+ &:focus {
115
+ background-color: var(--gl-dropdown-option-background-color-selected-focus);
116
+ }
117
+
118
+ &:active,
119
+ &:focus:active {
120
+ background-color: var(--gl-dropdown-option-background-color-selected-active);
121
+ }
122
+ }
123
+ }
4
124
  }
5
125
  }
@@ -248,7 +248,7 @@ export default {
248
248
  ref="suggestionsMenu"
249
249
  data-testid="combobox-dropdown"
250
250
  role="listbox"
251
- class="dropdown-menu gl-form-combobox-inner gl-mb-0 gl-flex gl-w-full gl-list-none gl-flex-col gl-border-dropdown gl-bg-dropdown gl-pl-0"
251
+ class="gl-form-combobox-inner -gl-mt-3 gl-mb-0 gl-flex gl-w-full gl-list-none gl-flex-col gl-bg-dropdown gl-pl-0 gl-drop-shadow-md"
252
252
  @keydown.down="onArrowDown"
253
253
  @keydown.up="onArrowUp"
254
254
  @keydown.left="onArrowLeft"
@@ -102,6 +102,9 @@ export { default as GlDashboardPanel } from './dashboards/dashboard_panel/dashbo
102
102
  // Experimental
103
103
  export { default as GlExperimentBadge } from './experimental/experiment_badge/experiment_badge.vue';
104
104
 
105
+ // Shared components
106
+ export { default as GlClipboardButton } from './shared_components/clipboard_button/clipboard_button.vue';
107
+
105
108
  // Utilities
106
109
  export { default as GlAnimatedNumber } from './utilities/animated_number/animated_number.vue';
107
110
  export { default as GlFriendlyWrap } from './utilities/friendly_wrap/friendly_wrap.vue';
@@ -0,0 +1,78 @@
1
+ <script>
2
+ import { GlTooltipDirective } from '../../../directives/tooltip/tooltip';
3
+ import GlButton from '../../base/button/button.vue';
4
+ import { translate } from '../../../utils/i18n';
5
+
6
+ export default {
7
+ name: 'ClipboardButton',
8
+ components: {
9
+ GlButton,
10
+ },
11
+ directives: {
12
+ GlTooltip: GlTooltipDirective,
13
+ },
14
+ props: {
15
+ /**
16
+ * The text to copy to clipboard
17
+ */
18
+ text: {
19
+ type: String,
20
+ required: true,
21
+ },
22
+ /**
23
+ * The tooltip text shown on hover
24
+ */
25
+ title: {
26
+ type: String,
27
+ required: false,
28
+ default: () => translate('ClipboardButton.title', 'Copy to clipboard'),
29
+ },
30
+ /**
31
+ * Button size
32
+ */
33
+ size: {
34
+ type: String,
35
+ required: false,
36
+ default: 'medium',
37
+ },
38
+ },
39
+ data() {
40
+ return {
41
+ localTitle: this.title,
42
+ titleTimeout: null,
43
+ };
44
+ },
45
+ methods: {
46
+ updateTooltip(title) {
47
+ this.localTitle = title;
48
+
49
+ clearTimeout(this.titleTimeout);
50
+
51
+ this.titleTimeout = setTimeout(() => {
52
+ this.localTitle = this.title;
53
+ }, 1000);
54
+ },
55
+ async handleClick() {
56
+ try {
57
+ await navigator.clipboard.writeText(this.text);
58
+ this.updateTooltip(translate('ClipboardButton.copied', 'Copied'));
59
+ } catch {
60
+ this.updateTooltip(translate('ClipboardButton.error', 'Copy failed'));
61
+ }
62
+ },
63
+ },
64
+ };
65
+ </script>
66
+
67
+ <template>
68
+ <gl-button
69
+ v-gl-tooltip.hover.focus.top
70
+ variant="default"
71
+ category="tertiary"
72
+ icon="copy-to-clipboard"
73
+ :size="size"
74
+ :title="localTitle"
75
+ :aria-label="localTitle"
76
+ @click="handleClick"
77
+ />
78
+ </template>
package/translations.js CHANGED
@@ -1,6 +1,9 @@
1
1
  /* eslint-disable import/no-default-export */
2
2
  export default {
3
3
  'ClearIconButton.title': 'Clear',
4
+ 'ClipboardButton.copied': 'Copied',
5
+ 'ClipboardButton.error': 'Copy failed',
6
+ 'ClipboardButton.title': 'Copy to clipboard',
4
7
  'CloseButton.title': 'Close',
5
8
  'GlAlert.closeButtonTitle': 'Dismiss',
6
9
  'GlBanner.closeButtonTitle': 'Dismiss',