Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/cool-worlds-appear.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@vingy/vueltip": patch
---

updates tooltip content when it changes dynamically
5 changes: 5 additions & 0 deletions .changeset/little-dancers-jog.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@vingy/vueltip": minor
---

allow placement via directive arg
5 changes: 5 additions & 0 deletions .changeset/lovely-bananas-ask.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@vingy/vueltip": patch
---

update placementAttribute and truncateAttribute when binding update
4 changes: 2 additions & 2 deletions packages/vueltip/src/composables.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import {
type StyleValue,
watch,
} from 'vue'
import { options } from './options'
import { getOption } from './options'
import {
debouncedHoveredElement,
debouncedTooltipPlacement,
Expand Down Expand Up @@ -95,7 +95,7 @@ export const useVueltip = ({
() => !!debouncedHoveredElement.value,
)

if (options.handleDialogModals) {
if (getOption('handleDialogModals')) {
watch(show, (value) => {
if (
!value ||
Expand Down
154 changes: 150 additions & 4 deletions packages/vueltip/src/directive.browser.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ const setupDirective = () => {
setOptions({
keyAttribute: 'tooltip-key',
placementAttribute: 'tooltip-placement',
truncateAttribute: 'tooltip-truncate',
})
return el
}
Expand Down Expand Up @@ -52,24 +53,44 @@ describe('created hook', () => {
teardownDirective(el)
})

it('sets placement attribute with string binding', () => {
it('sets placement attribute with object binding', () => {
const el = setupDirective()
const binding = { value: 'Default text' }
const binding = {
value: {
content: 'Text',
placement: 'bottom' as const,
},
}

vueltipDirective.created?.(el, binding as any)

expect(el.getAttribute('tooltip-placement')).toBe('top')
expect(el.getAttribute('tooltip-placement')).toBe(
'bottom',
)

teardownDirective(el)
})

it('sets placement attribute with object binding', () => {
it('sets placement attribute with arg parameter', () => {
const el = setupDirective()
const binding = { value: 'Text', arg: 'left' }

vueltipDirective.created?.(el, binding as any)

expect(el.getAttribute('tooltip-placement')).toBe(
'left',
)

teardownDirective(el)
})
it('prioritizes placement in value over arg', () => {
const el = setupDirective()
const binding = {
value: {
content: 'Text',
placement: 'bottom' as const,
},
arg: 'left',
}

vueltipDirective.created?.(el, binding as any)
Expand All @@ -80,6 +101,75 @@ describe('created hook', () => {

teardownDirective(el)
})
it('sets truncate attribute with none modifier', () => {
const el = setupDirective()
const binding = {
value: 'Text',
modifiers: { none: true },
}

vueltipDirective.created?.(el, binding as any)

expect(el.getAttribute('tooltip-truncate')).toBe('none')

teardownDirective(el)
})

it('sets truncate attribute with both modifier', () => {
const el = setupDirective()
const binding = {
value: 'Text',
modifiers: { both: true },
}

vueltipDirective.created?.(el, binding as any)

expect(el.getAttribute('tooltip-truncate')).toBe('both')

teardownDirective(el)
})

it('sets truncate attribute with x modifier', () => {
const el = setupDirective()
const binding = {
value: 'Text',
modifiers: { x: true },
}

vueltipDirective.created?.(el, binding as any)

expect(el.getAttribute('tooltip-truncate')).toBe('x')

teardownDirective(el)
})

it('sets truncate attribute with y modifier', () => {
const el = setupDirective()
const binding = {
value: 'Text',
modifiers: { y: true },
}

vueltipDirective.created?.(el, binding as any)

expect(el.getAttribute('tooltip-truncate')).toBe('y')

teardownDirective(el)
})

it('sets truncate attribute with both x and y modifiers', () => {
const el = setupDirective()
const binding = {
value: 'Text',
modifiers: { x: true, y: true },
}

vueltipDirective.created?.(el, binding as any)

expect(el.getAttribute('tooltip-truncate')).toBe('both')

teardownDirective(el)
})

it('attaches event listeners', () => {
const el = setupDirective()
Expand Down Expand Up @@ -129,6 +219,62 @@ describe('updated hook', () => {

teardownDirective(el)
})

it('updates placement attribute via arg', () => {
const el = setupDirective()
const binding1 = { value: 'Text', arg: 'top' }
vueltipDirective.created?.(el, binding1 as any)

const binding2 = { value: 'Text', arg: 'right' }
vueltipDirective.updated?.(el, binding2 as any)

expect(el.getAttribute('tooltip-placement')).toBe(
'right',
)

teardownDirective(el)
})

it('updates placement attribute via object binding', () => {
const el = setupDirective()
const binding1 = {
value: { content: 'Text', placement: 'top' as const },
}
vueltipDirective.created?.(el, binding1 as any)

const binding2 = {
value: {
content: 'Text',
placement: 'bottom' as const,
},
}
vueltipDirective.updated?.(el, binding2 as any)

expect(el.getAttribute('tooltip-placement')).toBe(
'bottom',
)

teardownDirective(el)
})

it('updates truncate attribute with new modifiers', () => {
const el = setupDirective()
const binding1 = {
value: 'Text',
modifiers: { x: true },
}
vueltipDirective.created?.(el, binding1 as any)

const binding2 = {
value: 'Text',
modifiers: { y: true },
}
vueltipDirective.updated?.(el, binding2 as any)

expect(el.getAttribute('tooltip-truncate')).toBe('y')

teardownDirective(el)
})
})

describe('beforeUnmount hook', () => {
Expand Down
42 changes: 31 additions & 11 deletions packages/vueltip/src/directive.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { Placement } from '@floating-ui/vue'
import { DirectiveBinding } from 'vue'
import { onMouseout, onMouseover } from './listeners'
import { options } from './options'
import { getOption } from './options'
import {
deleteContent,
generateKey,
Expand All @@ -20,8 +22,17 @@ const toContent = (value: Binding): Content =>
? { text: value.content }
: value.content

const extractPlacement = (value: Binding) =>
typeof value === 'string' ? 'top' : value.placement
const extractPlacement = (
binding: DirectiveBinding<Binding, Modifier, Placement>,
) => {
const { value, arg } = binding

if (typeof value !== 'string' && 'placement' in value) {
return value.placement
}
if (!arg) return getOption('defaultPlacement')
return arg
}

const truncationDirection = (
modifiers: Partial<Record<Modifier, boolean | undefined>>,
Expand All @@ -31,26 +42,35 @@ const truncationDirection = (
if (modifiers.x && modifiers.y) return 'both'
if (modifiers.x) return 'x'
if (modifiers.y) return 'y'
return options.defaultTruncateDetection
return getOption('defaultTruncateDetection')
}

export const vueltipDirective = {
updated: (el, binding) => {
ensureKey(el, (key) =>
setContent(key, toContent(binding.value)),
)
ensureKey(el, (key) => {
el.setAttribute(
getOption('placementAttribute'),
extractPlacement(binding),
)
el.setAttribute(
getOption('truncateAttribute'),
truncationDirection(binding.modifiers ?? {}),
)

setContent(key, toContent(binding.value))
})
},
created: (el, binding) => {
const key = generateKey()
setContent(key, toContent(binding.value))
el.setAttribute(options.keyAttribute, key)
el.setAttribute(getOption('keyAttribute'), key)

el.setAttribute(
options.placementAttribute,
extractPlacement(binding.value),
getOption('placementAttribute'),
extractPlacement(binding),
)
el.setAttribute(
options.truncateAttribute,
getOption('truncateAttribute'),
truncationDirection(binding.modifiers ?? {}),
)

Expand Down
4 changes: 2 additions & 2 deletions packages/vueltip/src/listeners.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { Placement } from '@floating-ui/vue'
import { options } from './options'
import { getOption } from './options'
import {
getContent,
hoveredElement,
Expand Down Expand Up @@ -39,7 +39,7 @@ export const onMouseover = ensureEventTarget((target) =>
}

const placement = target.getAttribute(
options.placementAttribute,
getOption('placementAttribute'),
) as Placement

tooltipKey.value = key
Expand Down
7 changes: 6 additions & 1 deletion packages/vueltip/src/options.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,20 @@
import type { Options } from './types'

export let options: Options = {
let options: Options = {
placementAttribute: 'vueltip-placement',
keyAttribute: 'vueltip-key',
truncateAttribute: 'vueltip-truncate',
showDelay: 0,
hideDelay: 200,
handleDialogModals: false,
defaultTruncateDetection: 'both',
defaultPlacement: 'top',
}

export const setOptions = (opts?: Partial<Options>) => {
options = { ...options, ...opts }
}

export const getOption = <T extends keyof Options>(
key: T,
): Options[T] => options[key]
21 changes: 13 additions & 8 deletions packages/vueltip/src/state.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { Placement } from '@floating-ui/vue'
import type { Maybe } from '@vingy/shared/types'
import { ref, watch } from 'vue'
import { options } from './options'
import { getOption } from './options'
import type { Content } from './types'

export const tooltipPlacement = ref<Placement>('top')
Expand All @@ -12,21 +12,26 @@ export const hoveredElement = ref<Maybe<HTMLElement>>()
export const debouncedHoveredElement =
ref<Maybe<HTMLElement>>()

const contentMap = new Map<string, Content>()
const contentMap = ref(new Map<string, Content>())
export const getContent = (key: string) =>
contentMap.get(key)
contentMap.value.get(key)
export const setContent = (key: string, value: Content) =>
contentMap.set(key, value)
contentMap.value.set(key, value)
export const deleteContent = (key: string) =>
contentMap.delete(key)
contentMap.value.delete(key)
export const generateKey = () => crypto.randomUUID()

export const tooltipKey = ref<Maybe<string>>()
export const tooltipContent = ref<Maybe<Content>>()

let timerId: Maybe<ReturnType<typeof setTimeout>>
watch(
[tooltipKey, hoveredElement, tooltipPlacement],
[
tooltipKey,
hoveredElement,
tooltipPlacement,
() => getContent(tooltipKey.value ?? ''),
],
([key, el, placement]) => {
if (!key) {
return
Expand All @@ -35,8 +40,8 @@ watch(
clearTimeout(timerId)
}
const timeout = el
? options.showDelay
: options.hideDelay
? getOption('showDelay')
: getOption('hideDelay')
timerId = setTimeout(() => {
tooltipContent.value = getContent(key)
debouncedHoveredElement.value = el
Expand Down
Loading