@gitlab/ui 86.5.0 → 86.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gitlab/ui",
3
- "version": "86.5.0",
3
+ "version": "86.6.0",
4
4
  "description": "GitLab UI Components",
5
5
  "license": "MIT",
6
6
  "main": "dist/index.js",
@@ -66,14 +66,24 @@ $badge-min-width: $gl-spacing-scale-3;
66
66
  /* Basic badge styles */
67
67
 
68
68
  .gl-badge {
69
- @include gl-display-inline-flex;
70
- @include gl-align-items-center;
71
- @include gl-justify-content-center;
72
69
  @include gl-font-sm;
73
70
  @include gl-font-weight-normal;
74
71
  @include gl-line-height-normal;
72
+
73
+ /*
74
+ CSS Grid is used here to deal with 3 cases:
75
+ * Badge shouldn't shrink inside flex containers by default
76
+ * Content inside the badge should shrink when gl-text-truncate class is used on the contents
77
+ * Badge should have a minimal width of 20 pixels (for example when `1` is passed inside the slot)
78
+ */
79
+ display: inline-grid;
80
+ grid-auto-flow: column;
81
+ grid-template-columns: minmax($badge-min-width, auto);
82
+ align-items: center;
83
+ justify-content: center;
75
84
  gap: $gl-spacing-scale-2;
76
85
  padding: $gl-spacing-scale-1 $badge-padding-horizontal;
86
+ min-width: fit-content;
77
87
 
78
88
  @media (forced-colors: active) {
79
89
  border: 1px solid;
@@ -85,10 +95,6 @@ $badge-min-width: $gl-spacing-scale-3;
85
95
  @include gl-flex-shrink-0;
86
96
  top: auto;
87
97
  }
88
-
89
- .gl-badge-content {
90
- min-width: $badge-min-width;
91
- }
92
98
  }
93
99
 
94
100
  /* Variants */
