@gitlab/ui 38.0.1 → 38.1.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.
Files changed (61) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/README.md +1 -1
  3. package/dist/components/base/breadcrumb/breadcrumb.js +10 -5
  4. package/dist/components/base/{filtered_search/examples/filtered_search.single_unique.example.js → breadcrumb/breadcrumb_item.js} +32 -28
  5. package/dist/components/base/filtered_search/filtered_search.documentation.js +2 -66
  6. package/dist/components/base/filtered_search/filtered_search.js +38 -0
  7. package/dist/components/base/filtered_search/filtered_search_suggestion.documentation.js +2 -8
  8. package/dist/components/base/filtered_search/filtered_search_suggestion.js +4 -0
  9. package/dist/components/base/filtered_search/filtered_search_suggestion_list.documentation.js +2 -7
  10. package/dist/components/base/filtered_search/filtered_search_suggestion_list.js +4 -0
  11. package/dist/components/base/filtered_search/filtered_search_term.documentation.js +2 -44
  12. package/dist/components/base/filtered_search/filtered_search_term.js +37 -0
  13. package/dist/components/base/filtered_search/filtered_search_token.documentation.js +2 -31
  14. package/dist/components/base/filtered_search/filtered_search_token.js +49 -0
  15. package/dist/components/base/filtered_search/filtered_search_token_segment.documentation.js +2 -46
  16. package/dist/components/base/filtered_search/filtered_search_token_segment.js +48 -0
  17. package/dist/components/charts/series_label/series_label.js +6 -1
  18. package/documentation/documented_stories.js +6 -0
  19. package/package.json +9 -7
  20. package/src/components/base/breadcrumb/breadcrumb.spec.js +24 -10
  21. package/src/components/base/breadcrumb/breadcrumb.vue +11 -6
  22. package/src/components/base/breadcrumb/breadcrumb_item.spec.js +45 -0
  23. package/src/components/base/breadcrumb/breadcrumb_item.vue +43 -0
  24. package/src/components/base/filtered_search/filtered_search.documentation.js +0 -76
  25. package/src/components/base/filtered_search/filtered_search.md +3 -4
  26. package/src/components/base/filtered_search/filtered_search.stories.js +248 -13
  27. package/src/components/base/filtered_search/filtered_search.vue +45 -0
  28. package/src/components/base/filtered_search/filtered_search_suggestion.documentation.js +0 -6
  29. package/src/components/base/filtered_search/filtered_search_suggestion.md +1 -7
  30. package/src/components/base/filtered_search/filtered_search_suggestion.stories.js +26 -18
  31. package/src/components/base/filtered_search/filtered_search_suggestion.vue +5 -0
  32. package/src/components/base/filtered_search/filtered_search_suggestion_list.documentation.js +0 -5
  33. package/src/components/base/filtered_search/filtered_search_suggestion_list.md +1 -7
  34. package/src/components/base/filtered_search/filtered_search_suggestion_list.stories.js +33 -25
  35. package/src/components/base/filtered_search/filtered_search_suggestion_list.vue +5 -0
  36. package/src/components/base/filtered_search/filtered_search_term.documentation.js +0 -41
  37. package/src/components/base/filtered_search/filtered_search_term.md +0 -2
  38. package/src/components/base/filtered_search/filtered_search_term.stories.js +33 -26
  39. package/src/components/base/filtered_search/filtered_search_term.vue +54 -0
  40. package/src/components/base/filtered_search/filtered_search_token.documentation.js +0 -26
  41. package/src/components/base/filtered_search/filtered_search_token.md +1 -3
  42. package/src/components/base/filtered_search/filtered_search_token.stories.js +136 -132
  43. package/src/components/base/filtered_search/filtered_search_token.vue +63 -0
  44. package/src/components/base/filtered_search/filtered_search_token_segment.documentation.js +0 -43
  45. package/src/components/base/filtered_search/filtered_search_token_segment.md +0 -2
  46. package/src/components/base/filtered_search/filtered_search_token_segment.stories.js +86 -79
  47. package/src/components/base/filtered_search/filtered_search_token_segment.vue +42 -0
  48. package/src/components/base/form/form_radio/form_radio.spec.js +21 -8
  49. package/src/components/charts/series_label/series_label.stories.js +6 -3
  50. package/src/components/charts/series_label/series_label.vue +3 -0
  51. package/dist/components/base/filtered_search/examples/filtered_search.default.example.js +0 -422
  52. package/dist/components/base/filtered_search/examples/filtered_search.friendly.example.js +0 -423
  53. package/dist/components/base/filtered_search/examples/filtered_search.history.example.js +0 -91
  54. package/dist/components/base/filtered_search/examples/filtered_search.multi_select.example.js +0 -196
  55. package/dist/components/base/filtered_search/examples/index.js +0 -32
  56. package/src/components/base/filtered_search/examples/filtered_search.default.example.vue +0 -298
  57. package/src/components/base/filtered_search/examples/filtered_search.friendly.example.vue +0 -300
  58. package/src/components/base/filtered_search/examples/filtered_search.history.example.vue +0 -50
  59. package/src/components/base/filtered_search/examples/filtered_search.multi_select.example.vue +0 -132
  60. package/src/components/base/filtered_search/examples/filtered_search.single_unique.example.vue +0 -31
  61. package/src/components/base/filtered_search/examples/index.js +0 -38
