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
30 changes: 15 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,21 +110,21 @@ yarn add @untemps/svelte-use-tooltip

## API

| Props | Type | Default | Description |
|---------------------------|---------|---------|-----------------------------------------------------------------------------------------------------------------|
| `content` | string | null | Text content to display in the tooltip. |
| `contentSelector` | string | null | Selector of the content to display in the tooltip. |
| `contentClone` | boolean | null | Flag to clone the content to display in the tooltip. If false, the content is removed from its previous parent. |
| `contentActions` | object | null | Configuration of the tooltip actions (see [Content Actions](#content-actions)). |
| `containerClassName` | string | null | Class name to apply to the tooltip container. |
| `position` | string | 'top' | Position of the tooltip. Available values: 'top', 'bottom', 'left', 'right' |
| `animated` | boolean | false | Flag to animate tooltip transitions. |
| `animationEnterClassName` | string | null | Class name to apply to the tooltip enter transition. |
| `animationLeaveClassName` | string | null | Class name to apply to the tooltip leave transition. |
| `enterDelay` | number | 0 | Delay before showing the tooltip in milliseconds. |
| `leaveDelay` | number | 0 | Delay before hiding the tooltip in milliseconds. |
| `offset` | number | 10 | Distance between the tooltip and the target in pixels. |
| `disabled` | boolean | false | Flag to disable the tooltip content. |
| Props | Type | Default | Description |
|---------------------------|---------|-------------------|-----------------------------------------------------------------------------------------------------------------|
| `content` | string | null | Text content to display in the tooltip. |
| `contentSelector` | string | null | Selector of the content to display in the tooltip. |
| `contentClone` | boolean | false | Flag to clone the content to display in the tooltip. If false, the content is removed from its previous parent. |
| `contentActions` | object | null | Configuration of the tooltip actions (see [Content Actions](#content-actions)). |
| `containerClassName` | string | '__tooltip' | Class name to apply to the tooltip container. |
| `position` | string | 'top' | Position of the tooltip. Available values: 'top', 'bottom', 'left', 'right' |
| `animated` | boolean | false | Flag to animate tooltip transitions. |
| `animationEnterClassName` | string | '__tooltip-enter' | Class name to apply to the tooltip enter transition. |
| `animationLeaveClassName` | string | '__tooltip-leave' | Class name to apply to the tooltip leave transition. |
| `enterDelay` | number | 0 | Delay before showing the tooltip in milliseconds. |
| `leaveDelay` | number | 0 | Delay before hiding the tooltip in milliseconds. |
| `offset` | number | 10 | Distance between the tooltip and the target in pixels. |
| `disabled` | boolean | false | Flag to disable the tooltip content. |

### Content Actions

Expand Down
24 changes: 21 additions & 3 deletions src/Tooltip.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ class Tooltip {

#boundEnterHandler = null
#boundLeaveHandler = null
#boundKeyDownHandler = null

#target = null
#content = null
Expand Down Expand Up @@ -65,11 +66,11 @@ class Tooltip {

this.#observer = new DOMObserver()

this.#createTooltip()

this.#target.title = ''
this.#target.setAttribute('style', 'position: relative')

this.#createTooltip()
this.#tooltip.setAttribute('class', this.#containerClassName || `__tooltip __tooltip-${this.#position}`)
this.#target.setAttribute('aria-describedby', 'tooltip')

disabled ? this.#disableTarget() : this.#enableTarget()

Expand Down Expand Up @@ -141,21 +142,32 @@ class Tooltip {
#enableTarget() {
this.#boundEnterHandler = this.#onTargetEnter.bind(this)
this.#boundLeaveHandler = this.#onTargetLeave.bind(this)
this.#boundKeyDownHandler = this.#onTargetKeyDown.bind(this)

this.#target.addEventListener('mouseenter', this.#boundEnterHandler)
this.#target.addEventListener('mouseleave', this.#boundLeaveHandler)
this.#target.addEventListener('focusin', this.#boundEnterHandler)
this.#target.addEventListener('focusout', this.#boundLeaveHandler)
window.addEventListener('keydown', this.#boundKeyDownHandler)
}

#disableTarget() {
this.#target.removeEventListener('mouseenter', this.#boundEnterHandler)
this.#target.removeEventListener('mouseleave', this.#boundLeaveHandler)
this.#target.removeEventListener('focusin', this.#boundEnterHandler)
this.#target.removeEventListener('focusout', this.#boundLeaveHandler)
window.removeEventListener('keydown', this.#boundKeyDownHandler)

this.#boundEnterHandler = null
this.#boundLeaveHandler = null
this.#boundKeyDownHandler = null
}

#createTooltip() {
this.#tooltip = document.createElement('div')
this.#tooltip.setAttribute('id', 'tooltip')
this.#tooltip.setAttribute('class', this.#containerClassName || `__tooltip __tooltip-${this.#position}`)
this.#tooltip.setAttribute('role', 'tooltip')

if (this.#contentSelector) {
this.#observer
Expand Down Expand Up @@ -331,6 +343,12 @@ class Tooltip {
await this.#waitForDelay(this.#leaveDelay)
await this.#removeTooltipFromTarget()
}

async #onTargetKeyDown(e) {
if (e.key === 'Escape' || e.key === 'Esc' || e.keyCode === 27) {
await this.#onTargetLeave()
}
}
}

export default Tooltip
88 changes: 84 additions & 4 deletions src/__tests__/useTooltip.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,14 @@ describe('useTooltip', () => {
new Promise(async (resolve) => {
await fireEvent.mouseOver(target) // fireEvent.mouseEnter only works if mouseOver is triggered before
await fireEvent.mouseEnter(target)
await _sleep(10)
await _sleep(1)
resolve()
})

const _leave = async () =>
new Promise(async (resolve) => {
await fireEvent.mouseLeave(target)
await _sleep(10)
await _sleep(1)
resolve()
})

Expand All @@ -35,6 +35,34 @@ describe('useTooltip', () => {
resolve()
})

const _focus = async () =>
new Promise(async (resolve) => {
await fireEvent.focusIn(target)
await _sleep(1)
resolve()
})

const _blur = async () =>
new Promise(async (resolve) => {
await fireEvent.focusOut(target)
await _sleep(1)
resolve()
})

const _focusAndBlur = async () =>
new Promise(async (resolve) => {
await _focus()
await _blur()
resolve()
})

const _keyDown = async (key) =>
new Promise(async (resolve) => {
await fireEvent.keyDown(target, key || { key: 'Escape', code: 'Escape', charCode: 27 })
await _sleep(1)
resolve()
})

beforeEach(() => {
target = _createElement('target', { class: 'bar' })
template = _createElement('template')
Expand Down Expand Up @@ -78,6 +106,13 @@ describe('useTooltip', () => {
await _enterAndLeave()
expect(template).not.toBeInTheDocument()
})

it('Hides tooltip on escape key down', async () => {
action = useTooltip(target, options)
await _enter()
await _keyDown()
expect(template).not.toBeInTheDocument()
})
})

describe('update', () => {
Expand All @@ -95,6 +130,27 @@ describe('useTooltip', () => {
expect(newTemplate).toBeInTheDocument()
})
})

describe('focus', () => {
it('Shows tooltip on focus in', async () => {
action = useTooltip(target, options)
await _focus()
expect(template).toBeInTheDocument()
})

it('Hides tooltip on focus out', async () => {
action = useTooltip(target, options)
await _focusAndBlur()
expect(template).not.toBeInTheDocument()
})

it('Hides tooltip on escape key down', async () => {
action = useTooltip(target, options)
await _focus()
await _keyDown()
expect(template).not.toBeInTheDocument()
})
})
})

describe('useTooltip lifecycle', () => {
Expand All @@ -109,20 +165,44 @@ describe('useTooltip', () => {
describe('useTooltip props: content', () => {
it('Displays text content', async () => {
const content = 'Foo'
action = useTooltip(target, { ...options, contentSelector: null, content })
action = useTooltip(target, {
...options,
contentSelector: null,
content,
})
await _enter()
expect(target).toHaveTextContent(content)
})

it('Displays content element over text', async () => {
const content = 'Foo'
action = useTooltip(target, { ...options, content })
action = useTooltip(target, {
...options,
content,
})
await _enter()
expect(target).not.toHaveTextContent(content)
expect(template).toBeInTheDocument()
})
})

describe('useTooltip props: contentClone', () => {
it('Does not clone content element', async () => {
action = useTooltip(target, options)
await _sleep(1)
expect(template).not.toBeVisible()
})

it('Clones content element', async () => {
action = useTooltip(target, {
...options,
contentClone: true,
})
await _sleep(1)
expect(template).toBeVisible()
})
})

describe('useTooltip props: contentActions', () => {
it('Triggers callback on tooltip click', async () => {
action = useTooltip(target, options)
Expand Down