@@ -83,10 +83,6 @@ export default {
83
83
  :class="{ '-gl-ml-2 gl-ml-n2': isCircularIcon }"
84
84
  :name="icon"
85
85
  />
86
- <!-- eslint-disable-next-line @gitlab/vue-prefer-dollar-scopedslots -->
87
- <span v-if="$slots.default" class="gl-badge-content">
88
- <!-- @slot The badge content to display. -->
89
- <slot></slot>
90
- </span>
86
+ <slot></slot>
91
87
  </b-badge>
92
88
  </template>
@@ -1,20 +1,29 @@
1
1
  /**
2
- * Map<HTMLElement, Function>
2
+ * Map<HTMLElement, { callback: Function, eventTypes: Array<string> }>
3
3
  */
4
4
  const callbacks = new Map();
5
+ const click = 'click';
6
+ const focusin = 'focusin';
7
+ const supportedEventTypes = [click, focusin];
8
+ const defaultEventType = click;
5
9
 
6
10
  /**
7
- * Is a global listener already set up?
11
+ * A Set to keep track of currently active event types.
12
+ * This ensures that event listeners are only added for the event types that are in use.
13
+ *
14
+ * @type {Set<string>}
8
15
  */
9
- let listening = false;
16
+ const activeEventTypes = new Set();
10
17
  let lastMousedown = null;
11
18
 
12
19
  const globalListener = (event) => {
13
- callbacks.forEach((callback, element) => {
14
- const originalEvent = lastMousedown || event;
20
+ callbacks.forEach(({ callback, eventTypes }, element) => {
21
+ const originalEvent = event.type === click ? lastMousedown || event : event;
15
22
  if (
16
23
  // Ignore events that aren't targeted outside the element
17
- element.contains(originalEvent.target)
24
+ element.contains(originalEvent.target) ||
25
+ // Ignore events that aren't the specified types for this element
26
+ !eventTypes.includes(event.type)
18
27
  ) {
19
28
  return;
20
29
  }
@@ -28,7 +37,9 @@ const globalListener = (event) => {
28
37
  }
29
38
  }
30
39
  });
31
- lastMousedown = null;
40
+ if (event.type === click) {
41
+ lastMousedown = null;
42
+ }
32
43
  };
33
44
 
34
45
  // We need to listen for mouse events because text selection fires click event only when selection ends.
@@ -38,41 +49,73 @@ const onMousedown = (event) => {
38
49
  lastMousedown = event;
39
50
  };
40
51
 
41
- const startListening = () => {
42
- if (listening) {
43
- return;
44
- }
52
+ const startListening = (eventTypes) => {
53
+ eventTypes.forEach((eventType) => {
54
+ if (!activeEventTypes.has(eventType)) {
55
+ // Listening to mousedown events, ensures that a text selection doesn't trigger the
56
+ // GlOutsideDirective 'click' callback if the selection started within the target element.
57
+ if (eventType === click) {
58
+ document.addEventListener('mousedown', onMousedown);
59
+ }
60
+
61
+ // Added { capture: true } to all event types to prevent the behavior discussed in https://gitlab.com/gitlab-org/gitlab-ui/-/merge_requests/1686#note_412545027
62
+ // Ensures the event listener handles the event in the capturing phase, avoiding issues encountered previously.
63
+ // Cannot be tested with Jest or Cypress, but can be tested with Playwright in the future: https://gitlab.com/gitlab-org/gitlab-ui/-/merge_requests/4272#note_1947425384
64
+ document.addEventListener(eventType, globalListener, { capture: true });
65
+ activeEventTypes.add(eventType);
66
+ }
67
+ });
45
68
 
46
- document.addEventListener('mousedown', onMousedown);
47
- // Added { capture: true } to prevent the behavior discussed in https://gitlab.com/gitlab-org/gitlab-ui/-/merge_requests/1686#note_412545027
48
- // Ensures the event listener handles the event in the capturing phase, avoiding issues encountered previously.
49
- // Cannot be tested with Jest or Cypress, but can be tested with Playwright in the future: https://gitlab.com/gitlab-org/gitlab-ui/-/merge_requests/4272#note_1947425384
50
- document.addEventListener('click', globalListener, { capture: true });
51
- listening = true;
52
69
  lastMousedown = null;
53
70
  };
54
71
 
55
- const stopListening = () => {
56
- if (!listening) {
57
- return;
58
- }
72
+ const stopListening = (eventTypesToUnbind) => {
73
+ eventTypesToUnbind.forEach((eventType) => {
74
+ if (activeEventTypes.has(eventType)) {
75
+ if ([...callbacks.values()].every(({ eventTypes }) => !eventTypes.includes(eventType))) {
76
+ document.removeEventListener(eventType, globalListener);
77
+ activeEventTypes.delete(eventType);
78
+ }
79
+ }
80
+ });
59
81
 
60
- document.removeEventListener('mousedown', onMousedown);
61
- document.removeEventListener('click', globalListener);
62
- listening = false;
82
+ if (eventTypesToUnbind.includes(click) && !activeEventTypes.has(click)) {
83
+ document.removeEventListener('mousedown', onMousedown);
84
+ }
63
85
  };
64
86
 
65
- const bind = (el, { value, arg = 'click' }) => {
66
- if (typeof value !== 'function') {
67
- throw new Error(`[GlOutsideDirective] Value must be a function; got ${typeof value}!`);
68
- }
87
+ function parseBinding({ arg, value, modifiers }) {
88
+ const modifiersList = Object.keys(modifiers);
89
+
90
+ if (process.env.NODE_ENV !== 'production') {
91
+ if (typeof value !== 'function') {
92
+ throw new Error(`[GlOutsideDirective] Value must be a function; got ${typeof value}!`);
93
+ }
94
+
95
+ if (typeof arg !== 'undefined') {
96
+ throw new Error(
97
+ `[GlOutsideDirective] Arguments are not supported. Consider using modifiers instead.`
98
+ );
99
+ }
69
100
 
70
- if (arg !== 'click') {
71
- throw new Error(
72
- `[GlOutsideDirective] Cannot bind ${arg} events; only click events are currently supported!`
73
- );
101
+ if (modifiersList.some((modifier) => !supportedEventTypes.includes(modifier))) {
102
+ throw new Error(
103
+ `[GlOutsideDirective] Cannot bind ${modifiersList} events; supported event types are: ${supportedEventTypes.join(
104
+ ', '
105
+ )}`
106
+ );
107
+ }
74
108
  }
75
109
 
110
+ return {
111
+ callback: value,
112
+ eventTypes: modifiersList.length > 0 ? modifiersList : [defaultEventType],
113
+ };
114
+ }
115
+
116
+ const bind = (el, bindings) => {
117
+ const { callback, eventTypes } = parseBinding(bindings);
118
+
76
119
  if (callbacks.has(el)) {
77
120
  // This element is already bound. This is possible if two components, which
78
121
  // share the same root node, (i.e., one is a higher-order component
@@ -86,18 +129,15 @@ const bind = (el, { value, arg = 'click' }) => {
86
129
  return;
87
130
  }
88
131
 
89
- if (!listening) {
90
- startListening();
91
- }
92
-
93
- callbacks.set(el, value);
132
+ callbacks.set(el, { callback, eventTypes });
133
+ startListening(eventTypes);
94
134
  };
95
135
 
96
136
  const unbind = (el) => {
97
- callbacks.delete(el);
98
-
99
- if (callbacks.size === 0) {
100
- stopListening();
137
+ const entry = callbacks.get(el);
138
+ if (entry) {
139
+ callbacks.delete(el);
140
+ stopListening(entry.eventTypes);
101
141
  }
102
142
  };
103
143
 
@@ -1,8 +1,14 @@
1
- A Vue Directive to call a callback when a click occurs *outside* of the element
2
- the directive is bound to. Any clicks on the element or any descendant elements are ignored.
1
+ A Vue Directive to call a callback when a supported event type occurs *outside* of the element
2
+ the directive is bound to. Any events on the element or any descendant elements are ignored.
3
+ The directive supports the event types `click` and `focusin` and can be configured in several ways.
4
+ If no event type is set, `click` is the default.
3
5
 
4
6
  ## Usage
5
7
 
8
+ ### Default
9
+
10
+ The following example listens for click events outside the specified element:
11
+
6
12
  ```html
7
13
  <script>
8
14
  import { GlOutsideDirective as Outside } from '@gitlab/ui';
@@ -22,6 +28,51 @@ export default {
22
28
  </template>
23
29
  ```
24
30
 
31
+ ### When binding another event type than `click`
32
+
33
+ You can specify event types as modifiers. The following example listens for `focusin` events,
34
+ but not for `click`. With this implementation:
35
+
36
+ ```html
37
+ <script>
38
+ export default {
39
+ methods: {
40
+ onFocusin(event) {
41
+ console.log('User set the focus somewhere outside of this component', event);
42
+ }
43
+ }
44
+ };
45
+ </script>
46
+
47
+ <template>
48
+ <div v-outside.focusin="onFocusin">...</div>
49
+ </template>
50
+ ```
51
+
52
+ ### When binding multiple event types
53
+
54
+ You can specify multiple event types by providing multiple modifiers. The following example
55
+ listens for `click` and `focusin` events:
56
+
57
+ ```html
58
+ <script>
59
+ export default {
60
+ methods: {
61
+ onEvent(event) {
62
+ console.log('Event occurred outside the element:', event);
63
+ }
64
+ }
65
+ };
66
+ </script>
67
+
68
+ <template>
69
+ <div v-outside.click.focusin="onEvent">...</div>
70
+ </template>
71
+ ```
72
+
73
+ 💡 The callback function receives the `event` as a parameter. You can use the `event.type`
74
+ property to execute different code paths depending on which event triggered the callback.
75
+
25
76
  ### When handler expects arguments
26
77
 
27
78
  In case a click handler expects an arument to be passed, simple `v-outside="onClick('foo')"` will
@@ -36,19 +87,54 @@ import { GlOutsideDirective as Outside } from '@gitlab/ui';
36
87
  export default {
37
88
  directives: { Outside },
38
89
  methods: {
39
- onClick(foo) {
40
- // This
90
+ onClick(event, foo) {
91
+ console.log('Event occurred outside the element:', event);
92
+ console.log('An argument was passed along:', foo);
41
93
  },
42
94
  },
43
95
  };
44
96
  </script>
45
97
 
46
98
  <template>
47
- <div v-outside="() => onClick('foo')">Click anywhere but here</div>
99
+ <div v-outside="(event) => onClick(event, 'foo')">Click anywhere but here</div>
48
100
  </template>
49
101
  ```
50
102
 
51
103
  ## Caveats
52
104
 
53
- - Clicks cannot be detected across document boundaries (e.g., across an
105
+ * Clicks cannot be detected across document boundaries (e.g., across an
54
106
  `iframe` boundary), in either direction.
107
+ * Clicks on focusable elements, such as buttons or input fields, will fire both
108
+ `click` and `focusin` events. When both event types are registered,
109
+ the callback will be executed twice. To prevent executing the same code twice
110
+ after only one user interaction, use a flag in the callback to stop its
111
+ execution. Example:
112
+
113
+ ```html
114
+ <script>
115
+ export default {
116
+ data: () => ({
117
+ isOpen: false,
118
+ }),
119
+ methods: {
120
+ openDropdown() {
121
+ this.isOpen = true;
122
+ },
123
+ closeDropdown() {
124
+ if(!this.isOpen) {
125
+ return
126
+ }
127
+
128
+ // more code
129
+
130
+ this.isOpen = false;
131
+ }
132
+ }
133
+ };
134
+ </script>
135
+
136
+ <template>
137
+ <button type="button" @click="openDropdown">Open</button>
138
+ <div v-outside.click.focusin="closeDropdown">...</div>
139
+ </template>
140
+ ```