wave-ui 2.28.1 → 2.31.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.
@@ -1,19 +1,14 @@
1
1
  <template lang="pug">
2
- .w-tooltip-wrap(ref="wrapper" :class="{ 'w-tooltip-wrap--attached': !detachTo }")
3
- slot(name="activator" :on="eventHandlers")
2
+ .w-tooltip-wrap
3
+ slot(name="activator" :on="activatorEventHandlers")
4
4
  transition(:name="transitionName" appear)
5
- //- In Vue 3, a ref in a transition doesn't stay in $refs, it must be set as a function.
6
5
  .w-tooltip(
7
- :ref="el => tooltipEl = el"
6
+ v-if="detachableVisible"
7
+ ref="detachable"
8
8
  :key="_.uid"
9
- v-show="showTooltip"
10
9
  :class="classes"
11
10
  :style="styles")
12
- //- When there is a bg color, another div wrapper is needed for the triangle
13
- //- to inherit the current color.
14
- div(v-if="bgColor" :class="color")
15
- slot
16
- slot(v-else)
11
+ slot
17
12
  </template>
18
13
 
19
14
  <script>
@@ -27,12 +22,13 @@
27
22
  */
28
23
 
29
24
  import { objectifyClasses } from '../utils/index'
30
- import { consoleWarn } from '../utils/console'
25
+ import DetachableMixin from '../mixins/detachable'
31
26
 
32
- const marginFromWindowSide = 4 // Amount of px from a window side, instead of overflowing.
27
+ // const marginFromWindowSide = 4 // Amount of px from a window side, instead of overflowing.
33
28
 
