@oddbird/css-anchor-positioning
Advanced tools
Comparing version 0.0.2 to 0.0.3
@@ -16,2 +16,2 @@ import { type Rect } from '@floating-ui/dom'; | ||
export declare const getPixelValue: ({ targetEl, targetProperty, anchorRect, anchorSide, anchorSize, fallback, }: GetPixelValueOpts) => Promise<string>; | ||
export declare function polyfill(): Promise<AnchorPositions>; | ||
export declare function polyfill(animationFrame?: boolean): Promise<AnchorPositions>; |
{ | ||
"name": "@oddbird/css-anchor-positioning", | ||
"version": "0.0.2", | ||
"version": "0.0.3", | ||
"description": "Polyfill for the proposed CSS anchor positioning spec", | ||
@@ -81,15 +81,15 @@ "license": "BSD-3-Clause", | ||
"dependencies": { | ||
"@floating-ui/dom": "^1.2.1", | ||
"@floating-ui/dom": "^1.2.5", | ||
"@types/css-tree": "^2.3.1", | ||
"css-tree": "^2.3.1", | ||
"nanoid": "^4.0.1" | ||
"nanoid": "^4.0.2" | ||
}, | ||
"devDependencies": { | ||
"@playwright/test": "^1.31.1", | ||
"@playwright/test": "^1.32.1", | ||
"@types/async": "^3.2.18", | ||
"@types/node": "*", | ||
"@types/selenium-webdriver": "^4.1.12", | ||
"@typescript-eslint/eslint-plugin": "^5.53.0", | ||
"@typescript-eslint/parser": "^5.53.0", | ||
"@vitest/coverage-istanbul": "^0.29.1", | ||
"@types/selenium-webdriver": "^4.1.13", | ||
"@typescript-eslint/eslint-plugin": "^5.56.0", | ||
"@typescript-eslint/parser": "^5.56.0", | ||
"@vitest/coverage-istanbul": "^0.29.7", | ||
"async": "^3.2.4", | ||
@@ -99,4 +99,4 @@ "browserslist": "^4.21.5", | ||
"cross-env": "^7.0.3", | ||
"eslint": "^8.35.0", | ||
"eslint-config-prettier": "^8.6.0", | ||
"eslint": "^8.36.0", | ||
"eslint-config-prettier": "^8.8.0", | ||
"eslint-import-resolver-typescript": "^3.5.3", | ||
@@ -108,14 +108,14 @@ "eslint-plugin-import": "^2.27.5", | ||
"fetch-mock": "^9.11.0", | ||
"jsdom": "^21.1.0", | ||
"liquidjs": "^10.6.0", | ||
"jsdom": "^21.1.1", | ||
"liquidjs": "^10.7.0", | ||
"node-fetch": "^2.6.7", | ||
"npm-run-all": "^4.1.5", | ||
"prettier": "^2.8.4", | ||
"selenium-webdriver": "^4.8.1", | ||
"stylelint": "^15.2.0", | ||
"stylelint-config-standard": "^30.0.1", | ||
"prettier": "^2.8.7", | ||
"selenium-webdriver": "^4.8.2", | ||
"stylelint": "^15.3.0", | ||
"stylelint-config-standard": "^31.0.0", | ||
"ts-node": "^10.9.1", | ||
"typescript": "^4.9.5", | ||
"vite": "^4.1.4", | ||
"vitest": "^0.29.1" | ||
"typescript": "^5.0.2", | ||
"vite": "^4.2.1", | ||
"vitest": "^0.29.7" | ||
}, | ||
@@ -122,0 +122,0 @@ "resolutions": { |
@@ -30,2 +30,33 @@ # CSS Anchor Positioning Polyfill | ||
## Configuration | ||
The polyfill accepts one argument (type: `boolean`, default: `false`), which | ||
determines whether anchor calculations should [update on every animation | ||
frame](https://floating-ui.com/docs/autoUpdate#animationframe) (e.g. when the | ||
anchor element moves), in addition to always updating on scroll/resize. While | ||
this option is optimized for performance, it should be used sparingly. | ||
```js | ||
<script type="module"> | ||
if (!("anchorName" in document.documentElement.style)) { | ||
const { default: polyfill } = await import("https://unpkg.com/@oddbird/css-anchor-positioning/dist/css-anchor-positioning-fn.js"); | ||
polyfill(true); | ||
} | ||
</script> | ||
``` | ||
When using the default version of the polyfill that executes automatically, this | ||
option can be set by setting the value of | ||
`window.UPDATE_ANCHOR_ON_ANIMATION_FRAME`. | ||
```js | ||
<script type="module"> | ||
if (!("anchorName" in document.documentElement.style)) { | ||
window.UPDATE_ANCHOR_ON_ANIMATION_FRAME = true; | ||
import("https://unpkg.com/@oddbird/css-anchor-positioning"); | ||
} | ||
</script> | ||
``` | ||
## Limitations | ||
@@ -35,3 +66,2 @@ | ||
- top layer anchor elements | ||
- `anchor-default` property | ||
@@ -42,6 +72,10 @@ - `anchor-scroll` property | ||
anchor-side | ||
- dynamic anchor movement other than container resize/scroll | ||
([#73](https://github.com/oddbird/css-anchor-positioning/issues/73)) | ||
- dynamically added/removed anchors or targets | ||
- anchors or targets in the shadow-dom | ||
- tracking the order of elements in the | ||
[top-layer](https://fullscreen.spec.whatwg.org/#new-stacking-layer) to | ||
invalidate top-layer target elements from anchoring to succeeding top-layer | ||
anchors. See [this | ||
WPT](https://github.com/web-platform-tests/wpt/blob/master/css/css-anchor-position/anchor-position-top-layer-006.html) | ||
for an example. | ||
- anchor functions assigned to `inset-*` properties or `inset` shorthand | ||
@@ -52,10 +86,10 @@ property | ||
CSS Grid layout | ||
- `@position-fallback` where targets overflow the grid area but do not overflow | ||
the containing block | ||
- `@position-fallback` where targets in a CSS Grid layout overflow the grid area | ||
but do not overflow the containing block | ||
- `@position-fallback` where targets overflow their inset-modified containing | ||
block, overlapping the anchor element | ||
- anchors in multi-column layouts | ||
- anchor functions used as the fallback value for another anchor function | ||
- anchor functions used as the fallback value in another anchor function | ||
- anchor functions assigned to `bottom` or `right` properties on inline targets | ||
whose offset-parent is inline with `clientHeight`/`clientWidth` of `0` | ||
(partial support -- does not account for possible scrollbar width) |
import { polyfill } from './polyfill.js'; | ||
// @ts-expect-error Used by the WPT test harness to delay test assertions | ||
// Used by the WPT test harness to delay test assertions | ||
// and give the polyfill time to apply changes | ||
@@ -10,6 +10,6 @@ window.CHECK_LAYOUT_DELAY = true; | ||
window.addEventListener('load', () => { | ||
polyfill(); | ||
polyfill(true); | ||
}); | ||
} else { | ||
polyfill(); | ||
polyfill(true); | ||
} |
@@ -290,5 +290,9 @@ import * as csstree from 'css-tree'; | ||
if (name) { | ||
if (isIdentifier(name) && name.name.startsWith('--')) { | ||
// Store anchor name | ||
anchorName = name.name; | ||
if (isIdentifier(name)) { | ||
if (name.name === 'implicit') { | ||
name = undefined; | ||
} else if (name.name.startsWith('--')) { | ||
// Store anchor name | ||
anchorName = name.name; | ||
} | ||
} else if (isVarFunction(name) && name.children.first) { | ||
@@ -295,0 +299,0 @@ // Store CSS custom prop for anchor name |
@@ -279,3 +279,6 @@ import { | ||
async function applyAnchorPositions(declarations: AnchorFunctionDeclaration) { | ||
async function applyAnchorPositions( | ||
declarations: AnchorFunctionDeclaration, | ||
useAnimationFrame = false, | ||
) { | ||
const root = document.documentElement; | ||
@@ -291,18 +294,23 @@ | ||
if (anchor && target) { | ||
autoUpdate(anchor, target, async () => { | ||
const rects = await platform.getElementRects({ | ||
reference: anchor, | ||
floating: target, | ||
strategy: 'absolute', | ||
}); | ||
const resolved = await getPixelValue({ | ||
targetEl: target, | ||
targetProperty: property, | ||
anchorRect: rects.reference, | ||
anchorSide: anchorValue.anchorSide, | ||
anchorSize: anchorValue.anchorSize, | ||
fallback: anchorValue.fallbackValue, | ||
}); | ||
root.style.setProperty(anchorValue.uuid, resolved); | ||
}); | ||
autoUpdate( | ||
anchor, | ||
target, | ||
async () => { | ||
const rects = await platform.getElementRects({ | ||
reference: anchor, | ||
floating: target, | ||
strategy: 'absolute', | ||
}); | ||
const resolved = await getPixelValue({ | ||
targetEl: target, | ||
targetProperty: property, | ||
anchorRect: rects.reference, | ||
anchorSide: anchorValue.anchorSide, | ||
anchorSize: anchorValue.anchorSize, | ||
fallback: anchorValue.fallbackValue, | ||
}); | ||
root.style.setProperty(anchorValue.uuid, resolved); | ||
}, | ||
{ animationFrame: useAnimationFrame }, | ||
); | ||
} else { | ||
@@ -325,2 +333,3 @@ // Use fallback value | ||
fallbacks: TryBlock[], | ||
useAnimationFrame = false, | ||
) { | ||
@@ -336,52 +345,57 @@ if (!fallbacks.length) { | ||
const offsetParent = await getOffsetParent(target); | ||
autoUpdate(offsetParent, target, async () => { | ||
// If this auto-update was triggered while the polyfill is already looping | ||
// through the possible `@try` blocks, do not check again. | ||
if (checking) { | ||
return; | ||
} | ||
checking = true; | ||
// Apply the styles from each `@try` block (in order), stopping when we | ||
// reach one that does not cause the target's margin-box to overflow | ||
// its offsetParent (containing block). | ||
for (const [index, { uuid }] of fallbacks.entries()) { | ||
target.setAttribute('data-anchor-polyfill', uuid); | ||
if (index === fallbacks.length - 1) { | ||
checking = false; | ||
break; | ||
autoUpdate( | ||
target, | ||
target, | ||
async () => { | ||
// If this auto-update was triggered while the polyfill is already looping | ||
// through the possible `@try` blocks, do not check again. | ||
if (checking) { | ||
return; | ||
} | ||
const rects = await platform.getElementRects({ | ||
reference: offsetParent, | ||
floating: target, | ||
strategy: 'absolute', | ||
}); | ||
const overflow = await detectOverflow( | ||
{ | ||
x: target.offsetLeft, | ||
y: target.offsetTop, | ||
platform: platformWithCache, | ||
rects, | ||
elements: { floating: target }, | ||
checking = true; | ||
// Apply the styles from each `@try` block (in order), stopping when we | ||
// reach one that does not cause the target's margin-box to overflow | ||
// its offsetParent (containing block). | ||
for (const [index, { uuid }] of fallbacks.entries()) { | ||
target.setAttribute('data-anchor-polyfill', uuid); | ||
if (index === fallbacks.length - 1) { | ||
checking = false; | ||
break; | ||
} | ||
const rects = await platform.getElementRects({ | ||
reference: target, | ||
floating: target, | ||
strategy: 'absolute', | ||
} as unknown as MiddlewareState, | ||
{ | ||
boundary: offsetParent, | ||
rootBoundary: 'document', | ||
padding: getMargins(target), | ||
}, | ||
); | ||
// If none of the sides overflow, use this `@try` block and stop loop... | ||
if (Object.values(overflow).every((side) => side <= 0)) { | ||
checking = false; | ||
break; | ||
}); | ||
const overflow = await detectOverflow( | ||
{ | ||
x: target.offsetLeft, | ||
y: target.offsetTop, | ||
platform: platformWithCache, | ||
rects, | ||
elements: { floating: target }, | ||
strategy: 'absolute', | ||
} as unknown as MiddlewareState, | ||
{ | ||
boundary: offsetParent, | ||
rootBoundary: 'document', | ||
padding: getMargins(target), | ||
}, | ||
); | ||
// If none of the sides overflow, use this `@try` block and stop loop... | ||
if (Object.values(overflow).every((side) => side <= 0)) { | ||
checking = false; | ||
break; | ||
} | ||
} | ||
} | ||
}); | ||
}, | ||
{ animationFrame: useAnimationFrame }, | ||
); | ||
} | ||
} | ||
async function position(rules: AnchorPositions) { | ||
async function position(rules: AnchorPositions, useAnimationFrame = false) { | ||
for (const pos of Object.values(rules)) { | ||
// Handle `anchor()` and `anchor-size()` functions... | ||
await applyAnchorPositions(pos.declarations ?? {}); | ||
await applyAnchorPositions(pos.declarations ?? {}, useAnimationFrame); | ||
} | ||
@@ -391,7 +405,15 @@ | ||
// Handle `@position-fallback` blocks... | ||
await applyPositionFallbacks(targetSel, position.fallbacks ?? []); | ||
await applyPositionFallbacks( | ||
targetSel, | ||
position.fallbacks ?? [], | ||
useAnimationFrame, | ||
); | ||
} | ||
} | ||
export async function polyfill() { | ||
export async function polyfill(animationFrame?: boolean) { | ||
const useAnimationFrame = | ||
animationFrame === undefined | ||
? Boolean(window.UPDATE_ANCHOR_ON_ANIMATION_FRAME) | ||
: animationFrame; | ||
// fetch CSS from stylesheet and inline style | ||
@@ -408,3 +430,3 @@ const styleData = await fetchCSS(); | ||
// calculate position values | ||
await position(rules); | ||
await position(rules, useAnimationFrame); | ||
} | ||
@@ -411,0 +433,0 @@ |
@@ -45,2 +45,14 @@ import { platform } from '@floating-ui/dom'; | ||
function isTopLayer(el: HTMLElement) { | ||
// check for the specific top layer element types... | ||
// Currently, the top layer elements are: | ||
// popovers, modal dialogs, and elements in a fullscreen mode. | ||
// See https://developer.chrome.com/blog/top-layer-devtools/#what-are-the-top-layer-and-top-layer-elements | ||
// TODO: | ||
// - only check for "open" popovers | ||
// - add support for fullscreen elements | ||
const topLayerElements = document.querySelectorAll('dialog, [popover]'); | ||
return Boolean(Array.from(topLayerElements).includes(el)); | ||
} | ||
// Validates that anchor element is a valid anchor for given target element | ||
@@ -51,2 +63,10 @@ export async function isValidAnchorElement( | ||
) { | ||
if (isTopLayer(anchor) && isTopLayer(target)) { | ||
// TODO: keep track of top layer order | ||
// if (isTargetPrecedingAnchor(target, anchor)) { | ||
// return false; | ||
// } | ||
return true; | ||
} | ||
const anchorContainingBlock = await platform.getOffsetParent?.(anchor); | ||
@@ -57,7 +77,9 @@ const targetContainingBlock = await platform.getOffsetParent?.(target); | ||
// el must not be absolutely positioned. | ||
if ( | ||
isAbsolutelyPositioned(anchor) && | ||
anchorContainingBlock === targetContainingBlock | ||
) { | ||
return false; | ||
if (isAbsolutelyPositioned(anchor)) { | ||
if (isTopLayer(target)) { | ||
return true; | ||
} | ||
if (anchorContainingBlock === targetContainingBlock) { | ||
return false; | ||
} | ||
} | ||
@@ -64,0 +86,0 @@ |
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
3859235
28
18814
92
Updated@floating-ui/dom@^1.2.5
Updatednanoid@^4.0.2