LibWeb: Support anchor() in calc() trees
Previously, `anchor()` was only resolved when it appeared bare in an inset property. We now allow it to appear anywhere inside a `calc()` tree.
This commit is contained in:
parent
b62a099e91
commit
340ef361d8
10 changed files with 338 additions and 42 deletions
|
|
@ -6,14 +6,23 @@
|
|||
|
||||
#pragma once
|
||||
|
||||
#include <AK/Optional.h>
|
||||
#include <LibWeb/CSS/Angle.h>
|
||||
#include <LibWeb/CSS/Frequency.h>
|
||||
#include <LibWeb/CSS/Length.h>
|
||||
#include <LibWeb/CSS/StyleValues/ComputationContext.h>
|
||||
#include <LibWeb/CSS/Time.h>
|
||||
#include <LibWeb/Forward.h>
|
||||
#include <LibWeb/PixelUnits.h>
|
||||
|
||||
namespace Web::CSS {
|
||||
|
||||
class AnchorResolver {
|
||||
public:
|
||||
virtual ~AnchorResolver() = default;
|
||||
virtual Optional<CSSPixels> resolve(AnchorStyleValue const&) const = 0;
|
||||
};
|
||||
|
||||
struct CalculationResolutionContext {
|
||||
using PercentageBasis = Variant<Empty, Angle, Frequency, Length, Time>;
|
||||
|
||||
|
|
@ -21,6 +30,8 @@ struct CalculationResolutionContext {
|
|||
Optional<Length::ResolutionContext> length_resolution_context {};
|
||||
Optional<DOM::AbstractElement> abstract_element {};
|
||||
|
||||
AnchorResolver const* anchor_resolver { nullptr };
|
||||
|
||||
static CalculationResolutionContext from_computation_context(ComputationContext const& computation_context, PercentageBasis percentage_basis = {})
|
||||
{
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -5148,6 +5148,10 @@ RefPtr<CalculationNode const> Parser::convert_to_calculation_node(CalcParsing::N
|
|||
if (auto tree_counting_function = parse_tree_counting_function(tree_counting_function_tokens, TreeCountingFunctionStyleValue::ComputedType::Number))
|
||||
return NonMathFunctionCalculationNode::create(tree_counting_function.release_nonnull(), NumericType {});
|
||||
|
||||
auto anchor_function_tokens = TokenStream<ComponentValue>::of_single_token(component_value);
|
||||
if (auto anchor_function = parse_anchor(anchor_function_tokens))
|
||||
return NonMathFunctionCalculationNode::create(anchor_function->as_anchor(), NumericType { NumericType::BaseType::Length, 1 });
|
||||
|
||||
// NOTE: If we get here, then we have a ComponentValue that didn't get replaced with something else,
|
||||
// so the calc() is invalid.
|
||||
ErrorReporter::the().report(InvalidValueError {
|
||||
|
|
|
|||
|
|
@ -4,7 +4,9 @@
|
|||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#include <LibWeb/CSS/CalculationResolutionContext.h>
|
||||
#include <LibWeb/CSS/StyleValues/AnchorStyleValue.h>
|
||||
#include <LibWeb/CSS/StyleValues/CalculatedStyleValue.h>
|
||||
|
||||
namespace Web::CSS {
|
||||
|
||||
|
|
@ -19,7 +21,7 @@ ValueComparingNonnullRefPtr<AnchorStyleValue const> AnchorStyleValue::create(
|
|||
AnchorStyleValue::AnchorStyleValue(Optional<FlyString> const& anchor_name,
|
||||
ValueComparingNonnullRefPtr<StyleValue const> const& anchor_side,
|
||||
ValueComparingRefPtr<StyleValue const> const& fallback_value)
|
||||
: StyleValueWithDefaultOperators(Type::Anchor)
|
||||
: AbstractNonMathCalcFunctionStyleValue(Type::Anchor)
|
||||
, m_properties { .anchor_name = anchor_name, .anchor_side = anchor_side, .fallback_value = fallback_value }
|
||||
{
|
||||
}
|
||||
|
|
@ -43,4 +45,38 @@ void AnchorStyleValue::serialize(StringBuilder& builder, SerializationMode seria
|
|||
builder.append(')');
|
||||
}
|
||||
|
||||
// https://drafts.csswg.org/css-anchor-position-1/#anchor-pos
|
||||
RefPtr<CalculationNode const> AnchorStyleValue::resolve_to_calculation_node(CalculationContext const& calculation_context, CalculationResolutionContext const& calculation_resolution_context) const
|
||||
{
|
||||
if (!calculation_resolution_context.anchor_resolver)
|
||||
return nullptr;
|
||||
|
||||
// An anchor() function representing a resolvable anchor function resolves at computed value time (using style &
|
||||
// layout interleaving) to the <length> that would align the edge of the positioned boxes' inset-modified containing
|
||||
// block corresponding to the property the function appears in with the specified edge of the target anchor
|
||||
// element’s anchor box.
|
||||
if (auto side_px = calculation_resolution_context.anchor_resolver->resolve(*this); side_px.has_value())
|
||||
return NumericCalculationNode::create(Length::make_px(side_px.release_value()), calculation_context);
|
||||
|
||||
// If any of these conditions are false, the anchor() function computes to its specified fallback value. If no
|
||||
// fallback value is specified, it makes the declaration referencing it invalid at computed-value time.
|
||||
auto const& fallback_value = m_properties.fallback_value;
|
||||
if (!fallback_value)
|
||||
return nullptr;
|
||||
|
||||
// NB: The fallback value can itself be an anchor(), which is resolved when the substituted tree is simplified.
|
||||
NonnullRefPtr<CalculationNode const> fallback_node = fallback_value->is_anchor()
|
||||
? static_cast<NonnullRefPtr<CalculationNode const>>(NonMathFunctionCalculationNode::create(fallback_value->as_anchor(), NumericType { NumericType::BaseType::Length, 1 }))
|
||||
: CalculationNode::from_style_value(*fallback_value, calculation_context);
|
||||
return simplify_a_calculation_tree(fallback_node, calculation_context, calculation_resolution_context);
|
||||
}
|
||||
|
||||
bool AnchorStyleValue::equals(StyleValue const& other) const
|
||||
{
|
||||
if (type() != other.type())
|
||||
return false;
|
||||
|
||||
return m_properties == other.as_anchor().m_properties;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,12 +8,12 @@
|
|||
|
||||
#include <AK/FlyString.h>
|
||||
#include <LibWeb/CSS/PercentageOr.h>
|
||||
#include <LibWeb/CSS/StyleValues/StyleValue.h>
|
||||
#include <LibWeb/CSS/StyleValues/AbstractNonMathCalcFunctionStyleValue.h>
|
||||
|
||||
namespace Web::CSS {
|
||||
|
||||
// https://drafts.csswg.org/css-anchor-position-1/#funcdef-anchor
|
||||
class AnchorStyleValue final : public StyleValueWithDefaultOperators<AnchorStyleValue> {
|
||||
class AnchorStyleValue final : public AbstractNonMathCalcFunctionStyleValue {
|
||||
public:
|
||||
static ValueComparingNonnullRefPtr<AnchorStyleValue const> create(Optional<FlyString> const& anchor_name,
|
||||
ValueComparingNonnullRefPtr<StyleValue const> const& anchor_side,
|
||||
|
|
@ -21,8 +21,9 @@ public:
|
|||
virtual ~AnchorStyleValue() override = default;
|
||||
|
||||
virtual void serialize(StringBuilder&, SerializationMode) const override;
|
||||
virtual RefPtr<CalculationNode const> resolve_to_calculation_node(CalculationContext const&, CalculationResolutionContext const&) const override;
|
||||
|
||||
bool properties_equal(AnchorStyleValue const& other) const { return m_properties == other.m_properties; }
|
||||
virtual bool equals(StyleValue const& other) const override;
|
||||
|
||||
virtual bool is_computationally_independent() const override { return true; }
|
||||
|
||||
|
|
|
|||
|
|
@ -5,7 +5,9 @@
|
|||
*/
|
||||
|
||||
#include <LibWeb/CSS/ComputedProperties.h>
|
||||
#include <LibWeb/CSS/PropertyNameAndID.h>
|
||||
#include <LibWeb/CSS/StyleValues/AnchorStyleValue.h>
|
||||
#include <LibWeb/CSS/StyleValues/CalculatedStyleValue.h>
|
||||
#include <LibWeb/CSS/StyleValues/KeywordStyleValue.h>
|
||||
#include <LibWeb/CSS/StyleValues/PercentageStyleValue.h>
|
||||
#include <LibWeb/DOM/Document.h>
|
||||
|
|
@ -1549,6 +1551,58 @@ AbsposContainingBlockInfo FormattingContext::resolve_abspos_containing_block_inf
|
|||
return { rect, horizontal_axis_mode, vertical_axis_mode, {}, {} };
|
||||
}
|
||||
|
||||
static bool calculation_tree_contains_anchor(CSS::CalculationNode const& root)
|
||||
{
|
||||
if (root.type() == CSS::CalculationNode::Type::NonMathFunction && as<CSS::NonMathFunctionCalculationNode>(root).function()->is_anchor())
|
||||
return true;
|
||||
|
||||
for (auto const& child : root.children()) {
|
||||
if (calculation_tree_contains_anchor(child))
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
template<typename ResolveAnchorSide>
|
||||
class AnchorInsetResolver final : public CSS::AnchorResolver {
|
||||
public:
|
||||
AnchorInsetResolver(ResolveAnchorSide const& resolve_anchor_side, bool box_is_absolutely_positioned, bool is_from_end, bool is_horizontal_axis, CSSPixels containing_block_extent)
|
||||
: m_resolve_anchor_side(resolve_anchor_side)
|
||||
, m_box_is_absolutely_positioned(box_is_absolutely_positioned)
|
||||
, m_is_from_end(is_from_end)
|
||||
, m_is_horizontal_axis(is_horizontal_axis)
|
||||
, m_containing_block_extent(containing_block_extent)
|
||||
{
|
||||
}
|
||||
|
||||
virtual Optional<CSSPixels> resolve(CSS::AnchorStyleValue const& anchor) const override
|
||||
{
|
||||
if (!m_box_is_absolutely_positioned)
|
||||
return {};
|
||||
|
||||
auto side_px = m_resolve_anchor_side(anchor, m_is_from_end, m_is_horizontal_axis);
|
||||
if (!side_px.has_value())
|
||||
return {};
|
||||
|
||||
// For inset properties measuring from the end edge (right, bottom), the resolved length is the distance from
|
||||
// the anchor side to the corresponding edge of the containing block's padding box.
|
||||
if (m_is_from_end)
|
||||
return m_containing_block_extent - side_px.value();
|
||||
return side_px.value();
|
||||
}
|
||||
|
||||
private:
|
||||
ResolveAnchorSide const& m_resolve_anchor_side;
|
||||
bool m_box_is_absolutely_positioned { false };
|
||||
bool m_is_from_end { false };
|
||||
bool m_is_horizontal_axis { false };
|
||||
CSSPixels m_containing_block_extent { 0 };
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
// https://drafts.csswg.org/css-anchor-position-1/#anchor-pos
|
||||
void FormattingContext::resolve_anchor_insets(Box& box) const
|
||||
{
|
||||
|
|
@ -1563,18 +1617,40 @@ void FormattingContext::resolve_anchor_insets(Box& box) const
|
|||
// and we only resolve anchor() values in inset properties.
|
||||
// FIXME: Support anchor-scope, position-try-fallbacks, anchor-size(), and other anchor positioning features.
|
||||
|
||||
auto const* element = as_if<DOM::Element>(box.dom_node());
|
||||
// NB: Generated boxes for pseudo-elements are anonymous, so their anchor insets live in the generator element's
|
||||
// computed properties for the relevant pseudo-element rather than on a DOM node of their own.
|
||||
DOM::Element const* element = nullptr;
|
||||
Optional<CSS::PseudoElement> pseudo_element;
|
||||
if (box.is_generated_for_pseudo_element()) {
|
||||
element = box.pseudo_element_generator();
|
||||
pseudo_element = box.generated_for_pseudo_element();
|
||||
} else {
|
||||
element = as_if<DOM::Element>(box.dom_node());
|
||||
}
|
||||
if (!element)
|
||||
return;
|
||||
|
||||
auto computed = element->computed_properties();
|
||||
auto computed = element->computed_properties(pseudo_element);
|
||||
if (!computed)
|
||||
return;
|
||||
auto const& top = computed->property(CSS::PropertyID::Top);
|
||||
auto const& right = computed->property(CSS::PropertyID::Right);
|
||||
auto const& bottom = computed->property(CSS::PropertyID::Bottom);
|
||||
auto const& left = computed->property(CSS::PropertyID::Left);
|
||||
if (!top.is_anchor() && !right.is_anchor() && !bottom.is_anchor() && !left.is_anchor())
|
||||
|
||||
auto style_value_contains_anchor = [](CSS::StyleValue const& value) {
|
||||
if (value.is_anchor())
|
||||
return true;
|
||||
if (value.is_calculated())
|
||||
return calculation_tree_contains_anchor(value.as_calculated().calculation());
|
||||
return false;
|
||||
};
|
||||
|
||||
bool top_contains_anchor = style_value_contains_anchor(top);
|
||||
bool right_contains_anchor = style_value_contains_anchor(right);
|
||||
bool bottom_contains_anchor = style_value_contains_anchor(bottom);
|
||||
bool left_contains_anchor = style_value_contains_anchor(left);
|
||||
if (!top_contains_anchor && !right_contains_anchor && !bottom_contains_anchor && !left_contains_anchor)
|
||||
return;
|
||||
|
||||
auto containing_block = box.containing_block();
|
||||
|
|
@ -1711,42 +1787,45 @@ void FormattingContext::resolve_anchor_insets(Box& box) const
|
|||
return {};
|
||||
};
|
||||
|
||||
auto resolve_anchor_for_inset = [&](CSS::AnchorStyleValue const& anchor, bool is_from_end, bool is_horizontal_axis)
|
||||
-> CSS::LengthPercentageOrAuto {
|
||||
auto maybe_side_px = resolve_anchor_side(anchor, is_from_end, is_horizontal_axis);
|
||||
auto resolve_inset = [&](bool contains_anchor, CSS::StyleValue const& value, CSS::LengthPercentageOrAuto const& existing_value, CSS::PropertyID property_id, bool is_from_end, bool is_horizontal_axis) -> CSS::LengthPercentageOrAuto {
|
||||
if (!contains_anchor)
|
||||
return existing_value;
|
||||
|
||||
// If any of these conditions are false, the anchor() function computes to its specified fallback value. If no
|
||||
// fallback value is specified, it makes the declaration referencing it invalid at computed-value time.
|
||||
// NB: The fallback value can itself be an anchor(), so we walk the chain.
|
||||
auto const* current = &anchor;
|
||||
while (!maybe_side_px.has_value()) {
|
||||
auto const& fallback = current->fallback_value();
|
||||
if (!fallback)
|
||||
auto containing_block_extent = is_horizontal_axis
|
||||
? containing_block_state.padding_box_width()
|
||||
: containing_block_state.padding_box_height();
|
||||
|
||||
AnchorInsetResolver anchor_resolver { resolve_anchor_side, box.is_absolutely_positioned(), is_from_end, is_horizontal_axis, containing_block_extent };
|
||||
|
||||
CSS::CalculationResolutionContext resolution_context {
|
||||
.percentage_basis = CSS::Length::make_px(containing_block_extent),
|
||||
.length_resolution_context = CSS::Length::ResolutionContext::for_layout_node(box),
|
||||
.anchor_resolver = &anchor_resolver,
|
||||
};
|
||||
|
||||
auto to_inset = [](Optional<CSS::Length> resolved_length) -> CSS::LengthPercentageOrAuto {
|
||||
if (!resolved_length.has_value())
|
||||
return CSS::LengthPercentageOrAuto::make_auto();
|
||||
if (!fallback->is_anchor())
|
||||
return CSS::LengthPercentageOrAuto::from_style_value(*fallback);
|
||||
current = &fallback->as_anchor();
|
||||
maybe_side_px = resolve_anchor_side(*current, is_from_end, is_horizontal_axis);
|
||||
return { CSS::LengthPercentage { resolved_length.release_value() } };
|
||||
};
|
||||
|
||||
// A bare anchor() inset is wrapped in a calculation so it resolves through the same path as calc(anchor()).
|
||||
if (value.is_anchor()) {
|
||||
auto calculation_context = CSS::CalculationContext::for_property(CSS::PropertyNameAndID::from_id(property_id));
|
||||
auto calculation_node = CSS::NonMathFunctionCalculationNode::create(value.as_anchor(), CSS::NumericType { CSS::NumericType::BaseType::Length, 1 });
|
||||
auto calculated_value = CSS::CalculatedStyleValue::create(calculation_node, CSS::NumericType { CSS::NumericType::BaseType::Length, 1 }, calculation_context);
|
||||
return to_inset(calculated_value->resolve_length(resolution_context));
|
||||
}
|
||||
|
||||
// For inset properties measuring from the end edge (right, bottom), the resolved length is the distance from
|
||||
// the anchor side to the corresponding edge of the containing block's padding box.
|
||||
auto side_px = maybe_side_px.release_value();
|
||||
if (is_from_end) {
|
||||
auto containing_block_extent = is_horizontal_axis
|
||||
? containing_block_state.padding_box_width()
|
||||
: containing_block_state.padding_box_height();
|
||||
return { CSS::LengthPercentage { CSS::Length::make_px(containing_block_extent - side_px) } };
|
||||
}
|
||||
return { CSS::LengthPercentage { CSS::Length::make_px(side_px) } };
|
||||
return to_inset(value.as_calculated().resolve_length(resolution_context));
|
||||
};
|
||||
|
||||
auto const& existing_inset = box.computed_values().inset();
|
||||
box.mutable_computed_values().set_inset({
|
||||
top.is_anchor() ? resolve_anchor_for_inset(top.as_anchor(), false, false) : existing_inset.top(),
|
||||
right.is_anchor() ? resolve_anchor_for_inset(right.as_anchor(), true, true) : existing_inset.right(),
|
||||
bottom.is_anchor() ? resolve_anchor_for_inset(bottom.as_anchor(), true, false) : existing_inset.bottom(),
|
||||
left.is_anchor() ? resolve_anchor_for_inset(left.as_anchor(), false, true) : existing_inset.left(),
|
||||
resolve_inset(top_contains_anchor, top, existing_inset.top(), CSS::PropertyID::Top, false, false),
|
||||
resolve_inset(right_contains_anchor, right, existing_inset.right(), CSS::PropertyID::Right, true, true),
|
||||
resolve_inset(bottom_contains_anchor, bottom, existing_inset.bottom(), CSS::PropertyID::Bottom, true, false),
|
||||
resolve_inset(left_contains_anchor, left, existing_inset.left(), CSS::PropertyID::Left, false, true),
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -1989,6 +2068,21 @@ void FormattingContext::compute_height_for_absolutely_positioned_replaced_elemen
|
|||
// https://www.w3.org/TR/css-position-3/#relpos-insets
|
||||
void FormattingContext::compute_inset(NodeWithStyleAndBoxModelMetrics const& box, CSSPixelSize containing_block_size)
|
||||
{
|
||||
// anchor() functions are unresolvable in the insets of non-absolutely-positioned boxes. Substitute them with their
|
||||
// fallback value (or auto) here so the resulting calc() does not reach length resolution unresolved and crash. This
|
||||
// also covers sticky boxes, whose insets are read later from these computed values.
|
||||
// NB: The box is logically mutable during layout (resolve_anchor_insets rewrites its computed insets), it is only
|
||||
// passed as const& through the compute_inset() call chain.
|
||||
if (auto const* anchored_box = as_if<Box>(box)) {
|
||||
auto inset_contains_anchor = [](CSS::LengthPercentageOrAuto const& value) {
|
||||
return value.is_calculated() && calculation_tree_contains_anchor(value.calculated()->calculation());
|
||||
};
|
||||
auto const& inset = anchored_box->computed_values().inset();
|
||||
if (inset_contains_anchor(inset.top()) || inset_contains_anchor(inset.right())
|
||||
|| inset_contains_anchor(inset.bottom()) || inset_contains_anchor(inset.left()))
|
||||
resolve_anchor_insets(const_cast<Box&>(*anchored_box));
|
||||
}
|
||||
|
||||
if (box.computed_values().position() != CSS::Positioning::Relative)
|
||||
return;
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,26 @@
|
|||
<!DOCTYPE html>
|
||||
|
||||
<style>
|
||||
.container {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 100px);
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.box {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
}
|
||||
|
||||
.green {
|
||||
background-color: green;
|
||||
}
|
||||
</style>
|
||||
|
||||
<p>The test passes if there are three green boxes with no red.</p>
|
||||
|
||||
<div class="container">
|
||||
<div class="box green"></div>
|
||||
<div class="box green"></div>
|
||||
<div class="box green"></div>
|
||||
</div>
|
||||
|
|
@ -0,0 +1,63 @@
|
|||
<!DOCTYPE html>
|
||||
<link rel="help" href="https://drafts.csswg.org/css-anchor-position-1/#anchor-pos">
|
||||
<link rel="author" href="mailto:kiet.ho@apple.com">
|
||||
<link rel="match" href="../../../../expected/wpt-import/css/css-anchor-position/reference/anchor-in-css-min-max-function-ref.html">
|
||||
|
||||
<style>
|
||||
.container {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 100px);
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.box {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
}
|
||||
|
||||
.green {
|
||||
background-color: green;
|
||||
}
|
||||
|
||||
.red {
|
||||
background-color: red;
|
||||
}
|
||||
|
||||
#anchor1 {
|
||||
anchor-name: --anchor1;
|
||||
}
|
||||
|
||||
#anchor2 {
|
||||
anchor-name: --anchor2;
|
||||
}
|
||||
|
||||
#anchor3 {
|
||||
anchor-name: --anchor3;
|
||||
}
|
||||
|
||||
#target-fail {
|
||||
position: relative;
|
||||
left: 300px;
|
||||
}
|
||||
|
||||
#target {
|
||||
position: absolute;
|
||||
top: min(anchor(--anchor1 bottom), anchor(--anchor2 bottom), anchor(--anchor3 top));
|
||||
left: max(anchor(--anchor1 left), anchor(--anchor2 left), anchor(--anchor3 left));
|
||||
z-index: 1;
|
||||
}
|
||||
</style>
|
||||
|
||||
<p>The test passes if there are three green boxes with no red.</p>
|
||||
|
||||
<div class="container">
|
||||
<!-- These boxes are 100px by 100px and sits next to each other in a row.
|
||||
First two boxes are green and last box is red -->
|
||||
<div class="box green" id="anchor1"></div>
|
||||
<div class="box green" id="anchor2"></div>
|
||||
<div class="box red" id="anchor3"></div>
|
||||
</div>
|
||||
|
||||
<!-- If min()/max() works correctly, this green box should completely
|
||||
cover the red box. -->
|
||||
<div class="box green" id="target"></div>
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
Harness status: OK
|
||||
|
||||
Found 2 tests
|
||||
|
||||
2 Pass
|
||||
Pass Initial anchored position
|
||||
Pass Anchored position after moving
|
||||
|
|
@ -2,8 +2,7 @@ Harness status: OK
|
|||
|
||||
Found 2358 tests
|
||||
|
||||
2353 Pass
|
||||
5 Fail
|
||||
2358 Pass
|
||||
Pass e.style['left'] = "anchor(inside)" should set the property value
|
||||
Pass e.style['left'] = "anchor(inside, 1px)" should set the property value
|
||||
Pass e.style['left'] = "anchor(inside, 50%)" should set the property value
|
||||
|
|
@ -2356,9 +2355,9 @@ Pass e.style['inset-inline-end'] = "anchor(--foo min(50%, 100%), anchor(--bar le
|
|||
Pass e.style['inset-inline-end'] = "anchor(min(50%, 100%) --foo, anchor(--bar left))" should set the property value
|
||||
Pass e.style['inset-inline-end'] = "anchor(--foo min(50%, 100%), anchor(--bar left, anchor(--baz right)))" should set the property value
|
||||
Pass e.style['inset-inline-end'] = "anchor(min(50%, 100%) --foo, anchor(--bar left, anchor(--baz right)))" should set the property value
|
||||
Fail e.style['top'] = "calc((anchor(--foo top) + anchor(--bar bottom)) / 2)" should set the property value
|
||||
Fail e.style['top'] = "calc(0.5 * (anchor(--foo top) + anchor(--bar bottom)))" should set the property value
|
||||
Fail e.style['top'] = "anchor(--foo top, calc(0.5 * anchor(--bar bottom)))" should set the property value
|
||||
Fail e.style['top'] = "min(100px, 10%, anchor(--foo top), anchor(--bar bottom))" should set the property value
|
||||
Pass e.style['top'] = "calc((anchor(--foo top) + anchor(--bar bottom)) / 2)" should set the property value
|
||||
Pass e.style['top'] = "calc(0.5 * (anchor(--foo top) + anchor(--bar bottom)))" should set the property value
|
||||
Pass e.style['top'] = "anchor(--foo top, calc(0.5 * anchor(--bar bottom)))" should set the property value
|
||||
Pass e.style['top'] = "min(100px, 10%, anchor(--foo top), anchor(--bar bottom))" should set the property value
|
||||
Pass e.style['top'] = "anchor(--foo left, 0)" should set the property value
|
||||
Fail e.style['top'] = "calc(anchor(--foo left, 0))" should set the property value
|
||||
Pass e.style['top'] = "calc(anchor(--foo left, 0))" should set the property value
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
<!DOCTYPE html>
|
||||
<title>Positioning pseudo-elements using anchor functions</title>
|
||||
<link rel="help" href="https://drafts.csswg.org/css-anchor-position-1/#positioning">
|
||||
<script src="../../resources/testharness.js"></script>
|
||||
<script src="../../resources/testharnessreport.js"></script>
|
||||
<style>
|
||||
body { margin: 0 }
|
||||
#anchor, #target::before, #target::after {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
position: absolute;
|
||||
}
|
||||
#anchor.moved {
|
||||
left: 200px;
|
||||
top: 200px;
|
||||
}
|
||||
#anchor {
|
||||
left: 50px;
|
||||
top: 100px;
|
||||
anchor-name: --a;
|
||||
background: blue;
|
||||
}
|
||||
#target::before {
|
||||
position-anchor: --a;
|
||||
left: anchor(right);
|
||||
top: anchor(top);
|
||||
background: green;
|
||||
content:'';
|
||||
}
|
||||
#target::after {
|
||||
position-anchor: --a;
|
||||
left: anchor(left);
|
||||
top: anchor(bottom);
|
||||
background: green;
|
||||
content:'';
|
||||
}
|
||||
</style>
|
||||
<div id=anchor></div>
|
||||
<div id=target></div>
|
||||
<script>
|
||||
test(() => {
|
||||
assert_equals(getComputedStyle(target, '::before').top, '100px', "#target::before top is positioned against anchor");
|
||||
assert_equals(getComputedStyle(target, '::before').left, '150px', "#target::before left is positioned against anchor");
|
||||
assert_equals(getComputedStyle(target, '::after').top, '200px', "#target::after top is positioned against anchor");
|
||||
assert_equals(getComputedStyle(target, '::after').left, '50px', "#target::after left is positioned against anchor");
|
||||
}, "Initial anchored position");
|
||||
|
||||
test(() => {
|
||||
anchor.classList.add("moved");
|
||||
assert_equals(getComputedStyle(target, '::before').top, '200px', "#target::before top is positioned against anchor");
|
||||
assert_equals(getComputedStyle(target, '::before').left, '300px', "#target::before left is positioned against anchor");
|
||||
assert_equals(getComputedStyle(target, '::after').top, '300px', "#target::after top is positioned against anchor");
|
||||
assert_equals(getComputedStyle(target, '::after').left, '200px', "#target::after left is positioned against anchor");
|
||||
}, "Anchored position after moving");
|
||||
</script>
|
||||
Loading…
Reference in a new issue