Skip to content

Commit 3cd72ce

Browse files
authored
Update how violation highlight is rendered (#137)
* Update how violation highlight is rendered - replace requestAnimationFrame approach with vanilla css. This has the benefit of being simpler and handling multi-line text much better. - bump the version number closes MAT-1134 * bump package version and update changelog * update package version * Updates in response to review comments. * Update indicate.js fix a typo
1 parent aa69a93 commit 3cd72ce

File tree

8 files changed

+138
-218
lines changed

8 files changed

+138
-218
lines changed

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
## [4.2.3] - 2023-01-30
11+
12+
- Change the violation highlight from an absolutely positioned blue box
13+
to an outline created with just CSS
14+
1015
## [4.1.2] - 2022-11-30
1116

1217
- Changes to cope with the RCE being put in browser-native fullscreen

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "tinymce-a11y-checker",
3-
"version": "4.1.2",
3+
"version": "4.1.3",
44
"description": "An accessibility checker plugin for TinyMCE.",
55
"main": "lib/plugin.js",
66
"module": "es/plugin.js",

src/components/__tests__/checker.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,9 @@ beforeEach(() => {
3333
getContainer: () => ({
3434
querySelector: () => fakeIframe
3535
}),
36+
dom: {
37+
doc: document,
38+
},
3639
on: jest.fn(),
3740
focus: jest.fn()
3841
}

src/components/checker.js

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,6 @@ export default class Checker extends React.Component {
6868
}
6969

7070
onFullscreenChange = (event) => {
71-
clearIndicators(event.target)
7271
this.selectCurrent()
7372
}
7473

@@ -145,11 +144,11 @@ export default class Checker extends React.Component {
145144
}
146145

147146
selectCurrent() {
148-
clearIndicators()
147+
clearIndicators(this.props.editor.dom.doc)
149148
const errorNode = this.errorNode()
150149
if (errorNode) {
151150
this.getFormState()
152-
dom.select(this.props.editor, errorNode)
151+
dom.select(errorNode)
153152
} else {
154153
this.firstError()
155154
}
@@ -282,7 +281,7 @@ export default class Checker extends React.Component {
282281

283282
handleClose() {
284283
this.onLeaveError()
285-
clearIndicators()
284+
clearIndicators(this.props.editor.dom.doc)
286285
this.setState({ open: false })
287286
}
288287

src/utils/__tests__/dom.js

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -27,13 +27,12 @@ describe("select", () => {
2727
})
2828

2929
test("scrolls the node into view", () => {
30-
dom.select(editor, node, indicateFn)
30+
dom.select(node, indicateFn)
3131
expect(node.scrollIntoView).toBeCalled()
3232
})
33-
34-
test("calls the indicator function with the editor and the node", () => {
35-
dom.select(editor, node, indicateFn)
36-
expect(indicateFn).toHaveBeenCalledWith(editor, node)
33+
test("calls the indicator function with the node", () => {
34+
dom.select(node, indicateFn)
35+
expect(indicateFn).toHaveBeenCalledWith(node)
3736
})
3837

3938
test("select does not throw if node is underfined or null", () => {

src/utils/__tests__/indicate.js

Lines changed: 59 additions & 97 deletions
Original file line numberDiff line numberDiff line change
@@ -1,118 +1,80 @@
1-
import indicate, { clearIndicators } from "../indicate"
2-
3-
let fakeEditor, fakeIframe, fakeElem, mockRAF
1+
import indicate, {
2+
clearIndicators,
3+
findChildDepth,
4+
buildDepthSelector,
5+
INDICATOR_STYLE,
6+
A11Y_CHECKER_STYLE_ELEM_ID,
7+
ensureA11yCheckerStyleElement,
8+
} from "../indicate"
49

510
beforeEach(() => {
6-
Element.prototype.getBoundingClientRect = jest.fn(() => {
7-
return {
8-
width: 120,
9-
height: 120,
10-
top: 0,
11-
left: 0,
12-
bottom: 0,
13-
right: 0,
14-
}
15-
})
16-
17-
fakeElem = document.createElement("div")
18-
fakeIframe = document.createElement("iframe")
19-
fakeEditor = {
20-
getContainer: () => ({
21-
querySelector: () => fakeIframe,
22-
}),
23-
}
24-
25-
mockRAF = jest.spyOn(window, "requestAnimationFrame")
11+
document.body.innerHTML = `
12+
<p id="p1">
13+
first para
14+
</p>
15+
<p id="p2">2nd para</p>
16+
<p id="p3">
17+
this is the 3nd para.
18+
<span id="s1">with first span</span>
19+
<span id="s2">with 2nd span</span>
20+
<span id="s3">with 3rd span</span>
21+
more text
22+
<span id="s4">target span</span>
23+
<span id="s5">last span</span>
24+
</p>`
2625
})
2726

2827
afterEach(() => {
2928
document.fullscreenElement = null
30-
window.requestAnimationFrame.mockRestore()
3129
})
3230

33-
describe("indicate", () => {
34-
it("removes any existing indicators when run", () => {
35-
mockRAF
36-
.mockImplementationOnce((cb) => cb(15)) // This only allows it to happen twice, preventing an infinite loop
37-
.mockImplementationOnce((cb) => cb(30))
38-
39-
const el = document.createElement("div")
40-
el.className = "a11y-checker-selection-indicator"
41-
el.id = "this_should_be_gone"
42-
indicate(fakeEditor, fakeElem)
43-
expect(document.getElementById("this_should_be_gone")).toBeFalsy()
31+
describe("findChildDepth", () => {
32+
it("finds the depth of a child element in its parent", () => {
33+
let depth = findChildDepth(
34+
document.getElementById("p3"),
35+
document.getElementById("s3")
36+
)
37+
expect(depth).toEqual(3)
4438
})
4539

46-
it("stops adjusting when the indicator is gone", () => {
47-
mockRAF
48-
.mockImplementationOnce((cb) => cb(15))
49-
.mockImplementationOnce((cb) => {
50-
document.querySelector('.a11y-checker-selection-indicator').remove()
51-
cb(30)
52-
})
53-
.mockImplementationOnce((cb) => cb(45))
54-
55-
indicate(fakeEditor, fakeElem)
56-
expect(mockRAF).toHaveBeenCalledTimes(2)
40+
it("returns 0 if any arguments are missing", () => {
41+
expect(findChildDepth(null, document.getElementById("p3"))).toEqual(0)
42+
expect(findChildDepth(document.getElementById("p3"), null)).toEqual(0)
5743
})
44+
})
5845

59-
it("puts the indicator in the fullscreeenElement if it exists", () => {
60-
mockRAF
61-
.mockImplementationOnce((cb) => cb(15))
62-
.mockImplementationOnce((cb) => cb(30))
63-
64-
const d = document.createElement('div')
65-
d.id = "fullscreen_element"
66-
document.body.appendChild(d)
67-
document.fullscreenElement = d
68-
indicate(fakeEditor, fakeElem)
69-
expect(d.querySelector('.a11y-checker-selection-indicator')).toBeTruthy()
70-
46+
describe("buildDepthSelector", () => {
47+
it("returns the css selector to the given element", () => {
48+
const sel = buildDepthSelector(document.getElementById("s3"))
49+
expect(sel).toEqual("body>:nth-child(3)>:nth-child(3)")
7150
})
7251
})
7352

74-
describe("clearIndicators", () => {
75-
it("removes any existing indicators when called", () => {
76-
const el = document.createElement("div")
77-
el.className = "a11y-checker-selection-indicator"
78-
el.id = "this_should_be_gone"
79-
document.body.appendChild(el)
80-
clearIndicators()
81-
expect(document.getElementById("this_should_be_gone")).toBeFalsy()
53+
describe("indicate", () => {
54+
it("adds the style element to the head if it doesn't exist", () => {
55+
expect(document.querySelector("style")).toBe(null)
56+
indicate(document.getElementById("p1"))
57+
expect(
58+
document.querySelector(`style#${A11Y_CHECKER_STYLE_ELEM_ID}`)
59+
).toBeTruthy()
60+
indicate(document.getElementById("p1"))
61+
expect(document.querySelectorAll("style").length).toEqual(1)
8262
})
8363

84-
it("removes indicators from the passed in parent", () => {
85-
const d1 = document.createElement("div")
86-
d1.className = "a11y-checker-selection-indicator"
87-
d1.id = "this_should_be_here"
88-
document.body.appendChild(d1)
89-
90-
const parent = document.createElement("div")
91-
const el = document.createElement("div")
92-
el.className = "a11y-checker-selection-indicator"
93-
el.id = "this_should_be_gone"
94-
parent.appendChild(el)
95-
document.body.appendChild(parent)
96-
clearIndicators(parent)
97-
expect(document.getElementById("this_should_be_gone")).toBeFalsy()
98-
expect(document.getElementById("this_should_be_here")).toBeTruthy()
64+
it("injects the css into the style element", () => {
65+
indicate(document.getElementById("s3"))
66+
expect(
67+
document.getElementById(A11Y_CHECKER_STYLE_ELEM_ID).textContent
68+
).toEqual(`body>:nth-child(3)>:nth-child(3){${INDICATOR_STYLE}}`)
9969
})
70+
})
10071

101-
it("removes indicators from the fullscreenElement", () => {
102-
const d1 = document.createElement("div")
103-
d1.className = "a11y-checker-selection-indicator"
104-
d1.id = "this_should_be_here"
105-
document.body.appendChild(d1)
106-
107-
const parent = document.createElement("div")
108-
const el = document.createElement("div")
109-
el.className = "a11y-checker-selection-indicator"
110-
el.id = "this_should_be_gone"
111-
parent.appendChild(el)
112-
document.body.appendChild(parent)
113-
document.fullscreenElement = parent
114-
clearIndicators()
115-
expect(document.getElementById("this_should_be_gone")).toBeFalsy()
116-
expect(document.getElementById("this_should_be_here")).toBeTruthy()
72+
describe("clearIndicators", () => {
73+
it("removes the indicator css when called", () => {
74+
ensureA11yCheckerStyleElement(document)
75+
clearIndicators(document)
76+
expect(
77+
document.getElementById(A11Y_CHECKER_STYLE_ELEM_ID).textContent
78+
).toEqual("")
11779
})
11880
})

src/utils/dom.js

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -28,12 +28,15 @@ export function walk(node, fn, done) {
2828
processBatch()
2929
}
3030

31-
export function select(editor, elem, indicateFn = indicate) {
31+
export function select(elem, indicateFn = indicate) {
3232
if (elem == null) {
3333
return
3434
}
35-
elem.scrollIntoView()
36-
indicateFn(editor, elem)
35+
elem.scrollIntoView(false)
36+
if (elem.ownerDocument?.documentElement) {
37+
elem.ownerDocument.documentElement.scrollTop += 5 // room for the indicator highlight
38+
}
39+
indicateFn(elem)
3740
}
3841

3942
export function prepend(parent, child) {
@@ -106,7 +109,7 @@ export function splitStyleAttribute(styleString) {
106109

107110
export function createStyleString(styleObj) {
108111
let styleString = Object.keys(styleObj)
109-
.map(key => `${key}:${styleObj[key]}`)
112+
.map((key) => `${key}:${styleObj[key]}`)
110113
.join(";")
111114
if (styleString) {
112115
styleString = `${styleString};`
@@ -116,5 +119,5 @@ export function createStyleString(styleObj) {
116119

117120
export function hasTextNode(elem) {
118121
const nodes = Array.from(elem.childNodes)
119-
return nodes.some(x => x.nodeType === Node.TEXT_NODE)
122+
return nodes.some((x) => x.nodeType === Node.TEXT_NODE)
120123
}

0 commit comments

Comments
 (0)