diff --git a/.github/workflows/gh-pages.yml b/.github/workflows/gh-pages.yml index 3eb820c..90ce6a4 100644 --- a/.github/workflows/gh-pages.yml +++ b/.github/workflows/gh-pages.yml @@ -45,8 +45,8 @@ jobs: uses: actions/configure-pages@v3 - name: Install Node.js dependencies run: "[[ -f package-lock.json || -f npm-shrinkwrap.json ]] && npm ci || true" - - name: Build - run: "npm run build" + - name: Test build + run: "npm run build -- --mode test" - name: Checkout WPT run: "npm run test-setup" - name: WPT hosts @@ -57,6 +57,8 @@ jobs: run: "npm run test:compare" - name: Test results summary run: echo "Passed $(grep -c '^PASS' ./test/report/summary.txt) of $(grep -c '^' ./test/report/summary.txt) tests" >> $GITHUB_STEP_SUMMARY + - name: Build + run: "npm run build" - name: Clean build files run: "rm -rf node_modules test/wpt" - name: Upload artifact diff --git a/src/scroll-timeline-base.js b/src/scroll-timeline-base.js index 195d1b8..d63ffb9 100644 --- a/src/scroll-timeline-base.js +++ b/src/scroll-timeline-base.js @@ -188,7 +188,7 @@ export function measureSource (source) { /** * Measure subject element relative to source - * @param {HTMLElement} source + * @param {HTMLElement|SVGElement} source * @param {HTMLElement|undefined} subject * @param subject */ @@ -201,18 +201,70 @@ export function measureSubject(source, subject) { let node = subject; const ancestor = source.offsetParent; while (node && node != ancestor) { - left += node.offsetLeft; - top += node.offsetTop; - node = node.offsetParent; + if (node instanceof SVGElement) { + let rootSvgElement = node.ownerSVGElement; + while (rootSvgElement.ownerSVGElement) + rootSvgElement = rootSvgElement.ownerSVGElement; + + const matrix = node.getCTM(); + const bbox = node.getBBox(); + let point = rootSvgElement.createSVGPoint(); + point.x = bbox.x; + point.y = bbox.y; + + // Position relative to svg-element + const nodePosition = point.matrixTransform(matrix); + const svgParent = rootSvgElement.parentElement; + const svgLeft = rootSvgElement.getBoundingClientRect().left - svgParent.getBoundingClientRect().left; + const svgTop = rootSvgElement.getBoundingClientRect().top - svgParent.getBoundingClientRect().top; + left += svgLeft + nodePosition.x; + top += svgTop + nodePosition.y; + node = svgParent; + } else { + left += node.offsetLeft; + top += node.offsetTop; + if (!node.offsetParent) { + // The top level element (body) does not have an offsetParent, and no offsetTop/Left + // If the body has margins, they need to be added + const style = getComputedStyle(node); + top += parseInt(style.marginTop); + left += parseInt(style.marginLeft); + } + node = node.offsetParent; + } } left -= source.offsetLeft + source.clientLeft; top -= source.offsetTop + source.clientTop; + + let offsetWidth, offsetHeight + if (subject instanceof SVGElement) { + let rootSvgElement = subject.ownerSVGElement; + while (rootSvgElement.ownerSVGElement) + rootSvgElement = rootSvgElement.ownerSVGElement; + + const matrix = subject.getCTM(); + const bbox = subject.getBBox(); + let topLeftPoint = rootSvgElement.createSVGPoint(); + topLeftPoint.x = bbox.x; + topLeftPoint.y = bbox.y; + let bottomRightPoint = rootSvgElement.createSVGPoint(); + bottomRightPoint.x = bbox.x + bbox.width; + bottomRightPoint.y = bbox.y + bbox.height; + + const tlPosition = topLeftPoint.matrixTransform(matrix); + const brPosition = bottomRightPoint.matrixTransform(matrix); + offsetWidth = Math.abs(brPosition.x - tlPosition.x); + offsetHeight = Math.abs(brPosition.y - tlPosition.y); + } else { + offsetWidth = subject.offsetWidth; + offsetHeight = subject.offsetHeight; + } const style = getComputedStyle(subject); return { top, left, - offsetWidth: subject.offsetWidth, - offsetHeight: subject.offsetHeight, + offsetWidth, + offsetHeight, fontSize: style.fontSize, }; } diff --git a/src/scroll-timeline-css-parser.js b/src/scroll-timeline-css-parser.js index 7720a09..2e3d819 100644 --- a/src/scroll-timeline-css-parser.js +++ b/src/scroll-timeline-css-parser.js @@ -248,22 +248,20 @@ export class StyleParser { // Add 1s as duration to fix this. if(hasAnimationTimeline) { if(!this.hasDuration(shorthand)) { - + let previousShorthand = shorthand // `auto` also is valid duration. Older browsers can’t always // handle it properly, so we remove it from the shorthand. if (this.hasAutoDuration(shorthand)) { - rule.block.contents = rule.block.contents.replace( - 'auto', - ' ' - ); + shorthand = previousShorthand.replace('auto', ' ') + rule.block.contents = rule.block.contents.replace(previousShorthand, shorthand); + previousShorthand = shorthand } // TODO: Should keep track of whether duration is artificial or not, // so that we can later track that we need to update timing to // properly see duration as 'auto' for the polyfill. - rule.block.contents = rule.block.contents.replace( - shorthand, " 1s " + shorthand - ); + shorthand = " 1s " + previousShorthand + rule.block.contents = rule.block.contents.replace(previousShorthand, shorthand); shouldReplacePart = true; } } diff --git a/test/entry.js b/test/entry.js new file mode 100644 index 0000000..d3b2f07 --- /dev/null +++ b/test/entry.js @@ -0,0 +1,37 @@ +import '../src/index.js' +// The polyfill is dependent on the animationstart event for polyfilling animations declared in css. +// This causes timing issues when running some wpt tests that wait for animation.ready. +// The tests might run before the animations are polyfilled, making them flaky. + +// The following code delays a selected list of tests until animations are polyfilled. + +// List of names of tests that should wait for animationstart +const cssAnimationTests = [ + 'View timeline attached to SVG graphics element' +] + +const animationsStarted = new Promise((resolve) => { + window.addEventListener('animationstart', () => { + setTimeout(() => resolve(), 1); + }); +}) + +// Proxy the promise_test function +let nativePromiseTest; +Reflect.defineProperty(window, 'promise_test', { + get() { + return (func, name, properties) => { + if (cssAnimationTests.includes(name)) { + // Wait for animationstart before starting tests + return nativePromiseTest.call(null, async (...args) => { + await animationsStarted; + return func.call(null, ...args); + }, name, properties); + } else { + nativePromiseTest.call(null, func, name, properties); + } + }; + }, set(v) { + nativePromiseTest = v; + } +}); \ No newline at end of file diff --git a/test/expected.txt b/test/expected.txt index d7913de..15495bd 100644 --- a/test/expected.txt +++ b/test/expected.txt @@ -75,7 +75,7 @@ FAIL /scroll-animations/css/animation-timeline-computed.html Property animation- FAIL /scroll-animations/css/animation-timeline-computed.html Property animation-timeline value 'view(1px y)' FAIL /scroll-animations/css/animation-timeline-computed.html Property animation-timeline value 'view(y auto)' FAIL /scroll-animations/css/animation-timeline-computed.html Property animation-timeline value 'view(y auto auto)' -FAIL /scroll-animations/css/animation-timeline-deferred.html Animation.timeline returns attached timeline +PASS /scroll-animations/css/animation-timeline-deferred.html Animation.timeline returns attached timeline FAIL /scroll-animations/css/animation-timeline-deferred.html Animation.timeline returns null for inactive deferred timeline FAIL /scroll-animations/css/animation-timeline-deferred.html Animation.timeline returns null for inactive (overattached) deferred timeline FAIL /scroll-animations/css/animation-timeline-ignored.tentative.html Changing animation-timeline changes the timeline (sanity check) @@ -307,7 +307,7 @@ FAIL /scroll-animations/css/scroll-timeline-name-shadow.html Outer animation can FAIL /scroll-animations/css/scroll-timeline-name-shadow.html Inner animation can see scroll timeline defined by ::part FAIL /scroll-animations/css/scroll-timeline-name-shadow.html Slotted element can see scroll timeline within the shadow PASS /scroll-animations/css/scroll-timeline-nearest-dirty.html Unrelated style mutation does not affect anonymous timeline -FAIL /scroll-animations/css/scroll-timeline-nearest-with-absolute-positioned-element.html Resolving scroll(nearest) for an absolutely positioned element +PASS /scroll-animations/css/scroll-timeline-nearest-with-absolute-positioned-element.html Resolving scroll(nearest) for an absolutely positioned element FAIL /scroll-animations/css/scroll-timeline-paused-animations.html Test that the scroll animation is paused FAIL /scroll-animations/css/scroll-timeline-paused-animations.html Test that the scroll animation is paused by updating animation-play-state FAIL /scroll-animations/css/scroll-timeline-range-animation.html Animation with ranges [initial, initial] @@ -405,15 +405,15 @@ PASS /scroll-animations/css/timeline-scope-parsing.tentative.html e.style['timel PASS /scroll-animations/css/timeline-scope-parsing.tentative.html e.style['timeline-scope'] = "rgb(1, 2, 3)" should not set the property value PASS /scroll-animations/css/timeline-scope-parsing.tentative.html e.style['timeline-scope'] = "#fefefe" should not set the property value FAIL /scroll-animations/css/timeline-scope.html Descendant can attach to deferred timeline -PASS /scroll-animations/css/timeline-scope.html Deferred timeline with no attachments -FAIL /scroll-animations/css/timeline-scope.html Inner timeline does not interfere with outer timeline -PASS /scroll-animations/css/timeline-scope.html Deferred timeline with two attachments +FAIL /scroll-animations/css/timeline-scope.html Deferred timeline with no attachments +PASS /scroll-animations/css/timeline-scope.html Inner timeline does not interfere with outer timeline +FAIL /scroll-animations/css/timeline-scope.html Deferred timeline with two attachments FAIL /scroll-animations/css/timeline-scope.html Dynamically re-attaching FAIL /scroll-animations/css/timeline-scope.html Dynamically detaching FAIL /scroll-animations/css/timeline-scope.html Removing/inserting element with attaching timeline FAIL /scroll-animations/css/timeline-scope.html Ancestor attached element becoming display:none/block FAIL /scroll-animations/css/timeline-scope.html A deferred timeline appearing dynamically in the ancestor chain -FAIL /scroll-animations/css/timeline-scope.html Animations prefer non-deferred timelines +PASS /scroll-animations/css/timeline-scope.html Animations prefer non-deferred timelines FAIL /scroll-animations/css/view-timeline-animation-range-update.tentative.html Ensure that animation is updated on a style change FAIL /scroll-animations/css/view-timeline-animation.html Default view-timeline FAIL /scroll-animations/css/view-timeline-animation.html Horizontal view-timeline @@ -1032,7 +1032,7 @@ FAIL /scroll-animations/view-timelines/get-keyframes-with-timeline-offset.html C FAIL /scroll-animations/view-timelines/get-keyframes-with-timeline-offset.html Retain specified ordering of keyframes with timeline offsets FAIL /scroll-animations/view-timelines/get-keyframes-with-timeline-offset.html Include unreachable keyframes FAIL /scroll-animations/view-timelines/get-keyframes-with-timeline-offset.html Mix of computed and timeline offsets. -FAIL /scroll-animations/view-timelines/inline-subject.html View timeline attached to SVG graphics element +TIMEOUT /scroll-animations/view-timelines/inline-subject.html View timeline attached to SVG graphics element FAIL /scroll-animations/view-timelines/inline-view-timeline-current-time.tentative.html View timeline with start and end scroll offsets that do not align with the scroll boundaries FAIL /scroll-animations/view-timelines/inline-view-timeline-current-time.tentative.html View timeline does not clamp starting scroll offset at 0 PASS /scroll-animations/view-timelines/inline-view-timeline-current-time.tentative.html View timeline does not clamp end scroll offset at max scroll @@ -1044,9 +1044,9 @@ FAIL /scroll-animations/view-timelines/sticky/view-timeline-sticky-offscreen-4.h FAIL /scroll-animations/view-timelines/sticky/view-timeline-sticky-offscreen-5.html View timeline bottom-sticky before entry and top-sticky after exit. FAIL /scroll-animations/view-timelines/sticky/view-timeline-sticky-offscreen-6.html View timeline target > viewport, bottom-sticky during entry and top-sticky during exit. FAIL /scroll-animations/view-timelines/sticky/view-timeline-sticky-offscreen-7.html View timeline target > viewport, bottom-sticky and top-sticky during contain. -FAIL /scroll-animations/view-timelines/svg-graphics-element-001.html View timeline attached to SVG graphics element -FAIL /scroll-animations/view-timelines/svg-graphics-element-002.html View timeline attached to SVG graphics element -FAIL /scroll-animations/view-timelines/svg-graphics-element-003.html View timeline attached to SVG graphics element +PASS /scroll-animations/view-timelines/svg-graphics-element-001.html View timeline attached to SVG graphics element +PASS /scroll-animations/view-timelines/svg-graphics-element-002.html View timeline attached to SVG graphics element +PASS /scroll-animations/view-timelines/svg-graphics-element-003.html View timeline attached to SVG graphics element FAIL /scroll-animations/view-timelines/timeline-offset-in-keyframe.html Timeline offsets in programmatic keyframes FAIL /scroll-animations/view-timelines/timeline-offset-in-keyframe.html String offsets in programmatic keyframes PASS /scroll-animations/view-timelines/timeline-offset-in-keyframe.html Invalid timeline offset in programmatic keyframe throws diff --git a/vite.config.js b/vite.config.js index 6a27226..3e1b540 100644 --- a/vite.config.js +++ b/vite.config.js @@ -1,28 +1,29 @@ import { resolve } from 'path' import { defineConfig } from 'vite' -export default defineConfig({ - build: { - sourcemap: true, - lib: { - // Could also be a dictionary or array of multiple entry points - entry: resolve(__dirname, 'src/index.js'), - name: 'ScrollTimeline', - // the proper extensions will be added - fileName: (format, entryAlias) => `scroll-timeline${format=='iife'?'':'-' + format}.js`, - formats: ['iife'], - }, - minify: 'terser', - terserOptions: { - keep_classnames: /^((View|Scroll)Timeline)|CSS.*$/ - }, - rollupOptions: { - output: { - // Provide global variables to use in the UMD build - // for externalized deps - globals: { - }, +export default defineConfig(({ mode }) => { + return { + build: { + sourcemap: true, + lib: { + // Could also be a dictionary or array of multiple entry points + entry: resolve(__dirname, mode === 'test' ? 'test/entry.js' : 'src/index.js'), + name: 'ScrollTimeline', + // the proper extensions will be added + fileName: (format, entryAlias) => `scroll-timeline${format == 'iife' ? '' : '-' + format}.js`, + formats: ['iife'], }, - } - }, + minify: 'terser', + terserOptions: { + keep_classnames: /^((View|Scroll)Timeline)|CSS.*$/ + }, + rollupOptions: { + output: { + // Provide global variables to use in the UMD build + // for externalized deps + globals: {}, + }, + } + }, + }; })