wave-ui 3.27.2 → 3.28.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 (32) hide show
  1. package/dist/types/types/$waveui.d.ts +6 -0
  2. package/dist/types/types/components/WAccordion.d.ts +7 -0
  3. package/dist/types/types/components/WBreadcrumbs.d.ts +7 -0
  4. package/dist/types/types/components/WButton.d.ts +7 -0
  5. package/dist/types/types/components/WList.d.ts +7 -0
  6. package/dist/types/types/components/WScrollable.d.ts +143 -0
  7. package/dist/types/types/components/WScrollable.js +2 -0
  8. package/dist/types/types/components/WTabs.d.ts +7 -0
  9. package/dist/types/types/components/WTag.d.ts +7 -0
  10. package/dist/types/types/components/index.d.ts +1 -0
  11. package/dist/wave-ui.cjs.js +3 -3
  12. package/dist/wave-ui.css +1 -1
  13. package/dist/wave-ui.esm.js +1392 -925
  14. package/dist/wave-ui.umd.js +3 -3
  15. package/package.json +6 -6
  16. package/src/wave-ui/components/w-accordion/index.vue +5 -1
  17. package/src/wave-ui/components/w-accordion/item.vue +42 -12
  18. package/src/wave-ui/components/w-breadcrumbs.vue +13 -2
  19. package/src/wave-ui/components/w-button/button.vue +15 -1
  20. package/src/wave-ui/components/w-button/index.vue +2 -1
  21. package/src/wave-ui/components/w-list.vue +12 -0
  22. package/src/wave-ui/components/w-scrollable.vue +667 -94
  23. package/src/wave-ui/components/w-tabs/index.vue +10 -0
  24. package/src/wave-ui/components/w-tag.vue +14 -0
  25. package/src/wave-ui/core.js +2 -0
  26. package/src/wave-ui/mixins/ripple.js +39 -0
  27. package/src/wave-ui/scss/_ripple.scss +37 -0
  28. package/src/wave-ui/scss/index.scss +1 -0
  29. package/src/wave-ui/scss/variables/_variables.scss +0 -2
  30. package/src/wave-ui/utils/config.js +2 -0
  31. package/src/wave-ui/utils/ripple.js +71 -0
  32. package/src/wave-ui/utils/wave-ripple-directive.js +40 -0
@@ -5,6 +5,7 @@
5
5
  v-for="(item, i) in tabs"
6
6
  :key="i"
7
7
  :class="barItemClasses(item)"
8
+ @pointerdown="onTabsBarPointerDown($event, item)"
8
9
  @click="!item._disabled && item._uid !== activeTabUid && openTab(item._uid)"
9
10
  @focus="$emit('focus', getOriginalItem(item))"
10
11
  :tabindex="!item._disabled && 0"
@@ -73,11 +74,14 @@
73
74
  <script>
74
75
  import { useId } from 'vue'
75
76
  import { objectifyClasses } from '../../utils/index'
77
+ import RippleMixin from '../../mixins/ripple'
76
78
  import TabContent from './tab-content.vue'
77
79
 
