Skip to content

Answer the user-preference media features from the render device - #235

Merged
FlorianRappl merged 2 commits into
AngleSharp:develfrom
lahma:feature/#234
Sep 3, 2026
Merged

Answer the user-preference media features from the render device#235
FlorianRappl merged 2 commits into
AngleSharp:develfrom
lahma:feature/#234

Conversation

@lahma

@lahma lahma commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

IRenderDevice models the Media Queries Level 3/4 device features but none of the Level 5 user-preference features, so @media (prefers-color-scheme: dark) never applied in the cascade and matchMedia("(prefers-color-scheme: dark)").matches was always false — with no way for a host that has a preference (a headless browser emulating a dark theme, say) to say so.

This is the non-breaking route picked in #234: a small IRenderDevicePreferences with the single Preferences member, already implemented by DefaultRenderDevice, which the validators probe with as. Nothing that implements IRenderDevice today has to change.

var config = Configuration.Default
    .WithCss()
    .WithRenderDevice(new DefaultRenderDevice
    {
        Preferences = new Dictionary<String, String>
        {
            { "prefers-color-scheme", "dark" },
            { "prefers-reduced-motion", "reduce" },
        },
    });

What is in it

  • IRenderDevicePreferences beside IRenderDevice, with IReadOnlyDictionary<String, String> Preferences. DefaultRenderDevice implements it with a settable property defaulting to an empty, case-insensitive dictionary; its existing members are untouched.
  • One PreferenceFeatureValidator, parameterized by the feature name, registered in DefaultFeatureValidatorFactory for every key below. It answers false when the device does not implement the interface or does not carry the key, and otherwise compares the queried keyword with the value case insensitively.
  • hover / pointer already had validators that read nothing from the device and assumed a headless browser ((hover: none) is true). They now route through the dictionary when it carries the key and keep exactly that answer when it does not. any-hover / any-pointer were unregistered — they are now registered and behave like their siblings, so (any-hover: none) answers true on a bare device where it previously answered false as an unknown feature. That is the only behaviour change for a host that sets no preferences.
  • FeatureNames and CssKeywords entries, and the key table in README.md and docs/general/05-Extensibility.md.
Key Keywords
prefers-color-scheme light, dark
prefers-reduced-motion no-preference, reduce
prefers-reduced-transparency no-preference, reduce
prefers-contrast no-preference, more, less, custom
prefers-reduced-data no-preference, reduce
forced-colors none, active
hover, any-hover none, hover
pointer, any-pointer none, coarse, fine
display-mode fullscreen, standalone, minimal-ui, browser

The keyword is compared verbatim rather than against a hard-coded grammar per feature, so a value newer than this library (another display-mode, another prefers-contrast level) works without a release — which is the point of the untyped dictionary. Used without a value the feature evaluates in a boolean context per MQ5 §3.1: (prefers-reduced-motion) is true unless the value is no-preference, (forced-colors) unless it is none, and (hover) is true when the value is hover.

Tests

41 new tests in src/AngleSharp.Css.Tests/Rules/CssMediaPreferenceFeatures.cs (validator level: each feature true / false / absent, boolean context, case insensitivity, a device that does not implement the interface, the hover and pointer fallbacks) and src/AngleSharp.Css.Tests/Extensions/MediaPreferences.cs (matchMedia end to end beside the tests from #228, a third-party device implementing both interfaces, and @media (prefers-reduced-motion: reduce) applying a declaration under ComputeCurrentStyle). Full suite: 2073 passing, 0 failing; solution builds with 0 warnings over all five target frameworks. Rebased onto devel at 87a30c7, i.e., on top of the #230-#233 fixes.

Run against the unfixed behaviour first, 31 of the 41 fail and 10 pass on both sides — the 10 are the ones asserting that an absent preference, or a device without the interface, changes nothing, plus the two pinning that hover and pointer keep their current answer.

Not in this PR

The four media-query evaluation defects filed alongside #234 (#230-#233) are untouched here - they are already fixed on devel, and this branch is rebased on top of those fixes.

Fixes #234

🤖 Generated with Claude Code

https://claude.ai/code/session_01NqCcJrL3MJecCPRBMQsZyC

Adds IRenderDevicePreferences, a small interface with a single
IReadOnlyDictionary<String, String> Preferences member that
DefaultRenderDevice implements, so a host can say which user preferences
its device carries without every existing IRenderDevice implementation
having to change.

A generic PreferenceFeatureValidator is registered for
prefers-color-scheme, prefers-reduced-motion,
prefers-reduced-transparency, prefers-contrast, prefers-reduced-data,
forced-colors and display-mode, and hover/any-hover and
pointer/any-pointer now read the dictionary when it carries them, while
keeping their previous answer when it does not. A key that is not set
leaves its feature unknown, i.e., the query does not match.

Fixes AngleSharp#234

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NqCcJrL3MJecCPRBMQsZyC
Comment thread src/AngleSharp.Css/Extensions/RenderDeviceExtensions.cs Outdated

@FlorianRappl FlorianRappl left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good - I'd just make the RenderDeviceExtensions public as somebody might want to also have this convenience layer on top of the render device (in particular the DefaultRenderDevice).

Review feedback on AngleSharp#235: the convenience layer over IRenderDevice is
useful to hosts that configure a DefaultRenderDevice, so the class is
public and documented.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NqCcJrL3MJecCPRBMQsZyC
@lahma

lahma commented Sep 3, 2026

Copy link
Copy Markdown
Contributor Author

Done: RenderDeviceExtensions is public now, with the class and its GetPreference documented, so a host holding a DefaultRenderDevice (or any IRenderDevice) can read a preference through the same helper the validators use.

@FlorianRappl FlorianRappl left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

IRenderDevice has no Media Queries Level 5 user-preference features

2 participants