Implement flex-flow shorthand semantics
Parse the unordered flex-direction/flex-wrap shorthand atomically, reset omitted constituents to their initial values, and expose the same grammar through CSS.supports and computed-style capability surfaces. Add a Chromium-paired fixture covering both token orders, shorthand resets, invalid-declaration fallback, and JavaScript capability reporting.
This commit is contained in:
1 parent
c2b57c74a1
commit
98c53b6b15
5 files changed
+204
-4
No files matched your search
Vendored
+19
-2
@@ -6714,7 +6714,7 @@ globalThis.getComputedStyle = (el) => {
|
||||
'z-index': 'auto', 'pointer-events': 'auto',
|
||||
'box-sizing': 'content-box', cursor: 'auto',
|
||||
'white-space': 'normal', 'text-align': 'start',
|
||||
'flex-direction': 'row', 'flex-wrap': 'nowrap', 'align-items': 'normal',
|
||||
'flex-flow': 'row nowrap', 'flex-direction': 'row', 'flex-wrap': 'nowrap', 'align-items': 'normal',
|
||||
'justify-content': 'normal', gap: 'normal',
|
||||
'grid-template-columns': 'none', 'grid-template-rows': 'none',
|
||||
'will-change': 'auto', 'backface-visibility': 'visible',
|
||||
@@ -8563,7 +8563,7 @@ const _CSS_SUPPORTED_DECLARATIONS = new Set((
|
||||
"stroke stroke-width border-color font-size font font-weight font-family font-style text-align " +
|
||||
"text-transform text-decoration text-decoration-line line-height white-space overflow-wrap word-wrap word-break text-wrap text-wrap-style align-items justify-items " +
|
||||
"place-items align-self justify-self place-self align-content justify-content place-content " +
|
||||
"flex-direction flex-wrap flex-grow flex-shrink flex-basis flex order position float object-fit " +
|
||||
"flex-flow flex-direction flex-wrap flex-grow flex-shrink flex-basis flex order position float object-fit " +
|
||||
"top right bottom left inset overflow overflow-x overflow-y scrollbar-gutter visibility opacity animation " +
|
||||
"animation-name animation-fill-mode animation-iteration-count z-index clear vertical-align " +
|
||||
"list-style list-style-type gap grid-gap row-gap grid-row-gap column-gap grid-column-gap " +
|
||||
@@ -8739,6 +8739,23 @@ function _cssSupportsDeclaration(name, value) {
|
||||
if (name === "align-content" || name === "justify-content") {
|
||||
return _cssSupportsContentAlignment(lower) || (name === "justify-content" && ["left", "right"].includes(lower));
|
||||
}
|
||||
if (name === "flex-flow") {
|
||||
const tokens = _cssSplitWhitespace(lower);
|
||||
if (tokens.length < 1 || tokens.length > 2) return false;
|
||||
let direction = false, wrap = false;
|
||||
for (const token of tokens) {
|
||||
if (["row", "row-reverse", "column", "column-reverse"].includes(token)) {
|
||||
if (direction) return false;
|
||||
direction = true;
|
||||
} else if (["nowrap", "wrap", "wrap-reverse"].includes(token)) {
|
||||
if (wrap) return false;
|
||||
wrap = true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
if (name === "flex-direction") return ["row", "row-reverse", "column", "column-reverse"].includes(lower);
|
||||
if (name === "flex-wrap") return ["nowrap", "wrap", "wrap-reverse"].includes(lower);
|
||||
if (name === "flex-grow" || name === "flex-shrink") {
|
||||
|
||||
@@ -4708,13 +4708,19 @@ mod tests {
|
||||
CSS.supports("word-break", "break-all"),
|
||||
CSS.supports("filter", "blur(2px)"),
|
||||
CSS.supports("content", "attr(data-label)"),
|
||||
CSS.supports("display", "grid;")
|
||||
CSS.supports("display", "grid;"),
|
||||
CSS.supports("flex-flow", "column"),
|
||||
CSS.supports("flex-flow", "wrap column"),
|
||||
CSS.supports("flex-flow", "column wrap"),
|
||||
CSS.supports("flex-flow", "row column"),
|
||||
CSS.supports("flex-flow", "nowrap wrap-reverse"),
|
||||
CSS.supports("(flex-flow:column)")
|
||||
])"#,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
result,
|
||||
serde_json::json!("[false,false,false,false,false,true,true,true,false,false,false,true,false,true,true,false,false,true,false,true,false,true,false]")
|
||||
serde_json::json!("[false,false,false,false,false,true,true,true,false,false,false,true,false,true,true,false,false,true,false,true,false,true,false,true,true,true,false,false,true]")
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1220,6 +1220,15 @@ fn apply_value(style: &mut LayoutStyle, name: &str, value: &str) {
|
||||
style.justify_content = Some(justify);
|
||||
}
|
||||
}
|
||||
"flex-flow" => {
|
||||
if let Some((direction, wrap)) = parse_flex_flow_shorthand(value) {
|
||||
// A shorthand always assigns both longhands. In particular,
|
||||
// `flex-flow: column` resets an earlier flex-wrap to nowrap,
|
||||
// while `flex-flow: wrap` resets direction to row.
|
||||
style.flex_direction = Some(direction);
|
||||
style.flex_wrap = Some(wrap);
|
||||
}
|
||||
}
|
||||
"flex-direction" => match value {
|
||||
"row" => style.flex_direction = Some(taffy::FlexDirection::Row),
|
||||
"row-reverse" => style.flex_direction = Some(taffy::FlexDirection::RowReverse),
|
||||
@@ -1970,6 +1979,7 @@ pub fn supports_declaration(name: &str, value: &str) -> bool {
|
||||
| "align-content"
|
||||
| "justify-content"
|
||||
| "place-content"
|
||||
| "flex-flow"
|
||||
| "flex-direction"
|
||||
| "flex-wrap"
|
||||
| "flex-grow"
|
||||
@@ -2527,6 +2537,7 @@ fn supports_conservative_known_value(name: &str, value: &str) -> bool {
|
||||
matches!(lower.as_str(), "left" | "right") || content_alignment_value(value).is_some()
|
||||
}
|
||||
"place-content" => content_alignment_pair(value).is_some(),
|
||||
"flex-flow" => parse_flex_flow_shorthand(value).is_some(),
|
||||
"flex-direction" => matches!(
|
||||
lower.as_str(),
|
||||
"row" | "row-reverse" | "column" | "column-reverse"
|
||||
@@ -7388,6 +7399,61 @@ fn parse_flex_shorthand(style: &mut LayoutStyle, value: &str) {
|
||||
};
|
||||
}
|
||||
|
||||
/// Parse `flex-flow: <flex-direction> || <flex-wrap>`. The two constituents
|
||||
/// may appear in either order and each may appear at most once. Shorthand
|
||||
/// omission resets the other constituent to its initial value rather than
|
||||
/// retaining an earlier longhand winner.
|
||||
fn parse_flex_flow_shorthand(
|
||||
value: &str,
|
||||
) -> Option<(taffy::FlexDirection, taffy::FlexWrap)> {
|
||||
let lower = value.trim().to_ascii_lowercase();
|
||||
if matches!(
|
||||
lower.as_str(),
|
||||
"initial" | "unset" | "revert" | "revert-layer"
|
||||
) {
|
||||
return Some((taffy::FlexDirection::Row, taffy::FlexWrap::NoWrap));
|
||||
}
|
||||
|
||||
let tokens = lower.split_whitespace().collect::<Vec<_>>();
|
||||
if tokens.is_empty() || tokens.len() > 2 {
|
||||
return None;
|
||||
}
|
||||
let mut direction = None;
|
||||
let mut wrap = None;
|
||||
for token in tokens {
|
||||
let parsed_direction = match token {
|
||||
"row" => Some(taffy::FlexDirection::Row),
|
||||
"row-reverse" => Some(taffy::FlexDirection::RowReverse),
|
||||
"column" => Some(taffy::FlexDirection::Column),
|
||||
"column-reverse" => Some(taffy::FlexDirection::ColumnReverse),
|
||||
_ => None,
|
||||
};
|
||||
if let Some(parsed) = parsed_direction {
|
||||
if direction.replace(parsed).is_some() {
|
||||
return None;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
let parsed_wrap = match token {
|
||||
"nowrap" => Some(taffy::FlexWrap::NoWrap),
|
||||
"wrap" => Some(taffy::FlexWrap::Wrap),
|
||||
"wrap-reverse" => Some(taffy::FlexWrap::WrapReverse),
|
||||
_ => None,
|
||||
};
|
||||
if let Some(parsed) = parsed_wrap {
|
||||
if wrap.replace(parsed).is_some() {
|
||||
return None;
|
||||
}
|
||||
} else {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
Some((
|
||||
direction.unwrap_or(taffy::FlexDirection::Row),
|
||||
wrap.unwrap_or(taffy::FlexWrap::NoWrap),
|
||||
))
|
||||
}
|
||||
|
||||
/// `background-size: 10px` / `0.857em` / `10px 20px` -> explicit px pair.
|
||||
/// Keyword values (`cover`, `contain`, `auto`) are left unhandled (`None`, the
|
||||
/// "stretch to fill the box" fallback) since evaluating them needs the
|
||||
@@ -9831,6 +9897,64 @@ mod tests {
|
||||
assert_eq!(s.flex_shrink, Some(1.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn flex_flow_is_unordered_resets_omissions_and_rejects_atomically() {
|
||||
for value in [
|
||||
"column",
|
||||
"wrap",
|
||||
"column wrap",
|
||||
"wrap column",
|
||||
"row-reverse wrap-reverse",
|
||||
"WRAP COLUMN-REVERSE",
|
||||
] {
|
||||
assert!(supports_declaration("flex-flow", value), "{value}");
|
||||
}
|
||||
for value in [
|
||||
"",
|
||||
"none",
|
||||
"row column",
|
||||
"nowrap wrap-reverse",
|
||||
"row wrap extra",
|
||||
"10px",
|
||||
] {
|
||||
assert!(!supports_declaration("flex-flow", value), "{value}");
|
||||
}
|
||||
|
||||
let direction_only = compute_style(
|
||||
"div",
|
||||
Some("flex-wrap:wrap;flex-flow:column"),
|
||||
);
|
||||
assert_eq!(
|
||||
direction_only.flex_direction,
|
||||
Some(taffy::FlexDirection::Column)
|
||||
);
|
||||
assert_eq!(direction_only.flex_wrap, Some(taffy::FlexWrap::NoWrap));
|
||||
|
||||
let wrap_only = compute_style(
|
||||
"div",
|
||||
Some("flex-direction:column;flex-flow:wrap"),
|
||||
);
|
||||
assert_eq!(wrap_only.flex_direction, Some(taffy::FlexDirection::Row));
|
||||
assert_eq!(wrap_only.flex_wrap, Some(taffy::FlexWrap::Wrap));
|
||||
|
||||
let reversed = compute_style("div", Some("flex-flow:wrap column-reverse"));
|
||||
assert_eq!(
|
||||
reversed.flex_direction,
|
||||
Some(taffy::FlexDirection::ColumnReverse)
|
||||
);
|
||||
assert_eq!(reversed.flex_wrap, Some(taffy::FlexWrap::Wrap));
|
||||
|
||||
let invalid_later = compute_style(
|
||||
"div",
|
||||
Some("flex-flow:column wrap;flex-flow:row column"),
|
||||
);
|
||||
assert_eq!(
|
||||
invalid_later.flex_direction,
|
||||
Some(taffy::FlexDirection::Column)
|
||||
);
|
||||
assert_eq!(invalid_later.flex_wrap, Some(taffy::FlexWrap::Wrap));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn box_shadow_outset_parses() {
|
||||
let s = compute_style("div", Some("box-shadow: 0 2px 8px rgba(0,0,0,.15)"));
|
||||
|
||||
Reference in new issue
Block a user