78
80
  export default {
79
81
  name: 'w-tabs',
80
82
 
83
+ mixins: [RippleMixin],
84
+
81
85
  setup () {
82
86
  return { tabsStableId: useId() }
83
87
  },
@@ -235,6 +239,7 @@ export default {
235
239
  const isActive = item._index === this.activeTabIndex
236
240
 
237
241
  return {
242
+ 'w-ripple': this.rippleActive,
238
243
  [`${this.bgColor}--bg`]: this.bgColor,
239
244
  [this.color]: this.color && !item._disabled && !(this.activeClass && isActive),
240
245
  'w-tabs__bar-item--active': isActive,
@@ -244,6 +249,11 @@ export default {
244
249
  }
245
250
  },
246
251
 
252
+ onTabsBarPointerDown (e, item) {
253
+ if (item._disabled) return
254
+ this.onRipple(e)
255
+ },
256
+
247
257
  // Switching tabs.
248
258
  openTab (uid) {
249
259
  this.prevTabIndex = this.activeTabIndex // To resolve the transition direction.
@@ -1,6 +1,7 @@
1
1
  <template lang="pug">
2
2
  span.w-tag(
3
3
  @click="$emit('update:modelValue', !modelValue);$emit('input', !modelValue)"
4
+ @pointerdown="onTagPointerDown"
4
5
  @keypress.enter="$emit('update:modelValue', !modelValue);$emit('input', !modelValue)"
5
6
  :class="classes"
6
7
  :role="modelValue !== -1 && 'button'"
@@ -17,9 +18,13 @@ span.w-tag(
17
18
  </template>
18
19
 
19
20
  <script>
21
+ import RippleMixin from '../mixins/ripple'
22
+
20
23
  export default {
21
24
  name: 'w-tag',
22
25
 
26
+ mixins: [RippleMixin],
27
+
23
28
  props: {
24
29
  modelValue: { type: [Boolean, Number], default: -1 },
25
30
  color: { type: String },
@@ -58,6 +63,7 @@ export default {
58
63
  [this.color]: this.color,
59
64
  [`${this.bgColor}--bg`]: this.bgColor,
60
65
  [`size--${this.presetSize}`]: true,
66
+ 'w-ripple': this.rippleActive && this.modelValue !== -1,
61
67
  'w-tag--dark': this.dark,
62
68
  'w-tag--light': this.light,
63
69
  'w-tag--clickable': this.modelValue !== -1,
@@ -74,6 +80,14 @@ export default {
74
80
  height: (!isNaN(this.height) ? `${this.height}px` : this.height) || null
75
81
  }
76
82
  }
83
+ },
84
+
85
+ methods: {
86
+ onTagPointerDown (e) {
87
+ if (this.modelValue === -1) return
88
+ if (e.target.closest?.('.w-tag__closable')) return
89
+ this.onRipple(e)
90
+ }
77
91
  }
78
92
  }
79
93
  </script>
@@ -4,6 +4,7 @@ import { consoleWarn } from './utils/console'
4
4
  import { colorPalette, generateColorShades, flattenColors } from './utils/colors'
5
5
  import { injectColorsCSSInDOM, injectCSSInDOM } from './utils/dynamic-css'
6
6
  import { injectNotifManagerInDOM, NotificationManager } from './utils/notification-manager'
7
+ import { waveRippleDirective } from './utils/wave-ripple-directive'
7
8
  import './scss/index.scss'
8
9
 
9
10
  let mounted = false
@@ -122,6 +123,7 @@ export default class WaveUI {
122
123
  window.addEventListener('scroll', f)
123
124
  }
124
125
  })
126
+ app.directive('waveRipple', waveRippleDirective)
125
127
 
126
128
  // Register a-la-carte components from the given list.
127
129
  const { components = {} } = options || {}
@@ -0,0 +1,39 @@
1
+ import { applyRipple, isRippleEnabled } from '../utils/ripple'
2
+
3
+ export default {
4
+ inject: {
5
+ $waveui: { from: '$waveui', default: () => ({ config: { ripple: true } }) }
6
+ },
7
+
8
+ props: {
9
+ /** Set to `true` to disable the ripple for this component only. */
10
+ noRipple: { type: Boolean, default: undefined }
11
+ },
12
+
13
+ computed: {
14
+ rippleActive () {
15
+ return isRippleEnabled(this.noRipple ? false : undefined, this.$waveui)
16
+ }
17
+ },
18
+
19
+ methods: {
20
+ /**
21
+ * Resolve host for applyRipple. Vue (and some browsers) can leave `event.currentTarget` null
22
+ * on delegated handlers, which would otherwise skip the ripple entirely.
23
+ */
24
+ onRipple (e, hostEl) {
25
+ if (!this.rippleActive || !e) return
26
+ let host = hostEl ?? e.currentTarget
27
+ if (!host?.getBoundingClientRect) {
28
+ const raw = e.target
29
+ const el = raw?.nodeType === 1 ? raw : raw?.parentElement
30
+ host = el?.closest?.('.w-ripple') ?? null
31
+ }
32
+ if (!host?.getBoundingClientRect && this.$el instanceof Element && this.$el.classList?.contains('w-ripple')) {
33
+ host = this.$el
34
+ }
35
+ if (!host?.getBoundingClientRect) return
36
+ applyRipple(host, e)
37
+ }
38
+ }
39
+ }
@@ -0,0 +1,37 @@
1
+ // Pointer ripple (v-wave-ripple + components using applyRipple).
2
+ // Note: avoid overflow:hidden here so hosts like w-button keep focus rings that extend past the box.
3
+ .w-ripple {
4
+ position: relative;
5
+
6
+ .w-ripple__ink-container {
7
+ position: absolute;
8
+ inset: 0;
9
+ overflow: hidden;
10
+ border-radius: inherit;
11
+ pointer-events: none;
12
+ }
13
+ }
14
+
15
+ .w-ripple__ink {
16
+ position: absolute;
17
+ z-index: 2; // Above .w-button:before hover overlay (explicit z-index:0 on :before keeps ink visible).
18
+ border-radius: 50%;
19
+ pointer-events: none;
20
+ transform: scale(0);
21
+ opacity: 0.5;
22
+ // currentColor is often invisible on filled buttons (e.g. white on white); use a neutral ink.
23
+ background-color: rgba(#000, 0.22);
24
+ will-change: transform, opacity;
25
+ animation: w-ripple-ink-expand 0.65s ease-out forwards;
26
+ }
27
+
28
+ html[data-theme='dark'] .w-ripple__ink {
29
+ background-color: rgba(#fff, 0.2);
30
+ }
31
+
32
+ @keyframes w-ripple-ink-expand {
33
+ to {
34
+ transform: scale(1);
35
+ opacity: 0;
36
+ }
37
+ }
@@ -4,3 +4,4 @@
4
4
  @forward 'colors';
5
5
  @forward 'icons';
6
6
  @forward 'transitions';
7
+ @forward 'ripple';
@@ -152,6 +152,4 @@ $tooltip-border-color: $border-color !default;
152
152
  // w-scrollable.
153
153
  // --------------------------------------------------------
154
154
  $scrollbar-size: 8px !default;
155
- $scrollbar-bg-color: color-mix(in srgb, var(--w-contrast-bg-color) 8%, var(--w-base-bg-color)) !default;
156
- $scrollbar-thumb-color: color-mix(in srgb, var(--w-contrast-bg-color) 25%, var(--w-base-bg-color)) !default;
157
155
  // --------------------------------------------------------
@@ -57,6 +57,8 @@ const config = reactive({
57
57
  align: 'right',
58
58
  transition: 'default' // Sliding from the side by default.
59
59
  },
60
+ // Default ripple for v-wave-ripple and components (unless their `no-ripple` prop is set).
61
+ ripple: true,
60
62
  presets: {} // User presets for each component.
61
63
  })
62
64
 
@@ -0,0 +1,71 @@
1
+ /**
2
+ * Material-style pointer ripple: appends a transient node at the pointer, removes after animation.
3
+ * SSR-safe: no-op when window/document are unavailable.
4
+ *
5
+ * @param {HTMLElement} hostEl
6
+ * @param {PointerEvent|MouseEvent|TouchEvent} event
7
+ * @param {{ disabled?: boolean }} [options]
8
+ */
9
+ export function applyRipple (hostEl, event, options = {}) {
10
+ if (typeof window === 'undefined' || typeof document === 'undefined') return
11
+ if (!hostEl?.ownerDocument || options.disabled) return
12
+ if (window.matchMedia?.('(prefers-reduced-motion: reduce)')?.matches) return
13
+
14
+ let clientX = typeof event.clientX === 'number' ? event.clientX : null
15
+ let clientY = typeof event.clientY === 'number' ? event.clientY : null
16
+ if ((clientX == null || clientY == null) && event.touches?.length) {
17
+ clientX = event.touches[0].clientX
18
+ clientY = event.touches[0].clientY
19
+ }
20
+ if (clientX == null || clientY == null) return
21
+
22
+ const rect = hostEl.getBoundingClientRect()
23
+ const maxSide = Math.max(rect.width, rect.height)
24
+ const size = Math.max(maxSide * 2.5, 48)
25
+ const x = clientX - rect.left - size / 2
26
+ const y = clientY - rect.top - size / 2
27
+
28
+ const inkContainer = document.createElement('span')
29
+ inkContainer.className = 'w-ripple__ink-container'
30
+ inkContainer.setAttribute('aria-hidden', 'true')
31
+
32
+ const ink = document.createElement('span')
33
+ ink.className = 'w-ripple__ink'
34
+ ink.style.width = `${size}px`
35
+ ink.style.height = `${size}px`
36
+ ink.style.left = `${x}px`
37
+ ink.style.top = `${y}px`
38
+
39
+ inkContainer.appendChild(ink)
40
+ hostEl.appendChild(inkContainer)
41
+
42
+ const done = () => {
43
+ ink.removeEventListener('animationend', done)
44
+ inkContainer.remove()
45
+ }
46
+ ink.addEventListener('animationend', done, { once: true })
47
+ // Fallback if animationend does not fire
48
+ window.setTimeout(() => {
49
+ if (inkContainer.parentNode === hostEl) inkContainer.remove()
50
+ }, 600)
51
+ }
52
+
53
+ /**
54
+ * @param {import('vue').ComponentPublicInstance | null | undefined} instance
55
+ * @returns {{ config: { ripple?: boolean } }}
56
+ */
57
+ export function getWaveUiFromInstance (instance) {
58
+ const $waveui = instance?.$waveui
59
+ if ($waveui && typeof $waveui === 'object') return $waveui
60
+ return { config: { ripple: true } }
61
+ }
62
+
63
+ /**
64
+ * @param {boolean|undefined} propRipple
65
+ * @param {{ config?: { ripple?: boolean } }} $waveui
66
+ */
67
+ export function isRippleEnabled (propRipple, $waveui) {
68
+ if (typeof propRipple === 'boolean') return propRipple
69
+ const g = $waveui?.config?.ripple
70
+ return g !== false
71
+ }
@@ -0,0 +1,40 @@
1
+ import { applyRipple, getWaveUiFromInstance, isRippleEnabled } from './ripple'
2
+
3
+ function bindingDisabled (binding) {
4
+ const v = binding.value
5
+ if (v === false) return true
6
+ if (v && typeof v === 'object' && v.disabled) return true
7
+ return false
8
+ }
9
+
10
+ function rippleAllowed (binding, instance) {
11
+ if (bindingDisabled(binding)) return false
12
+ const $waveui = getWaveUiFromInstance(instance)
13
+ if (binding.value === true) return isRippleEnabled(true, $waveui)
14
+ return isRippleEnabled(undefined, $waveui)
15
+ }
16
+
17
+ export const waveRippleDirective = {
18
+ mounted (el, binding) {
19
+ el.classList.add('w-ripple')
20
+ el.__waveRippleLastBinding = binding
21
+ const handler = e => {
22
+ if (typeof e.button === 'number' && e.button !== 0) return
23
+ const b = el.__waveRippleLastBinding
24
+ if (!rippleAllowed(b, b?.instance)) return
25
+ applyRipple(el, e)
26
+ }
27
+ el.__waveRippleHandler = handler
28
+ el.addEventListener('pointerdown', handler)
29
+ },
30
+
31
+ updated (el, binding) {
32
+ el.__waveRippleLastBinding = binding
33
+ },
34
+
35
+ unmounted (el) {
36
+ el.removeEventListener('pointerdown', el.__waveRippleHandler)
37
+ delete el.__waveRippleHandler
38
+ delete el.__waveRippleLastBinding
39
+ }
40
+ }