mirror of
https://github.com/servo/servo.git
synced 2025-07-23 15:23:42 +01:00
Improve logic for establishing a stacking context (#35947)
In particular: - `z-index` will now work on unpositioned grid items. - `will-change: z-index` will only establish a stacking context if `z-index` applies, i.e. if the box is positioned or a flex/grid item. - The conditions in `establishes_stacking_context()` are reordered, so that the most likely ones are checked first. Signed-off-by: Oriol Brufau <obrufau@igalia.com>
This commit is contained in:
parent
205b97d5ed
commit
f93006af95
33 changed files with 233 additions and 114 deletions
|
@ -257,7 +257,9 @@ impl FlexLineItem<'_> {
|
|||
}
|
||||
|
||||
let mut fragment_info = self.item.box_.base_fragment_info();
|
||||
fragment_info.flags.insert(FragmentFlags::IS_FLEX_ITEM);
|
||||
fragment_info
|
||||
.flags
|
||||
.insert(FragmentFlags::IS_FLEX_OR_GRID_ITEM);
|
||||
if self.item.depends_on_block_constraints {
|
||||
fragment_info.flags.insert(
|
||||
FragmentFlags::SIZE_DEPENDS_ON_BLOCK_CONSTRAINTS_AND_CAN_BE_CHILD_OF_FLEX_ITEM,
|
||||
|
|
|
@ -80,8 +80,8 @@ bitflags! {
|
|||
const IS_BODY_ELEMENT_OF_HTML_ELEMENT_ROOT = 1 << 0;
|
||||
/// Whether or not the node that created this Fragment is a `<br>` element.
|
||||
const IS_BR_ELEMENT = 1 << 1;
|
||||
/// Whether or not this Fragment is a flex item.
|
||||
const IS_FLEX_ITEM = 1 << 2;
|
||||
/// Whether or not this Fragment is a flex item or a grid item.
|
||||
const IS_FLEX_OR_GRID_ITEM = 1 << 2;
|
||||
/// Whether or not this Fragment was created to contain a replaced element or is
|
||||
/// a replaced element.
|
||||
const IS_REPLACED = 1 << 3;
|
||||
|
|
|
@ -301,6 +301,7 @@ pub(crate) trait ComputedValuesExt {
|
|||
) -> LogicalSides<LengthPercentageOrAuto<'_>>;
|
||||
fn is_transformable(&self, fragment_flags: FragmentFlags) -> bool;
|
||||
fn has_transform_or_perspective(&self, fragment_flags: FragmentFlags) -> bool;
|
||||
fn z_index_applies(&self, fragment_flags: FragmentFlags) -> bool;
|
||||
fn effective_z_index(&self, fragment_flags: FragmentFlags) -> i32;
|
||||
fn effective_overflow(&self, fragment_flags: FragmentFlags) -> AxesOverflow;
|
||||
fn establishes_block_formatting_context(&self, fragment_flags: FragmentFlags) -> bool;
|
||||
|
@ -503,18 +504,35 @@ impl ComputedValuesExt for ComputedValues {
|
|||
self.get_box().perspective != Perspective::None)
|
||||
}
|
||||
|
||||
/// Whether the `z-index` property applies to this fragment.
|
||||
fn z_index_applies(&self, fragment_flags: FragmentFlags) -> bool {
|
||||
// As per CSS 2 § 9.9.1, `z-index` applies to positioned elements.
|
||||
// <http://www.w3.org/TR/CSS2/visuren.html#z-index>
|
||||
if self.get_box().position != ComputedPosition::Static {
|
||||
return true;
|
||||
}
|
||||
// More modern specs also apply it to flex and grid items.
|
||||
// - From <https://www.w3.org/TR/css-flexbox-1/#painting>:
|
||||
// > Flex items paint exactly the same as inline blocks [CSS2], except that order-modified
|
||||
// > document order is used in place of raw document order, and z-index values other than auto
|
||||
// > create a stacking context even if position is static (behaving exactly as if position
|
||||
// > were relative).
|
||||
// - From <https://drafts.csswg.org/css-flexbox/#painting>:
|
||||
// > The painting order of grid items is exactly the same as inline blocks [CSS2], except that
|
||||
// > order-modified document order is used in place of raw document order, and z-index values
|
||||
// > other than auto create a stacking context even if position is static (behaving exactly
|
||||
// > as if position were relative).
|
||||
fragment_flags.contains(FragmentFlags::IS_FLEX_OR_GRID_ITEM)
|
||||
}
|
||||
|
||||
/// Get the effective z-index of this fragment. Z-indices only apply to positioned elements
|
||||
/// per CSS 2 9.9.1 (<http://www.w3.org/TR/CSS2/visuren.html#z-index>), so this value may differ
|
||||
/// from the value specified in the style.
|
||||
fn effective_z_index(&self, fragment_flags: FragmentFlags) -> i32 {
|
||||
// From <https://drafts.csswg.org/css-flexbox/#painting>:
|
||||
// > Flex items paint exactly the same as inline blocks [CSS2], except that order-modified
|
||||
// > document order is used in place of raw document order, and z-index values other than auto
|
||||
// > create a stacking context even if position is static (behaving exactly as if position
|
||||
// > were relative).
|
||||
match self.get_box().position {
|
||||
ComputedPosition::Static if !fragment_flags.contains(FragmentFlags::IS_FLEX_ITEM) => 0,
|
||||
_ => self.get_position().z_index.integer_or(0),
|
||||
if self.z_index_applies(fragment_flags) {
|
||||
self.get_position().z_index.integer_or(0)
|
||||
} else {
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -617,78 +635,92 @@ impl ComputedValuesExt for ComputedValues {
|
|||
|
||||
/// Returns true if this fragment establishes a new stacking context and false otherwise.
|
||||
fn establishes_stacking_context(&self, fragment_flags: FragmentFlags) -> bool {
|
||||
// From <https://www.w3.org/TR/css-will-change/#valdef-will-change-custom-ident>:
|
||||
// > If any non-initial value of a property would create a stacking context on the element,
|
||||
// > specifying that property in will-change must create a stacking context on the element.
|
||||
let will_change_bits = self.clone_will_change().bits;
|
||||
if will_change_bits
|
||||
.intersects(WillChangeBits::STACKING_CONTEXT_UNCONDITIONAL | WillChangeBits::OPACITY)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// From <https://www.w3.org/TR/CSS2/visuren.html#z-index>, values different than `auto`
|
||||
// make the box establish a stacking context.
|
||||
if self.z_index_applies(fragment_flags) &&
|
||||
(!self.get_position().z_index.is_auto() ||
|
||||
will_change_bits.intersects(WillChangeBits::Z_INDEX))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// Fixed position and sticky position always create stacking contexts.
|
||||
// Note `will-change: position` is handled above by `STACKING_CONTEXT_UNCONDITIONAL`.
|
||||
if matches!(
|
||||
self.get_box().position,
|
||||
ComputedPosition::Fixed | ComputedPosition::Sticky
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// From <https://www.w3.org/TR/css-transforms-1/#transform-rendering>
|
||||
// > For elements whose layout is governed by the CSS box model, any value other than
|
||||
// > `none` for the `transform` property results in the creation of a stacking context.
|
||||
// From <https://www.w3.org/TR/css-transforms-2/#transform-style-property>
|
||||
// > A computed value of `preserve-3d` for `transform-style` on a transformable element
|
||||
// > establishes both a stacking context and a containing block for all descendants.
|
||||
// From <https://www.w3.org/TR/css-transforms-2/#perspective-property>
|
||||
// > any value other than none establishes a stacking context.
|
||||
// TODO: handle individual transform properties (`translate`, `scale` and `rotate`).
|
||||
// <https://www.w3.org/TR/css-transforms-2/#individual-transforms>
|
||||
if self.is_transformable(fragment_flags) &&
|
||||
(!self.get_box().transform.0.is_empty() ||
|
||||
self.get_box().transform_style == ComputedTransformStyle::Preserve3d ||
|
||||
self.get_box().perspective != Perspective::None ||
|
||||
will_change_bits
|
||||
.intersects(WillChangeBits::TRANSFORM | WillChangeBits::PERSPECTIVE))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// From <https://www.w3.org/TR/css-color-3/#transparency>
|
||||
// > implementations must create a new stacking context for any element with opacity less than 1.
|
||||
// Note `will-change: opacity` is handled above by `WillChangeBits::OPACITY`.
|
||||
let effects = self.get_effects();
|
||||
if effects.opacity != 1.0 {
|
||||
return true;
|
||||
}
|
||||
|
||||
if effects.mix_blend_mode != ComputedMixBlendMode::Normal {
|
||||
return true;
|
||||
}
|
||||
|
||||
// From <https://www.w3.org/TR/filter-effects-1/#FilterProperty>
|
||||
// > A computed value of other than `none` results in the creation of a stacking context
|
||||
// Note `will-change: filter` is handled above by `STACKING_CONTEXT_UNCONDITIONAL`.
|
||||
if !effects.filter.0.is_empty() {
|
||||
return true;
|
||||
}
|
||||
|
||||
if self.has_transform_or_perspective(fragment_flags) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// See <https://drafts.csswg.org/css-transforms-2/#transform-style-property>.
|
||||
if self.is_transformable(fragment_flags) &&
|
||||
self.get_box().transform_style == ComputedTransformStyle::Preserve3d
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if self.get_box().isolation == ComputedIsolation::Isolate {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Fixed position and sticky position always create stacking contexts.
|
||||
// TODO(mrobinson): We need to handle sticky positioning here when we support it.
|
||||
if self.get_box().position == ComputedPosition::Fixed {
|
||||
// From <https://www.w3.org/TR/compositing-1/#mix-blend-mode>
|
||||
// > Applying a blendmode other than `normal` to the element must establish a new stacking context
|
||||
// Note `will-change: mix-blend-mode` is handled above by `STACKING_CONTEXT_UNCONDITIONAL`.
|
||||
if effects.mix_blend_mode != ComputedMixBlendMode::Normal {
|
||||
return true;
|
||||
}
|
||||
|
||||
// From <https://www.w3.org/TR/css-masking-1/#the-clip-path>
|
||||
// > A computed value of other than `none` results in the creation of a stacking context.
|
||||
// Note `will-change: clip-path` is handled above by `STACKING_CONTEXT_UNCONDITIONAL`.
|
||||
if self.get_svg().clip_path != ClipPath::None {
|
||||
return true;
|
||||
}
|
||||
|
||||
// From <https://www.w3.org/TR/css-will-change/#valdef-will-change-custom-ident>:
|
||||
// > If any non-initial value of a property would create a stacking context on the element,
|
||||
// > specifying that property in will-change must create a stacking context on the element.
|
||||
let will_change_bits = self.clone_will_change().bits;
|
||||
if will_change_bits.intersects(
|
||||
WillChangeBits::OPACITY |
|
||||
WillChangeBits::STACKING_CONTEXT_UNCONDITIONAL |
|
||||
WillChangeBits::Z_INDEX,
|
||||
) || (will_change_bits
|
||||
.intersects(WillChangeBits::PERSPECTIVE | WillChangeBits::TRANSFORM) &&
|
||||
self.is_transformable(fragment_flags))
|
||||
{
|
||||
// From <https://www.w3.org/TR/compositing-1/#isolation>
|
||||
// > For CSS, setting `isolation` to `isolate` will turn the element into a stacking context.
|
||||
// Note `will-change: isolation` is handled above by `STACKING_CONTEXT_UNCONDITIONAL`.
|
||||
if self.get_box().isolation == ComputedIsolation::Isolate {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Statically positioned fragments don't establish stacking contexts if the previous
|
||||
// conditions are not fulfilled. Furthermore, z-index doesn't apply to statically
|
||||
// positioned fragments (except for flex items, see below).
|
||||
//
|
||||
// From <https://drafts.csswg.org/css-flexbox/#painting>:
|
||||
// > Flex items paint exactly the same as inline blocks [CSS2], except that order-modified
|
||||
// > document order is used in place of raw document order, and z-index values other than auto
|
||||
// > create a stacking context even if position is static (behaving exactly as if position
|
||||
// > were relative).
|
||||
if self.get_box().position == ComputedPosition::Static &&
|
||||
!fragment_flags.contains(FragmentFlags::IS_FLEX_ITEM)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// For absolutely and relatively positioned fragments we only establish a stacking
|
||||
// context if there is a z-index set.
|
||||
// See https://www.w3.org/TR/CSS2/visuren.html#z-index
|
||||
!self.get_position().z_index.is_auto()
|
||||
// TODO: We need to handle CSS Contain here.
|
||||
false
|
||||
}
|
||||
|
||||
/// Returns true if this style establishes a containing block for absolute
|
||||
|
|
|
@ -20,7 +20,9 @@ use crate::formatting_contexts::{
|
|||
Baselines, IndependentFormattingContext, IndependentFormattingContextContents,
|
||||
IndependentLayout,
|
||||
};
|
||||
use crate::fragment_tree::{BoxFragment, CollapsedBlockMargins, Fragment, SpecificLayoutInfo};
|
||||
use crate::fragment_tree::{
|
||||
BoxFragment, CollapsedBlockMargins, Fragment, FragmentFlags, SpecificLayoutInfo,
|
||||
};
|
||||
use crate::geom::{
|
||||
LogicalSides, LogicalVec2, PhysicalPoint, PhysicalRect, PhysicalSides, PhysicalSize, Size,
|
||||
SizeConstraint, Sizes,
|
||||
|
@ -560,8 +562,12 @@ impl TaffyContainer {
|
|||
|
||||
match &mut child.taffy_level_box {
|
||||
TaffyItemBoxInner::InFlowBox(independent_box) => {
|
||||
let mut fragment_info = independent_box.base_fragment_info();
|
||||
fragment_info
|
||||
.flags
|
||||
.insert(FragmentFlags::IS_FLEX_OR_GRID_ITEM);
|
||||
let mut box_fragment = BoxFragment::new(
|
||||
independent_box.base_fragment_info(),
|
||||
fragment_info,
|
||||
independent_box.style().clone(),
|
||||
std::mem::take(&mut child.child_fragments),
|
||||
content_size,
|
||||
|
|
39
tests/wpt/meta/MANIFEST.json
vendored
39
tests/wpt/meta/MANIFEST.json
vendored
|
@ -307667,6 +307667,45 @@
|
|||
{}
|
||||
]
|
||||
],
|
||||
"will-change-stacking-context-z-index-2.html": [
|
||||
"9379185048a78b031e8ac290bd7bb3e5df24fa5f",
|
||||
[
|
||||
null,
|
||||
[
|
||||
[
|
||||
"/css/reference/ref-filled-green-100px-square.xht",
|
||||
"=="
|
||||
]
|
||||
],
|
||||
{}
|
||||
]
|
||||
],
|
||||
"will-change-stacking-context-z-index-3.html": [
|
||||
"4ec40ae1a430a095ea6f703c8c27698e04792220",
|
||||
[
|
||||
null,
|
||||
[
|
||||
[
|
||||
"/css/reference/ref-filled-green-100px-square.xht",
|
||||
"=="
|
||||
]
|
||||
],
|
||||
{}
|
||||
]
|
||||
],
|
||||
"will-change-stacking-context-z-index-4.html": [
|
||||
"d8a87b23415c3c10a6f4875f16cd63301e789152",
|
||||
[
|
||||
null,
|
||||
[
|
||||
[
|
||||
"/css/reference/ref-filled-green-100px-square.xht",
|
||||
"=="
|
||||
]
|
||||
],
|
||||
{}
|
||||
]
|
||||
],
|
||||
"will-change-transform-add-content.html": [
|
||||
"1d8568ee629e48adfe99abab2e2194a73ae1301a",
|
||||
[
|
||||
|
|
|
@ -1,2 +0,0 @@
|
|||
[grid-inline-z-axis-ordering-001.html]
|
||||
expected: FAIL
|
|
@ -1,2 +0,0 @@
|
|||
[grid-inline-z-axis-ordering-002.html]
|
||||
expected: FAIL
|
|
@ -1,2 +0,0 @@
|
|||
[grid-inline-z-axis-ordering-003.html]
|
||||
expected: FAIL
|
|
@ -1,2 +0,0 @@
|
|||
[grid-inline-z-axis-ordering-004.html]
|
||||
expected: FAIL
|
|
@ -1,2 +0,0 @@
|
|||
[grid-inline-z-axis-ordering-005.html]
|
||||
expected: FAIL
|
|
@ -1,2 +0,0 @@
|
|||
[grid-inline-z-axis-ordering-overlapped-items-001.html]
|
||||
expected: FAIL
|
|
@ -1,2 +0,0 @@
|
|||
[grid-inline-z-axis-ordering-overlapped-items-002.html]
|
||||
expected: FAIL
|
|
@ -1,2 +0,0 @@
|
|||
[grid-inline-z-axis-ordering-overlapped-items-003.html]
|
||||
expected: FAIL
|
|
@ -1,2 +0,0 @@
|
|||
[grid-inline-z-axis-ordering-overlapped-items-004.html]
|
||||
expected: FAIL
|
|
@ -1,2 +0,0 @@
|
|||
[grid-inline-z-axis-ordering-overlapped-items-005.html]
|
||||
expected: FAIL
|
|
@ -1,2 +0,0 @@
|
|||
[grid-inline-z-axis-ordering-overlapped-items-006.html]
|
||||
expected: FAIL
|
|
@ -1,2 +0,0 @@
|
|||
[grid-layout-z-order-a.html]
|
||||
expected: FAIL
|
|
@ -1,2 +0,0 @@
|
|||
[grid-layout-z-order-b.html]
|
||||
expected: FAIL
|
|
@ -1,2 +0,0 @@
|
|||
[grid-z-axis-ordering-001.html]
|
||||
expected: FAIL
|
|
@ -1,2 +0,0 @@
|
|||
[grid-z-axis-ordering-002.html]
|
||||
expected: FAIL
|
|
@ -1,2 +0,0 @@
|
|||
[grid-z-axis-ordering-003.html]
|
||||
expected: FAIL
|
|
@ -1,2 +0,0 @@
|
|||
[grid-z-axis-ordering-004.html]
|
||||
expected: FAIL
|
|
@ -1,2 +0,0 @@
|
|||
[grid-z-axis-ordering-005.html]
|
||||
expected: FAIL
|
|
@ -1,2 +0,0 @@
|
|||
[grid-z-axis-ordering-overlapped-items-001.html]
|
||||
expected: FAIL
|
|
@ -1,2 +0,0 @@
|
|||
[grid-z-axis-ordering-overlapped-items-002.html]
|
||||
expected: FAIL
|
|
@ -1,2 +0,0 @@
|
|||
[grid-z-axis-ordering-overlapped-items-003.html]
|
||||
expected: FAIL
|
|
@ -1,2 +0,0 @@
|
|||
[grid-z-axis-ordering-overlapped-items-004.html]
|
||||
expected: FAIL
|
|
@ -1,2 +0,0 @@
|
|||
[grid-z-axis-ordering-overlapped-items-005.html]
|
||||
expected: FAIL
|
|
@ -1,2 +0,0 @@
|
|||
[grid-z-axis-ordering-overlapped-items-006.html]
|
||||
expected: FAIL
|
2
tests/wpt/meta/css/css-will-change/will-change-stacking-context-z-index-3.html.ini
vendored
Normal file
2
tests/wpt/meta/css/css-will-change/will-change-stacking-context-z-index-3.html.ini
vendored
Normal file
|
@ -0,0 +1,2 @@
|
|||
[will-change-stacking-context-z-index-3.html]
|
||||
prefs: ["layout_grid_enabled:true"]
|
29
tests/wpt/tests/css/css-will-change/will-change-stacking-context-z-index-2.html
vendored
Normal file
29
tests/wpt/tests/css/css-will-change/will-change-stacking-context-z-index-2.html
vendored
Normal file
|
@ -0,0 +1,29 @@
|
|||
<!DOCTYPE html>
|
||||
<title>CSS Test: `will-change: z-index`</title>
|
||||
<link rel="author" title="Oriol Brufau" href="obrufau@igalia.com">
|
||||
<link rel="help" href="https://www.w3.org/TR/css-will-change/#valdef-will-change-custom-ident">
|
||||
<link rel="help" href="https://www.w3.org/TR/css-flexbox-1/#painting">
|
||||
<link rel="help" href="https://github.com/w3c/csswg-drafts/issues/11827">
|
||||
<link rel="match" href="../reference/ref-filled-green-100px-square.xht">
|
||||
<meta name="assert" content="
|
||||
`will-change: z-index` establishes a stacking context on a flex item.
|
||||
">
|
||||
<style>
|
||||
.test {
|
||||
will-change: z-index;
|
||||
width: 100px;
|
||||
background: red;
|
||||
}
|
||||
.test::before {
|
||||
content: "";
|
||||
display: block;
|
||||
position: relative;
|
||||
z-index: -1;
|
||||
height: 100px;
|
||||
background: green;
|
||||
}
|
||||
</style>
|
||||
<p>Test passes if there is a filled green square and <strong>no red</strong>.</p>
|
||||
<div style="display: flex">
|
||||
<div class="test"></div>
|
||||
</div>
|
29
tests/wpt/tests/css/css-will-change/will-change-stacking-context-z-index-3.html
vendored
Normal file
29
tests/wpt/tests/css/css-will-change/will-change-stacking-context-z-index-3.html
vendored
Normal file
|
@ -0,0 +1,29 @@
|
|||
<!DOCTYPE html>
|
||||
<title>CSS Test: `will-change: z-index`</title>
|
||||
<link rel="author" title="Oriol Brufau" href="obrufau@igalia.com">
|
||||
<link rel="help" href="https://www.w3.org/TR/css-will-change/#valdef-will-change-custom-ident">
|
||||
<link rel="help" href="https://www.w3.org/TR/css-grid-2/#z-order">
|
||||
<link rel="help" href="https://github.com/w3c/csswg-drafts/issues/11827">
|
||||
<link rel="match" href="../reference/ref-filled-green-100px-square.xht">
|
||||
<meta name="assert" content="
|
||||
`will-change: z-index` establishes a stacking context on a grid item.
|
||||
">
|
||||
<style>
|
||||
.test {
|
||||
will-change: z-index;
|
||||
width: 100px;
|
||||
background: red;
|
||||
}
|
||||
.test::before {
|
||||
content: "";
|
||||
display: block;
|
||||
position: relative;
|
||||
z-index: -1;
|
||||
height: 100px;
|
||||
background: green;
|
||||
}
|
||||
</style>
|
||||
<p>Test passes if there is a filled green square and <strong>no red</strong>.</p>
|
||||
<div style="display: grid">
|
||||
<div class="test"></div>
|
||||
</div>
|
28
tests/wpt/tests/css/css-will-change/will-change-stacking-context-z-index-4.html
vendored
Normal file
28
tests/wpt/tests/css/css-will-change/will-change-stacking-context-z-index-4.html
vendored
Normal file
|
@ -0,0 +1,28 @@
|
|||
<!DOCTYPE html>
|
||||
<title>CSS Test: `will-change: z-index`</title>
|
||||
<link rel="author" title="Oriol Brufau" href="obrufau@igalia.com">
|
||||
<link rel="help" href="https://www.w3.org/TR/css-will-change/#valdef-will-change-custom-ident">
|
||||
<link rel="help" href="https://www.w3.org/TR/CSS2/visuren.html#z-index">
|
||||
<link rel="help" href="https://github.com/w3c/csswg-drafts/issues/11827">
|
||||
<link rel="match" href="../reference/ref-filled-green-100px-square.xht">
|
||||
<meta name="assert" content="
|
||||
`will-change: z-index` doesn't establish a stacking context on a non-positioned block box,
|
||||
because `z-index` doesn't apply in that case.
|
||||
">
|
||||
<style>
|
||||
.test {
|
||||
will-change: z-index;
|
||||
width: 100px;
|
||||
background: green;
|
||||
}
|
||||
.test::before {
|
||||
content: "";
|
||||
display: block;
|
||||
position: relative;
|
||||
z-index: -1;
|
||||
height: 100px;
|
||||
background: red;
|
||||
}
|
||||
</style>
|
||||
<p>Test passes if there is a filled green square and <strong>no red</strong>.</p>
|
||||
<div class="test"></div>
|
Loading…
Add table
Add a link
Reference in a new issue