wave-ui 2.27.0 → 2.30.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.
@@ -1,19 +1,14 @@
1
1
  <template lang="pug">
2
- .w-tooltip-wrap(ref="wrapper" :class="{ 'w-tooltip-wrap--attached': !detachTo }")
2
+ .w-tooltip-wrap
3
3
  slot(name="activator" :on="eventHandlers")
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: {},
@@ -46,32 +42,44 @@ export default {
46
42
  transition: { type: String },
47
43
  tooltipClass: { type: [String, Object, Array] },
48
44
  // Position.
49
- detachTo: {},
45
+ detachTo: { type: [String, Boolean, Object], deprecated: true },
46
+ appendTo: { type: [String, Boolean, Object] },
50
47
  fixed: { type: Boolean },
51
48
  top: { type: Boolean },
52
49
  bottom: { type: Boolean },
53
50
  left: { type: Boolean },
54
51
  right: { type: Boolean },
55
- zIndex: { type: [Number, String, Boolean] }
52
+ alignTop: { type: Boolean },
53
+ alignBottom: { type: Boolean },
54
+ alignLeft: { type: Boolean },
55
+ alignRight: { type: Boolean },
56
+ zIndex: { type: [Number, String, Boolean] },
57
+ persistent: { type: Boolean },
58
+ noPosition: { type: Boolean }
56
59
  },
57
60
 
58
61
  emits: ['input', 'update:modelValue', 'open', 'close'],
59
62
 
