@gitlab/ui 37.7.0 → 37.9.1

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 CHANGED
@@ -1,3 +1,24 @@
1
+ ## [37.9.1](https://gitlab.com/gitlab-org/gitlab-ui/compare/v37.9.0...v37.9.1) (2022-03-16)
2
+
3
+
4
+ ### Bug Fixes
5
+
6
+ * **GlFilteredSearch:** Keep active state on backspace on first token ([bd241df](https://gitlab.com/gitlab-org/gitlab-ui/commit/bd241dfbbd87064c10a67f0a0857b54542362605))
7
+
8
+ # [37.9.0](https://gitlab.com/gitlab-org/gitlab-ui/compare/v37.8.0...v37.9.0) (2022-03-15)
9
+
10
+
11
+ ### Features
12
+
13
+ * **GlTruncate:** add slots to surround the text ([4a79750](https://gitlab.com/gitlab-org/gitlab-ui/commit/4a797503c85ac755e7dd4df1b2d46e95f3102e04))
14
+
15
+ # [37.8.0](https://gitlab.com/gitlab-org/gitlab-ui/compare/v37.7.0...v37.8.0) (2022-03-15)
16
+
17
+
18
+ ### Features
19
+
20
+ * **GlFilteredSearch:** Allow to pass token dynamically ([247ea01](https://gitlab.com/gitlab-org/gitlab-ui/commit/247ea011487a1ab0087d994cdb994c9c553565eb))
21
+
1
22
  # [37.7.0](https://gitlab.com/gitlab-org/gitlab-ui/compare/v37.6.0...v37.7.0) (2022-03-14)
2
23
 
3
24
 
@@ -1,3 +1,4 @@
1
+ import _isEqual from 'lodash/isEqual';
1
2
  import _cloneDeep from 'lodash/cloneDeep';
2
3
  import PortalVue from 'portal-vue';
3
4
  import Vue from 'vue';
@@ -150,6 +151,16 @@ var script = {
150
151
  this.$emit('input', this.tokens);
151
152
  },
152
153
 
154
+ deep: true,
155
+ immediate: true
156
+ },
157
+ value: {
158
+ handler(newValue, oldValue) {
159
+ if (newValue.length && !_isEqual(newValue, oldValue)) {
160
+ this.applyNewValue(_cloneDeep(newValue));
161
+ }
162
+ },
163
+
153
164
  deep: true,
154
165
  immediate: true
155
166
  }
@@ -230,8 +241,8 @@ var script = {
230
241
  // possible, make sure no token is active.
231
242
 
232
243
  if (intent === INTENT_ACTIVATE_PREVIOUS) {
233
- // If there is a previous token, activate it; else, deactivate all tokens
234
- this.activeTokenIdx = idx > 0 ? idx - 1 : null;
244
+ // If there is a previous token, activate it; else, activate the first token
245
+ this.activeTokenIdx = Math.max(idx - 1, 0);
235
246
  } else if (idx < this.activeTokenIdx) {
236
247
  // Preserve the active token's active status (it shifted down one index)
237
248
  this.activeTokenIdx -= 1;
@@ -1,4 +1,4 @@
1
- var description = "Modals are used to reveal critical information, show information without losing context, or when the\nsystem requires a user response. Modals can also fragment a complex workflow into simpler steps and\nshould serve a single purpose dedicated to completing the user’s task.\n\n## VModel\n\nYou can use the `v-model` directive to control the modal’s visibility. The `v-model`\ndirective interfaces with the `visible` property and the `@change` event.\n\n## Deprecation Warning\n\nWe are deprecating the `modal-ok` and `modal-cancel` slots. We are also changing the way the\n`modal-footer` slot content is populated. This is in order to align this component with the design\nsystem.\n\nThe `modal-footer` slot should only be populated via props: `action-primary`, `action-secondary` and\n`action-cancel`. These props allow you to handle how a primary, secondary and cancel button will\nbehave in the modals footer. The props receive an object as such:\n\n```js\n{\n text: 'Save Changes',\n attributes: [\n { variant: 'confirm' },\n { disabled: this.someState },\n { class: 'some-class' },\n ...\n ]\n}\n```\n";
1
+ var description = "Modals are used to reveal critical information, show information without losing context, or when the\nsystem requires a user response. Modals can also fragment a complex workflow into simpler steps and\nshould serve a single purpose dedicated to completing the user’s task.\n\n## VModel\n\nYou can use the `v-model` directive to control the modal’s visibility. The `v-model`\ndirective interfaces with the `visible` property and the `@change` event.\n\n## Deprecation Warning\n\nWe are deprecating the `modal-ok` and `modal-cancel` slots. We are also changing the way the\n`modal-footer` slot content is populated. This is in order to align this component with the design\nsystem.\n\nThe `modal-footer` slot should only be populated via props: `action-primary`, `action-secondary` and\n`action-cancel`. These props allow you to handle how a primary, secondary and cancel button will\nbehave in the modals footer. The props receive an object as such:\n\n```js\n{\n text: 'Save Changes',\n attributes: {\n variant: 'confirm',\n disabled: this.someState,\n class: 'some-class',\n ...\n }\n}\n```\n";
2
2
 
3
3
  var modal_documentation = {
4
4
  followsDesignSystem: true,
@@ -1,4 +1,4 @@
1
- var description = "Tabs are used to divide content into meaningful, related sections. Tabs allow users to focus on one\nspecific view at a time while maintaining sight of all the relevant content options available. Each\ntab, when active, will reveal it’s own unique content.\n\n## Using the component Vue\n\n~~~html\n<gl-tabs>\n <gl-tab title=\"Tab 1\">\n Tab panel 1\n </gl-tab>\n <gl-tab title=\"Tab 2\">\n Tab panel 2\n </gl-tab>\n</gl-tabs>\n~~~\n\n## Using the component HTML\n\n~~~html\n<div class=\"tabs gl-tabs\">\n <ul role=\"tablist\" class=\"nav gl-tabs-nav\">\n <li role=\"presentation\" class=\"nav-item\">\n <a\n role=\"tab\"\n target=\"_self\"\n href=\"#\"\n class=\"nav-link gl-tab-nav-item gl-tab-nav-item-active gl-tab-nav-item-active-indigo\"\n >Tab 1</a>\n </li>\n <li role=\"presentation\" class=\"nav-item\">\n <a role=\"tab\" target=\"_self\" href=\"#\" class=\"nav-link gl-tab-nav-item\">Tab 2</a>\n </li>\n </ul>\n <div class=\"tab-content gl-tab-content\">\n <div role=\"tabpanel\" class=\"tab-pane gl-tab-content active\">Tab panel 1</div>\n <div role=\"tabpanel\" class=\"tab-pane gl-tab-content\">Tab panel 2</div>\n </div>\n</div>\n~~~\n\n## Adding Action Buttons to the Tabs\n\nTabs start and end slot can be populated via props: `action-primary`, `action-secondary` and\n`action-tertiary`. These props allow you to handle how a primary, secondary and tertiary button will\nbehave and look. The props receive an object as such:\n\n~~~js\n{\n text: 'Save Changes',\n attributes: [\n { variant: 'info' },\n { disabled: this.someState },\n { class: 'some-class' },\n ...\n ]\n}\n~~~\n\n## Scrollable tab buttons\n\nBy default, `GlTab` will wrap tab buttons when they overflow the container. To\nenable horizontally scrolling for the tab buttons, use the `GlScrollableTabs`\ncomponent. This is a separate Vue component because of some limitations:\n\n- The action props (e.g., `action-primary`) are not respected in `GlScrollableTabs`. At the\n moment, BootstrapVue does not provide a reliable way for us to achieve this desired combination.\n\n`GlScrollableTabs` composes `GlTabs` and passes through every listener, slot, or prop (with the above\nexceptions).\n\n~~~html\n<gl-scrollable-tabs>\n <gl-tab v-for=\"tab in tabs\" :key=\"tab.key\" :title=\"tab.title\"> {{ tab.content }} </gl-tab>\n</gl-scrollable-tabs>\n~~~\n";
1
+ var description = "Tabs are used to divide content into meaningful, related sections. Tabs allow users to focus on one\nspecific view at a time while maintaining sight of all the relevant content options available. Each\ntab, when active, will reveal it’s own unique content.\n\n## Using the component Vue\n\n~~~html\n<gl-tabs>\n <gl-tab title=\"Tab 1\">\n Tab panel 1\n </gl-tab>\n <gl-tab title=\"Tab 2\">\n Tab panel 2\n </gl-tab>\n</gl-tabs>\n~~~\n\n## Using the component HTML\n\n~~~html\n<div class=\"tabs gl-tabs\">\n <ul role=\"tablist\" class=\"nav gl-tabs-nav\">\n <li role=\"presentation\" class=\"nav-item\">\n <a\n role=\"tab\"\n target=\"_self\"\n href=\"#\"\n class=\"nav-link gl-tab-nav-item gl-tab-nav-item-active gl-tab-nav-item-active-indigo\"\n >Tab 1</a>\n </li>\n <li role=\"presentation\" class=\"nav-item\">\n <a role=\"tab\" target=\"_self\" href=\"#\" class=\"nav-link gl-tab-nav-item\">Tab 2</a>\n </li>\n </ul>\n <div class=\"tab-content gl-tab-content\">\n <div role=\"tabpanel\" class=\"tab-pane gl-tab-content active\">Tab panel 1</div>\n <div role=\"tabpanel\" class=\"tab-pane gl-tab-content\">Tab panel 2</div>\n </div>\n</div>\n~~~\n\n## Adding Action Buttons to the Tabs\n\nTabs start and end slot can be populated via props: `action-primary`, `action-secondary` and\n`action-tertiary`. These props allow you to handle how a primary, secondary and tertiary button will\nbehave and look. The props receive an object as such:\n\n~~~js\n{\n text: 'Save Changes',\n attributes: {\n variant: 'info',\n disabled: this.someState,\n class: 'some-class',\n ...\n }\n}\n~~~\n\n## Scrollable tab buttons\n\nBy default, `GlTab` will wrap tab buttons when they overflow the container. To\nenable horizontally scrolling for the tab buttons, use the `GlScrollableTabs`\ncomponent. This is a separate Vue component because of some limitations:\n\n- The action props (e.g., `action-primary`) are not respected in `GlScrollableTabs`. At the\n moment, BootstrapVue does not provide a reliable way for us to achieve this desired combination.\n\n`GlScrollableTabs` composes `GlTabs` and passes through every listener, slot, or prop (with the above\nexceptions).\n\n~~~html\n<gl-scrollable-tabs>\n <gl-tab v-for=\"tab in tabs\" :key=\"tab.key\" :title=\"tab.title\"> {{ tab.content }} </gl-tab>\n</gl-scrollable-tabs>\n~~~\n";
2
2
 
3
3
  var tabs_documentation = {
4
4
  description,
@@ -1,5 +1,5 @@
1
- import { GlTooltipDirective } from '../../../directives/tooltip';
2
1
  import { GlResizeObserverDirective } from '../../../directives/resize_observer/resize_observer';
2
+ import { GlTooltipDirective } from '../../../directives/tooltip';
3
3
  import { POSITION } from './constants';
4
4
  import __vue_normalize__ from 'vue-runtime-helpers/dist/normalize-component.js';
5
5
 
@@ -84,7 +84,7 @@ var script = {
84
84
  const __vue_script__ = script;
85
85
 
86
86
  /* template */
87
- var __vue_render__ = function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return (_vm.position === _vm.$options.POSITION.START)?_c('span',{directives:[{name:"gl-tooltip",rawName:"v-gl-tooltip",value:({ disabled: _vm.isTooltipDisabled }),expression:"{ disabled: isTooltipDisabled }"},{name:"gl-resize-observer",rawName:"v-gl-resize-observer:[withTooltip]",value:(_vm.checkTruncationState),expression:"checkTruncationState",arg:_vm.withTooltip}],staticClass:"gl-truncate",attrs:{"title":_vm.text}},[_c('span',{ref:"text",staticClass:"gl-truncate-start gl-text-overflow-ellipsis!"},[_vm._v("‎"+_vm._s(_vm.text)+"‎")])]):(_vm.position === _vm.$options.POSITION.MIDDLE)?_c('span',{directives:[{name:"gl-tooltip",rawName:"v-gl-tooltip",value:({ disabled: _vm.isTooltipDisabled }),expression:"{ disabled: isTooltipDisabled }"},{name:"gl-resize-observer",rawName:"v-gl-resize-observer:[withTooltip]",value:(_vm.checkTruncationState),expression:"checkTruncationState",arg:_vm.withTooltip}],staticClass:"gl-truncate",attrs:{"title":_vm.text}},[_c('span',{ref:"text",staticClass:"gl-truncate-end"},[_vm._v(_vm._s(_vm.first))]),_c('span',{staticClass:"gl-truncate-start"},[_vm._v("‎"+_vm._s(_vm.last)+"‎")])]):_c('span',{directives:[{name:"gl-tooltip",rawName:"v-gl-tooltip",value:({ disabled: _vm.isTooltipDisabled }),expression:"{ disabled: isTooltipDisabled }"},{name:"gl-resize-observer",rawName:"v-gl-resize-observer:[withTooltip]",value:(_vm.checkTruncationState),expression:"checkTruncationState",arg:_vm.withTooltip}],staticClass:"gl-truncate",attrs:{"data-testid":"truncate-end-container","title":_vm.text}},[_c('span',{ref:"text",staticClass:"gl-truncate-end"},[_vm._v(_vm._s(_vm.text))])])};
87
+ var __vue_render__ = function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return (_vm.position === _vm.$options.POSITION.START)?_c('span',{directives:[{name:"gl-tooltip",rawName:"v-gl-tooltip",value:({ disabled: _vm.isTooltipDisabled }),expression:"{ disabled: isTooltipDisabled }"},{name:"gl-resize-observer",rawName:"v-gl-resize-observer:[withTooltip]",value:(_vm.checkTruncationState),expression:"checkTruncationState",arg:_vm.withTooltip}],staticClass:"gl-truncate",attrs:{"title":_vm.text}},[(_vm.$scopedSlots['before-text'])?_c('span',{staticClass:"gl-pr-3"},[_vm._t("before-text")],2):_vm._e(),_vm._v(" "),_c('span',{ref:"text",staticClass:"gl-truncate-start gl-text-overflow-ellipsis!"},[_vm._v("‎"+_vm._s(_vm.text)+"‎")]),_vm._v(" "),(_vm.$scopedSlots['after-text'])?_c('span',{staticClass:"gl-pl-3"},[_vm._t("after-text")],2):_vm._e()]):(_vm.position === _vm.$options.POSITION.MIDDLE)?_c('span',{directives:[{name:"gl-tooltip",rawName:"v-gl-tooltip",value:({ disabled: _vm.isTooltipDisabled }),expression:"{ disabled: isTooltipDisabled }"},{name:"gl-resize-observer",rawName:"v-gl-resize-observer:[withTooltip]",value:(_vm.checkTruncationState),expression:"checkTruncationState",arg:_vm.withTooltip}],staticClass:"gl-truncate",attrs:{"title":_vm.text}},[(_vm.$scopedSlots['before-text'])?_c('span',{staticClass:"gl-pr-3"},[_vm._t("before-text")],2):_vm._e(),_vm._v(" "),_c('span',{ref:"text",staticClass:"gl-truncate-end",attrs:{"data-testid":"text-beginning"}},[_vm._v(_vm._s(_vm.first))]),_c('span',{staticClass:"gl-truncate-start",attrs:{"data-testid":"text-ending"}},[_vm._v("‎"+_vm._s(_vm.last)+"‎")]),_vm._v(" "),(_vm.$scopedSlots['after-text'])?_c('span',{staticClass:"gl-pl-3"},[_vm._t("after-text")],2):_vm._e()]):_c('span',{directives:[{name:"gl-tooltip",rawName:"v-gl-tooltip",value:({ disabled: _vm.isTooltipDisabled }),expression:"{ disabled: isTooltipDisabled }"},{name:"gl-resize-observer",rawName:"v-gl-resize-observer:[withTooltip]",value:(_vm.checkTruncationState),expression:"checkTruncationState",arg:_vm.withTooltip}],staticClass:"gl-truncate",attrs:{"data-testid":"truncate-end-container","title":_vm.text}},[(_vm.$scopedSlots['before-text'])?_c('span',{staticClass:"gl-pr-3"},[_vm._t("before-text")],2):_vm._e(),_vm._v(" "),_c('span',{ref:"text",staticClass:"gl-truncate-end"},[_vm._v(_vm._s(_vm.text))]),_vm._v(" "),(_vm.$scopedSlots['after-text'])?_c('span',{staticClass:"gl-pl-3"},[_vm._t("after-text")],2):_vm._e()])};
88
88
  var __vue_staticRenderFns__ = [];
89
89
 
90
90
  /* style */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gitlab/ui",
3
- "version": "37.7.0",
3
+ "version": "37.9.1",
4
4
  "description": "GitLab UI Components",
5
5
  "license": "MIT",
6
6
  "main": "dist/index.js",
@@ -211,7 +211,7 @@ describe('Filtered search', () => {
211
211
  expect(wrapper.findComponent(FakeToken).props('active')).toBe(true);
212
212
  });
213
213
 
214
- it('makes no token active if user intends it on first token destruction', async () => {
214
+ it('keeps first token active on first token destruction with backspace', async () => {
215
215
  createComponent({
216
216
  value: ['foo', { type: 'faketoken', value: '' }],
217
217
  });
@@ -228,7 +228,7 @@ describe('Filtered search', () => {
228
228
 
229
229
  await nextTick();
230
230
 
231
- expect(wrapper.findComponent(FakeToken).props('active')).toBe(false);
231
+ expect(wrapper.findComponent(FakeToken).props('active')).toBe(true);
232
232
  });
233
233
 
234
234
  it('keeps active token active if later one destroyed', async () => {
@@ -1,5 +1,5 @@
1
1
  <script>
2
- import { cloneDeep } from 'lodash';
2
+ import { cloneDeep, isEqual } from 'lodash';
3
3
  import PortalVue from 'portal-vue';
4
4
  import Vue from 'vue';
5
5
  import { GlTooltipDirective } from '../../../directives/tooltip';
@@ -146,13 +146,21 @@ export default {
146
146
  deep: true,
147
147
  immediate: true,
148
148
  },
149
+ value: {
150
+ handler(newValue, oldValue) {
151
+ if (newValue.length && !isEqual(newValue, oldValue)) {
152
+ this.applyNewValue(cloneDeep(newValue));
153
+ }
154
+ },
155
+ deep: true,
156
+ immediate: true,
157
+ },
149
158
  },
150
159
  mounted() {
151
160
  if (this.value.length) {
152
161
  this.applyNewValue(cloneDeep(this.value));
153
162
  }
154
163
  },
155
-
156
164
  methods: {
157
165
  applyNewValue(newValue) {
158
166
  this.tokens = needDenormalization(newValue) ? denormalizeTokens(newValue) : newValue;
@@ -214,8 +222,8 @@ export default {
214
222
  // active state for the token that was active at the time. If that's not
215
223
  // possible, make sure no token is active.
216
224
  if (intent === INTENT_ACTIVATE_PREVIOUS) {
217
- // If there is a previous token, activate it; else, deactivate all tokens
218
- this.activeTokenIdx = idx > 0 ? idx - 1 : null;
225
+ // If there is a previous token, activate it; else, activate the first token
226
+ this.activeTokenIdx = Math.max(idx - 1, 0);
219
227
  } else if (idx < this.activeTokenIdx) {
220
228
  // Preserve the active token's active status (it shifted down one index)
221
229
  this.activeTokenIdx -= 1;
@@ -20,11 +20,11 @@ behave in the modals footer. The props receive an object as such:
20
20
  ```js
21
21
  {
22
22
  text: 'Save Changes',
23
- attributes: [
24
- { variant: 'confirm' },
25
- { disabled: this.someState },
26
- { class: 'some-class' },
23
+ attributes: {
24
+ variant: 'confirm',
25
+ disabled: this.someState,
26
+ class: 'some-class',
27
27
  ...
28
- ]
28
+ }
29
29
  }
30
30
  ```
@@ -107,14 +107,14 @@ describe('Modal component', () => {
107
107
  const props = {
108
108
  actionPrimary: {
109
109
  text: 'Primary',
110
- attributes: [{ variant: 'info' }],
110
+ attributes: { variant: 'info' },
111
111
  },
112
112
  actionSecondary: {
113
113
  text: 'Secondary',
114
114
  },
115
115
  actionCancel: {
116
116
  text: 'Cancel',
117
- attributes: [{ variant: 'danger' }],
117
+ attributes: { variant: 'danger' },
118
118
  },
119
119
  };
120
120
 
@@ -48,12 +48,12 @@ behave and look. The props receive an object as such:
48
48
  ~~~js
49
49
  {
50
50
  text: 'Save Changes',
51
- attributes: [
52
- { variant: 'info' },
53
- { disabled: this.someState },
54
- { class: 'some-class' },
51
+ attributes: {
52
+ variant: 'info',
53
+ disabled: this.someState,
54
+ class: 'some-class',
55
55
  ...
56
- ]
56
+ }
57
57
  }
58
58
  ~~~
59
59
 
@@ -16,10 +16,12 @@ describe('Truncate component', () => {
16
16
  text: 'ee/app/assets/javascripts/vue_shared/src/utils_reports/components/utils/index.js',
17
17
  };
18
18
 
19
- const createComponent = (props) => {
19
+ const findByTestId = (testId) => wrapper.find(`[data-testid="${testId}"]`);
20
+
21
+ const createComponent = (props, slots = {}) => {
20
22
  wrapper = shallowMount(
21
23
  { extends: Truncate, directives: { GlTooltip: createMockDirective('gl-tooltip') } },
22
- { propsData: { ...defaultProps, ...props } }
24
+ { propsData: { ...defaultProps, ...props }, slots }
23
25
  );
24
26
  };
25
27
 
@@ -55,12 +57,13 @@ describe('Truncate component', () => {
55
57
  createComponent({ position: 'start' });
56
58
  });
57
59
 
58
- it('should have the truncate-start class', () => {
59
- expect(wrapper.find('.gl-truncate-start').exists()).toBe(true);
60
+ it('should have the truncate-start class on the text span', () => {
61
+ const textSpan = wrapper.findComponent({ ref: 'text' });
62
+ expect(textSpan.classes('gl-truncate-start')).toBe(true);
60
63
  });
61
64
 
62
65
  it('should have the special char surrounded', () => {
63
- const spanTag = wrapper.findAll('.gl-truncate span').at(0).text();
66
+ const spanTag = wrapper.findComponent({ ref: 'text' }).text();
64
67
 
65
68
  expect(spanTag.charAt(0)).toBe('\u200E');
66
69
  expect(spanTag.charAt(spanTag.length - 1)).toBe('\u200E');
@@ -68,27 +71,27 @@ describe('Truncate component', () => {
68
71
  });
69
72
 
70
73
  describe('middle truncation', () => {
71
- let firstSpan;
72
- let secondSpan;
74
+ let firstTextSpan;
75
+ let secondTextSpan;
73
76
 
74
77
  beforeEach(() => {
75
78
  createComponent({ position: 'middle' });
76
- firstSpan = wrapper.findAll('.gl-truncate span').at(0);
77
- secondSpan = wrapper.findAll('.gl-truncate span').at(1);
79
+ firstTextSpan = findByTestId('text-beginning');
80
+ secondTextSpan = findByTestId('text-ending');
78
81
  });
79
82
 
80
83
  it('should have appropriate classes applied', () => {
81
- expect(firstSpan.classes('gl-truncate-end')).toBe(true);
82
- expect(secondSpan.classes('gl-truncate-start')).toBe(true);
84
+ expect(firstTextSpan.classes('gl-truncate-end')).toBe(true);
85
+ expect(secondTextSpan.classes('gl-truncate-start')).toBe(true);
83
86
  });
84
87
 
85
88
  it('should have the spans positioned correctly', () => {
86
- expect(firstSpan.text()).toBe('ee/app/assets/javascripts/vue_shared/src');
87
- expect(secondSpan.text()).toBe('‎/utils_reports/components/utils/index.js‎');
89
+ expect(firstTextSpan.text()).toBe('ee/app/assets/javascripts/vue_shared/src');
90
+ expect(secondTextSpan.text()).toBe('‎/utils_reports/components/utils/index.js‎');
88
91
  });
89
92
 
90
93
  it('last part should have the special char surrounded', () => {
91
- const lastPart = secondSpan.text();
94
+ const lastPart = secondTextSpan.text();
92
95
 
93
96
  expect(lastPart.charAt(0)).toBe('\u200E');
94
97
  expect(lastPart.charAt(lastPart.length - 1)).toBe('\u200E');
@@ -108,4 +111,36 @@ describe('Truncate component', () => {
108
111
  expect(wrapper.find('.gl-truncate-end').exists()).toBe(true);
109
112
  });
110
113
  });
114
+
115
+ describe('slots', () => {
116
+ const beforeText = 'Before Text Slot';
117
+ const afterText = 'After Text Slot';
118
+ const slotOptions = {
119
+ 'before-text': {
120
+ slot: `<span id="before-text">${beforeText}</span>`,
121
+ text: beforeText,
122
+ },
123
+ 'after-text': {
124
+ slot: `<span id="after-text">${afterText}</span>`,
125
+ text: afterText,
126
+ },
127
+ };
128
+
129
+ it.each`
130
+ position | slotType
131
+ ${'start'} | ${'before-text'}
132
+ ${'start'} | ${'after-text'}
133
+ ${'middle'} | ${'before-text'}
134
+ ${'middle'} | ${'after-text'}
135
+ ${'end'} | ${'before-text'}
136
+ ${'end'} | ${'after-text'}
137
+ `('$position truncation $slot slot', ({ position, slotType }) => {
138
+ const slot = { [slotType]: slotOptions[slotType].slot };
139
+
140
+ createComponent({ position }, slot);
141
+
142
+ const slotComponent = wrapper.find(`#${slotType}`);
143
+ expect(slotComponent.text()).toBe(slotOptions[slotType].text);
144
+ });
145
+ });
111
146
  });
@@ -1,8 +1,21 @@
1
- import { GlTruncate } from '../../../index';
1
+ import { GlIcon, GlTruncate } from '../../../index';
2
2
  import { POSITION } from './constants';
3
3
  import readme from './truncate.md';
4
4
 
5
5
  const template = '<gl-truncate :text="text" :position="position" :with-tooltip="withTooltip" />';
6
+ const beforeTextTemplate = `
7
+ <gl-truncate :text="text" :position="position" :with-tooltip="withTooltip">
8
+ <template #before-text><gl-icon name="container-image" /></template>
9
+ </gl-truncate>`;
10
+ const afterTextTemplate = `
11
+ <gl-truncate :text="text" :position="position" :with-tooltip="withTooltip">
12
+ <template #after-text><gl-icon name="abuse" /></template>
13
+ </gl-truncate>`;
14
+ const bothSlotsTemplate = `
15
+ <gl-truncate :text="text" :position="position" :with-tooltip="withTooltip">
16
+ <template #before-text><gl-icon name="rocket" /></template>
17
+ <template #after-text><gl-icon name="requirements" /></template>
18
+ </gl-truncate>`;
6
19
 
7
20
  const generateProps = ({
8
21
  text = 'src/thisIs/AVeryLongFilePath/that/needs/to/be/smartly/truncated/from/the/middle/so/we/dont/lose/important/information/here.vue',
@@ -24,6 +37,51 @@ const Template = (args, { argTypes }) => ({
24
37
  export const Default = Template.bind({});
25
38
  Default.args = generateProps();
26
39
 
40
+ const BeforeTextTemplate = (args, { argTypes }) => ({
41
+ components: { GlIcon, GlTruncate },
42
+ props: Object.keys(argTypes),
43
+ template: beforeTextTemplate,
44
+ });
45
+ export const BeforeText = BeforeTextTemplate.bind({});
46
+ BeforeText.args = generateProps({ position: 'start' });
47
+ BeforeText.parameters = {
48
+ docs: {
49
+ source: {
50
+ code: beforeTextTemplate,
51
+ },
52
+ },
53
+ };
54
+
55
+ const AfterTextTemplate = (args, { argTypes }) => ({
56
+ components: { GlIcon, GlTruncate },
57
+ props: Object.keys(argTypes),
58
+ template: afterTextTemplate,
59
+ });
60
+ export const AfterText = AfterTextTemplate.bind({});
61
+ AfterText.args = generateProps({ position: 'end' });
62
+ AfterText.parameters = {
63
+ docs: {
64
+ source: {
65
+ code: afterTextTemplate,
66
+ },
67
+ },
68
+ };
69
+
70
+ const BothSlotsTemplate = (args, { argTypes }) => ({
71
+ components: { GlIcon, GlTruncate },
72
+ props: Object.keys(argTypes),
73
+ template: bothSlotsTemplate,
74
+ });
75
+ export const BothSlots = BothSlotsTemplate.bind({});
76
+ BothSlots.args = generateProps();
77
+ BothSlots.parameters = {
78
+ docs: {
79
+ source: {
80
+ code: bothSlotsTemplate,
81
+ },
82
+ },
83
+ };
84
+
27
85
  export default {
28
86
  title: 'utilities/truncate',
29
87
  component: GlTruncate,
@@ -1,6 +1,6 @@
1
1
  <script>
2
- import { GlTooltipDirective } from '../../../directives/tooltip';
3
2
  import { GlResizeObserverDirective } from '../../../directives/resize_observer/resize_observer';
3
+ import { GlTooltipDirective } from '../../../directives/tooltip';
4
4
  import { POSITION } from './constants';
5
5
 
6
6
  export default {
@@ -81,9 +81,15 @@ export default {
81
81
  class="gl-truncate"
82
82
  :title="text"
83
83
  >
84
+ <span v-if="$scopedSlots['before-text']" class="gl-pr-3">
85
+ <slot name="before-text"></slot>
86
+ </span>
84
87
  <span ref="text" class="gl-truncate-start gl-text-overflow-ellipsis!"
85
88
  >&lrm;{{ text }}&lrm;</span
86
89
  >
90
+ <span v-if="$scopedSlots['after-text']" class="gl-pl-3">
91
+ <slot name="after-text"></slot>
92
+ </span>
87
93
  </span>
88
94
 
89
95
  <!-- MIDDLE -->
@@ -94,8 +100,14 @@ export default {
94
100
  class="gl-truncate"
95
101
  :title="text"
96
102
  >
97
- <span ref="text" class="gl-truncate-end">{{ first }}</span
98
- ><span class="gl-truncate-start">&lrm;{{ last }}&lrm;</span>
103
+ <span v-if="$scopedSlots['before-text']" class="gl-pr-3">
104
+ <slot name="before-text"></slot>
105
+ </span>
106
+ <span ref="text" class="gl-truncate-end" data-testid="text-beginning">{{ first }}</span
107
+ ><span class="gl-truncate-start" data-testid="text-ending">&lrm;{{ last }}&lrm;</span>
108
+ <span v-if="$scopedSlots['after-text']" class="gl-pl-3">
109
+ <slot name="after-text"></slot>
110
+ </span>
99
111
  </span>
100
112
 
101
113
  <!-- END -->
@@ -107,6 +119,12 @@ export default {
107
119
  data-testid="truncate-end-container"
108
120
  :title="text"
109
121
  >
122
+ <span v-if="$scopedSlots['before-text']" class="gl-pr-3">
123
+ <slot name="before-text"></slot>
124
+ </span>
110
125
  <span ref="text" class="gl-truncate-end">{{ text }}</span>
126
+ <span v-if="$scopedSlots['after-text']" class="gl-pl-3">
127
+ <slot name="after-text"></slot>
128
+ </span>
111
129
  </span>
112
130
  </template>