Skip to content

Commit 2a412e6

Browse files
authored
Merge pull request #199 from micmro/188
#188 make chart keyboard accessible.
2 parents ebeb95b + caf8f25 commit 2a412e6

13 files changed

+375
-147
lines changed

Diff for: src/css-raw/perf-cascade.css

+5-2
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@
9595
.block-undefined {fill: #0f0;}
9696

9797
/* Info overlay SVG - wrapper */
98-
.info-overlay {fill: #fff; stroke: #cdcdcd;}
98+
.info-overlay-bg {fill: #fff; stroke: #cdcdcd;}
9999
.info-overlay-close-btn {fill: rgba(205, 205, 205, 0.8); transform: translate(-23px, -23px); cursor: pointer;}
100100
.info-overlay-close-btn text {fill: #111; pointer-events: none;}
101101
.info-overlay-close-btn:focus {border: solid 1px #36c;}
@@ -149,8 +149,11 @@
149149
.info-overlay-holder .tab-nav li {margin: 0; padding: 0; display: inline-block;}
150150
.info-overlay-holder button { background: transparent; outline:0; border:0; border-bottom: solid 2px transparent; padding: 0.5em 1em; margin:0 0.25em;}
151151
.info-overlay-holder li:first-child button {margin-left: 1em;}
152-
.info-overlay-holder button:focus,.info-overlay-holder button:hover {border-color: rgba(255,255,255, 0.6);}
152+
.info-overlay-holder button:focus,
153+
.info-overlay-holder button.active:focus,
154+
.info-overlay-holder button:hover {border-color: rgba(255,255,255, 0.6);}
153155
.info-overlay-holder button.active {border-color: #fff; cursor: default;}
156+
.info-overlay-holder button.active:focus {border-color: rgba(255,255,255, 0.8);}
154157

155158
/* Info overlay HTML - content */
156159
.info-overlay-holder dt {float: left; clear: both; margin-top: 0.5em; width: 25%; text-align: right; font-weight: bold; }

Diff for: src/ts/helpers/dom.ts

+31
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,21 @@ export function removeClass<T extends Element>(el: T, className: string): T {
3131
return el;
3232
}
3333

34+
/**
35+
* Helper to recursively find parent with the `className` class
36+
* @param base `Element` to start from
37+
* @param className class that the parent should have
38+
*/
39+
export function getParentByClassName(base: Element, className: string) {
40+
if (base.parentElement === undefined) {
41+
return undefined;
42+
}
43+
if (base.parentElement.classList.contains(className)) {
44+
return base.parentElement;
45+
}
46+
return getParentByClassName(base.parentElement, className);
47+
};
48+
3449
/**
3550
* Removes all child DOM nodes from `el`
3651
* @param {Element} el
@@ -41,3 +56,19 @@ export function removeChildren<T extends Element>(el: T): T {
4156
}
4257
return el;
4358
}
59+
60+
/**
61+
* Get last element of `NodeList`
62+
* @param list NodeListOf e.g. return value of `getElementsByClassName`
63+
*/
64+
export function getLastItemOfNodeList<T extends Node>(list: NodeListOf<T>) {
65+
if (!list || list.length === 0) {
66+
return undefined;
67+
}
68+
return list.item(list.length - 1);
69+
}
70+
71+
// /** Calls `fn` with each element of `els` */
72+
export function forEachNodeList<T extends Node>(els: NodeListOf<T>, fn: (el: T, index: number) => any) {
73+
Array.prototype.forEach.call(els, fn);
74+
}

Diff for: src/ts/helpers/misc.ts

+49
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,39 @@ export function contains<T>(arr: T[], item: T): boolean {
2727
return arr.some((x) => x === item);
2828
}
2929

30+
/**
31+
* Returns Index of first match to `predicate` in `arr`
32+
* @param arr Array to search
33+
* @param predicate Function that returns true for a match
34+
*/
35+
export function findIndex<T>(arr: T[], predicate: {(el: T, index: number): Boolean}) {
36+
let i = 0;
37+
if (!arr || arr.length < 1) {
38+
return undefined;
39+
}
40+
const len = arr.length;
41+
while (i < len) {
42+
if (predicate(arr[i], i)) {
43+
return i;
44+
}
45+
i++;
46+
}
47+
return undefined;
48+
}
49+
50+
/**
51+
* Returns first match to `predicate` in `arr`
52+
* @param arr Array to search
53+
* @param predicate Function that returns true for a match
54+
*/
55+
export function find<T>(arr: T[], predicate: {(el: T, index: number): Boolean}) {
56+
const index = findIndex(arr, predicate);
57+
if (index === undefined) {
58+
return undefined;
59+
}
60+
return arr[index];
61+
}
62+
3063
/**
3164
* Formats and shortens a url for ui
3265
* @param {string} url
@@ -95,3 +128,19 @@ export function toCssClass(seed: string) {
95128
export function pluralize(word: string, count: number) {
96129
return word + (count > 1 ? "s" : "");
97130
}
131+
132+
/**
133+
* Check if event is `tab` + `shift` key, to move to previous input element
134+
* @param {KeyboardEvent} evt Keyboard event
135+
*/
136+
export function isTabUp(evt: KeyboardEvent) {
137+
return evt.which === 9 && evt.shiftKey;
138+
}
139+
140+
/**
141+
* Check if event is only `tab` key, to move to next input element
142+
* @param {KeyboardEvent} evt Keyboard event
143+
*/
144+
export function isTabDown(evt: KeyboardEvent) {
145+
return evt.which === 9 && !evt.shiftKey;
146+
}

Diff for: src/ts/helpers/svg.ts

+13-5
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,7 @@ let getTestSVGEl = (() => {
132132
clearTimeout(removeSvgTestElTimeout);
133133
removeSvgTestElTimeout = setTimeout(() => {
134134
svgTestEl.parentNode.removeChild(svgTestEl);
135-
}, 1000);
135+
}, 500);
136136

137137
return svgTestEl;
138138
};
@@ -146,15 +146,23 @@ let getTestSVGEl = (() => {
146146
*/
147147
export function getNodeTextWidth(textNode: SVGTextElement, skipClone: boolean = false): number {
148148
let tmp = getTestSVGEl();
149-
let tmpTextNode;
149+
let tmpTextNode: SVGTextElement;
150+
let shadow;
150151
if (skipClone) {
152+
shadow = textNode.style.textShadow;
151153
tmpTextNode = textNode;
152154
} else {
153-
tmpTextNode = textNode.cloneNode(false) as SVGTextElement;
155+
tmpTextNode = textNode.cloneNode(true) as SVGTextElement;
156+
tmpTextNode.setAttribute("x", "0");
157+
tmpTextNode.setAttribute("y", "0");
154158
}
155-
tmp.appendChild(tmpTextNode);
156159
// make sure to turn of shadow for performance
157160
tmpTextNode.style.textShadow = "0";
161+
tmp.appendChild(tmpTextNode);
158162
window.document.body.appendChild(tmp);
159-
return tmpTextNode.getBBox().width;
163+
const width = tmpTextNode.getBBox().width;
164+
if (skipClone && shadow !== undefined) {
165+
textNode.style.textShadow = shadow;
166+
}
167+
return width;
160168
}

Diff for: src/ts/typing/context.ts

+5-27
Original file line numberDiff line numberDiff line change
@@ -1,42 +1,20 @@
1-
import {OverlayChangeEvent, OverlayChangeSubscriber} from "./open-overlay";
2-
import {ChartRenderOption} from "./options";
3-
import {WaterfallEntry} from "./waterfall";
1+
import { OverlayManager } from "../waterfall/details-overlay/overlay-manager";
2+
import { PubSub } from "../waterfall/details-overlay/pub-sub";
3+
import { ChartRenderOption } from "./options";
44

55
/**
66
* Context object that is passed to (usually stateless) child-functions
77
* to inject state and dependencies
88
*/
99
export interface Context {
1010
/** Publish and Subscribe instance for overlay updates */
11-
pubSub: PubSubClass;
11+
pubSub: PubSub;
1212
/** Overlay (popup) instance manager */
13-
overlayManager: OverlayManagerClass;
13+
overlayManager: OverlayManager;
1414
/** horizontal unit (duration in ms of 1%) */
1515
unit: number;
1616
/** height of the requests part of the diagram in px */
1717
diagramHeight: number;
1818
/** Chart config/customization options */
1919
options: ChartRenderOption;
2020
}
21-
22-
export interface PubSubClass {
23-
subscribeToOverlayChanges: (fn: OverlayChangeSubscriber) => void;
24-
publishToOverlayChanges: (change: OverlayChangeEvent) => void;
25-
}
26-
27-
export interface OverlayManagerClass {
28-
/** all open overlays height combined */
29-
getCombinedOverlayHeight: () => number;
30-
31-
/** Opens an overlay - rerenders others */
32-
openOverlay: (index: number, y: number, detailsHeight: number, entry: WaterfallEntry,
33-
barEls: SVGGElement[]) => void;
34-
/** toggles an overlay - rerenders others */
35-
toggleOverlay: (index: number, y: number, detailsHeight: number, entry: WaterfallEntry,
36-
barEls: SVGGElement[]) => void;
37-
38-
/** closes on overlay - rerenders others internally */
39-
closeOverlay: (index: number, detailsHeight: number, barEls: SVGGElement[]) => void;
40-
41-
// constructor(context: Context, overlayHolder: SVGGElement);
42-
}

Diff for: src/ts/typing/open-overlay.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,8 @@ export interface OpenOverlay {
2222

2323
export interface OverlayChangeEvent {
2424
type: EventType;
25-
/** list of currenly open overlays */
26-
openOverlays: OpenOverlay[];
25+
/** index that triggerd the change */
26+
changedIndex?: number;
2727
combinedOverlayHeight: number;
2828
}
2929

Diff for: src/ts/waterfall/details-overlay/html-details-body.ts

+11-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,14 @@
11
import { escapeHtml, sanitizeUrlForLink } from "../../helpers/parse";
22
import { WaterfallEntry } from "../../typing/waterfall";
33

4+
/**
5+
* Creates the HTML body for the overlay
6+
*
7+
* _All tabable elements are set to `tabindex="-1"` to avoid tabing issues_
8+
* @param requestID ID
9+
* @param detailsHeight
10+
* @param entry
11+
*/
412
export function createDetailsBody(requestID: number, detailsHeight: number, entry: WaterfallEntry) {
513

614
let html = document.createElement("html") as HTMLHtmlElement;
@@ -33,7 +41,9 @@ export function createDetailsBody(requestID: number, detailsHeight: number, entr
3341
body.innerHTML = `
3442
<div class="wrapper">
3543
<header class="type-${entry.responseDetails.requestType}">
36-
<h3><strong>#${requestID}</strong> <a href="${sanitizeUrlForLink(entry.url)}">${escapeHtml(entry.url)}</a></h3>
44+
<h3><strong>#${requestID}</strong> <a href="${sanitizeUrlForLink(entry.url)}">
45+
${escapeHtml(entry.url)}
46+
</a></h3>
3747
<nav class="tab-nav">
3848
<ul>
3949
${tabMenu}

0 commit comments

Comments
 (0)