@gitlab/ui 39.3.0 → 39.4.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 (254) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/dist/components/base/modal/modal.js +14 -2
  3. package/dist/components/base/new_dropdowns/base_dropdown/base_dropdown.js +240 -0
  4. package/dist/components/base/new_dropdowns/constants.js +20 -0
  5. package/dist/components/base/new_dropdowns/listbox/listbox.js +381 -0
  6. package/dist/components/base/new_dropdowns/listbox/listbox_item.js +77 -0
  7. package/dist/index.css +1 -1
  8. package/dist/index.css.map +1 -1
  9. package/dist/index.js +2 -0
  10. package/dist/utility_classes.css +1 -1
  11. package/dist/utility_classes.css.map +1 -1
  12. package/dist/utils/utils.js +24 -1
  13. package/package.json +6 -12
  14. package/scss_to_js/scss_variables.js +1 -0
  15. package/scss_to_js/scss_variables.json +5 -0
  16. package/src/components/base/avatar_labeled/avatar_labeled.stories.js +2 -1
  17. package/src/components/base/avatar_link/avatar_link.stories.js +2 -3
  18. package/src/components/base/breadcrumb/breadcrumb.md +1 -1
  19. package/src/components/base/breadcrumb/breadcrumb.stories.js +2 -1
  20. package/src/components/base/broadcast_message/broadcast_message.scss +1 -1
  21. package/src/components/base/button/button.scss +1 -1
  22. package/src/components/base/dropdown/dropdown.scss +10 -3
  23. package/src/components/base/dropdown/dropdown_item.scss +1 -0
  24. package/src/components/base/modal/modal.spec.js +20 -0
  25. package/src/components/base/modal/modal.vue +14 -1
  26. package/src/components/base/new_dropdowns/base_dropdown/base_dropdown.spec.js +171 -0
  27. package/src/components/base/new_dropdowns/base_dropdown/base_dropdown.vue +221 -0
  28. package/src/components/base/new_dropdowns/constants.js +22 -0
  29. package/src/components/base/new_dropdowns/listbox/listbox.md +71 -0
  30. package/src/components/base/new_dropdowns/listbox/listbox.spec.js +236 -0
  31. package/src/components/base/new_dropdowns/listbox/listbox.stories.js +276 -0
  32. package/src/components/base/new_dropdowns/listbox/listbox.vue +348 -0
  33. package/src/components/base/new_dropdowns/listbox/listbox_item.spec.js +104 -0
  34. package/src/components/base/new_dropdowns/listbox/listbox_item.vue +59 -0
  35. package/src/index.js +4 -0
  36. package/src/scss/utilities.scss +8 -0
  37. package/src/scss/utility-mixins/color.scss +4 -0
  38. package/src/scss/variables.scss +1 -0
  39. package/src/utils/utils.js +18 -0
  40. package/src/utils/utils.spec.js +41 -1
  41. package/dist/components/base/accordion/accordion.documentation.js +0 -8
  42. package/dist/components/base/accordion/accordion_item.documentation.js +0 -7
  43. package/dist/components/base/alert/alert.documentation.js +0 -13
  44. package/dist/components/base/avatar/avatar.documentation.js +0 -8
  45. package/dist/components/base/avatar_labeled/avatar_labeled.documentation.js +0 -8
  46. package/dist/components/base/avatar_link/avatar_link.documentation.js +0 -8
  47. package/dist/components/base/avatars_inline/avatars_inline.documentation.js +0 -13
  48. package/dist/components/base/badge/badge.documentation.js +0 -8
  49. package/dist/components/base/banner/banner.documentation.js +0 -8
  50. package/dist/components/base/breadcrumb/breadcrumb.documentation.js +0 -8
  51. package/dist/components/base/broadcast_message/broadcast_message.documentation.js +0 -8
  52. package/dist/components/base/button/button.documentation.js +0 -24
  53. package/dist/components/base/button_group/button_group.documentation.js +0 -8
  54. package/dist/components/base/card/card.documentation.js +0 -13
  55. package/dist/components/base/carousel/carousel.documentation.js +0 -8
  56. package/dist/components/base/collapse/collapse.documentation.js +0 -7
  57. package/dist/components/base/datepicker/datepicker.documentation.js +0 -7
  58. package/dist/components/base/daterange_picker/daterange_picker.documentation.js +0 -8
  59. package/dist/components/base/drawer/drawer.documentation.js +0 -8
  60. package/dist/components/base/dropdown/dropdown.documentation.js +0 -8
  61. package/dist/components/base/dropdown/dropdown_item.documentation.js +0 -7
  62. package/dist/components/base/filtered_search/filtered_search.documentation.js +0 -13
  63. package/dist/components/base/filtered_search/filtered_search_suggestion.documentation.js +0 -7
  64. package/dist/components/base/filtered_search/filtered_search_suggestion_list.documentation.js +0 -7
  65. package/dist/components/base/filtered_search/filtered_search_term.documentation.js +0 -12
  66. package/dist/components/base/filtered_search/filtered_search_token.documentation.js +0 -12
  67. package/dist/components/base/filtered_search/filtered_search_token_segment.documentation.js +0 -12
  68. package/dist/components/base/form/form.documentation.js +0 -7
  69. package/dist/components/base/form/form_checkbox/form_checkbox.documentation.js +0 -8
  70. package/dist/components/base/form/form_checkbox_tree/form_checkbox_tree.documentation.js +0 -12
  71. package/dist/components/base/form/form_combobox/form_combobox.documentation.js +0 -13
  72. package/dist/components/base/form/form_group/form_group.documentation.js +0 -7
  73. package/dist/components/base/form/form_input/form_input.documentation.js +0 -8
  74. package/dist/components/base/form/form_input_group/form_input_group.documentation.js +0 -8
  75. package/dist/components/base/form/form_radio/form_radio.documentation.js +0 -8
  76. package/dist/components/base/form/form_radio_group/form_radio_group.documentation.js +0 -8
  77. package/dist/components/base/form/form_select/form_select.documentation.js +0 -8
  78. package/dist/components/base/form/form_text/form_text.documentation.js +0 -14
  79. package/dist/components/base/form/form_textarea/form_textarea.documentation.js +0 -7
  80. package/dist/components/base/form/input_group_text/input_group_text.documentation.js +0 -12
  81. package/dist/components/base/icon/icon.documentation.js +0 -8
  82. package/dist/components/base/infinite_scroll/examples/index.js +0 -49
  83. package/dist/components/base/infinite_scroll/examples/infinite_scroll.all_items.example.js +0 -49
  84. package/dist/components/base/infinite_scroll/examples/infinite_scroll.basic.example.js +0 -62
  85. package/dist/components/base/infinite_scroll/examples/infinite_scroll.finite_total_items.example.js +0 -72
  86. package/dist/components/base/infinite_scroll/examples/infinite_scroll.large_fetched_items.example.js +0 -62
  87. package/dist/components/base/infinite_scroll/examples/infinite_scroll.reverse.example.js +0 -63
  88. package/dist/components/base/infinite_scroll/examples/infinite_scroll.small_fetched_items.example.js +0 -62
  89. package/dist/components/base/infinite_scroll/examples/infinite_scroll.two_way.example.js +0 -94
  90. package/dist/components/base/infinite_scroll/infinite_scroll.documentation.js +0 -8
  91. package/dist/components/base/keyset_pagination/keyset_pagination.documentation.js +0 -13
  92. package/dist/components/base/label/label.documentation.js +0 -8
  93. package/dist/components/base/link/link.documentation.js +0 -7
  94. package/dist/components/base/loading_icon/loading_icon.documentation.js +0 -8
  95. package/dist/components/base/markdown/markdown.documentation.js +0 -12
  96. package/dist/components/base/modal/modal.documentation.js +0 -8
  97. package/dist/components/base/nav/nav.documentation.js +0 -12
  98. package/dist/components/base/navbar/navbar.documentation.js +0 -12
  99. package/dist/components/base/paginated_list/paginated_list.documentation.js +0 -7
  100. package/dist/components/base/pagination/pagination.documentation.js +0 -8
  101. package/dist/components/base/path/path.documentation.js +0 -8
  102. package/dist/components/base/popover/popover.documentation.js +0 -5
  103. package/dist/components/base/search_box_by_click/search_box_by_click.documentation.js +0 -8
  104. package/dist/components/base/search_box_by_type/search_box_by_type.documentation.js +0 -8
  105. package/dist/components/base/segmented_control/segmented_control.documentation.js +0 -8
  106. package/dist/components/base/skeleton_loader/skeleton_loader.documentation.js +0 -13
  107. package/dist/components/base/skeleton_loading/skeleton_loading.documentation.js +0 -7
  108. package/dist/components/base/sorting/sorting.documentation.js +0 -7
  109. package/dist/components/base/sorting/sorting_item.documentation.js +0 -8
  110. package/dist/components/base/table/table.documentation.js +0 -8
  111. package/dist/components/base/table_lite/table_lite.documentation.js +0 -13
  112. package/dist/components/base/tabs/tabs/tabs.documentation.js +0 -8
  113. package/dist/components/base/toast/toast.documentation.js +0 -8
  114. package/dist/components/base/toggle/toggle.documentation.js +0 -13
  115. package/dist/components/base/token/token.documentation.js +0 -5
  116. package/dist/components/base/token_selector/token_selector.documentation.js +0 -12
  117. package/dist/components/base/tooltip/tooltip.documentation.js +0 -8
  118. package/dist/components/charts/area/area.documentation.js +0 -5
  119. package/dist/components/charts/bar/bar.documentation.js +0 -8
  120. package/dist/components/charts/chart/chart.documentation.js +0 -7
  121. package/dist/components/charts/column/column.documentation.js +0 -5
  122. package/dist/components/charts/discrete_scatter/discrete_scatter.documentation.js +0 -5
  123. package/dist/components/charts/gauge/gauge.documentation.js +0 -12
  124. package/dist/components/charts/heatmap/heatmap.documentation.js +0 -8
  125. package/dist/components/charts/line/line.documentation.js +0 -8
  126. package/dist/components/charts/series_label/series_label.documentation.js +0 -7
  127. package/dist/components/charts/single_stat/single_stat.documentation.js +0 -7
  128. package/dist/components/charts/sparkline/sparkline.documentation.js +0 -8
  129. package/dist/components/charts/stacked_column/stacked_column.documentation.js +0 -8
  130. package/dist/components/charts/tooltip/tooltip.documentation.js +0 -8
  131. package/dist/components/editors/rich_text_editor/rich_text_editor.documentation.js +0 -12
  132. package/dist/components/regions/dashboard_skeleton/dashboard_skeleton.documentation.js +0 -7
  133. package/dist/components/regions/empty_state/empty_state.documentation.js +0 -7
  134. package/dist/components/utilities/animated_number/animated_number.documentation.js +0 -13
  135. package/dist/components/utilities/friendly_wrap/friendly_wrap.documentation.js +0 -7
  136. package/dist/components/utilities/intersection_observer/intersection_observer.documentation.js +0 -12
  137. package/dist/components/utilities/intersperse/intersperse.documentation.js +0 -8
  138. package/dist/components/utilities/sprintf/sprintf.documentation.js +0 -8
  139. package/dist/components/utilities/truncate/truncate.documentation.js +0 -7
  140. package/dist/directives/hover_load/hover_load.documentation.js +0 -13
  141. package/dist/directives/outside/outside.documentation.js +0 -13
  142. package/dist/directives/resize_observer/resize_observer.documentation.js +0 -8
  143. package/dist/directives/safe_html/safe_html.documentation.js +0 -8
  144. package/dist/directives/safe_link/safe_link.documentation.js +0 -8
  145. package/documentation/all_components.js +0 -8
  146. package/documentation/components/component_documentation_generator.vue +0 -321
  147. package/documentation/components/example_display.vue +0 -231
  148. package/documentation/components/example_explorer.vue +0 -63
  149. package/documentation/components_documentation.js +0 -111
  150. package/documentation/index.js +0 -8
  151. package/src/components/base/accordion/accordion.documentation.js +0 -6
  152. package/src/components/base/accordion/accordion_item.documentation.js +0 -5
  153. package/src/components/base/alert/alert.documentation.js +0 -6
  154. package/src/components/base/avatar/avatar.documentation.js +0 -6
  155. package/src/components/base/avatar_labeled/avatar_labeled.documentation.js +0 -6
  156. package/src/components/base/avatar_link/avatar_link.documentation.js +0 -6
  157. package/src/components/base/avatars_inline/avatars_inline.documentation.js +0 -6
  158. package/src/components/base/badge/badge.documentation.js +0 -6
  159. package/src/components/base/banner/banner.documentation.js +0 -6
  160. package/src/components/base/breadcrumb/breadcrumb.documentation.js +0 -6
  161. package/src/components/base/broadcast_message/broadcast_message.documentation.js +0 -6
  162. package/src/components/base/button/button.documentation.js +0 -24
  163. package/src/components/base/button_group/button_group.documentation.js +0 -6
  164. package/src/components/base/card/card.documentation.js +0 -6
  165. package/src/components/base/carousel/carousel.documentation.js +0 -6
  166. package/src/components/base/collapse/collapse.documentation.js +0 -5
  167. package/src/components/base/datepicker/datepicker.documentation.js +0 -5
  168. package/src/components/base/daterange_picker/daterange_picker.documentation.js +0 -6
  169. package/src/components/base/drawer/drawer.documentation.js +0 -6
  170. package/src/components/base/dropdown/dropdown.documentation.js +0 -6
  171. package/src/components/base/dropdown/dropdown_item.documentation.js +0 -5
  172. package/src/components/base/filtered_search/filtered_search.documentation.js +0 -6
  173. package/src/components/base/filtered_search/filtered_search_suggestion.documentation.js +0 -5
  174. package/src/components/base/filtered_search/filtered_search_suggestion_list.documentation.js +0 -5
  175. package/src/components/base/filtered_search/filtered_search_term.documentation.js +0 -5
  176. package/src/components/base/filtered_search/filtered_search_token.documentation.js +0 -5
  177. package/src/components/base/filtered_search/filtered_search_token_segment.documentation.js +0 -5
  178. package/src/components/base/form/form.documentation.js +0 -5
  179. package/src/components/base/form/form_checkbox/form_checkbox.documentation.js +0 -6
  180. package/src/components/base/form/form_checkbox_tree/form_checkbox_tree.documentation.js +0 -5
  181. package/src/components/base/form/form_combobox/form_combobox.documentation.js +0 -6
  182. package/src/components/base/form/form_group/form_group.documentation.js +0 -5
  183. package/src/components/base/form/form_input/form_input.documentation.js +0 -6
  184. package/src/components/base/form/form_input_group/form_input_group.documentation.js +0 -6
  185. package/src/components/base/form/form_radio/form_radio.documentation.js +0 -6
  186. package/src/components/base/form/form_radio_group/form_radio_group.documentation.js +0 -6
  187. package/src/components/base/form/form_select/form_select.documentation.js +0 -6
  188. package/src/components/base/form/form_text/form_text.documentation.js +0 -7
  189. package/src/components/base/form/form_textarea/form_textarea.documentation.js +0 -5
  190. package/src/components/base/form/input_group_text/input_group_text.documentation.js +0 -5
  191. package/src/components/base/icon/icon.documentation.js +0 -6
  192. package/src/components/base/infinite_scroll/examples/index.js +0 -57
  193. package/src/components/base/infinite_scroll/examples/infinite_scroll.all_items.example.vue +0 -25
  194. package/src/components/base/infinite_scroll/examples/infinite_scroll.basic.example.vue +0 -43
  195. package/src/components/base/infinite_scroll/examples/infinite_scroll.finite_total_items.example.vue +0 -44
  196. package/src/components/base/infinite_scroll/examples/infinite_scroll.large_fetched_items.example.vue +0 -43
  197. package/src/components/base/infinite_scroll/examples/infinite_scroll.reverse.example.vue +0 -46
  198. package/src/components/base/infinite_scroll/examples/infinite_scroll.small_fetched_items.example.vue +0 -43
  199. package/src/components/base/infinite_scroll/examples/infinite_scroll.two_way.example.vue +0 -75
  200. package/src/components/base/infinite_scroll/infinite_scroll.documentation.js +0 -6
  201. package/src/components/base/keyset_pagination/keyset_pagination.documentation.js +0 -6
  202. package/src/components/base/label/label.documentation.js +0 -6
  203. package/src/components/base/link/link.documentation.js +0 -5
  204. package/src/components/base/loading_icon/loading_icon.documentation.js +0 -6
  205. package/src/components/base/markdown/markdown.documentation.js +0 -5
  206. package/src/components/base/modal/modal.documentation.js +0 -6
  207. package/src/components/base/nav/nav.documentation.js +0 -5
  208. package/src/components/base/navbar/navbar.documentation.js +0 -5
  209. package/src/components/base/paginated_list/paginated_list.documentation.js +0 -5
  210. package/src/components/base/pagination/pagination.documentation.js +0 -6
  211. package/src/components/base/path/path.documentation.js +0 -6
  212. package/src/components/base/popover/popover.documentation.js +0 -3
  213. package/src/components/base/search_box_by_click/search_box_by_click.documentation.js +0 -6
  214. package/src/components/base/search_box_by_type/search_box_by_type.documentation.js +0 -6
  215. package/src/components/base/segmented_control/segmented_control.documentation.js +0 -6
  216. package/src/components/base/skeleton_loader/skeleton_loader.documentation.js +0 -6
  217. package/src/components/base/skeleton_loading/skeleton_loading.documentation.js +0 -5
  218. package/src/components/base/sorting/sorting.documentation.js +0 -5
  219. package/src/components/base/sorting/sorting_item.documentation.js +0 -6
  220. package/src/components/base/table/table.documentation.js +0 -6
  221. package/src/components/base/table_lite/table_lite.documentation.js +0 -6
  222. package/src/components/base/tabs/tabs/tabs.documentation.js +0 -6
  223. package/src/components/base/toast/toast.documentation.js +0 -6
  224. package/src/components/base/toggle/toggle.documentation.js +0 -6
  225. package/src/components/base/token/token.documentation.js +0 -3
  226. package/src/components/base/token_selector/token_selector.documentation.js +0 -5
  227. package/src/components/base/tooltip/tooltip.documentation.js +0 -6
  228. package/src/components/charts/area/area.documentation.js +0 -3
  229. package/src/components/charts/bar/bar.documentation.js +0 -6
  230. package/src/components/charts/chart/chart.documentation.js +0 -5
  231. package/src/components/charts/column/column.documentation.js +0 -3
  232. package/src/components/charts/discrete_scatter/discrete_scatter.documentation.js +0 -3
  233. package/src/components/charts/gauge/gauge.documentation.js +0 -5
  234. package/src/components/charts/heatmap/heatmap.documentation.js +0 -6
  235. package/src/components/charts/line/line.documentation.js +0 -6
  236. package/src/components/charts/series_label/series_label.documentation.js +0 -5
  237. package/src/components/charts/single_stat/single_stat.documentation.js +0 -5
  238. package/src/components/charts/sparkline/sparkline.documentation.js +0 -6
  239. package/src/components/charts/stacked_column/stacked_column.documentation.js +0 -6
  240. package/src/components/charts/tooltip/tooltip.documentation.js +0 -6
  241. package/src/components/editors/rich_text_editor/rich_text_editor.documentation.js +0 -5
  242. package/src/components/regions/dashboard_skeleton/dashboard_skeleton.documentation.js +0 -5
  243. package/src/components/regions/empty_state/empty_state.documentation.js +0 -5
  244. package/src/components/utilities/animated_number/animated_number.documentation.js +0 -6
  245. package/src/components/utilities/friendly_wrap/friendly_wrap.documentation.js +0 -5
  246. package/src/components/utilities/intersection_observer/intersection_observer.documentation.js +0 -5
  247. package/src/components/utilities/intersperse/intersperse.documentation.js +0 -6
  248. package/src/components/utilities/sprintf/sprintf.documentation.js +0 -6
  249. package/src/components/utilities/truncate/truncate.documentation.js +0 -5
  250. package/src/directives/hover_load/hover_load.documentation.js +0 -6
  251. package/src/directives/outside/outside.documentation.js +0 -6
  252. package/src/directives/resize_observer/resize_observer.documentation.js +0 -6
  253. package/src/directives/safe_html/safe_html.documentation.js +0 -6
  254. package/src/directives/safe_link/safe_link.documentation.js +0 -6
