Skip to content
This repository has been archived by the owner on Dec 24, 2023. It is now read-only.

Commit

Permalink
feat: add table lines method
Browse files Browse the repository at this point in the history
  • Loading branch information
wswebcreation committed Feb 23, 2020
1 parent 6c9151d commit 482caad
Show file tree
Hide file tree
Showing 2 changed files with 354 additions and 0 deletions.
329 changes: 329 additions & 0 deletions lib/clientSideScripts/createTabbableLines.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,329 @@
import {CircleOptions, ElementCoordinate, LineOptions, TabbableOptions} from "./tabbableOptions.interfaces";

// based on this https://vivrichards.co.uk/accessibility/automating-page-tab-flows-using-visual-testing-and-javascript
export default function drawTabbableOnCanvas(options: TabbableOptions) {
const defaultOptions: TabbableOptions = {
circle: {
backgroundColor: '#ff0000',
borderColor: '#000',
borderWidth: 1,
fontColor: '#fff',
fontFamily: 'Arial',
fontSize: 10,
size: 10,
showNumber: true,
},
line: {
color: '#000',
width: 1,
},
};
const drawOptions = {...defaultOptions, ...options};

// 1. Scroll to top of page
window.scrollTo(0, 0);

// 2. Insert canvas
const width = window.innerWidth;
const height = getDocumentScrollHeight();
const canvasNode = `<canvas id="tabCanvas" width="${width}" height="${height}" style="position:absolute;top:0;left:0;z-index:999999;">`;
document.body.insertAdjacentHTML('afterbegin', canvasNode);

// 3. Get all the elements
const accessibleElements = tabbable();

// 4a. Iterate over all accessibleElements and get the coordinates
const elementCoordinates: ElementCoordinate[] = accessibleElements.map(node => {
const currentElement = node.getBoundingClientRect();

return {
x: currentElement.left + (currentElement.width / 2),
y: currentElement.top + (currentElement.height / 2),
}
});
// 4b. Add the starting coordinates
elementCoordinates.unshift({x: 0, y: 0});
// 4c. Iterate over all coordinates and draw lines and circles
elementCoordinates.forEach((elementCoordinate, i) => {
if (i === 0) {
return;
}

drawLine(drawOptions.line, elementCoordinates[i - 1], elementCoordinate);
drawCircleAndNumber(drawOptions.circle, elementCoordinate, i);
});

/**
* Draw a line
*/
function drawLine(options: LineOptions, start: ElementCoordinate, end: ElementCoordinate): void {
const tabbableCanvasContext = (<HTMLCanvasElement>document.getElementById("tabCanvas")).getContext("2d");

// Draw the line
tabbableCanvasContext.beginPath();
tabbableCanvasContext.globalCompositeOperation = 'destination-over';
tabbableCanvasContext.lineWidth = options.width;
tabbableCanvasContext.strokeStyle = options.color;
tabbableCanvasContext.moveTo(start.x, start.y);
tabbableCanvasContext.lineTo(end.x, end.y);
tabbableCanvasContext.stroke();
}

/**
* Draw a circle
*/
function drawCircleAndNumber(options: CircleOptions, position: ElementCoordinate, i: number): void {
const tabbableCanvasContext = (<HTMLCanvasElement>document.getElementById("tabCanvas")).getContext("2d");

// Draw circle
tabbableCanvasContext.beginPath();
tabbableCanvasContext.globalCompositeOperation = 'source-over';
tabbableCanvasContext.fillStyle = options.backgroundColor;
tabbableCanvasContext.arc(position.x, position.y, options.size, 0, Math.PI * 2, true);
tabbableCanvasContext.fill();
// Draw border
tabbableCanvasContext.lineWidth = options.borderWidth;
tabbableCanvasContext.strokeStyle = options.borderColor;
tabbableCanvasContext.stroke();

if (options.showNumber) {
// Set the text
tabbableCanvasContext.font = `${options.fontSize}px ${options.fontFamily}`;
tabbableCanvasContext.textAlign = 'center';
tabbableCanvasContext.textBaseline = 'middle';
tabbableCanvasContext.fillStyle = options.fontColor;
tabbableCanvasContext.fillText(i.toString(), position.x, position.y);
}
}

/**
* This is coming from https://github.com/davidtheclark/tabbable
* and is modified a bit to work inside the browser.
* The original module couldn't be used for injection
*/

/**
* Get all tabbable elements based on tabindex and then regular dom order
*/
function tabbable(): HTMLElement[] {
let regularTabbables = [];
let orderedTabbables = [];
const candidateSelectors = [
'input',
'select',
'textarea',
'a[href]',
'button',
'[tabindex]',
'audio[controls]',
'video[controls]',
'[contenteditable]:not([contenteditable="false"])',
].join(',');
let candidates: NodeListOf<HTMLElement> = document.querySelectorAll(candidateSelectors);

for (let i = 0; i < candidates.length; i++) {
const candidate = candidates[i];

if (!isNodeMatchingSelectorTabbable(candidate)) {
continue;
}

const candidateTabindex = getTabindex(candidate);

if (candidateTabindex === 0) {
regularTabbables.push(candidate);
} else {
orderedTabbables.push({
documentOrder: i,
tabIndex: candidateTabindex,
node: candidate,
});
}
}

// This is so bad :(, fix typings!!!
return [...(orderedTabbables.sort(<any>sortOrderedTabbables).map(a => a.node).concat(regularTabbables))];
}

/**
* Is the node tabbable
*/
function isNodeMatchingSelectorTabbable(node: HTMLElement): boolean {
return !(
!isNodeMatchingSelectorFocusable(node) ||
isNonTabbableRadio(node) ||
getTabindex(node) < 0
);
}

/**
* Check if the node has a focused state
*/
function isNodeMatchingSelectorFocusable(node: HTMLElement): boolean {
return !(
(node.hasAttribute('disabled') && node.getAttribute('disabled'))
|| isHiddenInput(node)
|| isHidden(node)
);
}

/**
* Get the tab index of the node
*/
function getTabindex(node: HTMLElement): number {
const tabindexAttr = parseInt(node.getAttribute('tabindex'), 10);

if (!isNaN(tabindexAttr)) {
return tabindexAttr;
}
// Browsers do not return `tabIndex` correctly for contentEditable nodes;
// so if they don't have a tabindex attribute specifically set, assume it's 0.
if (isContentEditable(node)) {
return 0;
}

return node.tabIndex;
}

/**
* Return ordered tabbable nodes
*/
function sortOrderedTabbables(nodeA: HTMLElement, nodeB: HTMLElement): number {
return nodeA.tabIndex === nodeB.tabIndex
// This is so bad :(, fix this!
? (<any>nodeA).documentOrder - (<any>nodeB).documentOrder
: nodeA.tabIndex - nodeB.tabIndex;
}

/**
* Is the content editable
*/
function isContentEditable(node: HTMLElement): boolean {
return node.contentEditable === 'true';
}

/**
* Is the node an input
*/
function isInput(node: HTMLElement): boolean {
return node.tagName === 'INPUT';
}

/**
* Is the input hidden
*/
function isHiddenInput(node: HTMLElement): boolean {
return isInput(node) && (<HTMLInputElement>node).type === 'hidden';
}

/**
* Is the node a radio input
*/
function isRadio(node: HTMLElement): boolean {
return isInput(node) && (<HTMLInputElement>node).type === 'radio';
}

/**
* Is the node a radio input and can it be tabbed
*/
function isNonTabbableRadio(node: HTMLElement): boolean {
return isRadio(node) && !isTabbableRadio(<HTMLInputElement>node);
}

/**
* Get the checked radio input
*/
// @ts-ignore
function getCheckedRadio(nodes: HTMLInputElement[]) {
for (let i = 0; i < nodes.length; i++) {
if (nodes[i].checked) {
return nodes[i];
}
}
}

/**
* Is the radio input tabbable
*/
function isTabbableRadio(node: HTMLInputElement):boolean {
if (!node.name) {
return true;
}
// This won't account for the edge case where you have radio groups with the same
// in separate forms on the same page.
// This is bad :(, but don't know how to fix this typing
let radioSet = (<any>node.ownerDocument).querySelectorAll(
'input[type="radio"][name="' + node.name + '"]'
);
let checked = getCheckedRadio(radioSet);

return !checked || checked === node;
}

/**
* Is the node hidden
*/
function isHidden(node: HTMLElement): boolean {
// offsetParent being null will allow detecting cases where an element is invisible or inside an invisible element,
// as long as the element does not use position: fixed. For them, their visibility has to be checked directly as well.
return (
node.offsetParent === null || getComputedStyle(node).visibility === 'hidden'
);
}

/**
* Get the document scroll height
*/
function getDocumentScrollHeight(): number {
const viewPortHeight = Math.max(document.documentElement.clientHeight, window.innerHeight || 0);
const scrollHeight = document.documentElement.scrollHeight;
const bodyScrollHeight = document.body.scrollHeight;

// In some situations the default scrollheight can be equal to the viewport height
// but the body scroll height can be different, then return that one
if ((viewPortHeight === scrollHeight) && (bodyScrollHeight > scrollHeight)) {
return bodyScrollHeight;
}

// In some cases we can have a challenge determining the height of the page
// due to for example a `vh` property on the body element.
// If that is the case we need to walk over all the elements and determine the highest element
// this is a very time consuming thing, so our last hope :(
let pageHeight = 0;
let largestNodeElement = document.querySelector('body');

if (bodyScrollHeight === scrollHeight && bodyScrollHeight === viewPortHeight) {
findHighestNode(document.documentElement.childNodes);

// There could be some elements above this largest element,
// add that on top
return pageHeight + largestNodeElement.getBoundingClientRect().top;
}

// The scrollHeight is good enough
return scrollHeight;

/**
* Find the largest html element on the page
*/
// This is so bad :(, fix the typings!!!
function findHighestNode(nodesList: any) {
for (let i = nodesList.length - 1; i >= 0; i--) {
const currentNode = nodesList[i];

/* istanbul ignore next */
if (currentNode.scrollHeight && currentNode.clientHeight) {
const elHeight = Math.max(currentNode.scrollHeight, currentNode.clientHeight);
pageHeight = Math.max(elHeight, pageHeight);
if (elHeight === pageHeight) {
largestNodeElement = currentNode;
}
}

if (currentNode.childNodes.length) {
findHighestNode(currentNode.childNodes);
}
}
}
}
}
25 changes: 25 additions & 0 deletions lib/clientSideScripts/tabbableOptions.interfaces.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
export interface TabbableOptions {
circle?: CircleOptions;
line?: LineOptions;
}

export interface CircleOptions {
backgroundColor?: string;
borderColor?: string;
borderWidth?: number;
fontColor?: string;
fontFamily?: string;
fontSize?: number;
size?: number;
showNumber?: boolean;
}

export interface LineOptions {
color?: string;
width?: number;
}

export interface ElementCoordinate {
x: number;
y: number;
}

0 comments on commit 482caad

Please sign in to comment.