-
Notifications
You must be signed in to change notification settings - Fork 779
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
fix: CSSOM generation for shadowRoot in Safari #1113
Conversation
lib/core/utils/preload-cssom.js
Outdated
|
||
if (!root.styleSheets) { | ||
/** | ||
* In Safari: (Issue: https://github.com/dequelabs/axe-core/issues/1082) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please report this as a bug to Apple.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Reported, but do not have an issue/ bug reported link from them yet.
lib/core/utils/preload-cssom.js
Outdated
}) | ||
.map(node => { | ||
const sheet = convertTextToStylesheetFn({ | ||
data: node.textContent |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How does this solution work with link
elements?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Works, just fine as shown here: https://github.com/dequelabs/axe-core/pull/1113/files#diff-b195c7e1a05c62e60347f579d8f7dddeR339
It was the style
tag that needed special parsing as above.
); | ||
|
||
(shadowSupported ? it : xit)( | ||
'should return styles from shadow dom', |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Make sure to test link
element stylesheets as well.
@WilcoFiers Please review this again. |
@dylanb - review appreciated. |
lib/core/utils/preload-cssom.js
Outdated
/* eslint max-statements: ["error", 20] */ | ||
|
||
// basic priority | ||
const sheetLevelPriority = `${rootIndex}${sheetIndex}`; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think I understand what's happening here. It looks like we're concatenating (not adding) numbers together in a string to be used as a numerical value later on. Can you explain how this works?
I'm stuck on the following:
A "priority" string is created with a rootIndex
of 1
and a sheetIndex
of 3000
. It is then coerced to the integer 13000
.
Later on, another string is created with rootIndex
of 2
and sheetIndex
of 1
. It's then coerced to 21
.
13000
is larger than 21
, but its "root" was higher. Is this OK?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So there is multiple levels of complexity for calculating priority.
Before going into the details, let me just say the above can and is solved by introducing decimals rootIndex.sheetIndex
resulting in 1.1, 1.200, 2.1 and so on, hence the priority remains ordered as expected. Good spot. I have now done that.
Also, note that this is just the first pass at computing priority.
There are few more issues to tackle like below:
- CSSOM: Handle Priority of styles applied (PR #1113) #1078
- CSSOM: Tackle nested @import stylesheets #1054
Which I believe will introduce further complexity to the priority calculation.
This PR mainly handles shadowDOM stylesheets resolution, and introduces the concept of priority.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Couple minor points. I'd also like @dylanb to take a look.
lib/core/utils/preload-cssom.js
Outdated
shadowId | ||
) { | ||
// retrieve shadowRoot style sheets as [] | ||
styleSheets = Array.from(rootDocument.children).reduce((out, node) => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why are you only looking at child nodes, instead of doing getElementsByTagName for style
and link
elements? It might make sense to break up the function into different load methods depending on the elements. Reduces the complexity of this function.
lib/core/utils/preload-cssom.js
Outdated
/* eslint max-statements: ["error", 20] */ | ||
|
||
// padding sheet index with root index to create priority | ||
const sheetLevelPriority = Number(`${rootIndex}.${sheetIndex}`); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Doesn't this break if we've got more than 10 sheets? Is this priority something you came up with or is it part of a spec some place? This seems fragile.
lib/core/utils/preload-cssom.js
Outdated
@@ -158,7 +269,7 @@ function getAllRootsInTree(tree) { | |||
.map(node => { | |||
return { | |||
shadowId: node.shadowId, | |||
root: axe.utils.getRootNode(node.actualNode) | |||
rootDocument: axe.utils.getRootNode(node.actualNode) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe stick with rootNode? That's what we're calling it everywhere else.
lib/core/utils/preload-cssom.js
Outdated
*/ | ||
function loadCssom({ root, shadowId }, timeout, convertTextToStylesheetFn) { | ||
function loadCssom({ rootNode, rootIndex, shadowId, timeout, getStyleSheet }) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you please break this function up into more readable chunks? It's a 200 line function that does half a dozen different things.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've taken a stab at simplifying this. Here's a suggestion. Also notice this is readable without a comment on every line.
function loadCssom(options) {
const { rootIndex, rootNode, shadowId } = options;
const q = axe.utils.queue();
const styleSheets = (rootNode.nodeType === 7 && shadowId
? getSheetsFromShadowDom(options)
: getSheetsFromLightDom(options)
);
const filteredSheets = styleSheets
.filter(isFirstHrefSheet)
.filter(notPrintSheet);
filteredSheets.forEach((sheet, sheetIndex) => {
let sheetLoader;
try {
// The following line throws an error on cross-origin style sheets:
const cssRules = sheet.cssRules;
sheetLoader = getSameOriginSheet;
} catch () {
sheetLoader = getCrossOriginSheet;
}
q.defer((resolve, reject) => {
sheetLoader({
priority: [rootIndex, sheetIndex]
url: rule.href,
resolve,
reject,
...options
});
});
});
return q;
})
lib/core/utils/preload-cssom.js
Outdated
getStyleSheet, | ||
shadowId, | ||
priority | ||
}); | ||
}); | ||
} | ||
}, []); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This empty array is passed into sheets.forEach((sheet, sheetIndex) => {...}, [])
.
lib/core/utils/preload-cssom.js
Outdated
*/ | ||
function loadCssom({ root, shadowId }, timeout, convertTextToStylesheetFn) { | ||
function loadCssom({ rootNode, rootIndex, shadowId, timeout, getStyleSheet }) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've taken a stab at simplifying this. Here's a suggestion. Also notice this is readable without a comment on every line.
function loadCssom(options) {
const { rootIndex, rootNode, shadowId } = options;
const q = axe.utils.queue();
const styleSheets = (rootNode.nodeType === 7 && shadowId
? getSheetsFromShadowDom(options)
: getSheetsFromLightDom(options)
);
const filteredSheets = styleSheets
.filter(isFirstHrefSheet)
.filter(notPrintSheet);
filteredSheets.forEach((sheet, sheetIndex) => {
let sheetLoader;
try {
// The following line throws an error on cross-origin style sheets:
const cssRules = sheet.cssRules;
sheetLoader = getSameOriginSheet;
} catch () {
sheetLoader = getCrossOriginSheet;
}
q.defer((resolve, reject) => {
sheetLoader({
priority: [rootIndex, sheetIndex]
url: rule.href,
resolve,
reject,
...options
});
});
});
return q;
})
@WilcoFiers The suggestion as per this snippet #1113 (comment), to use Reviews welcome. |
url, | ||
timeout | ||
}) | ||
.then(({ data }) => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't recall off the top of my head. Does Axios treat non-2xx response codes as errors? If not, we might want to add something like:
.then(({ data, status }) => {
if ((status / 100 | 0) !== 2) {
throw new Error('non-2xx response')
}
// ...
})
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Docs say that it does.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Editorials. You're welcome to merge and raise a new PR with the editorial changes.
* @param {Array<Object>} styleSheets array of stylesheets | ||
* @returns an filtered array of stylesheets | ||
*/ | ||
function filterStyleSheets(styleSheets) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Filter it based on what? This needs a better name.
isExternal: false | ||
root: rootNode, | ||
isExternal: false, | ||
priority | ||
}) | ||
) | ||
); | ||
} catch (e) { | ||
// external sheet -> make an xhr and q the response |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's a bit misleading. This is a cross-origin stylesheet.
}); | ||
}); | ||
|
||
// in the same sheet - get inline rules in <style> tag or in a CSSStyleSheet excluding @import or nested link | ||
const inlineRules = rules.filter(rule => !rule.href); | ||
if (!inlineRules.length) { | ||
return; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
!!! OK It took me 10 minutes before I understood what this is doing. Can you add a comment here, something along the lines of: // Stylesheet only has @import rules in it
This PR handles the edge case where for a given shadowRoot, the
root.styleSheets
lookup is returned asundefined | null
.This meant evaluation of styles with in shadowDOM failed.
This fix, tackles the edge case by parsing the content of the shadowRoot, to reverse engineer
styleSheets
fromstyle
tag if any specified with in the shadowRoot.Upon reverse engineering the sheets, it is lead to flow into the generic
loadCssom
function to do the necessary generation of CSSOM as intended.The concept of priority in stylesheets loaded is also introduced. The priority computation will be enhanced when handling multiple nested
@import
stylesheets in a separate PR after this lands.Closes issue:
Reviewer checks
Required fields, to be filled out by PR reviewer(s)