UNPKG

visbug-lib

Version:

<p align="center"> <img src="./assets/visbug.png" width="300" height="300" alt="visbug"> <br> <a href="https://www.npmjs.org/package/visbug"><img src="https://img.shields.io/npm/v/visbug.svg?style=flat" alt="npm latest version number"></a> <a href

242 lines (197 loc) 5.94 kB
import $ from 'blingblingjs' import hotkeys from 'hotkeys-js' import { TinyColor, readability, isReadable } from '@ctrl/tinycolor' import { getStyle, getStyles, isOffBounds, getA11ys, getWCAG2TextSize, getComputedBackgroundColor, deepElementFromPoint } from '../utilities/' const state = { active: { tip: null, target: null, }, tips: new Map(), } export function Accessibility() { $('body').on('mousemove', mouseMove) $('body').on('click', togglePinned) hotkeys('esc', _ => removeAll()) restorePinnedTips() return () => { $('body').off('mousemove', mouseMove) $('body').off('click', togglePinned) hotkeys.unbind('esc') hideAll() } } const mouseMove = e => { const target = deepElementFromPoint(e.clientX, e.clientY) if (isOffBounds(target) || target.nodeName.toUpperCase() === 'SVG' || target.nodeName === 'VISBUG-ALLYTIP' || target.hasAttribute('data-allytip')) { // aka: mouse out if (state.active.tip) { wipe({ tip: state.active.tip, e: {target: state.active.target}, }) clearActive() } return } toggleTargetCursor(e.altKey, target) showTip(target, e) } export function showTip(target, e) { if (!state.active.tip) { // create const tip = render(target) document.body.appendChild(tip) positionTip(tip, e) observe({tip, target}) state.active.tip = tip state.active.target = target } else if (target == state.active.target) { // update position // update position positionTip(state.active.tip, e) } else { // update content render(target, state.active.tip) state.active.target = target positionTip(state.active.tip, e) } } export function positionTip(tip, e) { const { north, west } = mouse_quadrant(e) const {left, top} = tip_position(tip, e, north, west) tip.style.left = left tip.style.top = top tip.style.setProperty('--arrow', north ? 'var(--arrow-up)' : 'var(--arrow-down)') tip.style.setProperty('--shadow-direction', north ? 'var(--shadow-up)' : 'var(--shadow-down)') tip.style.setProperty('--arrow-top', !north ? '-7px' : 'calc(100% - 1px)') tip.style.setProperty('--arrow-left', west ? 'calc(100% - 15px - 15px)' : '15px') } const restorePinnedTips = () => { state.tips.forEach(({tip}, target) => { tip.style.display = 'block' render(target, tip) observe({tip, target}) }) } export function hideAll() { state.tips.forEach(({tip}, target) => tip.style.display = 'none') if (state.active.tip) { state.active.tip.remove() clearActive() } } export function removeAll() { state.tips.forEach(({tip}, target) => { tip.remove() unobserve({tip, target}) }) $('[data-allytip]').attr('data-allytip', null) state.tips.clear() } const render = (el, tip = document.createElement('visbug-ally')) => { const contrast_results = determineColorContrast(el) const ally_attributes = getA11ys(el) ally_attributes.map(ally => ally.prop.includes('alt') ? ally.value = `<span text>${ally.value}</span>` : ally) ally_attributes.map(ally => ally.prop.includes('title') ? ally.value = `<span text longform>${ally.value}</span>` : ally) tip.meta = { el, ally_attributes, contrast_results, } return tip } const determineColorContrast = el => { // question: how to know if the current node is actually a black background? // question: is there an api for composited values? const foreground = el instanceof SVGElement ? (getStyle(el, 'fill') || getStyle(el, 'stroke')) : getStyle(el, 'color') const textSize = getWCAG2TextSize(el) let background = getComputedBackgroundColor(el) const [ aa_contrast, aaa_contrast ] = [ isReadable(background, foreground, { level: "AA", size: textSize.toLowerCase() }), isReadable(background, foreground, { level: "AAA", size: textSize.toLowerCase() }) ] return ` <span prop>Color contrast</span> <span value contrast> <span style=" background-color:${background}; color:${foreground}; ">${Math.floor(readability(background, foreground) * 100) / 100}</span> </span> <span prop>› AA ${textSize}</span> <span value style="${aa_contrast ? 'color:green;' : 'color:red'}">${aa_contrast ? '✓' : '×'}</span> <span prop>› AAA ${textSize}</span> <span value style="${aaa_contrast ? 'color:green;' : 'color:red'}">${aaa_contrast ? '✓' : '×'}</span> ` } const mouse_quadrant = e => ({ north: e.clientY > window.innerHeight / 2, west: e.clientX > window.innerWidth / 2 }) const tip_position = (node, e, north, west) => ({ top: `${north ? e.pageY - node.clientHeight - 20 : e.pageY + 25}px`, left: `${west ? e.pageX - node.clientWidth + 23 : e.pageX - 21}px`, }) const handleBlur = ({target}) => { if (!target.hasAttribute('data-allytip') && state.tips.has(target)) wipe(state.tips.get(target)) } const wipe = ({tip, e:{target}}) => { tip.remove() unobserve({tip, target}) state.tips.delete(target) } const togglePinned = e => { const target = deepElementFromPoint(e.clientX, e.clientY) if (e.altKey && !target.hasAttribute('data-allytip')) { target.setAttribute('data-allytip', true) state.tips.set(target, { tip: state.active.tip, e, }) clearActive() } else if (target.hasAttribute('data-allytip')) { target.removeAttribute('data-allytip') wipe(state.tips.get(target)) } } const toggleTargetCursor = (key, target) => key ? target.setAttribute('data-pinhover', true) : target.removeAttribute('data-pinhover') const observe = ({tip, target}) => { $(target).on('DOMNodeRemoved', handleBlur) } const unobserve = ({tip, target}) => { $(target).off('DOMNodeRemoved', handleBlur) } const clearActive = () => { state.active.tip = null state.active.target = null }