Compare commits

...

3 Commits

Author SHA1 Message Date
Ryan Berliner
0c68e1cac9
Merge 53d64b595cefac3bee8f94ff0349b6b42f6f54ce into 4189b3075c003b2d5dc9335195be7b750dfc2526 2025-10-01 08:45:01 +02:00
dependabot[bot]
4189b3075c
Build(deps): Bump github/codeql-action in the github-actions group (#41782)
Bumps the github-actions group with 1 update: [github/codeql-action](https://github.com/github/codeql-action).


Updates `github/codeql-action` from 3.30.3 to 3.30.5
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](192325c861...3599b3baa1)

---
updated-dependencies:
- dependency-name: github/codeql-action
  dependency-version: 3.30.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: github-actions
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-09-30 18:01:50 +02:00
Ryan Berliner
53d64b595c tooltip accessibility (squashed + refactored) 2024-07-08 19:38:10 -04:00
4 changed files with 131 additions and 16 deletions

View File

@ -29,16 +29,16 @@ jobs:
persist-credentials: false
- name: Initialize CodeQL
uses: github/codeql-action/init@192325c86100d080feab897ff886c34abd4c83a3 # v3.30.3
uses: github/codeql-action/init@3599b3baa15b485a2e49ef411a7a4bb2452e7f93 # v3.30.5
with:
config-file: ./.github/codeql/codeql-config.yml
languages: "javascript"
queries: +security-and-quality
- name: Autobuild
uses: github/codeql-action/autobuild@192325c86100d080feab897ff886c34abd4c83a3 # v3.30.3
uses: github/codeql-action/autobuild@3599b3baa15b485a2e49ef411a7a4bb2452e7f93 # v3.30.5
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@192325c86100d080feab897ff886c34abd4c83a3 # v3.30.3
uses: github/codeql-action/analyze@3599b3baa15b485a2e49ef411a7a4bb2452e7f93 # v3.30.5
with:
category: "/language:javascript"

View File

@ -73,6 +73,6 @@ jobs:
# Upload the results to GitHub's code scanning dashboard (optional).
# Commenting out will disable upload of results to your repo's Code Scanning dashboard
- name: "Upload to code-scanning"
uses: github/codeql-action/upload-sarif@192325c86100d080feab897ff886c34abd4c83a3 # v3.30.3
uses: github/codeql-action/upload-sarif@3599b3baa15b485a2e49ef411a7a4bb2452e7f93 # v3.30.5
with:
sarif_file: results.sarif

View File

@ -35,6 +35,7 @@ const TRIGGER_HOVER = 'hover'
const TRIGGER_FOCUS = 'focus'
const TRIGGER_CLICK = 'click'
const TRIGGER_MANUAL = 'manual'
const ESCAPE_KEY = 'Escape'
const EVENT_HIDE = 'hide'
const EVENT_HIDDEN = 'hidden'
@ -46,6 +47,7 @@ const EVENT_FOCUSIN = 'focusin'
const EVENT_FOCUSOUT = 'focusout'
const EVENT_MOUSEENTER = 'mouseenter'
const EVENT_MOUSELEAVE = 'mouseleave'
const EVENT_KEYDOWN_DISMISS = 'keydown.dismiss'
const AttachmentMap = {
AUTO: 'auto',
@ -205,11 +207,38 @@ class Tooltip extends BaseComponent {
this._element.setAttribute('aria-describedby', tip.getAttribute('id'))
const { container } = this._config
const { container, _trigger } = this._config
if (!this._element.ownerDocument.documentElement.contains(this.tip)) {
container.append(tip)
EventHandler.trigger(this._element, this.constructor.eventName(EVENT_INSERTED))
if (_trigger !== TRIGGER_MANUAL) {
this._maybeDismissHandler = event => {
if (event.key !== ESCAPE_KEY || !this._isWithActiveTrigger()) {
return
}
event.preventDefault()
event.stopPropagation()
this.hide()
}
EventHandler.on(document, this.constructor.eventName(EVENT_KEYDOWN_DISMISS), '*', this._maybeDismissHandler)
if (_trigger !== TRIGGER_CLICK) {
this._tipEventOut = event => {
this._activeTrigger[event.type === 'focusout' ? TRIGGER_FOCUS : TRIGGER_HOVER] = this._isInside(event.relatedTarget)
this._leave()
}
for (const trigger of _trigger.split(' ')) {
const [, eventOut] = this._getTriggerEvents(trigger)
EventHandler.on(tip, eventOut, this._tipEventOut)
}
}
}
}
this._popper = this._createPopper(tip)
@ -452,12 +481,7 @@ class Tooltip extends BaseComponent {
context.toggle()
})
} else if (trigger !== TRIGGER_MANUAL) {
const eventIn = trigger === TRIGGER_HOVER ?
this.constructor.eventName(EVENT_MOUSEENTER) :
this.constructor.eventName(EVENT_FOCUSIN)
const eventOut = trigger === TRIGGER_HOVER ?
this.constructor.eventName(EVENT_MOUSELEAVE) :
this.constructor.eventName(EVENT_FOCUSOUT)
const [eventIn, eventOut] = this._getTriggerEvents(trigger)
EventHandler.on(this._element, eventIn, this._config.selector, event => {
const context = this._initializeOnDelegatedTarget(event)
@ -466,8 +490,7 @@ class Tooltip extends BaseComponent {
})
EventHandler.on(this._element, eventOut, this._config.selector, event => {
const context = this._initializeOnDelegatedTarget(event)
context._activeTrigger[event.type === 'focusout' ? TRIGGER_FOCUS : TRIGGER_HOVER] =
context._element.contains(event.relatedTarget)
context._activeTrigger[event.type === 'focusout' ? TRIGGER_FOCUS : TRIGGER_HOVER] = this._isInside(event.relatedTarget)
context._leave()
})
@ -536,6 +559,23 @@ class Tooltip extends BaseComponent {
return Object.values(this._activeTrigger).includes(true)
}
_isInside(el) {
return this._element.contains(el) || (this.tip && this.tip.contains(el))
}
_getTriggerEvents(trigger) {
return {
[TRIGGER_HOVER]: [
this.constructor.eventName(EVENT_MOUSEENTER),
this.constructor.eventName(EVENT_MOUSELEAVE)
],
[TRIGGER_FOCUS]: [
this.constructor.eventName(EVENT_FOCUSIN),
this.constructor.eventName(EVENT_FOCUSOUT)
]
}[trigger]
}
_getConfig(config) {
const dataAttributes = Manipulator.getDataAttributes(this._element)
@ -558,6 +598,11 @@ class Tooltip extends BaseComponent {
_configAfterMerge(config) {
config.container = config.container === false ? document.body : getElement(config.container)
// To support delegated tooltip events, tooltips created on the fly have their trigger config
// set to manual. This means that it's particularly difficult to check what triggers a delegated
// tooltip. This property stores the "original" trigger config for easy future reference.
config._trigger = config._trigger || config.trigger
if (typeof config.delay === 'number') {
config.delay = {
show: config.delay,
@ -600,10 +645,25 @@ class Tooltip extends BaseComponent {
this._popper = null
}
if (this.tip) {
this.tip.remove()
this.tip = null
if (!this.tip) {
return
}
const { _trigger } = this._config
if (_trigger !== TRIGGER_MANUAL) {
EventHandler.off(document, this.constructor.eventName(EVENT_KEYDOWN_DISMISS), '*', this._maybeDismissHandler)
if (_trigger !== TRIGGER_CLICK) {
for (const trigger of _trigger.split(' ')) {
const [, eventOut] = this._getTriggerEvents(trigger)
EventHandler.on(this.tip, eventOut, this._tipEventOut)
}
}
}
this.tip.remove()
this.tip = null
}
// Static

View File

@ -781,6 +781,35 @@ describe('Tooltip', () => {
})
})
it('should not hide a tooltip if cursor moves to tip element', done => {
fixtureEl.innerHTML = [
'<a href="#" rel="tooltip" title="tooltip">',
'trigger',
'</a>'
]
const tooltipEl = fixtureEl.querySelector('a')
const tooltip = new Tooltip(tooltipEl)
spyOn(tooltip, 'hide').and.callThrough()
tooltipEl.addEventListener('shown.bs.tooltip', () => {
const moveMouseToTipElementEvent = createEvent('mouseout')
Object.defineProperty(moveMouseToTipElementEvent, 'relatedTarget', {
value: tooltip._getTipElement()
})
tooltipEl.dispatchEvent(moveMouseToTipElementEvent)
})
tooltipEl.addEventListener('mouseout', () => {
expect(tooltip.hide).not.toHaveBeenCalled()
done()
})
tooltipEl.dispatchEvent(createEvent('mouseover'))
})
it('should not hide tooltip if leave event occurs and interaction remains inside trigger', () => {
return new Promise(resolve => {
fixtureEl.innerHTML = [
@ -1024,6 +1053,32 @@ describe('Tooltip', () => {
})
})
it('should hide a tooltip when escape key is pressed and active trigger', done => {
fixtureEl.innerHTML = [
'<a href="#" rel="tooltip" title="tooltip">',
'trigger',
'</a>'
]
const tooltipEl = fixtureEl.querySelector('a')
const tooltip = new Tooltip(tooltipEl)
spyOn(tooltip, 'hide').and.callThrough()
tooltipEl.addEventListener('shown.bs.tooltip', () => {
const keydownEscape = createEvent('keydown')
keydownEscape.key = 'Escape'
tooltipEl.dispatchEvent(keydownEscape)
})
tooltipEl.addEventListener('hidden.bs.tooltip', () => {
expect(tooltip.hide).toHaveBeenCalled()
done()
})
tooltipEl.dispatchEvent(createEvent('mouseover'))
})
it('should not hide a tooltip if hide event is prevented', () => {
return new Promise((resolve, reject) => {
fixtureEl.innerHTML = '<a href="#" rel="tooltip" title="Another tooltip"></a>'