@gitlab/ui 74.9.0 → 74.9.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.
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Do not edit directly
3
- * Generated on Mon, 19 Feb 2024 23:43:48 GMT
3
+ * Generated on Tue, 20 Feb 2024 13:40:26 GMT
4
4
  */
5
5
 
6
6
  :root {
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Do not edit directly
3
- * Generated on Mon, 19 Feb 2024 23:43:48 GMT
3
+ * Generated on Tue, 20 Feb 2024 13:40:26 GMT
4
4
  */
5
5
 
6
6
  :root.gl-dark {
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Do not edit directly
3
- * Generated on Mon, 19 Feb 2024 23:43:48 GMT
3
+ * Generated on Tue, 20 Feb 2024 13:40:26 GMT
4
4
  */
5
5
 
6
6
  export const DATA_VIZ_GREEN_50 = "#133a03";
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Do not edit directly
3
- * Generated on Mon, 19 Feb 2024 23:43:48 GMT
3
+ * Generated on Tue, 20 Feb 2024 13:40:26 GMT
4
4
  */
5
5
 
6
6
  export const DATA_VIZ_GREEN_50 = "#ddfab7";
@@ -1,6 +1,6 @@
1
1
 
2
2
  // Do not edit directly
3
- // Generated on Mon, 19 Feb 2024 23:43:48 GMT
3
+ // Generated on Tue, 20 Feb 2024 13:40:26 GMT
4
4
 
5
5
  $red-950: #fff4f3;
6
6
  $red-900: #fcf1ef;
@@ -1,6 +1,6 @@
1
1
 
2
2
  // Do not edit directly
3
- // Generated on Mon, 19 Feb 2024 23:43:48 GMT
3
+ // Generated on Tue, 20 Feb 2024 13:40:26 GMT
4
4
 
5
5
  $gl-line-height-52: 3.25rem;
6
6
  $gl-line-height-44: 2.75rem;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gitlab/ui",
3
- "version": "74.9.0",
3
+ "version": "74.9.2",
4
4
  "description": "GitLab UI Components",
5
5
  "license": "MIT",
6
6
  "main": "dist/index.js",
@@ -307,7 +307,7 @@ describe('base dropdown', () => {
307
307
  // close menu clicking toggle btn
308
308
  await toggle.trigger('click');
309
309
  expect(menu.classes('gl-display-block!')).toBe(false);
310
- expect(toggle.attributes('aria-expanded')).toBeUndefined();
310
+ expect(toggle.attributes('aria-expanded')).toBe('false');
311
311
  expect(wrapper.emitted(GL_DROPDOWN_HIDDEN)).toHaveLength(1);
312
312
  expect(toggle.element).toHaveFocus();
313
313
  });
@@ -332,7 +332,7 @@ describe('base dropdown', () => {
332
332
  await menu.trigger('keydown.esc');
333
333
 
334
334
  expect(menu.classes('gl-display-block!')).toBe(false);
335
- expect(toggle.attributes('aria-expanded')).toBeUndefined();
335
+ expect(toggle.attributes('aria-expanded')).toBe('false');
336
336
  expect(wrapper.emitted(GL_DROPDOWN_HIDDEN)).toHaveLength(1);
337
337
  expect(toggle.element).toHaveFocus();
338
338
  });
@@ -391,7 +391,7 @@ describe('base dropdown', () => {
391
391
  moveFocusWithinDropdown();
392
392
  await menu.trigger('keydown.esc');
393
393
  expect(menu.classes('gl-display-block!')).toBe(true);
394
- expect(toggle.attributes('aria-expanded')).toBeDefined();
394
+ expect(toggle.attributes('aria-expanded')).toBe('true');
395
395
  expect(wrapper.emitted(GL_DROPDOWN_HIDDEN)).toBeUndefined();
396
396
  expect(toggle.element).not.toHaveFocus();
397
397
  });
@@ -166,7 +166,7 @@ export default {
166
166
  ariaAttributes() {
167
167
  return {
168
168
  'aria-haspopup': this.ariaHaspopup,
169
- 'aria-expanded': this.visible,
169
+ 'aria-expanded': String(this.visible),
170
170
  'aria-controls': this.baseDropdownId,
171
171
  'aria-labelledby': this.toggleLabelledBy,
172
172
  };
@@ -110,4 +110,14 @@ table.gl-table {
110
110
  }
111
111
  }
112
112
  }
113
+
114
+ &.table-hover tbody tr:hover,
115
+ &.table-hover td.table-secondary:hover {
116
+ background-color: $gray-10;
117
+ }
118
+ }
119
+
120
+ .table.b-table > thead > tr > th,
121
+ .table.b-table > tfoot > tr > th {
122
+ background-image: none !important;
113
123
  }
@@ -1,7 +1,9 @@
1
- import { shallowMount } from '@vue/test-utils';
1
+ import { nextTick } from 'vue';
2
+ import { shallowMount, mount } from '@vue/test-utils';
2
3
  import { BTable } from 'bootstrap-vue';
3
4
  import { logWarning } from '../../../utils/utils';
4
5
  import { waitForAnimationFrame } from '../../../utils/test_utils';
6
+ import Icon from '../icon/icon.vue';
5
7
  import { glTableLiteWarning } from './constants';
6
8
  import Table from './table.vue';
7
9
 
@@ -18,14 +20,15 @@ describe('GlTable', () => {
18
20
  <p>Placeholder empty text</p>`,
19
21
  };
20
22
 
21
- const factory = ({ props = {}, scopedSlots = {} } = {}) => {
22
- wrapper = shallowMount(Table, {
23
+ const factory = ({ mountFn = shallowMount, props = {}, scopedSlots = {} } = {}) => {
24
+ wrapper = mountFn(Table, {
23
25
  scopedSlots,
24
26
  propsData: props,
25
27
  });
26
28
  };
27
29
 
28
30
  const findBTable = () => wrapper.findComponent(BTable);
31
+ const findFirstColHeader = () => findBTable().find('thead').findAll('th').at(0);
29
32
 
30
33
  afterEach(() => {
31
34
  logWarning.mockClear();
@@ -76,4 +79,69 @@ describe('GlTable', () => {
76
79
  expect(wrapper.props('fields')).toEqual(fields);
77
80
  expect(findBTable().props('fields')).toEqual(fields);
78
81
  });
82
+
83
+ describe('sortable columns', () => {
84
+ const field = {
85
+ key: 'name',
86
+ label: 'Name column',
87
+ sortable: true,
88
+ };
89
+
90
+ describe('without custom slots', () => {
91
+ beforeEach(() => {
92
+ factory({ mountFn: mount, props: { fields: [field] } });
93
+ });
94
+
95
+ it('sets the correct column label', () => {
96
+ expect(findFirstColHeader().text()).toMatch(field.label);
97
+ });
98
+
99
+ it('renders the ascending sort icon', async () => {
100
+ findFirstColHeader().trigger('click');
101
+ await nextTick();
102
+
103
+ const icon = findFirstColHeader().findComponent(Icon);
104
+
105
+ expect(icon.exists()).toBe(true);
106
+ expect(icon.props('name')).toBe('arrow-up');
107
+ });
108
+
109
+ it('renders the descending sort icon', async () => {
110
+ findFirstColHeader().trigger('click');
111
+ findFirstColHeader().trigger('click');
112
+ await nextTick();
113
+ const icon = findFirstColHeader().findComponent(Icon);
114
+
115
+ expect(icon.exists()).toBe(true);
116
+ expect(icon.props('name')).toBe('arrow-down');
117
+ });
118
+ });
119
+
120
+ describe('when headers are customized via slots', () => {
121
+ const customSlotContent = 'customSlotContent';
122
+
123
+ beforeEach(() => {
124
+ factory({
125
+ mountFn: mount,
126
+ props: {
127
+ fields: [field],
128
+ },
129
+ scopedSlots: {
130
+ 'head(name)': `<div>${customSlotContent}</div>`,
131
+ },
132
+ });
133
+ });
134
+
135
+ it('renders the ascending sort icon alongside the custom slot content', async () => {
136
+ findFirstColHeader().trigger('click');
137
+ await nextTick();
138
+
139
+ const icon = findFirstColHeader().findComponent(Icon);
140
+
141
+ expect(icon.exists()).toBe(true);
142
+ expect(icon.props('name')).toBe('arrow-up');
143
+ expect(findFirstColHeader().text()).toContain(customSlotContent);
144
+ });
145
+ });
146
+ });
79
147
  });
@@ -44,6 +44,8 @@ export const Default = (args, { argTypes }) => ({
44
44
  :fixed="fixed"
45
45
  :stacked="stacked"
46
46
  :foot-clone="footClone"
47
+ sort-by="col_2"
48
+ sort-desc
47
49
  hover
48
50
  selectable
49
51
  selected-variant="primary"
@@ -58,11 +60,12 @@ export const Default = (args, { argTypes }) => ({
58
60
  key: 'column_one',
59
61
  label: 'Column One',
60
62
  variant: 'secondary',
61
- sortable: false,
63
+ sortable: true,
62
64
  isRowHeader: false,
63
65
  },
64
66
  {
65
67
  key: 'col_2',
68
+ sortable: true,
66
69
  label: 'Column 2',
67
70
  formatter: (value) => value,
68
71
  },
@@ -1,6 +1,7 @@
1
1
  <!-- eslint-disable vue/multi-word-component-names -->
2
2
  <script>
3
3
  import { BTable } from 'bootstrap-vue';
4
+ import GlIcon from '../icon/icon.vue';
4
5
  import { logWarning, isDev } from '../../../utils/utils';
5
6
  import { tableFullSlots, tableFullProps, glTableLiteWarning } from './constants';
6
7
 
@@ -17,6 +18,7 @@ export default {
17
18
  name: 'GlTable',
18
19
  components: {
19
20
  BTable,
21
+ GlIcon,
20
22
  },
21
23
  inheritAttrs: false,
22
24
  props: {
@@ -31,6 +33,22 @@ export default {
31
33
  default: false,
32
34
  required: false,
33
35
  },
36
+ sortBy: {
37
+ type: String,
38
+ required: false,
39
+ default: undefined,
40
+ },
41
+ sortDesc: {
42
+ type: Boolean,
43
+ required: false,
44
+ default: false,
45
+ },
46
+ },
47
+ data() {
48
+ return {
49
+ localSortBy: this.sortBy,
50
+ localSortDesc: this.sortDesc,
51
+ };
34
52
  },
35
53
  computed: {
36
54
  stickyHeaderClass() {
@@ -39,6 +57,12 @@ export default {
39
57
  localTableClass() {
40
58
  return ['gl-table', this.tableClass, this.stickyHeaderClass];
41
59
  },
60
+ headSlots() {
61
+ return [
62
+ 'head()',
63
+ ...Object.keys(this.$scopedSlots).filter((slotName) => slotName.startsWith('head(')),
64
+ ];
65
+ },
42
66
  },
43
67
  mounted() {
44
68
  // logWarning will call isDev before logging any message
@@ -47,13 +71,44 @@ export default {
47
71
  logWarning(glTableLiteWarning, this.$el);
48
72
  }
49
73
  },
74
+ methods: {
75
+ isSortable({ field }) {
76
+ return field?.sortable;
77
+ },
78
+ getSortingIcon({ field }) {
79
+ if (this.localSortBy !== field?.key) {
80
+ return null;
81
+ }
82
+ return this.localSortDesc ? 'arrow-down' : 'arrow-up';
83
+ },
84
+ },
50
85
  };
51
86
  </script>
52
87
 
53
88
  <template>
54
- <b-table :table-class="localTableClass" :fields="fields" v-bind="$attrs" v-on="$listeners">
55
- <template v-for="slot in Object.keys($scopedSlots)" #[slot]="scope">
56
- <slot :name="slot" v-bind="scope"></slot>
89
+ <b-table
90
+ :table-class="localTableClass"
91
+ :fields="fields"
92
+ :sort-by.sync="localSortBy"
93
+ :sort-desc.sync="localSortDesc"
94
+ v-bind="$attrs"
95
+ v-on="$listeners"
96
+ >
97
+ <template v-for="slotName in Object.keys($scopedSlots)" #[slotName]="scope">
98
+ <slot :name="slotName" v-bind="scope"></slot>
99
+ </template>
100
+ <template v-for="headSlotName in headSlots" #[headSlotName]="scope">
101
+ <div :key="headSlotName" class="gl-display-flex gl-align-items-center">
102
+ <slot :name="headSlotName" v-bind="scope">{{ scope.label }}</slot
103
+ ><template v-if="isSortable(scope)">
104
+ <gl-icon
105
+ v-if="getSortingIcon(scope)"
106
+ :name="getSortingIcon(scope)"
107
+ class="gl-ml-3 gl-min-w-5 gl-text-gray-900"
108
+ />
109
+ <div v-else class="gl-display-inline-block gl-w-5 gl-h-5 gl-ml-3"></div>
110
+ </template>
111
+ </div>
57
112
  </template>
58
113
  </b-table>
59
114
  </template>