34
29
  export default {
35
30
  name: 'w-tooltip',
31
+ mixins: [DetachableMixin],
36
32
 
37
33
  props: {
38
34
  modelValue: {},
@@ -45,33 +41,37 @@ export default {
45
41
  round: { type: Boolean },
46
42
  transition: { type: String },
47
43
  tooltipClass: { type: [String, Object, Array] },
48
- // Position.
49
- detachTo: {},
50
- fixed: { type: Boolean },
51
- top: { type: Boolean },
52
- bottom: { type: Boolean },
53
- left: { type: Boolean },
54
- right: { type: Boolean },
55
- zIndex: { type: [Number, String, Boolean] }
44
+ persistent: { type: Boolean },
45
+ delay: { type: Number }
46
+ // Other props in the detachable mixin:
47
+ // detachTo, appendTo, fixed, top, bottom, left, right, alignTop, alignBottom, alignLeft,
48
+ // alignRight, noPosition, zIndex, activator.
56
49
  },
57
50
 
58
51
  emits: ['input', 'update:modelValue', 'open', 'close'],
59
52
 
60
53
  data: () => ({
61
- showTooltip: false,
62
- // The activator coordinates.
63
- coordinates: {
54
+ detachableVisible: false,
55
+ hoveringActivator: false,
56
+ // The tooltip computed top & left coordinates.
57
+ detachableCoords: {
64
58
  top: 0,
65
- left: 0,
66
- width: 0,
67
- height: 0
59
+ left: 0
68
60
  },
69
- activatorEl: null,
70
- tooltipEl: null,
61
+ detachableEl: null,
71
62
  timeoutId: null
72
63
  }),
73
64
 
74
65
  computed: {
66
+ /**
67
+ * Other computed in the detachable mixin:
68
+ * - `appendToTarget`
69
+ * - `detachableParentEl`
70
+ * - `activatorEl`
71
+ * - `position`
72
+ * - `alignment`
73
+ **/
74
+
75
75
  tooltipClasses () {
76
76
  return objectifyClasses(this.tooltipClass)
77
77
  },
@@ -81,114 +81,71 @@ export default {
81
81
  return this.transition || `w-tooltip-slide-fade-${direction}`
82
82
  },
83
83
 
84
- detachToTarget () {
85
- let target = this.detachTo || '.w-app'
86
- if (target === true) target = '.w-app'
87
- else if (target && !['object', 'string'].includes(typeof target)) target = '.w-app'
88
- else if (typeof target === 'object' && !target.nodeType) {
89
- target = '.w-app'
90
- consoleWarn('Invalid node provided in w-tooltip `attach-to`. Falling back to .w-app.', this)
91
- }
92
- if (typeof target === 'string') target = document.querySelector(target)
93
-
94
- if (!target) {
95
- consoleWarn(`Unable to locate ${this.detachTo ? `target ${this.detachTo}` : '.w-app'}`, this)
96
- target = document.querySelector('.w-app')
97
- }
98
-
99
- return target
100
- },
101
-
102
- // DOM element that will receive the tooltip.
103
- tooltipParentEl () {
104
- return this.detachTo ? this.detachToTarget : this.$refs.wrapper
105
- },
106
-
107
- position () {
108
- return (
109
- (this.top && 'top') ||
110
- (this.bottom && 'bottom') ||
111
- (this.left && 'left') ||
112
- (this.right && 'right') ||
113
- 'bottom'
114
- )
115
- },
116
-
117
- tooltipCoordinates () {
118
- const coords = {}
119
- const { top, left, width, height } = this.coordinates
120
-
121
- switch (this.position) {
122
- case 'top': {
123
- coords.top = top
124
- coords.left = left + width / 2 // left: 50%.
125
- break
126
- }
127
- case 'bottom': {
128
- coords.top = top + height
129
- coords.left = left + width / 2 // left: 50%.
130
- break
131
- }
132
- case 'left': {
133
- coords.top = top + height / 2 // top: 50%.
134
- coords.left = left
135
- break
136
- }
137
- case 'right': {
138
- coords.top = top + height / 2 // top: 50%.
139
- coords.left = left + width
140
- break
141
- }
142
- }
143
-
144
- return coords
145
- },
146
-
147
84
  classes () {
148
85
  return {
149
- [this.color]: !this.bgColor,
150
- [`${this.bgColor} ${this.bgColor}--bg`]: this.bgColor,
86
+ [this.color]: this.color,
87
+ [`${this.bgColor}--bg`]: this.bgColor,
151
88
  ...this.tooltipClasses,
152
- [`w-tooltip--${this.position}`]: true,
89
+ [`w-tooltip--${this.position}`]: !this.noPosition,
90
+ [`w-tooltip--align-${this.alignment}`]: !this.noPosition && this.alignment,
153
91
  'w-tooltip--tile': this.tile,
154
92
  'w-tooltip--round': this.round,
155
93
  'w-tooltip--shadow': this.shadow,
156
94
  'w-tooltip--fixed': this.fixed,
157
- 'w-tooltip--active': this.showTooltip,
158
95
  'w-tooltip--no-border': this.noBorder || this.bgColor,
159
96
  'w-tooltip--custom-transition': this.transition
160
97
  }
161
98
  },
162
99
 
100
+ // The tooltip styles.
163
101
  styles () {
164
102
  return {
165
103
  zIndex: this.zIndex || this.zIndex === 0 || null,
166
- top: `${~~this.tooltipCoordinates.top}px`,
167
- left: `${~~this.tooltipCoordinates.left}px`
104
+ top: (this.detachableCoords.top && `${~~this.detachableCoords.top}px`) || null,
105
+ left: (this.detachableCoords.left && `${~~this.detachableCoords.left}px`) || null,
106
+ '--w-tooltip-bg-color': this.$waveui.colors[this.bgColor || 'white']
168
107
  }
169
108
  },
170
109
 
171
- eventHandlers () {
110
+ activatorEventHandlers () {
172
111
  let handlers = {}
173
112
  if (this.showOnClick) handlers = { click: this.toggle }
174
113
  else {
175
114
  handlers = {
176
115
  focus: this.toggle,
177
116
  blur: this.toggle,
178
- mouseenter: this.toggle,
179
- mouseleave: this.toggle
117
+ mouseenter: e => {
118
+ this.hoveringActivator = true
119
+ this.open(e)
120
+ },
121
+ mouseleave: e => {
122
+ this.hoveringActivator = false
123
+ this.close()
124
+ }
180
125
  }
181
126
 
182
- if ('ontouchstart' in window) handlers.click = this.toggle
127
+ // Check the window exists: SSR-proof.
128
+ if (typeof window !== 'undefined' && 'ontouchstart' in window) handlers.click = this.toggle
183
129
  }
184
130
  return handlers
185
131
  }
186
132
  },
187
133
 
188
134
  methods: {
135
+ /**
136
+ * Other methods in the `detachable` mixin:
137
+ * - `getActivatorCoordinates`
138
+ * - `computeDetachableCoords`
139
+ * - `onResize`
140
+ * - `onOutsideMousedown`
141
+ * - `insertInDOM`
142
+ * - `removeFromDOM`
143
+ **/
144
+
145
+ // ! \ This function uses the DOM - NO SSR (only trigger from beforeMount and later).
189
146
  toggle (e) {
190
- let shouldShowTooltip = this.showTooltip
191
- if ('ontouchstart' in window) {
147
+ let shouldShowTooltip = this.detachableVisible
148
+ if (typeof window !== 'undefined' && 'ontouchstart' in window) {
192
149
  if (e.type === 'click') shouldShowTooltip = !shouldShowTooltip
193
150
  }
194
151
  else if (e.type === 'click' && this.showOnClick) shouldShowTooltip = !shouldShowTooltip
@@ -196,130 +153,77 @@ export default {
196
153
  else if (['mouseleave', 'blur'].includes(e.type) && !this.showOnClick) shouldShowTooltip = false
197
154
 
198
155
  this.timeoutId = clearTimeout(this.timeoutId)
199
- if (shouldShowTooltip) {
200
- this.coordinates = this.getCoordinates(e)
201
- // In `getCoordinates` accessing the tooltip computed styles takes a few ms (less than 10ms),
202
- // if we don't postpone the tooltip apparition it will start transition from a visible tooltip and
203
- // thus will not transition.
204
- this.timeoutId = setTimeout(() => {
205
- this.showTooltip = true
206
- this.$emit('update:modelValue', true)
207
- this.$emit('input', true)
208
- this.$emit('open')
209
- }, 10)
210
- }
211
- else {
212
- this.showTooltip = false
213
- this.$emit('update:modelValue', false)
214
- this.$emit('input', false)
215
- this.$emit('close')
216
- }
156
+ if (shouldShowTooltip) this.open(e)
157
+ else this.close()
217
158
  },
218
159
 
219
- getCoordinates () {
220
- const { top, left, width, height } = this.activatorEl.getBoundingClientRect()
221
- let coords = { top, left, width, height }
160
+ // ! \ This function uses the DOM - NO SSR (only trigger from beforeMount and later).
161
+ async open (e) {
162
+ // A tiny delay may help positioning the detachable correctly in case of multiple activators
163
+ // with different menu contents.
164
+ if (this.delay) await new Promise(resolve => setTimeout(resolve, this.delay))
222
165
 
223
- if (!this.fixed) {
224
- const { top: targetTop, left: targetLeft } = this.tooltipParentEl.getBoundingClientRect()
225
- coords = { ...coords, top: top - targetTop, left: left - targetLeft }
226
- }
166
+ this.detachableVisible = true
227
167
 
228
- const tooltip = this.tooltipEl
229
-
230
- // 1. First display the tooltip but hide it (So we can get its dimension).
231
- tooltip.style.visibility = 'hidden'
232
- tooltip.style.display = 'table'
233
- const computedStyles = window.getComputedStyle(tooltip, null)
234
-
235
- // Keep fully in viewport.
236
- // --------------------------------------------------
237
- if (this.position === 'top' && ((top - tooltip.offsetHeight) < 0)) {
238
- const margin = -parseInt(computedStyles.getPropertyValue('margin-top'))
239
- coords.top -= top - tooltip.offsetHeight - margin - marginFromWindowSide
240
- }
241
- else if (this.position === 'left' && left - tooltip.offsetWidth < 0) {
242
- const margin = -parseInt(computedStyles.getPropertyValue('margin-left'))
243
- coords.left -= left - tooltip.offsetWidth - margin - marginFromWindowSide
244
- }
245
- else if (this.position === 'right' && left + width + tooltip.offsetWidth > window.innerWidth) {
246
- const margin = parseInt(computedStyles.getPropertyValue('margin-left'))
247
- coords.left -= left + width + tooltip.offsetWidth - window.innerWidth + margin + marginFromWindowSide
248
- }
249
- else if (this.position === 'bottom' && top + height + tooltip.offsetHeight > window.innerHeight) {
250
- const margin = parseInt(computedStyles.getPropertyValue('margin-top'))
251
- coords.top -= top + height + tooltip.offsetHeight - window.innerHeight + margin + marginFromWindowSide
252
- }
253
- // --------------------------------------------------
254
-
255
- // 2. Update left & top if there is a custom transition.
256
- // Tooltip position relies on transform translate, the custom animation may override the transform
257
- // property so do without it and subtract half width or height manually.
258
- if (this.transition) {
259
- // If tooltip is on top or bottom.
260
- if (['top', 'bottom'].includes(this.position)) coords.left -= tooltip.offsetWidth / 2
261
- // If tooltip is on left or right.
262
- if (['left', 'right'].includes(this.position)) coords.top -= tooltip.offsetHeight / 2
263
-
264
- if (this.position === 'left') coords.left -= tooltip.offsetWidth
265
- if (this.position === 'top') coords.top -= tooltip.offsetHeight
266
- }
168
+ // If the activator is external, there might be multiple,
169
+ // so on open, the activator will be set to the event target.
170
+ if (this.activator) this.activatorEl = e.target
267
171
 
268
- // 3. Hide the tooltip again so the transition happens correctly.
269
- tooltip.style.visibility = null
270
- tooltip.style.display = 'none'
172
+ await this.insertInDOM()
271
173
 
272
- return coords
273
- },
174
+ if (this.minWidth === 'activator') this.activatorWidth = this.activatorEl.offsetWidth
274
175
 
275
- insertTooltip () {
276
- const wrapper = this.$refs.wrapper
176
+ if (!this.noPosition) this.computeDetachableCoords(e)
277
177
 
278
- // Unwrap the activator element.
279
- wrapper.parentNode.insertBefore(this.activatorEl, wrapper)
178
+ // In `getActivatorCoordinates` accessing the tooltip computed styles takes a few ms (less than 10ms),
179
+ // if we don't postpone the Tooltip apparition it will start transition from a visible tooltip and
180
+ // thus will not transition.
181
+ this.timeoutId = setTimeout(() => {
182
+ this.$emit('update:modelValue', true)
183
+ this.$emit('input', true)
184
+ this.$emit('open')
185
+ }, 0)
280
186
 
281
- // Move the tooltip elsewhere in the DOM.
282
- // wrapper.parentNode.insertBefore(this.tooltipEl, wrapper)
283
- // this.tooltipEl is set in the dynamic ref.
284
- this.detachToTarget.appendChild(this.tooltipEl)
187
+ if (!this.persistent) document.addEventListener('mousedown', this.onOutsideMousedown)
188
+ if (!this.noPosition) window.addEventListener('resize', this.onResize)
285
189
  },
286
190
 
287
- removeTooltip () {
288
- if (this.tooltipEl && this.tooltipEl.parentNode) this.tooltipEl.remove()
289
- }
290
- },
291
-
292
- mounted () {
293
- this.activatorEl = this.$refs.wrapper.firstElementChild
294
- if (this.detachTo) this.insertTooltip()
295
-
296
- if (this.modelValue) this.toggle({ type: 'click', target: this.activatorEl })
297
- },
298
-
299
- beforeUnmount () {
300
- this.removeTooltip()
301
-
302
- if (this.activatorEl && this.activatorEl.parentNode) this.activatorEl.remove()
303
- },
191
+ /**
192
+ * Closes the tooltip. Can happen on:
193
+ * - click of activator
194
+ * - hover outside if showOnHover
195
+ * - click inside tooltip if hideOnTooltipClick.
196
+ * / ! \ This function uses the DOM - NO SSR (only trigger from beforeMount and later).
197
+ *
198
+ * @param {Boolean} force when showOnHover is set to true, hovering tooltip should keep it open.
199
+ * But if hideOnTooltipClick is also set to true, this should force close
200
+ * even while hovering the tooltip.
201
+ */
202
+ async close (force = false) {
203
+ // Might be already closed.
204
+ // E.g. showOnHover & hideOnTooltipClick: on click, force hide then mouseleave is also firing.
205
+ if (!this.detachableVisible) return
206
+
207
+ if (this.showOnHover && !force) {
208
+ await new Promise(resolve => setTimeout(resolve, 10))
209
+ if (this.showOnHover && this.hoveringActivator) return
210
+ }
304
211
 
305
- watch: {
306
- modelValue (bool) {
307
- if (bool !== this.showTooltip) this.toggle({ type: 'click', target: this.activatorEl })
308
- },
309
- detachTo () {
310
- this.removeTooltip()
311
- this.insertTooltip()
212
+ this.$emit('update:modelValue', (this.detachableVisible = false))
213
+ this.$emit('input', false)
214
+ this.$emit('close')
215
+ // Remove the mousedown listener if the tooltip got closed without a mousedown outside of the tooltip.
216
+ document.removeEventListener('mousedown', this.onOutsideMousedown)
217
+ window.removeEventListener('resize', this.onResize)
312
218
  }
313
219
  }
220
+
221
+ // watch, mounted & beforeDestroy hooks are set in the detachable.js mixin.
314
222
  }
315
223
  </script>
316
224
 
317
225
  <style lang="scss">
318
- .w-tooltip-wrap {
319
- display: none;
320
-
321
- &--attached {display: inline-block;position: relative;}
322
- }
226
+ .w-tooltip-wrap {display: none;}
323
227
 
324
228
  .w-tooltip {
325
229
  // Fix Safari where `width: max-content` does not take padding and border into consideration.
@@ -347,113 +251,22 @@ export default {
347
251
  &--shadow {box-shadow: $box-shadow;}
348
252
  &--no-border {border: none;}
349
253
 
350
- &--top {
351
- transform: translate(-50%, -100%);
352
- margin-top: -3 * $base-increment;
353
- }
354
- &--bottom {
355
- transform: translateX(-50%);
356
- margin-top: 3 * $base-increment;
357
- }
358
- &--left {
359
- transform: translate(-100%, -50%);
360
- margin-left: -3 * $base-increment;
361
- }
362
- &--right {
363
- transform: translateY(-50%);
364
- margin-left: 3 * $base-increment;
365
- }
254
+ &--top {margin-top: -3 * $base-increment;}
255
+ &--bottom {margin-top: 3 * $base-increment;}
256
+ &--left {margin-left: -3 * $base-increment;}
257
+ &--right {margin-left: 3 * $base-increment;}
366
258
 
367
259
  &--custom-transition {transform: none;}
368
260
 
369
- &:after {
370
- content: '';
371
- position: absolute;
372
- width: 0;
373
- height: 0;
374
- border: 6px solid transparent;
375
- }
376
- &--top:after {
377
- top: 100%;
378
- left: 50%;
379
- border-top-color: $tooltip-bg-color;
380
- transform: translateX(-50%);
381
- margin-top: 1px;
382
- }
383
- &--bottom:after {
384
- bottom: 100%;
385
- left: 50%;
386
- border-bottom-color: $tooltip-bg-color;
387
- transform: translateX(-50%);
388
- margin-bottom: 1px;
389
- }
390
- &--left:after {
391
- left: 100%;
392
- top: 50%;
393
- border-left-color: $tooltip-bg-color;
394
- transform: translateY(-50%);
395
- margin-left: 1px;
396
- }
397
- &--right:after {
398
- right: 100%;
399
- top: 50%;
400
- border-right-color: $tooltip-bg-color;
401
- transform: translateY(-50%);
402
- margin-right: 1px;
403
- }
404
-
405
261
  // Tooltip without border.
406
- // --------------------------------------------------------
407
- &--no-border.w-tooltip--top:after {margin-top: -1px;border-top-color: inherit;}
408
- &--no-border.w-tooltip--bottom:after {margin-bottom: -1px;border-bottom-color: inherit;}
409
- &--no-border.w-tooltip--left:after {margin-left: -1px;border-left-color: inherit;}
410
- &--no-border.w-tooltip--right:after {margin-right: -1px;border-right-color: inherit;}
262
+ &--no-border {
263
+ @include triangle(var(--w-tooltip-bg-color), '.w-tooltip', 7px, 0);
264
+ }
411
265
 
412
266
  // Tooltip with border.
413
- // --------------------------------------------------------
414
- &:not(&--no-border).w-tooltip--top:after {margin-top: -1px;}
415
- &:not(&--no-border).w-tooltip--bottom:after {margin-bottom: -1px;}
416
- &:not(&--no-border).w-tooltip--left:after {margin-left: -1px;}
417
- &:not(&--no-border).w-tooltip--right:after {margin-right: -1px;}
418
-
419
267
  &:not(&--no-border) {
420
- &:before {
421
- content: '';
422
- position: absolute;
423
- width: 0;
424
- height: 0;
425
- border: 7px solid transparent;
426
- }
427
- &.w-tooltip--top:before {
428
- top: 100%;
429
- left: 50%;
430
- border-top-color: inherit;
431
- transform: translateX(-50%);
432
- margin-top: 0;
433
- }
434
- &.w-tooltip--bottom:before {
435
- bottom: 100%;
436
- left: 50%;
437
- border-bottom-color: inherit;
438
- transform: translateX(-50%);
439
- margin-bottom: 0;
440
- }
441
- &.w-tooltip--left:before {
442
- left: 100%;
443
- top: 50%;
444
- border-left-color: inherit;
445
- transform: translateY(-50%);
446
- margin-left: 0;
447
- }
448
- &.w-tooltip--right:before {
449
- right: 100%;
450
- top: 50%;
451
- border-right-color: inherit;
452
- transform: translateY(-50%);
453
- margin-right: 0;
454
- }
268
+ @include triangle(var(--w-tooltip-bg-color), '.w-tooltip', 7px);
455
269
  }
456
- // --------------------------------------------------------
457
270
  }
458
271
 
459
272
  // Transitions.
@@ -85,14 +85,6 @@ export default class WaveUI {
85
85
  // Merge user options into the default config.
86
86
  mergeConfig(options)
87
87
 
88
- // @todo: remove this warning in version 1.40+.
89
- if (config.disableColorShades) {
90
- consoleWarn(
91
- 'WARNING - Since version 1.30 (Vue 2) & 2.17 (Vue 3), the option `disableColorShades` is replaced with `css.colorShades`.\n' +
92
- 'https://antoniandre.github.io/wave-ui/release-notes'
93
- )
94
- }
95
-
96
88
  // Add color shades for each custom color given in options.
97
89
  if (config.css.colorShades) {
98
90
  config.colorShades = {}