60
63
  data: () => ({
61
- showTooltip: false,
64
+ detachableVisible: false,
65
+ hoveringActivator: false,
62
66
  // The activator coordinates.
63
- coordinates: {
67
+ detachableCoords: {
64
68
  top: 0,
65
- left: 0,
66
- width: 0,
67
- height: 0
69
+ left: 0
68
70
  },
69
71
  activatorEl: null,
70
- tooltipEl: null,
72
+ detachableEl: null,
71
73
  timeoutId: null
72
74
  }),
73
75
 
74
76
  computed: {
77
+ /**
78
+ * Other computed in the detachable mixin:
79
+ * - `appendToTarget`
80
+ * - `detachableParentEl`
81
+ **/
82
+
75
83
  tooltipClasses () {
76
84
  return objectifyClasses(this.tooltipClass)
77
85
  },
@@ -81,29 +89,6 @@ export default {
81
89
  return this.transition || `w-tooltip-slide-fade-${direction}`
82
90
  },
83
91
 
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
92
  position () {
108
93
  return (
109
94
  (this.top && 'top') ||
@@ -114,57 +99,39 @@ export default {
114
99
  )
115
100
  },
116
101
 
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
102
+ alignment () {
103
+ return (
104
+ (['top', 'bottom'].includes(this.position) && this.alignLeft && 'left') ||
105
+ (['top', 'bottom'].includes(this.position) && this.alignRight && 'right') ||
106
+ (['left', 'right'].includes(this.position) && this.alignTop && 'top') ||
107
+ (['left', 'right'].includes(this.position) && this.alignBottom && 'bottom') ||
108
+ ''
109
+ )
145
110
  },
146
111
 
147
112
  classes () {
148
113
  return {
149
- [this.color]: !this.bgColor,
150
- [`${this.bgColor} ${this.bgColor}--bg`]: this.bgColor,
114
+ [this.color]: this.color,
115
+ [`${this.bgColor}--bg`]: this.bgColor,
151
116
  ...this.tooltipClasses,
152
- [`w-tooltip--${this.position}`]: true,
117
+ [`w-tooltip--${this.position}`]: !this.noPosition,
118
+ [`w-tooltip--align-${this.alignment}`]: !this.noPosition && this.alignment,
153
119
  'w-tooltip--tile': this.tile,
154
120
  'w-tooltip--round': this.round,
155
121
  'w-tooltip--shadow': this.shadow,
156
122
  'w-tooltip--fixed': this.fixed,
157
- 'w-tooltip--active': this.showTooltip,
158
123
  'w-tooltip--no-border': this.noBorder || this.bgColor,
159
124
  'w-tooltip--custom-transition': this.transition
160
125
  }
161
126
  },
162
127
 
128
+ // The tooltip styles.
163
129
  styles () {
164
130
  return {
165
131
  zIndex: this.zIndex || this.zIndex === 0 || null,
166
- top: `${~~this.tooltipCoordinates.top}px`,
167
- left: `${~~this.tooltipCoordinates.left}px`
132
+ top: (this.detachableCoords.top && `${~~this.detachableCoords.top}px`) || null,
133
+ left: (this.detachableCoords.left && `${~~this.detachableCoords.left}px`) || null,
134
+ '--w-tooltip-bg-color': this.$waveui.colors[this.bgColor || 'white']
168
135
  }
169
136
  },
170
137
 
@@ -175,20 +142,38 @@ export default {
175
142
  handlers = {
176
143
  focus: this.toggle,
177
144
  blur: this.toggle,
178
- mouseenter: this.toggle,
179
- mouseleave: this.toggle
145
+ mouseenter: e => {
146
+ this.hoveringActivator = true
147
+ this.open(e)
148
+ },
149
+ mouseleave: e => {
150
+ this.hoveringActivator = false
151
+ this.close()
152
+ }
180
153
  }
181
154
 
182
- if ('ontouchstart' in window) handlers.click = this.toggle
155
+ // Check the window exists: SSR-proof.
156
+ if (typeof window !== 'undefined' && 'ontouchstart' in window) handlers.click = this.toggle
183
157
  }
184
158
  return handlers
185
159
  }
186
160
  },
187
161
 
188
162
  methods: {
163
+ /**
164
+ * Other methods in the `detachable` mixin:
165
+ * - `getActivatorCoordinates`
166
+ * - `computeDetachableCoords`
167
+ * - `onResize`
168
+ * - `onOutsideMousedown`
169
+ * - `insertInDOM`
170
+ * - `removeFromDOM`
171
+ **/
172
+
173
+ // ! \ This function uses the DOM - NO SSR (only trigger from beforeMount and later).
189
174
  toggle (e) {
190
- let shouldShowTooltip = this.showTooltip
191
- if ('ontouchstart' in window) {
175
+ let shouldShowTooltip = this.detachableVisible
176
+ if (typeof window !== 'undefined' && 'ontouchstart' in window) {
192
177
  if (e.type === 'click') shouldShowTooltip = !shouldShowTooltip
193
178
  }
194
179
  else if (e.type === 'click' && this.showOnClick) shouldShowTooltip = !shouldShowTooltip
@@ -197,129 +182,101 @@ export default {
197
182
 
198
183
  this.timeoutId = clearTimeout(this.timeoutId)
199
184
  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
- }
217
- },
218
-
219
- getCoordinates () {
220
- const { top, left, width, height } = this.activatorEl.getBoundingClientRect()
221
- let coords = { top, left, width, height }
185
+ this.$emit('update:modelValue', (this.detachableVisible = true))
186
+ this.$emit('input', true)
187
+ this.$emit('open')
222
188
 
223
- if (!this.fixed) {
224
- const { top: targetTop, left: targetLeft } = this.tooltipParentEl.getBoundingClientRect()
225
- coords = { ...coords, top: top - targetTop, left: left - targetLeft }
189
+ this.open(e)
226
190
  }
191
+ else this.close()
192
+ },
227
193
 
228
- const tooltip = this.tooltipEl
194
+ // ! \ This function uses the DOM - NO SSR (only trigger from beforeMount and later).
195
+ async open (e) {
196
+ this.detachableVisible = true
197
+ await this.insertInDOM()
229
198
 
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)
199
+ if (this.minWidth === 'activator') this.activatorWidth = this.activatorEl.offsetWidth
234
200
 
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
- }
201
+ if (!this.noPosition) this.computeDetachableCoords(e)
267
202
 
268
- // 3. Hide the tooltip again so the transition happens correctly.
269
- tooltip.style.visibility = null
270
- tooltip.style.display = 'none'
203
+ // In `getActivatorCoordinates` accessing the tooltip computed styles takes a few ms (less than 10ms),
204
+ // if we don't postpone the Tooltip apparition it will start transition from a visible tooltip and
205
+ // thus will not transition.
206
+ this.timeoutId = setTimeout(() => {
207
+ this.$emit('update:modelValue', true)
208
+ this.$emit('input', true)
209
+ this.$emit('open')
210
+ }, 0)
271
211
 
272
- return coords
212
+ if (!this.persistent) document.addEventListener('mousedown', this.onOutsideMousedown)
213
+ if (!this.noPosition) window.addEventListener('resize', this.onResize)
273
214
  },
274
215
 
275
- insertTooltip () {
276
- const wrapper = this.$refs.wrapper
277
-
278
- // Unwrap the activator element.
279
- wrapper.parentNode.insertBefore(this.activatorEl, wrapper)
280
-
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)
285
- },
216
+ /**
217
+ * Closes the tooltip. Can happen on:
218
+ * - click of activator
219
+ * - hover outside if showOnHover
220
+ * - click inside tooltip if hideOnTooltipClick.
221
+ * / ! \ This function uses the DOM - NO SSR (only trigger from beforeMount and later).
222
+ *
223
+ * @param {Boolean} force when showOnHover is set to true, hovering tooltip should keep it open.
224
+ * But if hideOnTooltipClick is also set to true, this should force close
225
+ * even while hovering the tooltip.
226
+ */
227
+ async close (force = false) {
228
+ // Might be already closed.
229
+ // E.g. showOnHover & hideOnTooltipClick: on click, force hide then mouseleave is also firing.
230
+ if (!this.detachableVisible) return
231
+
232
+ if (this.showOnHover && !force) {
233
+ await new Promise(resolve => setTimeout(resolve, 10))
234
+ if (this.showOnHover && this.hoveringActivator) return
235
+ }
286
236
 
287
- removeTooltip () {
288
- if (this.tooltipEl && this.tooltipEl.parentNode) this.tooltipEl.remove()
237
+ this.$emit('update:modelValue', (this.detachableVisible = false))
238
+ this.$emit('input', false)
239
+ this.$emit('close')
240
+ // Remove the mousedown listener if the tooltip got closed without a mousedown outside of the tooltip.
241
+ document.removeEventListener('mousedown', this.onOutsideMousedown)
242
+ window.removeEventListener('resize', this.onResize)
289
243
  }
290
244
  },