@@ -17,6 +17,7 @@ const DEFAULT_OPERATORS = [{
17
17
  description: 'is not'
18
18
  }];
19
19
  var script = {
20
+ name: 'GlFilteredSearchToken',
20
21
  components: {
21
22
  GlToken,
22
23
  GlFilteredSearchTokenSegment
@@ -28,11 +29,19 @@ var script = {
28
29
  required: false,
29
30
  default: () => []
30
31
  },
32
+
33
+ /**
34
+ * Token configuration with available operators and options.
35
+ */
31
36
  config: {
32
37
  type: Object,
33
38
  required: false,
34
39
  default: () => ({})
35
40
  },
41
+
42
+ /**
43
+ * Determines if the token is being edited or not.
44
+ */
36
45
  active: {
37
46
  type: Boolean,
38
47
  required: false,
@@ -43,6 +52,10 @@ var script = {
43
52
  required: false,
44
53
  default: () => []
45
54
  },
55
+
56
+ /**
57
+ * Current token value.
58
+ */
46
59
  value: {
47
60
  type: Object,
48
61
  required: false,
@@ -51,6 +64,10 @@ var script = {
51
64
  data: ''
52
65
  })
53
66
  },
67
+
68
+ /**
69
+ * Display operators' descriptions instead of their values (e.g., "is" instead of "=").
70
+ */
54
71
  showFriendlyText: {
55
72
  type: Boolean,
56
73
  required: false,
@@ -95,6 +112,12 @@ var script = {
95
112
  deep: true,
96
113
 
97
114
  handler(newValue) {
115
+ /**
116
+ * Emitted when the token changes its value.
117
+ *
118
+ * @event input
119
+ * @type {object} dataObj Object containing the update value.
120
+ */
98
121
  this.$emit('input', newValue);
99
122
  }
100
123
 
@@ -109,6 +132,12 @@ var script = {
109
132
  }
110
133
  } else if (this.value.data === '') {
111
134
  this.activeSegment = null;
135
+ /**
136
+ * Emitted when token is about to be destroyed.
137
+ *
138
+ * @event destroy
139
+ */
140
+
112
141
  this.$emit('destroy');
113
142
  }
114
143
  }
@@ -137,6 +166,11 @@ var script = {
137
166
  this.activeSegment = segment;
138
167
 
139
168
  if (!this.active) {
169
+ /**
170
+ * Emitted when this term token is clicked.
171
+ *
172
+ * @event activate
173
+ */
140
174
  this.$emit('activate');
141
175
  }
142
176
  },
@@ -153,6 +187,10 @@ var script = {
153
187
 
154
188
  replaceWithTermIfEmpty() {
155
189
  if (this.value.operator === '' && this.value.data === '') {
190
+ /**
191
+ * Emitted when this token is converted to another type
192
+ * @property {object} token Replacement token configuration
193
+ */
156
194
  this.$emit('replace', {
157
195
  type: TERM_TOKEN_TYPE,
158
196
  value: {
@@ -167,6 +205,11 @@ var script = {
167
205
 
168
206
  if (newTokenConfig === this.config) {
169
207
  this.$nextTick(() => {
208
+ /**
209
+ * Emitted when this term token will lose its focus.
210
+ *
211
+ * @event deactivate
212
+ */
170
213
  this.$emit('deactivate');
171
214
  });
172
215
  return;
@@ -230,6 +273,12 @@ var script = {
230
273
  data: this.multiSelectValues.join(COMMA)
231
274
  });
232
275
  }
276
+ /**
277
+ * Emitted when the token entry has been completed.
278
+ *
279
+ * @event complete
280
+ */
281
+
233
282
 
234
283
  this.$emit('complete');
235
284
  },
@@ -1,4 +1,4 @@
1
- var filtered_search_token_segment = "# Filtered Search Token Segment\n\nThe filtered search token segment is a component for managing token input either via free typing\nor by selecting item through dropdown list\n\n## Usage\n\nThis component is internal and is not intended to be used by `@gitlab/ui` users.\n\n## Internet Explorer 11\n\nThis component uses [`String.prototype.startsWith()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/startsWith)\nand [`String.prototype.endsWith()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/endsWith)\nunder the hood. Make sure those methods are polyfilled if you plan on using the component on IE11.\n\n> NOTE: These methods are already polyfilled in GitLab: [`app/assets/javascripts/commons/polyfills.js#L15-16`](https://gitlab.com/gitlab-org/gitlab/blob/dc60dee6ed6234dda9f032195577cd8fad9646d8/app/assets/javascripts/commons/polyfills.js#L15-16)\n";
1
+ var filtered_search_token_segment = "The filtered search token segment is a component for managing token input either via free typing\nor by selecting item through dropdown list\n\n## Usage\n\nThis component is internal and is not intended to be used by `@gitlab/ui` users.\n\n## Internet Explorer 11\n\nThis component uses [`String.prototype.startsWith()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/startsWith)\nand [`String.prototype.endsWith()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/endsWith)\nunder the hood. Make sure those methods are polyfilled if you plan on using the component on IE11.\n\n> NOTE: These methods are already polyfilled in GitLab: [`app/assets/javascripts/commons/polyfills.js#L15-16`](https://gitlab.com/gitlab-org/gitlab/blob/dc60dee6ed6234dda9f032195577cd8fad9646d8/app/assets/javascripts/commons/polyfills.js#L15-16)\n";
2
2
 
3
3
  var description = /*#__PURE__*/Object.freeze({
4
4
  __proto__: null,
@@ -6,51 +6,7 @@ var description = /*#__PURE__*/Object.freeze({
6
6
  });
7
7
 
8
8
  var filtered_search_token_segment_documentation = {
9
- description,
10
- bootstrapComponent: null,
11
- propsInfo: {
12
- active: {
13
- additionalInfo: 'If this term token is currently active'
14
- },
15
- options: {
16
- additionalInfo: ''
17
- },
18
- optionTextField: {
19
- additionalInfo: ''
20
- },
21
- customInputKeydownHandler: {
22
- additionalInfo: ''
23
- },
24
- value: {
25
- additionalInfo: 'Current term value'
26
- },
27
- searchInputAttributes: {
28
- additionalInfo: 'HTML attributes to add to the search input'
29
- },
30
- isLastToken: {
31
- additionalInfo: 'If this is the last token'
32
- }
33
- },
34
- events: [{
35
- event: 'activate',
36
- description: 'Emitted on mousedown event on the main component'
37
- }, {
38
- event: 'backspace',
39
- description: 'Emitted when Backspace is pressed and the value is empty'
40
- }, {
41
- event: 'complete',
42
- description: 'Emitted when suggestion is selected from the suggestion list'
43
- }, {
44
- event: 'submit',
45
- description: 'Emitted when Enter is pressed and no suggestion is selected'
46
- }, {
47
- event: 'split',
48
- args: [{
49
- arg: 'newStrings',
50
- description: '(Array of strings) New strings to be converted into term tokens'
51
- }],
52
- description: 'Emitted when Space appears in token segment value'
53
- }]
9
+ description
54
10
  };
55
11
 
56
12
  export default filtered_search_token_segment_documentation;
@@ -7,6 +7,7 @@ import { splitOnQuotes, wrapTokenInQuotes } from './filtered_search_utils';
7
7
  import __vue_normalize__ from 'vue-runtime-helpers/dist/normalize-component.js';
8
8
 
9
9
  var script = {
10
+ name: 'GlFilteredSearchTokenSegment',
10
11
  components: {
11
12
  Portal,
12
13
  GlFilteredSearchSuggestionList,
@@ -15,6 +16,9 @@ var script = {
15
16
  inject: ['portalName', 'alignSuggestions'],
16
17
  inheritAttrs: false,
17
18
  props: {
19
+ /**
20
+ * If this token segment is currently being edited.
21
+ */
18
22
  active: {
19
23
  type: Boolean,
20
24
  required: false,
@@ -45,15 +49,27 @@ var script = {
45
49
  required: false,
46
50
  default: () => () => false
47
51
  },
52
+
53
+ /**
54
+ * Current term value
55
+ */
48
56
  value: {
49
57
  required: true,
50
58
  validator: () => true
51
59
  },
60
+
61
+ /**
62
+ * HTML attributes to add to the search input
63
+ */
52
64
  searchInputAttributes: {
53
65
  type: Object,
54
66
  required: false,
55
67
  default: () => ({})
56
68
  },
69
+
70
+ /**
71
+ * If this is the last token
72
+ */
57
73
  isLastToken: {
58
74
  type: Boolean,
59
75
  required: false,
@@ -91,6 +107,11 @@ var script = {
91
107
  set(v) {
92
108
  var _this$getMatchingOpti, _this$getMatchingOpti2;
93
109
 
110
+ /**
111
+ * Emitted when this token segment's value changes.
112
+ *
113
+ * @type {object} option The current option.
114
+ */
94
115
  this.$emit('input', (_this$getMatchingOpti = (_this$getMatchingOpti2 = this.getMatchingOptionForInputValue(v)) === null || _this$getMatchingOpti2 === void 0 ? void 0 : _this$getMatchingOpti2.value) !== null && _this$getMatchingOpti !== void 0 ? _this$getMatchingOpti : v);
95
116
  }
96
117
 
@@ -152,6 +173,10 @@ var script = {
152
173
  this.$emit('input', (_this$getMatchingOpti3 = (_this$getMatchingOpti4 = this.getMatchingOptionForInputValue(firstWord)) === null || _this$getMatchingOpti4 === void 0 ? void 0 : _this$getMatchingOpti4.value) !== null && _this$getMatchingOpti3 !== void 0 ? _this$getMatchingOpti3 : firstWord);
153
174
 
154
175
  if (otherWords.length) {
176
+ /**
177
+ * Emitted when Space appears in token segment value
178
+ * @property {array|string} newStrings New strings to be converted into term tokens
179
+ */
155
180
  this.$emit('split', otherWords);
156
181
  }
157
182
  }
@@ -160,6 +185,9 @@ var script = {
160
185
  methods: {
161
186
  emitIfInactive(e) {
162
187
  if (!this.active) {
188
+ /**
189
+ * Emitted on mousedown event on the main component.
190
+ */
163
191
  this.$emit('activate');
164
192
  e.preventDefault();
165
193
  }
@@ -208,6 +236,12 @@ var script = {
208
236
 
209
237
  applySuggestion(suggestedValue) {
210
238
  const formattedSuggestedValue = wrapTokenInQuotes(suggestedValue);
239
+ /**
240
+ * Emitted when autocomplete entry is selected.
241
+ *
242
+ * @type {string} value The selected value.
243
+ */
244
+
211
245
  this.$emit('select', formattedSuggestedValue);
212
246
 
213
247
  if (!this.multiSelect) {
@@ -228,6 +262,10 @@ var script = {
228
262
  if (key === 'Backspace') {
229
263
  if (this.inputValue === '') {
230
264
  e.preventDefault();
265
+ /**
266
+ * Emitted when Backspace is pressed and the value is empty
267
+ */
268
+
231
269
  this.$emit('backspace');
232
270
  }
233
271
 
@@ -241,6 +279,9 @@ var script = {
241
279
  if (suggestedValue != null) {
242
280
  this.applySuggestion(suggestedValue);
243
281
  } else {
282
+ /**
283
+ * Emitted when Enter is pressed and no suggestion is selected
284
+ */
244
285
  this.$emit('submit');
245
286
  }
246
287
  },
@@ -252,6 +293,10 @@ var script = {
252
293
  },
253
294
  Escape: () => {
254
295
  e.preventDefault();
296
+ /**
297
+ * Emitted when suggestion is selected from the suggestion list
298
+ */
299
+
255
300
  this.$emit('complete');
256
301
  }
257
302
  };
@@ -282,6 +327,9 @@ var script = {
282
327
  if (this.multiSelect) {
283
328
  this.$emit('complete');
284
329
  } else if (this.active) {
330
+ /**
331
+ * Emitted when this term token will lose its focus.
332
+ */
285
333
  this.$emit('deactivate');
286
334
  }
287
335
  }
@@ -11,7 +11,12 @@ var script = {
11
11
  type: {
12
12
  type: String,
13
13
  required: false,
14
- default: 'solid'
14
+ default: 'solid',
15
+
16
+ validator(value) {
17
+ return ['solid', 'dashed'].indexOf(value) !== -1;
18
+ }
19
+
15
20
  }
16
21
  },
17
22
 
@@ -91,6 +91,12 @@ export const setupStorybookReadme = () =>
91
91
  'GlFormCheckbox',
92
92
  'GlAccordion',
93
93
  'GlAccordionItem',
94
+ 'GlFilteredSearch',
95
+ 'GlFilteredSearchSuggestion',
96
+ 'GlFilteredSearchSuggestionList',
97
+ 'GlFilteredSearchTerm',
98
+ 'GlFilteredSearchToken',
99
+ 'GlFilteredSearchTokenSegment',
94
100
  'GlIntersperse',
95
101
  'GlFormSelect',
96
102
  'GlDaterangePicker',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gitlab/ui",
3
- "version": "38.0.1",
3
+ "version": "38.1.0",
4
4
  "description": "GitLab UI Components",
5
5
  "license": "MIT",
6
6
  "main": "dist/index.js",
@@ -37,9 +37,10 @@
37
37
  "test:unit": "NODE_ENV=test jest --testPathIgnorePatterns storyshots.spec.js",
38
38
  "test:unit:watch": "yarn test:unit --watch --notify",
39
39
  "test:unit:debug": "NODE_ENV=test node --inspect node_modules/.bin/jest --testPathIgnorePatterns storyshot.spec.js --watch --runInBand",
40
- "test:visual": "NODE_ENV=test IS_VISUAL_TEST=true start-test http-get://localhost:9001 'jest ./tests/storyshots.spec.js'",
40
+ "test:visual": "./bin/run-visual-tests.sh 'jest ./tests/storyshots.spec.js'",
41
41
  "test:visual:minimal": "node ./bin/run_minimal_visual_tests.js",
42
- "test:visual:update": "NODE_ENV=test IS_VISUAL_TEST=true JEST_IMAGE_SNAPSHOT_TRACK_OBSOLETE=1 start-test http-get://localhost:9001 'jest ./tests/storyshots.spec.js --updateSnapshot'",
42
+ "test:visual:update": "./bin/run-visual-tests.sh 'JEST_IMAGE_SNAPSHOT_TRACK_OBSOLETE=1 jest ./tests/storyshots.spec.js --updateSnapshot'",
43
+ "test:visual:internal": "NODE_ENV=test IS_VISUAL_TEST=true start-test http-get://localhost:9001",
43
44
  "prettier": "prettier --check '**/*.{js,vue}'",
44
45
  "prettier:fix": "prettier --write '**/*.{js,vue}'",
45
46
  "eslint": "eslint --max-warnings 0 --ext .js,.vue .",
@@ -77,6 +78,8 @@
77
78
  },
78
79
  "resolutions": {
79
80
  "chokidar": "^3.5.2",
81
+ "node-gyp": "^9.0.0",
82
+ "node-sass": "^6.0.0",
80
83
  "sane": "^5.0.1"
81
84
  },
82
85
  "devDependencies": {
@@ -118,11 +121,9 @@
118
121
  "jest": "^26.6.3",
119
122
  "jest-raw-loader": "^1.0.1",
120
123
  "jest-serializer-vue": "^2.0.2",
121
- "markdown-loader-jest": "^0.1.1",
122
124
  "markdownlint-cli": "^0.29.0",
123
125
  "mockdate": "^2.0.5",
124
- "node-sass": "^4.14.1",
125
- "node-sass-magic-importer": "^5.3.2",
126
+ "node-sass": "^6.0.0",
126
127
  "npm-run-all": "^4.1.5",
127
128
  "pikaday": "^1.8.0",
128
129
  "plop": "^2.5.4",
@@ -139,8 +140,9 @@
139
140
  "rollup-plugin-string": "^3.0.0",
140
141
  "rollup-plugin-svg": "^2.0.0",
141
142
  "rollup-plugin-vue": "^5.1.6",
143
+ "sass": "^1.49.9",
142
144
  "sass-export": "^1.0.3",
143
- "sass-loader": "^7.1.0",
145
+ "sass-loader": "^10.2.0",
144
146
  "sass-true": "^5.0.0",
145
147
  "start-server-and-test": "^1.10.6",
146
148
  "storybook-readme": "5.0.9",
@@ -1,9 +1,10 @@
1
1
  import { shallowMount } from '@vue/test-utils';
2
- import { BBreadcrumbItem } from 'bootstrap-vue';
2
+ import { nextTick } from 'vue';
3
3
  import Breadcrumb, { COLLAPSE_AT_SIZE } from './breadcrumb.vue';
4
+ import GlBreadcrumbItem from './breadcrumb_item.vue';
4
5
  import { createMockDirective } from '~helpers/vue_mock_directive';
5
6
 
6
- describe('Broadcast message component', () => {
7
+ describe('Breadcrumb component', () => {
7
8
  let wrapper;
8
9
 
9
10
  const items = [
@@ -25,7 +26,7 @@ describe('Broadcast message component', () => {
25
26
 
26
27
  const findAvatarSlot = () => wrapper.find('[data-testid="avatar-slot"]');
27
28
  const findSeparatorSlot = () => wrapper.find('[data-testid="separator-slot"]');
28
- const findBreadcrumbItems = () => wrapper.findAllComponents(BBreadcrumbItem);
29
+ const findBreadcrumbItems = () => wrapper.findAllComponents(GlBreadcrumbItem);
29
30
  const findAllSeparators = () => wrapper.findAll('[data-testid="separator"]');
30
31
  const findCollapsedListExpander = () => wrapper.find('[data-testid="collapsed-expander"]');
31
32
  const findExpanderSeparator = () => wrapper.find('[data-testid="expander-separator"]');
@@ -42,6 +43,9 @@ describe('Broadcast message component', () => {
42
43
  separator: '<div data-testid="separator-slot"></div>',
43
44
  },
44
45
  directives: { GlTooltip: createMockDirective('gl-tooltip') },
46
+ stubs: {
47
+ GlBreadcrumbItem,
48
+ },
45
49
  });
46
50
 
47
51
  wrapper.vm.$refs.firstItem = [
@@ -86,21 +90,31 @@ describe('Broadcast message component', () => {
86
90
  });
87
91
 
88
92
  describe('bindings', () => {
89
- it('first breadcrumb has text and href bound', () => {
93
+ beforeEach(() => {
90
94
  createComponent();
95
+ });
91
96
 
92
- expect(findBreadcrumbItems().at(0).attributes()).toMatchObject({
97
+ it('first breadcrumb has text, href && ariaCurrent=`false` bound', () => {
98
+ expect(findBreadcrumbItems().at(0).props()).toMatchObject({
93
99
  text: items[0].text,
94
100
  href: items[0].href,
101
+ ariaCurrent: false,
95
102
  });
96
103
  });
97
104
 
98
- it('second breadcrumb has text and to bound', () => {
99
- createComponent();
100
-
101
- expect(findBreadcrumbItems().at(1).attributes()).toMatchObject({
105
+ it('second breadcrumb has text, to && ariaCurrent=`false` bound', () => {
106
+ expect(findBreadcrumbItems().at(1).props()).toMatchObject({
102
107
  text: items[1].text,
103
108
  to: items[1].to,
109
+ ariaCurrent: false,
110
+ });
111
+ });
112
+
113
+ it('last breadcrumb has text, to && ariaCurrent=`page` bound', () => {
114
+ expect(findBreadcrumbItems().at(2).props()).toMatchObject({
115
+ text: items[2].text,
116
+ href: items[2].href,
117
+ ariaCurrent: 'page',
104
118
  });
105
119
  });
106
120
  });
@@ -139,7 +153,7 @@ describe('Broadcast message component', () => {
139
153
 
140
154
  it('should expand the list on expander click', async () => {
141
155
  findCollapsedListExpander().vm.$emit('click');
142
- await wrapper.vm.$nextTick();
156
+ await nextTick();
143
157
  expect(findHiddenBreadcrumbItems()).toHaveLength(0);
144
158
  expect(findVisibleBreadcrumbItems()).toHaveLength(items.length + extraItems.length);
145
159
  });
@@ -1,17 +1,18 @@
1
1
  <script>
2
- import { BBreadcrumb, BBreadcrumbItem } from 'bootstrap-vue';
2
+ import { BBreadcrumb } from 'bootstrap-vue';
3
3
  import GlIcon from '../icon/icon.vue';
4
4
  import GlButton from '../button/button.vue';
5
5
  import { GlTooltipDirective } from '../../../directives/tooltip';
6
+ import GlBreadcrumbItem from './breadcrumb_item.vue';
6
7
 
7
8
  export const COLLAPSE_AT_SIZE = 4;
8
9
 
9
10
  export default {
10
11
  components: {
11
12
  BBreadcrumb,
12
- BBreadcrumbItem,
13
13
  GlIcon,
14
14
  GlButton,
15
+ GlBreadcrumbItem,
15
16
  },
16
17
  directives: {
17
18
  GlTooltip: GlTooltipDirective,
@@ -58,11 +59,12 @@ export default {
58
59
  },
59
60
  expandBreadcrumbs() {
60
61
  this.isListCollapsed = false;
62
+
61
63
  try {
62
64
  this.$refs.firstItem[0].querySelector('a').focus();
63
65
  } catch (e) {
64
66
  /* eslint-disable-next-line no-console */
65
- console.error(`Failed to set focus on the last breadcrumb item.`);
67
+ console.error(`Failed to set focus on the first breadcrumb item.`);
66
68
  }
67
69
  },
68
70
  showCollapsedBreadcrumbsExpander(index) {
@@ -73,6 +75,9 @@ export default {
73
75
  this.hasCollapsible && this.isListCollapsed && !this.nonCollapsibleIndices.includes(index)
74
76
  );
75
77
  },
78
+ getAriaCurrentAttr(index) {
79
+ return this.isLastItem(index) ? 'page' : false;
80
+ },
76
81
  },
77
82
  };
78
83
  </script>
@@ -82,14 +87,14 @@ export default {
82
87
  <slot name="avatar"></slot>
83
88
  <b-breadcrumb class="gl-breadcrumb-list" v-bind="$attrs" v-on="$listeners">
84
89
  <template v-for="(item, index) in items">
85
- <b-breadcrumb-item
90
+ <gl-breadcrumb-item
86
91
  :key="index"
87
92
  :ref="isFirstItem(index) ? 'firstItem' : null"
88
- class="gl-breadcrumb-item"
89
93
  :text="item.text"
90
94
  :href="item.href"
91
95
  :to="item.to"
92
96
  :class="{ 'gl-display-none': isItemCollapsed(index) }"
97
+ :aria-current="getAriaCurrentAttr(index)"
93
98
  >
94
99
  <span>{{ item.text }}</span>
95
100
  <span
@@ -103,7 +108,7 @@ export default {
103
108
  <gl-icon name="chevron-right" />
104
109
  </slot>
105
110
  </span>
106
- </b-breadcrumb-item>
111
+ </gl-breadcrumb-item>
107
112
 
108
113
  <template v-if="showCollapsedBreadcrumbsExpander(index)">
109
114
  <!-- eslint-disable-next-line vue/valid-v-for -->
@@ -0,0 +1,45 @@
1
+ import { shallowMount } from '@vue/test-utils';
2
+ import { BLink } from 'bootstrap-vue';
3
+ import BreadcrumbItem from './breadcrumb_item.vue';
4
+
5
+ describe('Breadcrumb Item Component', () => {
6
+ let wrapper;
7
+
8
+ const item = { href: 'http://about.gitlab.com', to: { name: 'about' }, ariaCurrent: 'page' };
9
+
10
+ const findBLink = () => wrapper.findComponent(BLink);
11
+ const findDefaultSlot = () => wrapper.find('[data-testid="default-slot"]');
12
+
13
+ const createComponent = () => {
14
+ wrapper = shallowMount(BreadcrumbItem, {
15
+ propsData: {
16
+ href: item.href,
17
+ to: item.to,
18
+ ariaCurrent: item.ariaCurrent,
19
+ },
20
+ slots: {
21
+ default: '<div data-testid="default-slot"></div>',
22
+ },
23
+ });
24
+ };
25
+
26
+ describe('slots', () => {
27
+ it('renders provided content to default slot', () => {
28
+ createComponent();
29
+
30
+ expect(findDefaultSlot().exists()).toBe(true);
31
+ });
32
+ });
33
+
34
+ describe('bindings', () => {
35
+ it('passes provided props down to BLink', () => {
36
+ createComponent();
37
+
38
+ const bLink = findBLink();
39
+
40
+ expect(bLink.props('to')).toMatchObject(item.to);
41
+ expect(bLink.props('href')).toBe(item.href);
42
+ expect(bLink.attributes('aria-current')).toBe(item.ariaCurrent);
43
+ });
44
+ });
45
+ });
@@ -0,0 +1,43 @@
1
+ <script>
2
+ import { BLink } from 'bootstrap-vue';
3
+
4
+ export default {
5
+ components: {
6
+ BLink,
7
+ },
8
+ inheritAttrs: false,
9
+ props: {
10
+ text: {
11
+ type: String,
12
+ required: false,
13
+ default: null,
14
+ },
15
+ to: {
16
+ type: [String, Object],
17
+ required: false,
18
+ default: null,
19
+ },
20
+ href: {
21
+ type: String,
22
+ required: false,
23
+ default: null,
24
+ },
25
+ ariaCurrent: {
26
+ type: [String, Boolean],
27
+ required: false,
28
+ default: false,
29
+ validator(value) {
30
+ return [false, 'page'].indexOf(value) !== -1;
31
+ },
32
+ },
33
+ },
34
+ };
35
+ </script>
36
+
37
+ <template>
38
+ <li class="gl-breadcrumb-item">
39
+ <b-link :href="href" :to="to" :aria-current="ariaCurrent">
40
+ <slot>{{ text }}</slot>
41
+ </b-link>
42
+ </li>
43
+ </template>