diff --git a/CHANGELOG.md b/CHANGELOG.md index da45c611..ab78a5b1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ ## Releases | Version | Notes | | ------- | ----- | +| Unreleased (18.1.0) | Add `Css.Container` for `@container` queries: typed size features, `and`/`or`/`not` combinators, range syntax (`gt`/`lt`/`ge`/`le`/`eq`/`between`), named containers, `containerType`/`containerName`/`container` properties, and raw escape hatches. Add an additive condition algebra to `Css.Media` (`condition`, `expr`, `anyOf`, `allOf`, `inverse`, plus range comparisons) sharing tokens with `Css.Container`. Add `cqw`/`cqh`/`cqi`/`cqb`/`cqmin`/`cqmax` container query length units. Add `Css.Global.container`/`Css.Global.containerQuery` for global `@container` snippets. | [18.0.0](https://github.com/rtfeldman/elm-css/tree/18.0.0) | Add `line-height: normal` ([#578](https://github.com/rtfeldman/elm-css/pull/578)) and `keyedLazy` ([#584](https://github.com/rtfeldman/elm-css/pull/584)) | [17.1.1](https://github.com/rtfeldman/elm-css/tree/17.1.1) | Allow multiple `css` attributes on node ([#566](https://github.com/rtfeldman/elm-css/pull/566)) | [17.1.0](https://github.com/rtfeldman/elm-css/tree/17.1.0) | Support nonces ([#569](https://github.com/rtfeldman/elm-css/pull/569)) diff --git a/elm.json b/elm.json index be3fdce9..6f2cb80a 100644 --- a/elm.json +++ b/elm.json @@ -22,6 +22,7 @@ "Css.Animations", "Css.Transitions", "Css.Media", + "Css.Container", "Css.Global" ] }, diff --git a/src/Css.elm b/src/Css.elm index c3812b72..111129e2 100644 --- a/src/Css.elm +++ b/src/Css.elm @@ -4,7 +4,7 @@ module Css exposing , Color, all, important, solid, transparent, rgb, rgba, hsl, hsla, hex , featureTag, featureTag2, featureOn, featureOff , borderCollapse, borderColor, borderColor2, borderColor3, borderColor4, borderBottomLeftRadius, borderBottomLeftRadius2, borderBottomRightRadius, borderBottomRightRadius2, borderTopLeftRadius, borderTopLeftRadius2, borderTopRightRadius, borderTopRightRadius2, borderRadius, borderRadius2, borderRadius3, borderRadius4, borderWidth, borderWidth2, borderWidth3, borderWidth4, borderBottomWidth, borderLeftWidth, borderRightWidth, borderTopWidth, borderBottomStyle, borderLeftStyle, borderRightStyle, borderTopStyle, borderStyle, borderBottomColor, borderLeftColor, borderRightColor, borderTopColor, borderBox, contentBox, border, border2, border3, borderTop, borderTop2, borderTop3, borderBottom, borderBottom2, borderBottom3, borderLeft, borderLeft2, borderLeft3, borderRight, borderRight2, borderRight3, borderImageOutset, borderImageOutset2, borderImageOutset3, borderImageOutset4, borderImageWidth, borderImageWidth2, borderImageWidth3, borderImageWidth4, scroll, visible, block, inlineBlock, inlineFlex, inline, none, auto, inherit, unset, initial, noWrap, top, static, fixed, sticky, relative, absolute, position, float, bottom, middle, baseline, sub, super, textTop, textBottom, hidden, wavy, dotted, dashed, double, groove, ridge, inset, outset, matrix, matrix3d, perspective, rotate3d, rotateX, rotateY, rotateZ, scale, scale2, scale3d, scaleX, scaleY, skew, skew2, skewX, skewY, translate, translate2, translate3d, translateX, translateY, translateZ, rotate, fillBox, viewBox, flat, preserve3d, content, wrapReverse, wrap, flexStart, flexEnd, stretch, row, rowReverse, column, columnReverse, serif, sansSerif, monospace, cursive, fantasy, xxSmall, xSmall, small, large, xLarge, xxLarge, smaller, larger, normal, italic, oblique, bold, lighter, bolder, smallCaps, allSmallCaps, petiteCaps, allPetiteCaps, unicase, titlingCaps, commonLigatures, noCommonLigatures, discretionaryLigatures, noDiscretionaryLigatures, historicalLigatures, noHistoricalLigatures, contextual, noContextual, liningNums, oldstyleNums, proportionalNums, tabularNums, diagonalFractions, stackedFractions, ordinal, slashedZero, default, pointer, crosshair, contextMenu, help, progress, wait, cell, text_, verticalText, cursorAlias, copy, move, noDrop, notAllowed, eResize, nResize, neResize, nwResize, sResize, seResize, swResize, wResize, ewResize, nsResize, neswResize, nwseResize, colResize, rowResize, allScroll, zoomIn, zoomOut, grab, grabbing, visiblePainted, visibleFill, visibleStroke, painted, stroke - , Length, pct, px, em, pt, ex, ch, rem, vh, vw, vmin, vmax, mm, cm, inches, pc, int, num, zero, calc, plus, minus + , Length, pct, px, em, pt, ex, ch, rem, vh, vw, vmin, vmax, cqw, cqh, cqi, cqb, cqmin, cqmax, mm, cm, inches, pc, int, num, zero, calc, plus, minus , Px, Em, Rem, Pct, Ex, Ch, Vh, Vw, Vmin, Vmax, Mm, Cm, In, Pt, Pc , deg, rad, grad, turn , Duration, sec, ms @@ -365,7 +365,7 @@ functions let you define custom properties and selectors, respectively. # Length -@docs Length, pct, px, em, pt, ex, ch, rem, vh, vw, vmin, vmax, mm, cm, inches, pc, int, num, zero, calc, plus, minus +@docs Length, pct, px, em, pt, ex, ch, rem, vh, vw, vmin, vmax, cqw, cqh, cqi, cqb, cqmin, cqmax, mm, cm, inches, pc, int, num, zero, calc, plus, minus # Length Units @@ -2337,6 +2337,108 @@ type VMaxUnits = VMaxUnits +{-| [`cqw`](https://developer.mozilla.org/en-US/docs/Web/CSS/length#cqw) container query width units. +-} +type alias Cqw = + ExplicitLength CqwUnits + + +{-| [`cqw`](https://developer.mozilla.org/en-US/docs/Web/CSS/length#cqw) container query width units. +-} +cqw : Float -> Cqw +cqw = + lengthConverter CqwUnits "cqw" + + +type CqwUnits + = CqwUnits + + +{-| [`cqh`](https://developer.mozilla.org/en-US/docs/Web/CSS/length#cqh) container query height units. +-} +type alias Cqh = + ExplicitLength CqhUnits + + +{-| [`cqh`](https://developer.mozilla.org/en-US/docs/Web/CSS/length#cqh) container query height units. +-} +cqh : Float -> Cqh +cqh = + lengthConverter CqhUnits "cqh" + + +type CqhUnits + = CqhUnits + + +{-| [`cqi`](https://developer.mozilla.org/en-US/docs/Web/CSS/length#cqi) container query inline-size units. +-} +type alias Cqi = + ExplicitLength CqiUnits + + +{-| [`cqi`](https://developer.mozilla.org/en-US/docs/Web/CSS/length#cqi) container query inline-size units. +-} +cqi : Float -> Cqi +cqi = + lengthConverter CqiUnits "cqi" + + +type CqiUnits + = CqiUnits + + +{-| [`cqb`](https://developer.mozilla.org/en-US/docs/Web/CSS/length#cqb) container query block-size units. +-} +type alias Cqb = + ExplicitLength CqbUnits + + +{-| [`cqb`](https://developer.mozilla.org/en-US/docs/Web/CSS/length#cqb) container query block-size units. +-} +cqb : Float -> Cqb +cqb = + lengthConverter CqbUnits "cqb" + + +type CqbUnits + = CqbUnits + + +{-| [`cqmin`](https://developer.mozilla.org/en-US/docs/Web/CSS/length#cqmin) container query min units. +-} +type alias Cqmin = + ExplicitLength CqminUnits + + +{-| [`cqmin`](https://developer.mozilla.org/en-US/docs/Web/CSS/length#cqmin) container query min units. +-} +cqmin : Float -> Cqmin +cqmin = + lengthConverter CqminUnits "cqmin" + + +type CqminUnits + = CqminUnits + + +{-| [`cqmax`](https://developer.mozilla.org/en-US/docs/Web/CSS/length#cqmax) container query max units. +-} +type alias Cqmax = + ExplicitLength CqmaxUnits + + +{-| [`cqmax`](https://developer.mozilla.org/en-US/docs/Web/CSS/length#cqmax) container query max units. +-} +cqmax : Float -> Cqmax +cqmax = + lengthConverter CqmaxUnits "cqmax" + + +type CqmaxUnits + = CqmaxUnits + + {-| [`px`](https://developer.mozilla.org/en-US/docs/Web/CSS/length#px) units. -} type alias Px = diff --git a/src/Css/Container.elm b/src/Css/Container.elm new file mode 100644 index 00000000..d07bbeb8 --- /dev/null +++ b/src/Css/Container.elm @@ -0,0 +1,460 @@ +module Css.Container exposing + ( Condition + , withContainer, withContainerNamed, withContainerQuery + , anyOf, allOf, not, rawCondition + , minWidth, maxWidth, minHeight, maxHeight + , minInlineSize, maxInlineSize, minBlockSize, maxBlockSize + , minAspectRatio, maxAspectRatio + , width, height, inlineSize, blockSize, aspectRatio + , gt, lt, ge, le, eq, between + , Ratio, ratio + , orientation, Landscape, Portrait, landscape, portrait + , ContainerTypeValue, containerType, normal, size + , containerName, containerNames, container + , toStructureCondition + ) + +{-| Functions for building [`@container` queries](https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/At-rules/@container) +and the container establishment properties. + + +# Data Structures + +@docs Condition + + +# `@container` rule constructors + +@docs withContainer, withContainerNamed, withContainerQuery + + +# Combinators + +@docs anyOf, allOf, not, rawCondition + + +# Size features + +@docs minWidth, maxWidth, minHeight, maxHeight +@docs minInlineSize, maxInlineSize, minBlockSize, maxBlockSize +@docs minAspectRatio, maxAspectRatio + + +# Range-syntax feature tokens + +@docs width, height, inlineSize, blockSize, aspectRatio +@docs gt, lt, ge, le, eq, between +@docs Ratio, ratio + + +# Orientation + +@docs orientation, Landscape, Portrait, landscape, portrait + + +# Establishment properties + +@docs ContainerTypeValue, containerType, normal, size +@docs containerName, containerNames, container + + +# Internal + +@docs toStructureCondition + +-} + +import Css exposing (Style) +import Css.Preprocess as Preprocess +import Css.Structure as Structure exposing (Compatible(..)) + + +{-| An opaque container query condition. Build with the feature functions and +combine with `anyOf`/`allOf`/`not`. +-} +type Condition + = Condition (Structure.QueryCondition Structure.ContainerFeature) + + +unwrap : Condition -> Structure.QueryCondition Structure.ContainerFeature +unwrap (Condition c) = + c + + +type alias AbsoluteLength compatible = + { compatible | value : String, absoluteLength : Compatible } + + +type alias Value compatible = + { compatible | value : String } + + +{-| Attach a container query to a set of styles. The `Condition`s in the list +are combined with `and` at the top level. + + withContainer [ minWidth (px 400) ] [ Css.color (hex "000000") ] + +-} +withContainer : List Condition -> List Style -> Style +withContainer conditions = + Preprocess.WithContainer Nothing (combineAnd conditions) + + +{-| Like `withContainer`, but scoped to a named container. + + withContainerNamed "sidebar" [ minWidth (px 400) ] [ Css.color (hex "000000") ] + +-} +withContainerNamed : String -> List Condition -> List Style -> Style +withContainerNamed name conditions = + Preprocess.WithContainer (Just name) (combineAnd conditions) + + +{-| Escape hatch: pass a raw container-query string (optionally including a +container name) verbatim. + + withContainerQuery "sidebar (min-width: 400px)" [ Css.color (hex "000000") ] + +-} +withContainerQuery : String -> List Style -> Style +withContainerQuery raw = + Preprocess.WithContainer Nothing (Structure.Raw raw) + + +combineAnd : List Condition -> Structure.QueryCondition Structure.ContainerFeature +combineAnd conditions = + case List.map unwrap conditions of + only :: [] -> + only + + many -> + Structure.And many + + +{-| Internal-use helper for `Css.Global.container`. Converts a list of +conditions (joined with `and`) into the underlying structure condition. +-} +toStructureCondition : List Condition -> Structure.QueryCondition Structure.ContainerFeature +toStructureCondition = + combineAnd + + +{-| Match if any of the given conditions match. +-} +anyOf : List Condition -> Condition +anyOf conditions = + Condition (Structure.Or (List.map unwrap conditions)) + + +{-| Match if all of the given conditions match. +-} +allOf : List Condition -> Condition +allOf conditions = + Condition (Structure.And (List.map unwrap conditions)) + + +{-| Negate a condition. +-} +not : Condition -> Condition +not condition = + Condition (Structure.Not (unwrap condition)) + + +{-| Escape hatch: pass a raw condition string verbatim, e.g. for +`style()`/`scroll-state()` container queries not otherwise supported. +-} +rawCondition : String -> Condition +rawCondition raw = + Condition (Structure.Raw raw) + + +feature : String -> Value compatible -> Condition +feature key { value } = + Condition (Structure.Feature { feature = key, value = Just value }) + + +{-| Container feature `min-width`. +-} +minWidth : AbsoluteLength compatible -> Condition +minWidth value = + feature "min-width" value + + +{-| Container feature `max-width`. +-} +maxWidth : AbsoluteLength compatible -> Condition +maxWidth value = + feature "max-width" value + + +{-| Container feature `min-height`. +-} +minHeight : AbsoluteLength compatible -> Condition +minHeight value = + feature "min-height" value + + +{-| Container feature `max-height`. +-} +maxHeight : AbsoluteLength compatible -> Condition +maxHeight value = + feature "max-height" value + + +{-| Container feature `min-inline-size`. +-} +minInlineSize : AbsoluteLength compatible -> Condition +minInlineSize value = + feature "min-inline-size" value + + +{-| Container feature `max-inline-size`. +-} +maxInlineSize : AbsoluteLength compatible -> Condition +maxInlineSize value = + feature "max-inline-size" value + + +{-| Container feature `min-block-size`. +-} +minBlockSize : AbsoluteLength compatible -> Condition +minBlockSize value = + feature "min-block-size" value + + +{-| Container feature `max-block-size`. +-} +maxBlockSize : AbsoluteLength compatible -> Condition +maxBlockSize value = + feature "max-block-size" value + + +{-| Container feature `min-aspect-ratio`. +-} +minAspectRatio : Ratio -> Condition +minAspectRatio value = + feature "min-aspect-ratio" value + + +{-| Container feature `max-aspect-ratio`. +-} +maxAspectRatio : Ratio -> Condition +maxAspectRatio value = + feature "max-aspect-ratio" value + + +{-| An aspect ratio, e.g. `ratio 16 9`. Structurally identical to `Css.Media.Ratio`. +-} +type alias Ratio = + { value : String, ratio : Compatible } + + +{-| Create a ratio. + + ratio 16 9 + +-} +ratio : Int -> Int -> Ratio +ratio numerator denominator = + { value = String.fromInt numerator ++ "/" ++ String.fromInt denominator, ratio = Compatible } + + +type alias Orientation a = + { a | value : String, orientation : Compatible } + + +{-| -} +type alias Landscape = + { value : String, orientation : Compatible } + + +{-| -} +type alias Portrait = + { value : String, orientation : Compatible } + + +{-| The `landscape` value for the `orientation` feature. +-} +landscape : Landscape +landscape = + { value = "landscape", orientation = Compatible } + + +{-| The `portrait` value for the `orientation` feature. +-} +portrait : Portrait +portrait = + { value = "portrait", orientation = Compatible } + + +{-| Container feature `orientation`. Accepts `landscape` or `portrait`. +-} +orientation : Orientation a -> Condition +orientation value = + feature "orientation" value + + +{-| Feature token for range comparisons: `width |> gt (px 400)` ⇒ `(width > 400px)`. +-} +width : { value : String, containerFeature : Compatible, mediaFeature : Compatible } +width = + { value = "width", containerFeature = Compatible, mediaFeature = Compatible } + + +{-| Feature token for range comparisons: `height |> gt (px 400)` ⇒ `(height > 400px)`. +-} +height : { value : String, containerFeature : Compatible, mediaFeature : Compatible } +height = + { value = "height", containerFeature = Compatible, mediaFeature = Compatible } + + +{-| Feature token for range comparisons: `aspectRatio |> ge (ratio 16 9)` ⇒ `(aspect-ratio >= 16/9)`. +-} +aspectRatio : { value : String, containerFeature : Compatible, mediaFeature : Compatible } +aspectRatio = + { value = "aspect-ratio", containerFeature = Compatible, mediaFeature = Compatible } + + +{-| Container-only feature token; also usable as a `container-type` value. +-} +inlineSize : { value : String, containerFeature : Compatible, containerTypeValue : Compatible } +inlineSize = + { value = "inline-size", containerFeature = Compatible, containerTypeValue = Compatible } + + +{-| Container-only feature token. +-} +blockSize : { value : String, containerFeature : Compatible } +blockSize = + { value = "block-size", containerFeature = Compatible } + + +type alias FeatureToken f = + { f | value : String, containerFeature : Compatible } + + +rangeCondition : Structure.Comparison -> Value compatible -> FeatureToken f -> Condition +rangeCondition comparison val token = + Condition + (Structure.Range + { feature = token.value + , lower = Just ( comparison, val.value ) + , upper = Nothing + } + ) + + +{-| `feature > value` +-} +gt : Value compatible -> FeatureToken f -> Condition +gt = + rangeCondition Structure.Gt + + +{-| `feature < value` +-} +lt : Value compatible -> FeatureToken f -> Condition +lt = + rangeCondition Structure.Lt + + +{-| `feature >= value` +-} +ge : Value compatible -> FeatureToken f -> Condition +ge = + rangeCondition Structure.Ge + + +{-| `feature <= value` +-} +le : Value compatible -> FeatureToken f -> Condition +le = + rangeCondition Structure.Le + + +{-| `feature = value` +-} +eq : Value compatible -> FeatureToken f -> Condition +eq = + rangeCondition Structure.Eq + + +{-| `low <= feature <= high` +-} +between : Value compatibleLow -> Value compatibleHigh -> FeatureToken f -> Condition +between low high token = + Condition + (Structure.Range + { feature = token.value + , lower = Just ( Structure.Le, low.value ) + , upper = Just ( Structure.Le, high.value ) + } + ) + + +{-| A value for the `container-type` property. +-} +type alias ContainerTypeValue a = + { a | value : String, containerTypeValue : Compatible } + + +{-| `container-type: normal` +-} +normal : { value : String, containerTypeValue : Compatible } +normal = + { value = "normal", containerTypeValue = Compatible } + + +{-| `container-type: size` +-} +size : { value : String, containerTypeValue : Compatible } +size = + { value = "size", containerTypeValue = Compatible } + + +{-| The [`container-type`](https://developer.mozilla.org/en-US/docs/Web/CSS/container-type) property. + + containerType size + + containerType inlineSize + + containerType normal + +-} +containerType : ContainerTypeValue a -> Style +containerType value = + Css.property "container-type" value.value + + +{-| The [`container-name`](https://developer.mozilla.org/en-US/docs/Web/CSS/container-name) property. + +Names must be valid CSS [custom-idents](https://developer.mozilla.org/en-US/docs/Web/CSS/custom-ident); this is not validated. + +-} +containerName : String -> Style +containerName name = + Css.property "container-name" name + + +{-| Set multiple container names. + + containerNames [ "sidebar", "layout" ] -- container-name: sidebar layout + +Names must be valid CSS [custom-idents](https://developer.mozilla.org/en-US/docs/Web/CSS/custom-ident); this is not validated. + +-} +containerNames : List String -> Style +containerNames names = + Css.property "container-name" (String.join " " names) + + +{-| The [`container`](https://developer.mozilla.org/en-US/docs/Web/CSS/container) shorthand. + + container "sidebar" inlineSize -- container: sidebar / inline-size + +The name must be a valid CSS [custom-ident](https://developer.mozilla.org/en-US/docs/Web/CSS/custom-ident); this is not validated. + +-} +container : String -> ContainerTypeValue a -> Style +container name typeValue = + Css.property "container" (name ++ " / " ++ typeValue.value) diff --git a/src/Css/Global.elm b/src/Css/Global.elm index c1f26b2a..634f67b6 100644 --- a/src/Css/Global.elm +++ b/src/Css/Global.elm @@ -1,6 +1,6 @@ module Css.Global exposing ( global, Snippet - , class, id, selector, everything, media, mediaQuery + , class, id, selector, everything, media, mediaQuery, container, containerQuery , children, descendants, adjacentSiblings, generalSiblings, each, withAttribute, withClass , typeSelector, html, body , article, header, footer, h1, h2, h3, h4, h5, h6, nav, menu, section, aside, time, details, summary @@ -28,7 +28,7 @@ do to it using `Style` instead! # Statements -@docs class, id, selector, everything, media, mediaQuery +@docs class, id, selector, everything, media, mediaQuery, container, containerQuery # Combinators @@ -77,6 +77,7 @@ do to it using `Style` instead! -} +import Css.Container import Css.Media exposing (MediaQuery) import Css.Preprocess as Preprocess exposing @@ -282,6 +283,84 @@ mediaQuery stringQueries snippets = media (List.map Structure.CustomQuery stringQueries) snippets +{-| Combines conditions into a global `@container` rule. + + global + [ container [ Container.minWidth (px 400) ] + [ footer [ Css.maxWidth (px 300) ] ] + ] + +-} +container : List Css.Container.Condition -> List Snippet -> Snippet +container conditions snippets = + containerRuleHelp Nothing (Css.Container.toStructureCondition conditions) snippets + + +{-| Manually specify a global `@container` rule with a raw whole-condition string. + + global + [ containerQuery "sidebar (min-width: 400px)" + [ footer [ Css.maxWidth (px 300) ] ] + ] + +-} +containerQuery : String -> List Snippet -> Snippet +containerQuery raw snippets = + containerRuleHelp Nothing (Structure.Raw raw) snippets + + +containerRuleHelp : Maybe String -> Structure.QueryCondition Structure.ContainerFeature -> List Snippet -> Snippet +containerRuleHelp name condition snippets = + let + snippetDeclarations : List Preprocess.SnippetDeclaration + snippetDeclarations = + List.concatMap unwrapSnippet snippets + + extractStyleBlocks : List Preprocess.SnippetDeclaration -> List Preprocess.StyleBlock + extractStyleBlocks declarations = + case declarations of + [] -> + [] + + (Preprocess.StyleBlockDeclaration styleBlock) :: rest -> + styleBlock :: extractStyleBlocks rest + + _ :: rest -> + extractStyleBlocks rest + + containerRuleFromStyleBlocks : Preprocess.SnippetDeclaration + containerRuleFromStyleBlocks = + Preprocess.ContainerRule name condition (extractStyleBlocks snippetDeclarations) + + nestedContainerRules : List Preprocess.SnippetDeclaration -> List Preprocess.SnippetDeclaration + nestedContainerRules declarations = + case declarations of + [] -> + [] + + (Preprocess.StyleBlockDeclaration _) :: rest -> + nestedContainerRules rest + + (Preprocess.ContainerRule innerName innerCondition styleBlocks) :: rest -> + -- nested @container: conditions and-combine, inner name wins + Preprocess.ContainerRule + (case innerName of + Just _ -> + innerName + + Nothing -> + name + ) + (Structure.And [ condition, innerCondition ]) + styleBlocks + :: nestedContainerRules rest + + first :: rest -> + first :: nestedContainerRules rest + in + Preprocess.Snippet (containerRuleFromStyleBlocks :: nestedContainerRules snippetDeclarations) + + {-| Apply styles in a list of snippets to the direct children of a selector typeSelector "div" diff --git a/src/Css/Internal.elm b/src/Css/Internal.elm index 6e1c9b2b..43ae1e95 100644 --- a/src/Css/Internal.elm +++ b/src/Css/Internal.elm @@ -129,6 +129,9 @@ getOverloadedProperty functionName desiredKey style = Preprocess.WithMedia _ _ -> property desiredKey ("elm-css-error-cannot-apply-" ++ functionName ++ "-with-inapplicable-Style-for-media-query") + Preprocess.WithContainer _ _ _ -> + property desiredKey ("elm-css-error-cannot-apply-" ++ functionName ++ "-with-inapplicable-Style-for-container-query") + Preprocess.WithKeyframes _ -> property desiredKey ("elm-css-error-cannot-apply-" ++ functionName ++ "-with-inapplicable-Style-for-keyframes") diff --git a/src/Css/Media.elm b/src/Css/Media.elm index 6a805e7a..ec7e7e23 100644 --- a/src/Css/Media.elm +++ b/src/Css/Media.elm @@ -16,6 +16,8 @@ module Css.Media exposing , Fine, Coarse, fine, coarse, pointer, anyPointer, CanHover, canHover , hover, anyHover , InitialOnly, Enabled, initialOnly, enabled, scripting + , Condition, expr, anyOf, allOf, inverse, condition + , gt, lt, ge, le, eq, between ) {-| Functions for building [`@media` queries](https://developer.mozilla.org/en-US/docs/Web/CSS/Media_Queries/Using_media_queries). @@ -73,6 +75,12 @@ module Css.Media exposing @docs InitialOnly, Enabled, initialOnly, enabled, scripting + +# Condition algebra + +@docs Condition, expr, anyOf, allOf, inverse, condition +@docs gt, lt, ge, le, eq, between + -} import Css exposing (Style) @@ -181,6 +189,135 @@ withMediaQuery queries = +{--Condition algebra--} + + +{-| An opaque media condition, for building `and`/`or`/`not` media queries with +the range syntax. Distinct from `Css.Container.Condition` so the compiler keeps +media features out of `@container` queries and vice versa. +-} +type Condition + = Condition (Structure.QueryCondition Structure.MediaExpression) + + +unwrapCondition : Condition -> Structure.QueryCondition Structure.MediaExpression +unwrapCondition (Condition c) = + c + + +{-| Lift an existing media `Expression` into the condition algebra. + + expr (minWidth (px 400)) + +-} +expr : Expression -> Condition +expr expression = + Condition (Structure.Feature expression) + + +{-| Combine conditions with `or`. +-} +anyOf : List Condition -> Condition +anyOf conditions = + Condition (Structure.Or (List.map unwrapCondition conditions)) + + +{-| Combine conditions with `and`. +-} +allOf : List Condition -> Condition +allOf conditions = + Condition (Structure.And (List.map unwrapCondition conditions)) + + +{-| Negate a condition. Named `inverse` because `not` is taken by media-type negation. +-} +inverse : Condition -> Condition +inverse c = + Condition (Structure.Not (unwrapCondition c)) + + +{-| Turn a list of conditions (joined with `and`) into a `MediaQuery` that slots +into `withMedia`'s query list. +-} +condition : List Condition -> MediaQuery +condition conditions = + Structure.ConditionQuery (combineAnd conditions) + + +combineAnd : List Condition -> Structure.QueryCondition Structure.MediaExpression +combineAnd conditions = + case List.map unwrapCondition conditions of + single :: [] -> + single + + many -> + Structure.And many + + +type alias MediaFeatureToken f = + { f | value : String, mediaFeature : Compatible } + + +mediaRange : Structure.Comparison -> Value compatible -> MediaFeatureToken f -> Condition +mediaRange comparison val token = + Condition + (Structure.Range + { feature = token.value + , lower = Just ( comparison, val.value ) + , upper = Nothing + } + ) + + +{-| `feature > value` +-} +gt : Value compatible -> MediaFeatureToken f -> Condition +gt = + mediaRange Structure.Gt + + +{-| `feature < value` +-} +lt : Value compatible -> MediaFeatureToken f -> Condition +lt = + mediaRange Structure.Lt + + +{-| `feature >= value` +-} +ge : Value compatible -> MediaFeatureToken f -> Condition +ge = + mediaRange Structure.Ge + + +{-| `feature <= value` +-} +le : Value compatible -> MediaFeatureToken f -> Condition +le = + mediaRange Structure.Le + + +{-| `feature = value` +-} +eq : Value compatible -> MediaFeatureToken f -> Condition +eq = + mediaRange Structure.Eq + + +{-| Inclusive on both ends: `(a <= feature <= b)`. +-} +between : Value compatibleLow -> Value compatibleHigh -> MediaFeatureToken f -> Condition +between low high token = + Condition + (Structure.Range + { feature = token.value + , lower = Just ( Structure.Le, low.value ) + , upper = Just ( Structure.Le, high.value ) + } + ) + + + {--Query Constructors--} diff --git a/src/Css/Preprocess.elm b/src/Css/Preprocess.elm index b6343552..2fd56bf0 100644 --- a/src/Css/Preprocess.elm +++ b/src/Css/Preprocess.elm @@ -30,6 +30,7 @@ type Style | NestSnippet Structure.SelectorCombinator (List Snippet) | WithPseudoElement Structure.PseudoElement (List Style) | WithMedia (List MediaQuery) (List Style) + | WithContainer (Maybe String) (Structure.QueryCondition Structure.ContainerFeature) (List Style) | WithKeyframes String | ApplyStyles (List Style) @@ -41,6 +42,7 @@ type Snippet type SnippetDeclaration = StyleBlockDeclaration StyleBlock | MediaRule (List MediaQuery) (List StyleBlock) + | ContainerRule (Maybe String) (Structure.QueryCondition Structure.ContainerFeature) (List StyleBlock) | SupportsRule String (List Snippet) | DocumentRule String String String String StyleBlock | PageRule (List Property) @@ -63,6 +65,11 @@ toMediaRule mediaQueries declaration = Structure.MediaRule newMediaQueries structureStyleBlocks -> Structure.MediaRule (mediaQueries ++ newMediaQueries) structureStyleBlocks + Structure.ContainerRule _ _ structureStyleBlocks -> + -- outer wins: withContainer inside withMedia collapses to a MediaRule, + -- dropping the container condition (documented v1 limitation). + Structure.MediaRule mediaQueries structureStyleBlocks + Structure.SupportsRule str declarations -> Structure.SupportsRule str (List.map (toMediaRule mediaQueries) declarations) @@ -107,6 +114,9 @@ mapProperties update style = WithMedia _ _ -> style + WithContainer _ _ _ -> + style + WithKeyframes _ -> style @@ -145,6 +155,9 @@ mapLastProperty update style = WithMedia _ _ -> style + WithContainer _ _ _ -> + style + WithKeyframes _ -> style diff --git a/src/Css/Preprocess/Resolve.elm b/src/Css/Preprocess/Resolve.elm index 2ef28f67..97105a97 100644 --- a/src/Css/Preprocess/Resolve.elm +++ b/src/Css/Preprocess/Resolve.elm @@ -31,6 +31,16 @@ resolveMediaRule mediaQueries styleBlocks = List.concatMap handleStyleBlock styleBlocks +resolveContainerRule : Maybe String -> Structure.QueryCondition Structure.ContainerFeature -> List Preprocess.StyleBlock -> List Structure.Declaration +resolveContainerRule name condition styleBlocks = + let + handleStyleBlock : Preprocess.StyleBlock -> List Structure.Declaration + handleStyleBlock styleBlock = + List.map (toContainerRule name condition) (expandStyleBlock styleBlock) + in + List.concatMap handleStyleBlock styleBlocks + + resolveSupportsRule : String -> List Snippet -> List Structure.Declaration resolveSupportsRule str snippets = let @@ -49,6 +59,11 @@ toMediaRule mediaQueries declaration = Structure.MediaRule newMediaQueries structureStyleBlocks -> Structure.MediaRule (mediaQueries ++ newMediaQueries) structureStyleBlocks + Structure.ContainerRule _ _ structureStyleBlocks -> + -- outer wins: withContainer inside withMedia collapses to a MediaRule, + -- dropping the container condition (documented v1 limitation). + Structure.MediaRule mediaQueries structureStyleBlocks + Structure.SupportsRule str declarations -> Structure.SupportsRule str (List.map (toMediaRule mediaQueries) declarations) @@ -75,6 +90,57 @@ toMediaRule mediaQueries declaration = declaration +toContainerRule : Maybe String -> Structure.QueryCondition Structure.ContainerFeature -> Structure.Declaration -> Structure.Declaration +toContainerRule name condition declaration = + case declaration of + Structure.StyleBlockDeclaration structureStyleBlock -> + Structure.ContainerRule name condition [ structureStyleBlock ] + + Structure.ContainerRule innerName innerCondition structureStyleBlocks -> + let + combinedName = + case innerName of + Just _ -> + innerName + + Nothing -> + name + + combinedCondition = + Structure.And [ condition, innerCondition ] + in + Structure.ContainerRule combinedName combinedCondition structureStyleBlocks + + Structure.MediaRule _ structureStyleBlocks -> + -- outer wins: the outer container rule replaces the inner media + -- rule, dropping the media condition (documented v1 limitation). + Structure.ContainerRule name condition structureStyleBlocks + + Structure.SupportsRule str declarations -> + Structure.SupportsRule str (List.map (toContainerRule name condition) declarations) + + Structure.DocumentRule str1 str2 str3 str4 structureStyleBlock -> + Structure.DocumentRule str1 str2 str3 str4 structureStyleBlock + + Structure.PageRule _ -> + declaration + + Structure.FontFace _ -> + declaration + + Structure.Keyframes _ -> + declaration + + Structure.Viewport _ -> + declaration + + Structure.CounterStyle _ -> + declaration + + Structure.FontFeatureValues _ -> + declaration + + resolveFontFeatureValues : List ( String, List Property ) -> List Structure.Declaration resolveFontFeatureValues tuples = let @@ -101,6 +167,9 @@ toDeclarations snippetDeclaration = Preprocess.MediaRule mediaQueries styleBlocks -> resolveMediaRule mediaQueries styleBlocks + Preprocess.ContainerRule name condition styleBlocks -> + resolveContainerRule name condition styleBlocks + Preprocess.SupportsRule str snippets -> resolveSupportsRule str snippets @@ -183,6 +252,9 @@ applyStyles styles declarations = Preprocess.MediaRule mediaQueries styleBlocks -> resolveMediaRule mediaQueries styleBlocks + Preprocess.ContainerRule name condition styleBlocks -> + resolveContainerRule name condition styleBlocks + Preprocess.SupportsRule str otherSnippets -> resolveSupportsRule str otherSnippets @@ -237,6 +309,17 @@ applyStyles styles declarations = (Preprocess.WithMedia mediaQueries nestedStyles) :: rest -> let + wrapInMedia declaration = + case declaration of + Structure.ContainerRule _ _ styleBlocks -> + -- outer wins: the outer media rule replaces the inner + -- container rule, dropping the container condition + -- (documented v1 limitation). + Structure.MediaRule mediaQueries styleBlocks + + _ -> + styleBlockToMediaRule mediaQueries declaration + extraDeclarations = case collectSelectors declarations of [] -> @@ -250,7 +333,23 @@ applyStyles styles declarations = -- Then apply the nested styles. |> applyStyles nestedStyles -- Finally, convert the block into a media rule. - |> List.map (styleBlockToMediaRule mediaQueries) + |> List.map wrapInMedia + in + applyStyles rest declarations ++ extraDeclarations + + (Preprocess.WithContainer name condition nestedStyles) :: rest -> + let + extraDeclarations = + case collectSelectors declarations of + [] -> + [] + + firstSelector :: otherSelectors -> + Structure.StyleBlock firstSelector otherSelectors [] + |> Structure.StyleBlockDeclaration + |> List.singleton + |> applyStyles nestedStyles + |> List.map (toContainerRule name condition) in applyStyles rest declarations ++ extraDeclarations diff --git a/src/Css/Structure.elm b/src/Css/Structure.elm index bdd2c119..c39fda10 100644 --- a/src/Css/Structure.elm +++ b/src/Css/Structure.elm @@ -1,4 +1,4 @@ -module Css.Structure exposing (Compatible(..), Declaration(..), KeyframeProperty, MediaExpression, MediaQuery(..), MediaType(..), Number, Property(..), PseudoElement(..), RepeatableSimpleSelector(..), Selector(..), SelectorCombinator(..), SimpleSelectorSequence(..), StyleBlock(..), Stylesheet, TypeSelector(..), appendProperty, appendPseudoElementToLastSelector, appendRepeatable, appendRepeatableSelector, appendRepeatableToLastSelector, appendRepeatableWithCombinator, appendToLastSelector, applyPseudoElement, compactDeclarations, compactHelp, compactStylesheet, concatMapLast, concatMapLastStyleBlock, extendLastSelector, mapLast, styleBlockToMediaRule, withKeyframeDeclarations, withPropertyAppended) +module Css.Structure exposing (Comparison(..), Compatible(..), ContainerFeature, Declaration(..), KeyframeProperty, MediaExpression, MediaQuery(..), MediaType(..), Number, Property(..), PseudoElement(..), QueryCondition(..), RangeExpression, RepeatableSimpleSelector(..), Selector(..), SelectorCombinator(..), SimpleSelectorSequence(..), StyleBlock(..), Stylesheet, TypeSelector(..), appendProperty, appendPseudoElementToLastSelector, appendRepeatable, appendRepeatableSelector, appendRepeatableToLastSelector, appendRepeatableWithCombinator, appendToLastSelector, applyPseudoElement, compactDeclarations, compactHelp, compactStylesheet, concatMapLast, concatMapLastStyleBlock, extendLastSelector, mapLast, styleBlockToContainerRule, styleBlockToMediaRule, withKeyframeDeclarations, withPropertyAppended) {-| A representation of the structure of a stylesheet. This module is concerned solely with representing valid stylesheets; it is not concerned with the @@ -59,6 +59,7 @@ enumerated as follows. type Declaration = StyleBlockDeclaration StyleBlock | MediaRule (List MediaQuery) (List StyleBlock) + | ContainerRule (Maybe String) (QueryCondition ContainerFeature) (List StyleBlock) | SupportsRule String (List Declaration) | DocumentRule String String String String StyleBlock | PageRule (List Property) @@ -87,6 +88,43 @@ type alias MediaExpression = { feature : String, value : Maybe String } +{-| A generic query-condition tree, parameterized by leaf type so each public +module supplies its own feature vocabulary. Used by both `@media` (via +`ConditionQuery`) and `@container` (via `ContainerRule`). +-} +type QueryCondition leaf + = Feature leaf + | Range RangeExpression + | Not (QueryCondition leaf) + | And (List (QueryCondition leaf)) + | Or (List (QueryCondition leaf)) + | Raw String + + +{-| A container feature test. Same shape as `MediaExpression`. +-} +type alias ContainerFeature = + { feature : String, value : Maybe String } + + +{-| A range feature test. `lower`/`upper` are the optional bounds: +gt/lt/ge/le/eq set exactly one; between sets both (inclusive, Le/Le). +-} +type alias RangeExpression = + { feature : String + , lower : Maybe ( Comparison, String ) + , upper : Maybe ( Comparison, String ) + } + + +type Comparison + = Lt + | Le + | Gt + | Ge + | Eq + + {-| The components that make up a media query -} type MediaQuery @@ -94,6 +132,7 @@ type MediaQuery | OnlyQuery MediaType (List MediaExpression) | NotQuery MediaType (List MediaExpression) | CustomQuery String + | ConditionQuery (QueryCondition MediaExpression) {-| A [CSS3 Selector](https://www.w3.org/TR/css3-selectors/). All selectors @@ -167,6 +206,12 @@ appendProperty property declarations = (mapLast (withPropertyAppended property) styleBlocks) ] + (ContainerRule name condition styleBlocks) :: [] -> + [ ContainerRule name + condition + (mapLast (withPropertyAppended property) styleBlocks) + ] + -- TODO _ :: [] -> declarations @@ -229,6 +274,31 @@ extendLastSelector selector declarations = newDeclarations -> newDeclarations + (ContainerRule name condition ((StyleBlock only [] properties) :: [])) :: [] -> + let + newStyleBlock = + StyleBlock (appendRepeatableSelector selector only) [] properties + in + [ ContainerRule name condition [ newStyleBlock ] ] + + (ContainerRule name condition ((StyleBlock first rest properties) :: [])) :: [] -> + let + newRest = + mapLast (appendRepeatableSelector selector) rest + + newStyleBlock = + StyleBlock first newRest properties + in + [ ContainerRule name condition [ newStyleBlock ] ] + + (ContainerRule name condition (first :: rest)) :: [] -> + case extendLastSelector selector [ ContainerRule name condition rest ] of + (ContainerRule newName newCondition newStyleBlocks) :: [] -> + [ ContainerRule newName newCondition (first :: newStyleBlocks) ] + + newDeclarations -> + newDeclarations + (SupportsRule str nestedDeclarations) :: [] -> [ SupportsRule str (extendLastSelector selector nestedDeclarations) ] @@ -328,6 +398,17 @@ concatMapLastStyleBlock update declarations = newDeclarations -> newDeclarations + (ContainerRule name condition (styleBlock :: [])) :: [] -> + [ ContainerRule name condition (update styleBlock) ] + + (ContainerRule name condition (first :: rest)) :: [] -> + case concatMapLastStyleBlock update [ ContainerRule name condition rest ] of + (ContainerRule newName newCondition newStyleBlocks) :: [] -> + [ ContainerRule newName newCondition (first :: newStyleBlocks) ] + + newDeclarations -> + newDeclarations + (SupportsRule str nestedDeclarations) :: [] -> [ SupportsRule str (concatMapLastStyleBlock update nestedDeclarations) ] @@ -465,6 +546,13 @@ compactHelp declaration ( keyframesByName, declarations ) = else ( keyframesByName, declaration :: declarations ) + ContainerRule _ _ styleBlocks -> + if List.all (\(StyleBlock _ _ properties) -> List.isEmpty properties) styleBlocks then + ( keyframesByName, declarations ) + + else + ( keyframesByName, declaration :: declarations ) + SupportsRule _ otherDeclarations -> if List.isEmpty otherDeclarations then ( keyframesByName, declarations ) @@ -529,3 +617,13 @@ styleBlockToMediaRule mediaQueries declaration = _ -> declaration + + +styleBlockToContainerRule : Maybe String -> QueryCondition ContainerFeature -> Declaration -> Declaration +styleBlockToContainerRule name condition declaration = + case declaration of + StyleBlockDeclaration styleBlock -> + ContainerRule name condition [ styleBlock ] + + _ -> + declaration diff --git a/src/Css/Structure/Output.elm b/src/Css/Structure/Output.elm index bfa2e9c3..4bae42d5 100644 --- a/src/Css/Structure/Output.elm +++ b/src/Css/Structure/Output.elm @@ -1,4 +1,4 @@ -module Css.Structure.Output exposing (mediaQueryToString, prettyPrint, selectorToString) +module Css.Structure.Output exposing (conditionToString, mediaQueryToString, prettyPrint, selectorToString) import Css.String import Css.Structure exposing (..) @@ -70,6 +70,26 @@ prettyPrintDeclaration decl = in "@media " ++ query ++ "{" ++ blocks ++ "}" + ContainerRule name condition styleBlocks -> + let + blocks = + Css.String.mapJoin prettyPrintStyleBlock "\n" styleBlocks + + namePrefix = + case name of + Just str -> + str ++ " " + + Nothing -> + "" + in + "@container " + ++ namePrefix + ++ conditionToString containerFeatureToString condition + ++ "{" + ++ blocks + ++ "}" + SupportsRule _ _ -> "TODO" @@ -117,6 +137,9 @@ mediaQueryToString mediaQuery = NotQuery mediaType expressions -> prefixWith "not" mediaType expressions + ConditionQuery condition -> + conditionToString mediaExpressionToString condition + CustomQuery str -> str @@ -145,6 +168,97 @@ mediaExpressionToString expression = ++ ")" +comparisonToString : Comparison -> String +comparisonToString comparison = + case comparison of + Lt -> + "<" + + Le -> + "<=" + + Gt -> + ">" + + Ge -> + ">=" + + Eq -> + "=" + + +containerFeatureToString : ContainerFeature -> String +containerFeatureToString = + mediaExpressionToString + + +rangeToString : RangeExpression -> String +rangeToString { feature, lower, upper } = + case ( lower, upper ) of + ( Just ( lowerCmp, lowerVal ), Just ( upperCmp, upperVal ) ) -> + "(" + ++ lowerVal + ++ " " + ++ comparisonToString lowerCmp + ++ " " + ++ feature + ++ " " + ++ comparisonToString upperCmp + ++ " " + ++ upperVal + ++ ")" + + ( Just ( cmp, val ), Nothing ) -> + "(" ++ feature ++ " " ++ comparisonToString cmp ++ " " ++ val ++ ")" + + ( Nothing, Just ( cmp, val ) ) -> + "(" ++ feature ++ " " ++ comparisonToString cmp ++ " " ++ val ++ ")" + + ( Nothing, Nothing ) -> + "(" ++ feature ++ ")" + + +{-| Serialize a query condition. `leafToString` renders the leaf feature +(already including its own parentheses). Composite children nested inside a +composite are wrapped in an extra pair of parens to keep the CSS valid. +-} +conditionToString : (leaf -> String) -> QueryCondition leaf -> String +conditionToString leafToString condition = + let + grouped child = + case child of + Feature _ -> + conditionToString leafToString child + + Range _ -> + conditionToString leafToString child + + Raw _ -> + conditionToString leafToString child + + _ -> + "(" ++ conditionToString leafToString child ++ ")" + in + case condition of + Feature leaf -> + leafToString leaf + + Range range -> + rangeToString range + + Not child -> + "not " ++ grouped child + + And children -> + String.join " and " (List.map grouped children) + + Or children -> + String.join " or " (List.map grouped children) + + Raw str -> + str + + simpleSelectorSequenceToString : SimpleSelectorSequence -> String simpleSelectorSequenceToString simpleSelectorSequence = case simpleSelectorSequence of diff --git a/tests/Container.elm b/tests/Container.elm new file mode 100644 index 00000000..1115fc10 --- /dev/null +++ b/tests/Container.elm @@ -0,0 +1,487 @@ +module Container exposing + ( containerCombinators + , containerFeatureTest + , containerFeatures + , containerHashDistinct + , containerProperties + , containerRanges + , containerRuleConstructors + , containerSelectorExtension + , containerUnits + , globalContainer + , globalContainerQuery + , globalNestedContainerCombine + , outputConditionForms + , outputConditionQuery + , outputContainerRule + , outputRangeForms + , resolveNestedContainer + , resolveWithContainer + , withContainerMediaOuterWins + , withContainerNamedInnerWins + , withMediaContainerOuterWins + ) + +import Css exposing (cqb, cqh, cqi, cqmax, cqmin, cqw, hex, px) +import Css.Container as Container exposing (..) +import Css.Global exposing (class, footer, p) +import Css.Media as Media exposing (only, screen, withMedia) +import Css.Preprocess as Preprocess exposing (stylesheet) +import Css.Structure as Structure exposing (..) +import Css.Structure.Output as Output +import Expect +import Test exposing (Test, describe, test) +import TestUtil exposing (outdented, prettyPrint) + + +sampleBlock : StyleBlock +sampleBlock = + StyleBlock + (Selector (TypeSelectorSequence (TypeSelector "p") []) [] Nothing) + [] + [ Property "color:#FF0000" ] + + +sheet : List Declaration -> Structure.Stylesheet +sheet declarations = + { charset = Nothing, imports = [], namespaces = [], declarations = declarations } + + +feat : String -> Maybe String -> QueryCondition ContainerFeature +feat name value = + Feature { feature = name, value = value } + + +outputContainerRule : Test +outputContainerRule = + describe "@container rule emission" + [ test "anonymous, single feature" <| + \_ -> + Output.prettyPrint (sheet [ ContainerRule Nothing (feat "min-width" (Just "400px")) [ sampleBlock ] ]) + |> Expect.equal "@container (min-width: 400px){p{color:#FF0000;}}" + , test "named container" <| + \_ -> + Output.prettyPrint (sheet [ ContainerRule (Just "sidebar") (feat "min-width" (Just "400px")) [ sampleBlock ] ]) + |> Expect.equal "@container sidebar (min-width: 400px){p{color:#FF0000;}}" + , test "boolean (valueless) feature" <| + \_ -> + Output.prettyPrint (sheet [ ContainerRule Nothing (feat "orientation" Nothing) [ sampleBlock ] ]) + |> Expect.equal "@container (orientation){p{color:#FF0000;}}" + ] + + +outputConditionForms : Test +outputConditionForms = + describe "condition composite emission + parenthesization" + [ test "and joins with ' and '" <| + \_ -> + Output.prettyPrint (sheet [ ContainerRule Nothing (And [ feat "min-width" (Just "400px"), feat "orientation" (Just "landscape") ]) [ sampleBlock ] ]) + |> Expect.equal "@container (min-width: 400px) and (orientation: landscape){p{color:#FF0000;}}" + , test "or with a nested not: the not child is extra-parenthesized, the top-level or is bare" <| + \_ -> + Output.prettyPrint (sheet [ ContainerRule Nothing (Or [ feat "min-width" (Just "400px"), Not (feat "orientation" (Just "landscape")) ]) [ sampleBlock ] ]) + |> Expect.equal "@container (min-width: 400px) or (not (orientation: landscape)){p{color:#FF0000;}}" + , test "not of a feature" <| + \_ -> + Output.prettyPrint (sheet [ ContainerRule Nothing (Not (feat "orientation" (Just "landscape"))) [ sampleBlock ] ]) + |> Expect.equal "@container not (orientation: landscape){p{color:#FF0000;}}" + , test "raw passthrough" <| + \_ -> + Output.prettyPrint (sheet [ ContainerRule Nothing (Raw "style(--theme: dark)") [ sampleBlock ] ]) + |> Expect.equal "@container style(--theme: dark){p{color:#FF0000;}}" + ] + + +outputRangeForms : Test +outputRangeForms = + describe "range emission" + [ test "single lower bound emits feature-first" <| + \_ -> + Output.prettyPrint (sheet [ ContainerRule Nothing (Range { feature = "width", lower = Just ( Gt, "400px" ), upper = Nothing }) [ sampleBlock ] ]) + |> Expect.equal "@container (width > 400px){p{color:#FF0000;}}" + , test "eq emits '='" <| + \_ -> + Output.prettyPrint (sheet [ ContainerRule Nothing (Range { feature = "width", lower = Just ( Eq, "400px" ), upper = Nothing }) [ sampleBlock ] ]) + |> Expect.equal "@container (width = 400px){p{color:#FF0000;}}" + , test "both bounds emit chained form" <| + \_ -> + Output.prettyPrint (sheet [ ContainerRule Nothing (Range { feature = "width", lower = Just ( Le, "200px" ), upper = Just ( Le, "700px" ) }) [ sampleBlock ] ]) + |> Expect.equal "@container (200px <= width <= 700px){p{color:#FF0000;}}" + ] + + +outputConditionQuery : Test +outputConditionQuery = + describe "ConditionQuery media-query emission" + [ test "renders the condition string inside @media" <| + \_ -> + Output.prettyPrint (sheet [ MediaRule [ ConditionQuery (And [ Feature { feature = "min-width", value = Just "400px" }, Not (Feature { feature = "hover", value = Just "hover" }) ]) ] [ sampleBlock ] ]) + |> Expect.equal "@media (min-width: 400px) and (not (hover: hover)){p{color:#FF0000;}}" + ] + + +resolveWithContainer : Test +resolveWithContainer = + let + cond name value = + Feature { feature = name, value = value } + + block selectorName props = + Preprocess.StyleBlock + (Selector (TypeSelectorSequence (TypeSelector selectorName) []) [] Nothing) + [] + props + + input = + Preprocess.stylesheet + [ Preprocess.Snippet + [ Preprocess.StyleBlockDeclaration + (block "p" + [ Preprocess.AppendProperty (Property "color:#AA0000") + , Preprocess.WithContainer Nothing (cond "min-width" (Just "400px")) [ Preprocess.AppendProperty (Property "color:#000000") ] + ] + ) + ] + ] + in + describe "withContainer resolves to a ContainerRule declaration" + [ test "outputs sibling @container after the base block" <| + \_ -> + TestUtil.outdented (prettyPrint input) + |> Expect.equal (TestUtil.outdented "p{color:#AA0000;}\n@container (min-width: 400px){p{color:#000000;}}") + ] + + +resolveNestedContainer : Test +resolveNestedContainer = + let + cond name value = + Feature { feature = name, value = value } + + input = + Preprocess.stylesheet + [ Preprocess.Snippet + [ Preprocess.StyleBlockDeclaration + (Preprocess.StyleBlock + (Selector (TypeSelectorSequence (TypeSelector "p") []) [] Nothing) + [] + [ Preprocess.WithContainer Nothing + (cond "min-width" (Just "400px")) + [ Preprocess.WithContainer Nothing + (cond "orientation" (Just "landscape")) + [ Preprocess.AppendProperty (Property "color:#000000") ] + ] + ] + ) + ] + ] + in + describe "nested withContainer and-combines into one rule" + [ test "combines conditions" <| + \_ -> + TestUtil.outdented (prettyPrint input) + |> Expect.equal (TestUtil.outdented "@container (min-width: 400px) and (orientation: landscape){p{color:#000000;}}") + ] + + +containerFeatureTest : String -> List ( Condition, String ) -> Test +containerFeatureTest label pairs = + describe (label ++ " container feature") + (List.indexedMap + (\n ( cond, expected ) -> + test (label ++ String.fromInt n) <| + \_ -> + prettyPrint (stylesheet [ p [ withContainer [ cond ] [ Css.color (hex "000000") ] ] ]) + |> outdented + |> Expect.equal (outdented ("@container " ++ expected ++ "{p{color:#000000;}}")) + ) + pairs + ) + + +containerFeatures : Test +containerFeatures = + describe "Css.Container features" + [ containerFeatureTest "min-width" [ ( minWidth (px 400), "(min-width: 400px)" ) ] + , containerFeatureTest "max-width" [ ( maxWidth (px 800), "(max-width: 800px)" ) ] + , containerFeatureTest "min-height" [ ( minHeight (px 300), "(min-height: 300px)" ) ] + , containerFeatureTest "max-height" [ ( maxHeight (px 600), "(max-height: 600px)" ) ] + , containerFeatureTest "min-inline-size" [ ( minInlineSize (px 400), "(min-inline-size: 400px)" ) ] + , containerFeatureTest "max-inline-size" [ ( maxInlineSize (px 400), "(max-inline-size: 400px)" ) ] + , containerFeatureTest "min-block-size" [ ( minBlockSize (px 400), "(min-block-size: 400px)" ) ] + , containerFeatureTest "max-block-size" [ ( maxBlockSize (px 400), "(max-block-size: 400px)" ) ] + , containerFeatureTest "min-aspect-ratio" [ ( minAspectRatio (ratio 4 3), "(min-aspect-ratio: 4/3)" ) ] + , containerFeatureTest "max-aspect-ratio" [ ( maxAspectRatio (ratio 16 9), "(max-aspect-ratio: 16/9)" ) ] + , containerFeatureTest "orientation" [ ( orientation landscape, "(orientation: landscape)" ), ( orientation portrait, "(orientation: portrait)" ) ] + ] + + +containerRanges : Test +containerRanges = + describe "Css.Container range comparisons" + [ containerFeatureTest "gt" [ ( width |> gt (px 400), "(width > 400px)" ) ] + , containerFeatureTest "lt" [ ( width |> lt (px 400), "(width < 400px)" ) ] + , containerFeatureTest "ge" [ ( width |> ge (px 400), "(width >= 400px)" ) ] + , containerFeatureTest "le" [ ( width |> le (px 400), "(width <= 400px)" ) ] + , containerFeatureTest "eq" [ ( width |> eq (px 400), "(width = 400px)" ) ] + , containerFeatureTest "between" [ ( width |> between (px 200) (px 700), "(200px <= width <= 700px)" ) ] + , containerFeatureTest "aspect-ratio ge" [ ( aspectRatio |> ge (ratio 16 9), "(aspect-ratio >= 16/9)" ) ] + , containerFeatureTest "inlineSize as feature token" [ ( inlineSize |> gt (px 400), "(inline-size > 400px)" ) ] + ] + + +containerCombinators : Test +containerCombinators = + describe "Css.Container combinators" + [ containerFeatureTest "anyOf" [ ( anyOf [ minWidth (px 400), orientation landscape ], "(min-width: 400px) or (orientation: landscape)" ) ] + , containerFeatureTest "allOf inside anyOf" [ ( anyOf [ allOf [ minWidth (px 400), minHeight (px 300) ], orientation landscape ], "((min-width: 400px) and (min-height: 300px)) or (orientation: landscape)" ) ] + , containerFeatureTest "not under anyOf" [ ( anyOf [ minWidth (px 400), Container.not (orientation landscape) ], "(min-width: 400px) or (not (orientation: landscape))" ) ] + , containerFeatureTest "rawCondition" [ ( rawCondition "style(--theme: dark)", "style(--theme: dark)" ) ] + ] + + +containerRuleConstructors : Test +containerRuleConstructors = + describe "Css.Container rule constructors" + [ test "withContainer joins top-level list with and" <| + \_ -> + prettyPrint (stylesheet [ p [ withContainer [ minWidth (px 400), orientation landscape ] [ Css.color (hex "000000") ] ] ]) + |> outdented + |> Expect.equal (outdented "@container (min-width: 400px) and (orientation: landscape){p{color:#000000;}}") + , test "withContainerNamed" <| + \_ -> + prettyPrint (stylesheet [ p [ withContainerNamed "sidebar" [ minWidth (px 400) ] [ Css.color (hex "000000") ] ] ]) + |> outdented + |> Expect.equal (outdented "@container sidebar (min-width: 400px){p{color:#000000;}}") + , test "withContainerQuery raw passthrough" <| + \_ -> + prettyPrint (stylesheet [ p [ withContainerQuery "sidebar (min-width: 400px)" [ Css.color (hex "000000") ] ] ]) + |> outdented + |> Expect.equal (outdented "@container sidebar (min-width: 400px){p{color:#000000;}}") + ] + + +containerUnits : Test +containerUnits = + describe "cq* units" + [ test "cqw renders inside a style" <| + \_ -> + prettyPrint (stylesheet [ p [ Css.width (cqw 50) ] ]) + |> outdented + |> Expect.equal (outdented "p{width:50cqw;}") + , test "cqh" <| + \_ -> prettyPrint (stylesheet [ p [ Css.width (cqh 50) ] ]) |> outdented |> Expect.equal (outdented "p{width:50cqh;}") + , test "cqi" <| + \_ -> prettyPrint (stylesheet [ p [ Css.width (cqi 50) ] ]) |> outdented |> Expect.equal (outdented "p{width:50cqi;}") + , test "cqb" <| + \_ -> prettyPrint (stylesheet [ p [ Css.width (cqb 50) ] ]) |> outdented |> Expect.equal (outdented "p{width:50cqb;}") + , test "cqmin" <| + \_ -> prettyPrint (stylesheet [ p [ Css.width (cqmin 50) ] ]) |> outdented |> Expect.equal (outdented "p{width:50cqmin;}") + , test "cqmax" <| + \_ -> prettyPrint (stylesheet [ p [ Css.width (cqmax 50) ] ]) |> outdented |> Expect.equal (outdented "p{width:50cqmax;}") + , test "cqw satisfies AbsoluteLength for container features" <| + \_ -> + prettyPrint (stylesheet [ p [ withContainer [ minWidth (cqw 50) ] [ Css.color (hex "000000") ] ] ]) + |> outdented + |> Expect.equal (outdented "@container (min-width: 50cqw){p{color:#000000;}}") + ] + + +globalContainer : Test +globalContainer = + let + input = + stylesheet + [ Css.Global.container [ Container.minWidth (px 400) ] + [ Css.Global.footer [ Css.maxWidth (px 300) ] ] + ] + + output = + "@container (min-width: 400px){footer{max-width:300px;}}" + in + describe "Css.Global.container" + [ test "renders a global @container rule" <| + \_ -> + outdented (prettyPrint input) + |> Expect.equal (outdented output) + ] + + +globalContainerQuery : Test +globalContainerQuery = + let + input = + stylesheet + [ Css.Global.containerQuery "sidebar (min-width: 400px)" + [ Css.Global.footer [ Css.maxWidth (px 300) ] ] + ] + + output = + "@container sidebar (min-width: 400px){footer{max-width:300px;}}" + in + describe "Css.Global.containerQuery" + [ test "renders a raw global @container rule" <| + \_ -> + outdented (prettyPrint input) + |> Expect.equal (outdented output) + ] + + +globalNestedContainerCombine : Test +globalNestedContainerCombine = + let + input = + stylesheet + [ Css.Global.container [ Container.minWidth (px 400) ] + [ Css.Global.container [ Container.orientation Container.landscape ] + [ Css.Global.footer [ Css.maxWidth (px 300) ] ] + ] + ] + in + describe "Css.Global.container nesting and-combines conditions" + [ test "and-combines conditions, dropping the (empty) outer style block" <| + \_ -> + outdented (prettyPrint input) + |> Expect.equal (outdented "@container (min-width: 400px) and (orientation: landscape){footer{max-width:300px;}}") + ] + + +containerProperties : Test +containerProperties = + describe "Css.Container establishment properties" + [ test "containerType size" <| + \_ -> + prettyPrint (stylesheet [ p [ containerType Container.size ] ]) + |> outdented + |> Expect.equal (outdented "p{container-type:size;}") + , test "containerType normal" <| + \_ -> + prettyPrint (stylesheet [ p [ containerType Container.normal ] ]) + |> outdented + |> Expect.equal (outdented "p{container-type:normal;}") + , test "containerType inlineSize (dual-role)" <| + \_ -> + prettyPrint (stylesheet [ p [ containerType inlineSize ] ]) + |> outdented + |> Expect.equal (outdented "p{container-type:inline-size;}") + , test "containerName" <| + \_ -> + prettyPrint (stylesheet [ p [ containerName "sidebar" ] ]) + |> outdented + |> Expect.equal (outdented "p{container-name:sidebar;}") + , test "containerNames" <| + \_ -> + prettyPrint (stylesheet [ p [ containerNames [ "a", "b" ] ] ]) + |> outdented + |> Expect.equal (outdented "p{container-name:a b;}") + , test "container shorthand" <| + \_ -> + prettyPrint (stylesheet [ p [ container "sidebar" inlineSize ] ]) + |> outdented + |> Expect.equal (outdented "p{container:sidebar / inline-size;}") + ] + + +containerSelectorExtension : Test +containerSelectorExtension = + let + input = + stylesheet + [ p + [ withContainer [ minWidth (px 400) ] + [ Css.Global.children [ Css.Global.a [ Css.color (hex "000000") ] ] ] + ] + ] + in + -- Confirm selectors extend correctly inside a container rule (compaction path). + describe "selector extension inside withContainer" + [ test "extends selector under @container" <| + \_ -> + outdented (prettyPrint input) + |> Expect.equal (outdented "@container (min-width: 400px){p > a{color:#000000;}}") + ] + + +withMediaContainerOuterWins : Test +withMediaContainerOuterWins = + let + input = + stylesheet + [ p + [ withMedia [ only screen [ Media.minWidth (px 600) ] ] + [ withContainer [ minWidth (px 400) ] + [ Css.color (hex "000000") ] + ] + ] + ] + in + -- Documented v1: outer withMedia wins, inner container condition dropped. + describe "withContainer inside withMedia: outer wins" + [ test "drops the inner container condition" <| + \_ -> + outdented (prettyPrint input) + |> Expect.equal (outdented "@media only screen and (min-width: 600px){p{color:#000000;}}") + ] + + +withContainerMediaOuterWins : Test +withContainerMediaOuterWins = + let + input = + stylesheet + [ p + [ withContainer [ minWidth (px 400) ] + [ withMedia [ only screen [] ] + [ Css.color (hex "000000") ] + ] + ] + ] + in + -- Mirror direction: outer withContainer wins, inner media condition dropped. + describe "withMedia inside withContainer: outer wins" + [ test "drops the inner media condition" <| + \_ -> + outdented (prettyPrint input) + |> Expect.equal (outdented "@container (min-width: 400px){p{color:#000000;}}") + ] + + +withContainerNamedInnerWins : Test +withContainerNamedInnerWins = + let + input = + stylesheet + [ p + [ withContainerNamed "outer" + [ minWidth (px 400) ] + [ withContainerNamed "inner" + [ maxWidth (px 700) ] + [ Css.color (hex "000000") ] + ] + ] + ] + in + describe "withContainerNamed nesting: conditions and-combine, inner name wins" + [ test "inner name wins, conditions and-combine" <| + \_ -> + outdented (prettyPrint input) + |> Expect.equal (outdented "@container inner (min-width: 400px) and (max-width: 700px){p{color:#000000;}}") + ] + + +containerHashDistinct : Test +containerHashDistinct = + let + render style = + prettyPrint (stylesheet [ p [ style ] ]) + + a = + render (withContainer [ minWidth (px 400) ] [ Css.color (hex "000000") ]) + + b = + render (withContainer [ minWidth (px 500) ] [ Css.color (hex "000000") ]) + in + describe "container condition affects generated CSS (and thus class hash)" + [ test "different conditions produce different CSS text" <| + \_ -> Expect.notEqual a b + ] diff --git a/tests/Media.elm b/tests/Media.elm index 6fd6adf6..a6cd84b0 100644 --- a/tests/Media.elm +++ b/tests/Media.elm @@ -1,6 +1,7 @@ -module Media exposing (basicMediaQuery, bug352, expectFeatureWorks, mediaFeatures, mediaTypes, testFeature, testMedia, testMediaQuery, testMediaType, testUnparameterizedFeature, testWithMedia, testWithMediaQuery, withMediaInside, withMediaInsideAndOtheDeclarations, withMediaOutside, withMediaOutsideAndOtherDeclarations) +module Media exposing (basicMediaQuery, bug352, expectFeatureWorks, mediaConditionAlgebra, mediaFeatures, mediaRangeComparisons, mediaTypes, testFeature, testMedia, testMediaQuery, testMediaType, testUnparameterizedFeature, testWithMedia, testWithMediaQuery, withMediaInside, withMediaInsideAndOtheDeclarations, withMediaOutside, withMediaOutsideAndOtherDeclarations) import Css exposing (..) +import Css.Container as Container import Css.Global exposing (Snippet, a, body, button, class, li, media, mediaQuery, p, ul) import Css.Media as Media exposing (..) import Css.Preprocess exposing (stylesheet) @@ -492,3 +493,52 @@ testWithMediaQuery = outdented (prettyPrint input) |> Expect.equal (outdented output) ] + + +mediaConditionAlgebra : Test +mediaConditionAlgebra = + let + input = + stylesheet + [ body + [ withMedia + [ condition + [ expr (Media.minWidth (px 400)) + , anyOf [ expr (orientation landscape), inverse (expr (Media.hover canHover)) ] + ] + ] + [ Css.color (hex "000000") ] + ] + ] + + output = + "@media (min-width: 400px) and ((orientation: landscape) or (not (hover: hover))){body{color:#000000;}}" + in + describe "Css.Media condition algebra" + [ test "renders nested and/or/not" <| + \_ -> + outdented (prettyPrint input) + |> Expect.equal (outdented output) + ] + + +mediaRangeComparisons : Test +mediaRangeComparisons = + let + input = + stylesheet + [ body + [ withMedia [ condition [ Container.width |> gt (px 600) ] ] + [ Css.color (hex "000000") ] + ] + ] + + output = + "@media (width > 600px){body{color:#000000;}}" + in + describe "Css.Media range comparisons with shared Container tokens" + [ test "renders width > 600px" <| + \_ -> + outdented (prettyPrint input) + |> Expect.equal (outdented output) + ]