291
245
 
292
246
  mounted () {
293
- this.activatorEl = this.$refs.wrapper.firstElementChild
294
- if (this.detachTo) this.insertTooltip()
247
+ const wrapper = this.$el
248
+ this.activatorEl = wrapper.firstElementChild
249
+
250
+ // Unwrap the activator element.
251
+ wrapper.parentNode.insertBefore(this.activatorEl, wrapper)
295
252
 
296
253
  if (this.modelValue) this.toggle({ type: 'click', target: this.activatorEl })
297
254
  },
298
255
 
299
256
  beforeUnmount () {
300
- this.removeTooltip()
257
+ this.removeFromDOM()
301
258
 
302
259
  if (this.activatorEl && this.activatorEl.parentNode) this.activatorEl.remove()
303
260
  },
304
261
 
305
262
  watch: {
306
263
  modelValue (bool) {
307
- if (bool !== this.showTooltip) this.toggle({ type: 'click', target: this.activatorEl })
264
+ if (bool !== this.detachableVisible) this.toggle({ type: 'click', target: this.activatorEl })
308
265
  },
309
266
  detachTo () {
310
- this.removeTooltip()
311
- this.insertTooltip()
267
+ this.removeFromDOM()
268
+ this.insertInDOM()
269
+ },
270
+ appendTo () {
271
+ this.removeFromDOM()
272
+ this.insertInDOM()
312
273
  }
313
274
  }
314
275
  }
315
276
  </script>
316
277
 
317
278
  <style lang="scss">
318
- .w-tooltip-wrap {
319
- display: none;
320
-
321
- &--attached {display: inline-block;position: relative;}
322
- }
279
+ .w-tooltip-wrap {display: none;}
323
280
 
324
281
  .w-tooltip {
325
282
  // Fix Safari where `width: max-content` does not take padding and border into consideration.
@@ -347,113 +304,22 @@ export default {
347
304
  &--shadow {box-shadow: $box-shadow;}
348
305
  &--no-border {border: none;}
349
306
 
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
- }
307
+ &--top {margin-top: -3 * $base-increment;}
308
+ &--bottom {margin-top: 3 * $base-increment;}
309
+ &--left {margin-left: -3 * $base-increment;}
310
+ &--right {margin-left: 3 * $base-increment;}
366
311
 
367
312
  &--custom-transition {transform: none;}
368
313
 
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
314
  // 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;}
315
+ &--no-border {
316
+ @include triangle(var(--w-tooltip-bg-color), '.w-tooltip', 7px, 0);
317
+ }
411
318
 
412
319
  // 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
320
  &: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
- }
321
+ @include triangle(var(--w-tooltip-bg-color), '.w-tooltip', 7px);
455
322
  }
456
- // --------------------------------------------------------
457
323
  }
458
324
 
459
325
  // Transitions.