@@ -0,0 +1,221 @@
1
+ <script>
2
+ import { createPopper } from '@popperjs/core';
3
+ import {
4
+ buttonCategoryOptions,
5
+ buttonSizeOptions,
6
+ dropdownVariantOptions,
7
+ } from '../../../../utils/constants';
8
+ import { POPPER_CONFIG, GL_DROPDOWN_SHOWN, GL_DROPDOWN_HIDDEN } from '../constants';
9
+
10
+ import GlButton from '../../button/button.vue';
11
+ import GlIcon from '../../icon/icon.vue';
12
+ import { OutsideDirective } from '../../../../directives/outside/outside';
13
+
14
+ export default {
15
+ components: {
16
+ GlButton,
17
+ GlIcon,
18
+ },
19
+ directives: { Outside: OutsideDirective },
20
+ props: {
21
+ toggleText: {
22
+ type: String,
23
+ required: false,
24
+ default: '',
25
+ },
26
+ textSrOnly: {
27
+ type: Boolean,
28
+ required: false,
29
+ default: false,
30
+ },
31
+ category: {
32
+ type: String,
33
+ required: false,
34
+ default: buttonCategoryOptions.primary,
35
+ validator: (value) => Object.keys(buttonCategoryOptions).includes(value),
36
+ },
37
+ variant: {
38
+ type: String,
39
+ required: false,
40
+ default: dropdownVariantOptions.default,
41
+ validator: (value) => Object.keys(dropdownVariantOptions).includes(value),
42
+ },
43
+ size: {
44
+ type: String,
45
+ required: false,
46
+ default: buttonSizeOptions.medium,
47
+ validator: (value) => Object.keys(buttonSizeOptions).includes(value),
48
+ },
49
+ icon: {
50
+ type: String,
51
+ required: false,
52
+ default: '',
53
+ },
54
+ disabled: {
55
+ type: Boolean,
56
+ required: false,
57
+ default: false,
58
+ },
59
+ loading: {
60
+ type: Boolean,
61
+ required: false,
62
+ default: false,
63
+ },
64
+ toggleClass: {
65
+ type: [String, Array, Object],
66
+ required: false,
67
+ default: null,
68
+ },
69
+ noCaret: {
70
+ type: Boolean,
71
+ required: false,
72
+ default: false,
73
+ },
74
+ /**
75
+ * Right align dropdown menu with respect to the toggle button
76
+ */
77
+ right: {
78
+ type: Boolean,
79
+ required: false,
80
+ default: false,
81
+ },
82
+ // ARIA props
83
+ ariaHaspopup: {
84
+ type: [String, Boolean],
85
+ required: false,
86
+ default: false,
87
+ validator: (value) => {
88
+ return ['menu', 'listbox', 'tree', 'grid', 'dialog', true, false].includes(value);
89
+ },
90
+ },
91
+ /**
92
+ * Id that will be referenced by `aria-labelledby` attribute of the dropdown content`
93
+ */
94
+ toggleId: {
95
+ type: String,
96
+ required: true,
97
+ },
98
+ /**
99
+ * The `aria-labelledby` attribute value for the toggle `button`
100
+ */
101
+ ariaLabelledby: {
102
+ type: String,
103
+ required: false,
104
+ default: null,
105
+ },
106
+ },
107
+ data() {
108
+ return {
109
+ visible: false,
110
+ };
111
+ },
112
+ computed: {
113
+ isIconOnly() {
114
+ return Boolean(this.icon && (!this.toggleText?.length || this.textSrOnly));
115
+ },
116
+ isIconWithText() {
117
+ return Boolean(this.icon && this.toggleText?.length && !this.textSrOnly);
118
+ },
119
+ toggleButtonClasses() {
120
+ return [
121
+ this.toggleClass,
122
+ {
123
+ 'gl-dropdown-toggle': true,
124
+ 'dropdown-toggle': true,
125
+ 'dropdown-icon-only': this.isIconOnly,
126
+ 'dropdown-icon-text': this.isIconWithText,
127
+ 'dropdown-toggle-no-caret': this.noCaret,
128
+ },
129
+ ];
130
+ },
131
+ toggleLabelledBy() {
132
+ return this.ariaLabelledby ? `${this.ariaLabelledby} ${this.toggleId}` : this.toggleId;
133
+ },
134
+ popperConfig() {
135
+ return {
136
+ placement: this.right ? 'bottom-end' : 'bottom-start',
137
+ ...POPPER_CONFIG,
138
+ };
139
+ },
140
+ },
141
+ updated() {
142
+ if (this.visible) {
143
+ this.popper?.update();
144
+ }
145
+ },
146
+ mounted() {
147
+ this.$nextTick(() => {
148
+ this.popper = createPopper(this.$refs.toggle.$el, this.$refs.content, this.popperConfig);
149
+ });
150
+ },
151
+ beforeDestroy() {
152
+ this.popper.destroy();
153
+ },
154
+ methods: {
155
+ toggle() {
156
+ this.visible = !this.visible;
157
+
158
+ if (this.visible) {
159
+ this.popper.update();
160
+ this.$emit(GL_DROPDOWN_SHOWN);
161
+ } else {
162
+ this.$emit(GL_DROPDOWN_HIDDEN);
163
+ }
164
+ },
165
+ close() {
166
+ if (!this.visible) {
167
+ return;
168
+ }
169
+ this.toggle();
170
+ },
171
+ closeAndFocus() {
172
+ if (!this.visible) {
173
+ return;
174
+ }
175
+ this.toggle();
176
+ this.focusToggle();
177
+ },
178
+ focusToggle() {
179
+ this.$refs.toggle.$el.focus();
180
+ },
181
+ },
182
+ };
183
+ </script>
184
+
185
+ <template>
186
+ <div v-outside="close" class="gl-new-dropdown dropdown btn-group">
187
+ <gl-button
188
+ :id="toggleId"
189
+ ref="toggle"
190
+ data-testid="base-dropdown-toggle"
191
+ :icon="icon"
192
+ :category="category"
193
+ :variant="variant"
194
+ :size="size"
195
+ :disabled="disabled"
196
+ :loading="loading"
197
+ :class="toggleButtonClasses"
198
+ :aria-haspopup="ariaHaspopup"
199
+ :aria-expanded="visible"
200
+ :aria-labelledby="toggleLabelledBy"
201
+ @click="toggle"
202
+ >
203
+ <span class="gl-new-dropdown-button-text" :class="{ 'gl-sr-only': textSrOnly }">
204
+ {{ toggleText }}
205
+ </span>
206
+ <gl-icon v-if="!noCaret" class="gl-button-icon dropdown-chevron" name="chevron-down" />
207
+ </gl-button>
208
+
209
+ <div
210
+ ref="content"
211
+ data-testid="base-dropdown-menu"
212
+ class="dropdown-menu"
213
+ :class="{ show: visible }"
214
+ @keydown.esc.stop.prevent="closeAndFocus"
215
+ >
216
+ <div class="gl-new-dropdown-inner">
217
+ <slot></slot>
218
+ </div>
219
+ </div>
220
+ </div>
221
+ </template>
@@ -0,0 +1,22 @@
1
+ export const POPPER_CONFIG = {
2
+ modifiers: [
3
+ {
4
+ name: 'offset',
5
+ options: {
6
+ offset: [0, 4],
7
+ },
8
+ },
9
+ ],
10
+ };
11
+
12
+ // base dropdown events
13
+ export const GL_DROPDOWN_SHOWN = 'shown';
14
+ export const GL_DROPDOWN_HIDDEN = 'hidden';
15
+
16
+ // KEY Codes
17
+ export const HOME = 'Home';
18
+ export const END = 'End';
19
+ export const ARROW_UP = 'ArrowUp';
20
+ export const ARROW_DOWN = 'ArrowDown';
21
+ export const ENTER = 'Enter';
22
+ export const SPACE = 'Space';
@@ -0,0 +1,71 @@
1
+ A listbox dropdown is a button that toggles a panel containing a list of options.
2
+ Listbox supports single and multi-selection.
3
+
4
+ **Single-select:** By default, selecting an option will update the toggle label with the choice.
5
+ But the custom toggle text can be provided.
6
+ When option is selected, the dropdown will be closed and focus set on the toggle button.
7
+
8
+ **Multi-select:** Selecting an option will not update the toggle, but it can be customized
9
+ providing `toggleText` property. Also, selecting or deselecting an item won't close the dropdown.
10
+
11
+ ### Icon-only listbox
12
+
13
+ Icon-only listboxes must have an accessible name.
14
+ You can provide this with the combination of `toggleText` and `textSrOnly` props.
15
+ For single-select listboxes `toggleText` will be set to the selected item's `text` property value
16
+ by default.
17
+
18
+ Optionally, you can use `no-caret` to remove the caret and `category="tertiary"` to remove the border.
19
+
20
+ ```html
21
+ <gl-listbox
22
+ icon="ellipsis_v"
23
+ toggle-text="More options"
24
+ text-sr-only
25
+ category="tertiary"
26
+ no-caret
27
+ >
28
+ ```
29
+
30
+ ### Opening the listbox
31
+
32
+ Listbox will open on toggle button click (if it was previously closed).
33
+ On open, `GlListbox` will emit the `shown` event.
34
+
35
+ ### Closing the listbox
36
+
37
+ The listbox is closed by any of the following:
38
+
39
+ - pressing <kbd>Esc</kbd>
40
+ - clicking anywhere outside the component
41
+ - selecting an option in single-select mode
42
+
43
+ After closing, `GlListbox` emits a `hidden` event.
44
+
45
+ ### Selecting items
46
+
47
+ Set the `v-model` on the listbox to have 2-way data binding for the selected items in the listbox.
48
+ Alternatively, you can set `selected` property to the array of selected items
49
+ `value` properties (for multi-select) or to the selected item `value` property for a single-select.
50
+ On selection the listbox will emit the `select` event with the selected values.
51
+
52
+ ### Setting listbox options
53
+
54
+ Provide the list of options for the listbox - each item in the array should have `value` property.
55
+ It is used as a primary key.
56
+ To render the default listbox item template, the item should also have `text` property.
57
+ If you want to use custom template for rendering the listbox item, use the `list-item` template.
58
+
59
+ ```html
60
+ <gl-listbox :items="items">
61
+ <template #list-item="{ item }">
62
+ <span class="gl-display-flex gl-align-items-center">
63
+ <gl-avatar :size="32" class-="gl-mr-3"/>
64
+ <span class="gl-display-flex gl-flex-direction-column">
65
+ <span class="gl-font-weight-bold gl-white-space-nowrap">{{ item.text }}</span>
66
+ <span class="gl-text-gray-400"> {{ item.secondaryText }}</span>
67
+ </span>
68
+ </span>
69
+ </template>
70
+ </gl-litsbox>
71
+ ```
@@ -0,0 +1,236 @@
1
+ import { mount } from '@vue/test-utils';
2
+ import { nextTick } from 'vue';
3
+ import GlBaseDropdown from '../base_dropdown/base_dropdown.vue';
4
+ import {
5
+ GL_DROPDOWN_SHOWN,
6
+ GL_DROPDOWN_HIDDEN,
7
+ ARROW_DOWN,
8
+ ARROW_UP,
9
+ HOME,
10
+ END,
11
+ } from '../constants';
12
+ import GlListbox, { ITEM_SELECTOR } from './listbox.vue';
13
+ import GlListboxItem from './listbox_item.vue';
14
+
15
+ const mockItems = [
16
+ {
17
+ value: 'eng',
18
+ text: 'Engineering',
19
+ },
20
+ {
21
+ value: 'sales',
22
+ text: 'Sales',
23
+ },
24
+ {
25
+ value: 'marketing',
26
+ text: 'Marketing',
27
+ },
28
+ ];
29
+
30
+ describe('GlListbox', () => {
31
+ let wrapper;
32
+
33
+ const buildWrapper = (propsData, slots = {}) => {
34
+ wrapper = mount(GlListbox, {
35
+ propsData,
36
+ slots,
37
+ attachTo: document.body,
38
+ });
39
+ };
40
+
41
+ const findBaseDropdown = () => wrapper.findComponent(GlBaseDropdown);
42
+ const findListContainer = () => wrapper.find('[role="listbox"]');
43
+ const findListboxItems = () => wrapper.findAllComponents(GlListboxItem);
44
+ const findListItem = (index) => findListboxItems().at(index).find(ITEM_SELECTOR);
45
+
46
+ describe('toggle text', () => {
47
+ describe.each`
48
+ toggleText | multiple | selected | expectedToggleText
49
+ ${'Toggle caption'} | ${true} | ${[mockItems[0].value]} | ${'Toggle caption'}
50
+ ${''} | ${true} | ${[mockItems[0]].value} | ${''}
51
+ ${''} | ${false} | ${mockItems[0].value} | ${mockItems[0].text}
52
+ ${''} | ${false} | ${''} | ${''}
53
+ `('when listbox', ({ toggleText, multiple, selected, expectedToggleText }) => {
54
+ beforeEach(() => {
55
+ buildWrapper({ items: mockItems, toggleText, multiple, selected });
56
+ });
57
+
58
+ it(`is ${multiple ? 'multi' : 'single'}-select, toggleText is ${
59
+ toggleText.length ? '' : 'not '
60
+ }provided and ${selected ? 'has' : 'does not have'} selected`, () => {
61
+ expect(findBaseDropdown().props('toggleText')).toBe(expectedToggleText);
62
+ });
63
+ });
64
+ });
65
+
66
+ describe('ARIA attributes', () => {
67
+ it('should provide `toggleId` to the base dropdown and reference it in`aria-labelledby` attribute of the list container` ', async () => {
68
+ await buildWrapper();
69
+ expect(findBaseDropdown().props('toggleId')).toBe(
70
+ findListContainer().attributes('aria-labelledby')
71
+ );
72
+ });
73
+ });
74
+
75
+ describe('selecting items', () => {
76
+ describe('multi-select', () => {
77
+ beforeEach(() => {
78
+ buildWrapper({
79
+ multiple: true,
80
+ selected: [mockItems[1].value, mockItems[2].value],
81
+ items: mockItems,
82
+ });
83
+ });
84
+
85
+ it('should render items as selected when `selected` provided ', () => {
86
+ expect(findListboxItems().at(1).props('isSelected')).toBe(true);
87
+ expect(findListboxItems().at(2).props('isSelected')).toBe(true);
88
+ });
89
+
90
+ it('should deselect previously selected', async () => {
91
+ findListboxItems().at(1).vm.$emit('select', false);
92
+ await nextTick();
93
+ expect(wrapper.emitted('select')[0][0]).toEqual([mockItems[2].value]);
94
+ });
95
+
96
+ it('should add to selection', async () => {
97
+ findListboxItems().at(0).vm.$emit('select', true);
98
+ await nextTick();
99
+ expect(wrapper.emitted('select')[0][0]).toEqual(
100
+ expect.arrayContaining(mockItems.map(({ value }) => value))
101
+ );
102
+ });
103
+ });
104
+
105
+ describe('single-select', () => {
106
+ beforeEach(() => {
107
+ buildWrapper({ selected: mockItems[1].value, items: mockItems });
108
+ });
109
+
110
+ it('should throw an error when array of selections is provided', () => {
111
+ expect(() => {
112
+ buildWrapper({
113
+ selected: [mockItems[1].value, mockItems[2].value],
114
+ items: mockItems,
115
+ });
116
+ }).toThrowError('To allow multi-selection, please, set "multiple" property to "true"');
117
+ expect(wrapper).toHaveLoggedVueErrors();
118
+ });
119
+
120
+ it('should render item as selected when `selected` provided ', () => {
121
+ expect(findListboxItems().at(1).props('isSelected')).toBe(true);
122
+ });
123
+
124
+ it('should deselect previously selected and select a new item', async () => {
125
+ findListboxItems().at(2).vm.$emit('select', true);
126
+ await nextTick();
127
+ expect(wrapper.emitted('select')[0][0]).toEqual(mockItems[2].value);
128
+ });
129
+ });
130
+ });
131
+
132
+ describe('onShow', () => {
133
+ beforeEach(async () => {
134
+ buildWrapper({
135
+ multiple: true,
136
+ items: mockItems,
137
+ selected: [mockItems[2].value, mockItems[1].value],
138
+ });
139
+ findBaseDropdown().vm.$emit(GL_DROPDOWN_SHOWN);
140
+ await nextTick();
141
+ });
142
+
143
+ it('should re-emit the event', () => {
144
+ expect(wrapper.emitted(GL_DROPDOWN_SHOWN)).toHaveLength(1);
145
+ });
146
+
147
+ it('should focus the first selected item', () => {
148
+ expect(findListboxItems().at(1).find(ITEM_SELECTOR).element).toHaveFocus();
149
+ });
150
+ });
151
+
152
+ describe('onHide', () => {
153
+ beforeEach(() => {
154
+ buildWrapper();
155
+ findBaseDropdown().vm.$emit(GL_DROPDOWN_HIDDEN);
156
+ });
157
+
158
+ it('should re-emit the event', () => {
159
+ expect(wrapper.emitted(GL_DROPDOWN_HIDDEN)).toHaveLength(1);
160
+ });
161
+ });
162
+
163
+ describe('navigating the items', () => {
164
+ let firstItem;
165
+ let secondItem;
166
+ let thirdItem;
167
+
168
+ beforeEach(() => {
169
+ buildWrapper({ items: mockItems });
170
+ findBaseDropdown().vm.$emit(GL_DROPDOWN_SHOWN);
171
+ firstItem = findListItem(0);
172
+ secondItem = findListItem(1);
173
+ thirdItem = findListItem(2);
174
+ });
175
+
176
+ it('should move the focus down the list of items on `arrow down` and stop on the last item', async () => {
177
+ expect(firstItem.element).toHaveFocus();
178
+ await firstItem.trigger('keydown', { code: ARROW_DOWN });
179
+ expect(secondItem.element).toHaveFocus();
180
+ await secondItem.trigger('keydown', { code: ARROW_DOWN });
181
+ expect(thirdItem.element).toHaveFocus();
182
+ await thirdItem.trigger('keydown', { code: ARROW_DOWN });
183
+ expect(thirdItem.element).toHaveFocus();
184
+ });
185
+
186
+ it('should move the focus up the list of items on `arrow up` and stop on the first item', async () => {
187
+ await firstItem.trigger('keydown', { code: ARROW_DOWN });
188
+ await secondItem.trigger('keydown', { code: ARROW_DOWN });
189
+ expect(thirdItem.element).toHaveFocus();
190
+ await thirdItem.trigger('keydown', { code: ARROW_UP });
191
+ expect(secondItem.element).toHaveFocus();
192
+ await secondItem.trigger('keydown', { code: ARROW_UP });
193
+ expect(firstItem.element).toHaveFocus();
194
+ await firstItem.trigger('keydown', { code: ARROW_UP });
195
+ expect(firstItem.element).toHaveFocus();
196
+ });
197
+
198
+ it('should move focus to the last item on `END` keydown', async () => {
199
+ expect(firstItem.element).toHaveFocus();
200
+ await firstItem.trigger('keydown', { code: END });
201
+ expect(thirdItem.element).toHaveFocus();
202
+ await thirdItem.trigger('keydown', { code: END });
203
+ expect(thirdItem.element).toHaveFocus();
204
+ });
205
+
206
+ it('should move focus to the first item on `HOME` keydown', async () => {
207
+ await firstItem.trigger('keydown', { code: ARROW_DOWN });
208
+ await secondItem.trigger('keydown', { code: ARROW_DOWN });
209
+ expect(thirdItem.element).toHaveFocus();
210
+ await thirdItem.trigger('keydown', { code: HOME });
211
+ expect(firstItem.element).toHaveFocus();
212
+ await thirdItem.trigger('keydown', { code: HOME });
213
+ expect(firstItem.element).toHaveFocus();
214
+ });
215
+ });
216
+
217
+ describe('when the header slot content is provided', () => {
218
+ const headerContent = 'Header Content';
219
+ const slots = { header: headerContent };
220
+
221
+ it('renders it', () => {
222
+ buildWrapper({}, slots);
223
+ expect(wrapper.text()).toContain(headerContent);
224
+ });
225
+ });
226
+
227
+ describe('when the footer slot content is provided', () => {
228
+ const footerContent = 'Footer Content';
229
+ const slots = { footer: footerContent };
230
+
231
+ it('renders it', () => {
232
+ buildWrapper({}, slots);
233
+ expect(wrapper.text()).toContain(footerContent);
234
+ });
235
+ });
236
+ });