Skip to content

Commit 93f4218

Browse files
authored
feat(VPie): support touch for segment interaction (#21871)
1 parent 6a50b44 commit 93f4218

File tree

5 files changed

+76
-49
lines changed

5 files changed

+76
-49
lines changed

packages/vuetify/src/labs/VPie/VPie.sass

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,9 @@
4343
.v-overlay__content
4444
pointer-events: none !important
4545

46+
&__segments
47+
border-radius: 50%
48+
4649
&__content-underlay
4750
border-radius: 50%
4851
position: absolute

packages/vuetify/src/labs/VPie/VPie.tsx

Lines changed: 56 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@ import { VDefaultsProvider } from '@/components/VDefaultsProvider'
1313
import { useColor } from '@/composables/color'
1414
import { makeDensityProps } from '@/composables/density'
1515

16+
// Directives
17+
import vClickOutside from '@/directives/click-outside'
18+
1619
// Utilities
1720
import { computed, shallowRef, toRef, watch } from 'vue'
1821
import { formatTextTemplate } from './utils'
@@ -103,6 +106,8 @@ export const makeVPieProps = propsFactory({
103106
export const VPie = genericComponent<VPieSlots>()({
104107
name: 'VPie',
105108

109+
directives: { vClickOutside },
110+
106111
props: makeVPieProps(),
107112

108113
setup (props, { slots }) {
@@ -153,7 +158,7 @@ export const VPie = genericComponent<VPieSlots>()({
153158
const visibleItems = computed(() => {
154159
// hidden items get (value: 0) to trigger disappearing animation
155160
return arcs.value.map(item => {
156-
return isActive(item)
161+
return isVisible(item)
157162
? item
158163
: { ...item, value: 0 }
159164
})
@@ -185,12 +190,12 @@ export const VPie = genericComponent<VPieSlots>()({
185190
return typeof paletteItem === 'object' ? paletteItem.pattern : undefined
186191
}
187192

188-
function isActive (item: PieItem) {
193+
function isVisible (item: PieItem) {
189194
return visibleItemsKeys.value.includes(item.key)
190195
}
191196

192197
function toggle (item: PieItem) {
193-
if (isActive(item)) {
198+
if (isVisible(item)) {
194199
visibleItemsKeys.value = visibleItemsKeys.value.filter(x => x !== item.key)
195200
} else {
196201
visibleItemsKeys.value = [...visibleItemsKeys.value, item.key]
@@ -199,29 +204,53 @@ export const VPie = genericComponent<VPieSlots>()({
199204

200205
const tooltipItem = shallowRef<PieItem | null>(null)
201206
const tooltipVisible = shallowRef(false)
207+
const tooltipTarget = shallowRef<[x: number, y: number]>([0, 0])
202208

203209
let mouseLeaveTimeout = null! as ReturnType<typeof setTimeout>
204210

205-
function onMouseenter (item: PieItem) {
206-
if (!props.tooltip) return
211+
function setItemActive (item: PieItem, active: boolean) {
212+
arcs.value.forEach(a => a.isActive = a.key === item.key && active)
207213

208-
clearTimeout(mouseLeaveTimeout)
209-
tooltipVisible.value = true
210-
tooltipItem.value = item
214+
if (props.tooltip) {
215+
setTooltip(item, active)
216+
}
211217
}
212218

213-
function onMouseleave () {
214-
if (!props.tooltip) return
215-
219+
function setTooltip (item: PieItem, active: boolean) {
216220
clearTimeout(mouseLeaveTimeout)
217-
mouseLeaveTimeout = setTimeout(() => {
218-
tooltipVisible.value = false
219221

220-
// intentionally reusing timeout here
222+
if (active) {
223+
tooltipVisible.value = true
224+
tooltipItem.value = item
225+
} else {
221226
mouseLeaveTimeout = setTimeout(() => {
222-
tooltipItem.value = null
223-
}, 500)
224-
}, 100)
227+
tooltipVisible.value = false
228+
229+
// intentionally reusing timeout here
230+
mouseLeaveTimeout = setTimeout(() => {
231+
tooltipItem.value = null
232+
}, 500)
233+
}, 100)
234+
}
235+
}
236+
237+
let frame = -1
238+
function onSvgMousemove ({ clientX, clientY }: MouseEvent) {
239+
cancelAnimationFrame(frame)
240+
frame = requestAnimationFrame(() => {
241+
tooltipTarget.value = [clientX, clientY]
242+
})
243+
}
244+
245+
function onSvgTouchstart ({ touches }: TouchEvent) {
246+
if (!touches) return
247+
const { clientX, clientY } = touches[0]
248+
tooltipTarget.value = [clientX, clientY]
249+
}
250+
251+
function onSvgClickOutside () {
252+
arcs.value.forEach(a => a.isActive = false)
253+
tooltipVisible.value = false
225254
}
226255

227256
return () => {
@@ -247,6 +276,7 @@ export const VPie = genericComponent<VPieSlots>()({
247276
subtitleFormat: typeof props.tooltip === 'object' ? props.tooltip.subtitleFormat : '[value]',
248277
transition: typeof props.tooltip === 'object' ? props.tooltip.transition : defaultTooltipTransition,
249278
offset: typeof props.tooltip === 'object' ? props.tooltip.offset : 16,
279+
target: tooltipTarget.value,
250280
}
251281

252282
const legendDefaults = {
@@ -311,17 +341,22 @@ export const VPie = genericComponent<VPieSlots>()({
311341
<svg
312342
xmlns="http://www.w3.org/2000/svg"
313343
viewBox="0 0 100 100"
344+
class="v-pie__segments"
345+
onMousemove={ onSvgMousemove }
346+
onTouchstart={ onSvgTouchstart }
347+
v-click-outside={{ handler: onSvgClickOutside }}
314348
>
315349
{ arcs.value.map((item, index) => (
316350
<VPieSegment
317351
{ ...segmentProps }
318352
key={ item.key }
353+
active={ item.isActive }
319354
color={ item.color }
320-
value={ isActive(item) ? arcSize(item.value) : 0 }
355+
value={ isVisible(item) ? arcSize(item.value) : 0 }
321356
rotate={ arcOffset(index) }
322357
pattern={ item.pattern }
323-
onMouseenter={ () => onMouseenter(item) }
324-
onMouseleave={ () => onMouseleave() }
358+
onUpdate:active={ val => setItemActive(item, val) }
359+
onTouchend={ () => setItemActive(item, true) }
325360
/>
326361
))}
327362
</svg>
@@ -343,7 +378,7 @@ export const VPie = genericComponent<VPieSlots>()({
343378
{ legendConfig.value.visible && (
344379
<VDefaultsProvider key="legend" defaults={ legendDefaults }>
345380
<div class="v-pie__legend">
346-
{ slots.legend?.({ isActive, toggle, items: arcs.value, total: total.value }) ?? (
381+
{ slots.legend?.({ isActive: isVisible, toggle, items: arcs.value, total: total.value }) ?? (
347382
<VChipGroup
348383
column
349384
multiple

packages/vuetify/src/labs/VPie/VPieSegment.tsx

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,17 @@
11
// Composables
2+
import { useProxiedModel } from '@/composables/proxiedModel'
23
import { makeRevealProps, useReveal } from '@/composables/reveal'
34

45
// Utilities
5-
import { computed, shallowRef, toRef } from 'vue'
6+
import { computed, toRef } from 'vue'
67
import { useInnerSlicePath, useOuterSlicePath, usePieArc } from './utils'
78
import { easingPatterns, genericComponent, propsFactory, useTransition } from '@/util'
89

910
// Types
1011
import type { PropType } from 'vue'
1112

1213
export const makeVPieSegmentProps = propsFactory({
14+
active: Boolean,
1315
rotate: [Number, String],
1416
value: {
1517
type: Number,
@@ -40,8 +42,12 @@ export const VPieSegment = genericComponent()({
4042

4143
props: makeVPieSegmentProps(),
4244

45+
emits: {
46+
'update:active': (val: boolean) => true,
47+
},
48+
4349
setup (props) {
44-
const isHovering = shallowRef(false)
50+
const isActive = useProxiedModel(props, 'active')
4551

4652
const { state: revealState, duration: revealDuration } = useReveal(props)
4753

@@ -70,15 +76,15 @@ export const VPieSegment = genericComponent()({
7076
outerX,
7177
outerY,
7278
arcWidth,
73-
} = usePieArc(props, isHovering)
79+
} = usePieArc(props, isActive)
7480

7581
const arcSize = toRef(() => revealState.value === 'initial' ? 0 : normalizedValue.value)
7682
const currentArcSize = useTransition(arcSize, transitionConfig)
7783

7884
const angle = toRef(() => revealState.value === 'initial' ? 0 : (Number(props.rotate ?? 0) + Number(props.gap ?? 0) / 2))
7985
const currentAngle = useTransition(angle, transitionConfig)
8086

81-
const arcRadius = toRef(() => 50 * (isHovering.value ? 1 : (1 - hoverZoomRatio.value)))
87+
const arcRadius = toRef(() => 50 * (isActive.value ? 1 : (1 - hoverZoomRatio.value)))
8288
const currentArcRadius = useTransition(arcRadius, transitionConfig)
8389
const currentArcWidth = useTransition(arcWidth, transitionConfig)
8490

@@ -129,8 +135,8 @@ export const VPieSegment = genericComponent()({
129135
transform={ `rotate(${currentAngle.value} 50 50)` }
130136
class="v-pie-segment__overlay"
131137
d={ overlayPath.value }
132-
onMouseenter={ () => isHovering.value = true }
133-
onMouseleave={ () => isHovering.value = false }
138+
onMouseenter={ () => isActive.value = true }
139+
onMouseleave={ () => isActive.value = false }
134140
/>
135141
)}
136142
</g>

packages/vuetify/src/labs/VPie/VPieTooltip.tsx

Lines changed: 4 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,9 @@ import { makeVTooltipProps, VTooltip } from '@/components/VTooltip/VTooltip'
66
import { makeTransitionProps, MaybeTransition } from '@/composables/transition'
77

88
// Utilities
9-
import { onBeforeUnmount, onMounted, shallowRef, toRef } from 'vue'
9+
import { toRef } from 'vue'
1010
import { formatTextTemplate } from './utils'
11-
import { genericComponent, getCurrentInstance, pick, propsFactory } from '@/util'
11+
import { genericComponent, pick, propsFactory } from '@/util'
1212

1313
// Types
1414
import type { PropType } from 'vue'
@@ -21,6 +21,7 @@ export type VPieTooltipSlots = {
2121

2222
export const makeVPieTooltipProps = propsFactory({
2323
modelValue: Boolean,
24+
target: Object as PropType<[x: number, y: number]>,
2425
item: {
2526
type: Object as PropType<PieItem | null>,
2627
default: null,
@@ -43,25 +44,6 @@ export const VPieTooltip = genericComponent<VPieTooltipSlots>()({
4344
props: makeVPieTooltipProps(),
4445

4546
setup (props, { slots }) {
46-
const target = shallowRef<[x: number, y: number]>([0, 0])
47-
const vm = getCurrentInstance('VPieTooltip')
48-
49-
let frame = -1
50-
function onMouseMove ({ clientX, clientY }: MouseEvent) {
51-
cancelAnimationFrame(frame)
52-
frame = requestAnimationFrame(() => {
53-
target.value = [clientX, clientY]
54-
})
55-
}
56-
57-
onMounted(() => {
58-
vm.proxy!.$el.parentNode.addEventListener('mousemove', onMouseMove)
59-
})
60-
61-
onBeforeUnmount(() => {
62-
vm.proxy!.$el.parentNode.removeEventListener('mousemove', onMouseMove)
63-
})
64-
6547
const tooltipTitleFormatFunction = toRef(() => (segment: PieItem) => {
6648
return typeof props.titleFormat === 'function'
6749
? props.titleFormat(segment)
@@ -78,7 +60,7 @@ export const VPieTooltip = genericComponent<VPieTooltipSlots>()({
7860
<VTooltip
7961
offset={ props.offset }
8062
modelValue={ props.modelValue }
81-
target={ target.value }
63+
target={ props.target }
8264
contentClass="v-pie__tooltip-content"
8365
>
8466
{ !!props.item && (

packages/vuetify/src/labs/VPie/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ export interface PieItem {
44
value: number
55
title: string
66
pattern?: string
7+
isActive: boolean
78
raw?: Record<string, any>
89
}
910

0 commit comments

Comments
 (0)