Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31,860 changes: 15,992 additions & 15,868 deletions bridge/core/bridge_polyfill.c

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions bridge/core/css/media_feature_names.json5
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,9 @@
"resizable",
"inverted-colors",
"prefers-color-scheme",
"prefers-reduced-motion",
"forced-colors",
"prefers-contrast",
"origin-trial-test"
],
}
73 changes: 73 additions & 0 deletions bridge/core/css/media_query_evaluator.cc
Original file line number Diff line number Diff line change
Expand Up @@ -772,6 +772,79 @@ static bool PrefersColorSchemeMediaFeatureEval(const MediaQueryExpValue& value,
}
}

static bool PrefersReducedMotionMediaFeatureEval(const MediaQueryExpValue& value,
MediaQueryOperator,
const MediaValues& media_values) {
bool prefers_reduced_motion = media_values.PrefersReducedMotion();

if (!value.IsValid()) {
return prefers_reduced_motion;
}

if (!value.IsId()) {
return false;
}

switch (value.Id()) {
case CSSValueID::kReduce:
return prefers_reduced_motion;
case CSSValueID::kNoPreference:
return !prefers_reduced_motion;
default:
return false;
}
}

static bool ForcedColorsMediaFeatureEval(const MediaQueryExpValue& value,
MediaQueryOperator,
const MediaValues& media_values) {
bool forced_active = media_values.ForcedColorsActive();

if (!value.IsValid()) {
return forced_active;
}

if (!value.IsId()) {
return false;
}

switch (value.Id()) {
case CSSValueID::kActive:
return forced_active;
case CSSValueID::kNone:
return !forced_active;
default:
return false;
}
}

static bool PrefersContrastMediaFeatureEval(const MediaQueryExpValue& value,
MediaQueryOperator,
const MediaValues& media_values) {
CSSValueID contrast = media_values.PreferredContrast();

if (!value.IsValid()) {
return contrast != CSSValueID::kNoPreference;
}

if (!value.IsId()) {
return false;
}

switch (value.Id()) {
case CSSValueID::kNoPreference:
return contrast == CSSValueID::kNoPreference;
case CSSValueID::kMore:
return contrast == CSSValueID::kMore;
case CSSValueID::kLess:
return contrast == CSSValueID::kLess;
case CSSValueID::kCustom:
return contrast == CSSValueID::kCustom;
default:
return false;
}
}

static bool Transform3dMediaFeatureEval(const MediaQueryExpValue& value,
MediaQueryOperator op,
const MediaValues& media_values) {
Expand Down
16 changes: 16 additions & 0 deletions bridge/core/css/media_query_exp.cc
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,22 @@ static inline bool FeatureWithValidIdent(const String& media_feature, CSSValueID
return ident == CSSValueID::kDark || ident == CSSValueID::kLight || ident == CSSValueID::kNoPreference;
}

// prefers-reduced-motion: reduce | no-preference
if (media_feature == media_feature_names_atomicstring::kPrefersReducedMotion) {
return ident == CSSValueID::kReduce || ident == CSSValueID::kNoPreference;
}

// forced-colors: active | none
if (media_feature == media_feature_names_atomicstring::kForcedColors) {
return ident == CSSValueID::kActive || ident == CSSValueID::kNone;
}

// prefers-contrast: no-preference | more | less | custom
if (media_feature == media_feature_names_atomicstring::kPrefersContrast) {
return ident == CSSValueID::kNoPreference || ident == CSSValueID::kMore ||
ident == CSSValueID::kLess || ident == CSSValueID::kCustom;
}

// dynamic-range / video-dynamic-range: standard | high
if (media_feature == media_feature_names_atomicstring::kDynamicRange ||
media_feature == media_feature_names_atomicstring::kVideoDynamicRange) {
Expand Down
4 changes: 4 additions & 0 deletions bridge/core/css/media_values.h
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,10 @@ class MediaValues : public CSSLengthResolver {
// Returns the preferred color scheme for evaluating prefers-color-scheme media queries.
// Defaults to light when not overridden.
virtual CSSValueID PreferredColorScheme() const { return CSSValueID::kLight; }
// Defaults to no-preference / inactive unless overridden.
virtual bool PrefersReducedMotion() const { return false; }
virtual bool ForcedColorsActive() const { return false; }
virtual CSSValueID PreferredContrast() const { return CSSValueID::kNoPreference; }
virtual bool Resizable() const = 0;
virtual bool StrictMode() const = 0;
virtual Document* GetDocument() const = 0;
Expand Down
12 changes: 12 additions & 0 deletions bridge/core/css/media_values_dynamic.cc
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,18 @@ CSSValueID MediaValuesDynamic::PreferredColorScheme() const {
return CSSValueID::kLight;
}

bool MediaValuesDynamic::PrefersReducedMotion() const {
return CalculatePrefersReducedMotion(context_);
}

bool MediaValuesDynamic::ForcedColorsActive() const {
return false;
}

CSSValueID MediaValuesDynamic::PreferredContrast() const {
return CSSValueID::kNoPreference;
}

bool MediaValuesDynamic::Resizable() const {
return CalculateResizable(context_);
}
Expand Down
5 changes: 4 additions & 1 deletion bridge/core/css/media_values_dynamic.h
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,10 @@ class MediaValuesDynamic : public MediaValues {
bool InvertedColors() const override;
bool ThreeDEnabled() const override;
const String MediaType() const override;
CSSValueID PreferredColorScheme() const override;
CSSValueID PreferredColorScheme() const override;
bool PrefersReducedMotion() const override;
bool ForcedColorsActive() const override;
CSSValueID PreferredContrast() const override;
bool Resizable() const override;
bool StrictMode() const override;
Document* GetDocument() const override;
Expand Down
42 changes: 39 additions & 3 deletions bridge/polyfill/src/match-media.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,10 +99,16 @@ export const matchMedia: MatchMedia = (mediaQuery: string): MediaQueryList => {
// Only support one expression now
let expression = query.expressions[0];
let feature = expression.feature;
let expValue = expression.value;
let expValue = (expression.value || '').trim().toLowerCase();
let expMatches: boolean;
let invalidFeature = false;

const getViewportSize = () => {
const width = typeof window.innerWidth === 'number' ? window.innerWidth : (document.documentElement?.clientWidth || 0);
const height = typeof window.innerHeight === 'number' ? window.innerHeight : (document.documentElement?.clientHeight || 0);
return { width, height };
};

switch (feature) {
case 'prefers-color-scheme':
_addListener = (listener: any) => {
Expand All @@ -119,8 +125,39 @@ export const matchMedia: MatchMedia = (mediaQuery: string): MediaQueryList => {
window.onColorSchemeChange = null;
};
// @ts-ignore
expMatches = expValue === '' || window.colorScheme === expValue;
expMatches = expValue === '' || String(window.colorScheme || '').toLowerCase() === expValue;
break;
case 'orientation': {
const { width, height } = getViewportSize();
if (expValue === '') {
expMatches = width >= 0 && height >= 0;
} else {
const isLandscape = width > height;
expMatches = expValue === 'landscape' ? isLandscape : expValue === 'portrait' ? !isLandscape : false;
}
break;
}
case 'forced-colors': {
// Default to 'none' unless host provides a value.
// @ts-ignore
const current = String(window.forcedColors || 'none').toLowerCase();
expMatches = expValue === '' ? current !== 'none' : expValue === current;
break;
}
case 'prefers-contrast': {
// Default to 'no-preference' unless host provides a value.
// @ts-ignore
const current = String(window.prefersContrast || 'no-preference').toLowerCase();
expMatches = expValue === '' ? current !== 'no-preference' : expValue === current;
break;
}
case 'prefers-reduced-motion': {
// Default to 'no-preference' unless host provides a value.
// @ts-ignore
const current = String(window.prefersReducedMotion || 'no-preference').toLowerCase();
expMatches = expValue === '' ? current !== 'no-preference' : expValue === current;
break;
}
default:
// If query is invalid, serialized text should turn into "not all".
media = 'not all';
Expand All @@ -143,4 +180,3 @@ export const matchMedia: MatchMedia = (mediaQuery: string): MediaQueryList => {
}
}
}

3 changes: 2 additions & 1 deletion docs/TAILWINDCSS_SUPPORT_PLAN.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,9 +106,10 @@ Tailwind v3.4.18 core utilities are exposed by `corePlugins` (179 keys). Below i

### P0 — Make Tailwind CSS parse + match correctly
1. ✅ **`@layer` support (parser + cascade order)**: implemented as Milestone M1; see `docs/CSS_CASCADE_LAYERS_PLAN.md`.
2. **Selectors Level 4 essentials**: implement parsing + matching for `:where(<selector-list>)` and `:is(<selector-list>)` (Tailwind emits `:where` today; `:is` appears in some variants/plugins).
2. **Selectors Level 4 essentials**: implement parsing + matching for `:where(<selector-list>)` and `:is(<selector-list>)` (Tailwind emits `:where` today; `:is` appears in some variants/plugins).
3. **State pseudo-classes**: implement `:hover/:focus/:focus-visible/:focus-within/:active/:enabled/:disabled` and wire element state updates from Flutter events.
4. **Media query evaluation coverage**: add Tailwind-required media features: `prefers-reduced-motion`, `orientation`, `forced-colors`, `prefers-contrast`, plus **media type `print`** handling (likely “always false” in WebF unless printing is supported).
- Add WPT-based integration tests for `orientation`, `forced-colors`, `prefers-contrast`, `prefers-reduced-motion`.
5. **Regression tests**: add a minimal “Tailwind preflight can load” integration test and a few utility/variant fixtures.

### P0 Dev Plan — Interactive pseudo-classes (Dart engine)
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
46 changes: 46 additions & 0 deletions integration_tests/specs/css/css-mediaqueries/forced-colors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
describe('MediaQuery forced-colors', () => {
// WPT: css/mediaqueries/forced-colors.html
it('applies forced-colors active/none rules', async () => {
const cssText = `
.mq-box {
width: 120px;
height: 120px;
background-color: rgb(255, 0, 0);
}

@media (forced-colors: active) {
.mq-box {
background-color: rgb(0, 128, 0);
}
}

@media (forced-colors: none) {
.mq-box {
background-color: rgb(0, 0, 255);
}
}
`;
const style = document.createElement('style');
style.textContent = cssText;
document.head.append(style);

const box = createElement('div', {
className: 'mq-box'
}, [createText('forced-colors')]);
BODY.appendChild(box);

await nextFrames(2);
await snapshot();

const active = window.matchMedia('(forced-colors: active)').matches;
const none = window.matchMedia('(forced-colors: none)').matches;
const booleanContext = window.matchMedia('(forced-colors)').matches;

expect(active || none).toBe(true);
expect(active).toBe(!none);
expect(booleanContext).toBe(!none);

const expected = active ? 'rgb(0, 128, 0)' : 'rgb(0, 0, 255)';
expect(getComputedStyle(box).backgroundColor).toBe(expected);
});
});
43 changes: 43 additions & 0 deletions integration_tests/specs/css/css-mediaqueries/orientation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
describe('MediaQuery orientation', () => {
// WPT: css/mediaqueries/test_media_queries.html (orientation section)
it('applies portrait/landscape rules based on viewport', async () => {
const cssText = `
.mq-box {
width: 120px;
height: 120px;
background-color: rgb(255, 0, 0);
}

@media (orientation: landscape) {
.mq-box {
background-color: rgb(0, 128, 0);
}
}

@media (orientation: portrait) {
.mq-box {
background-color: rgb(0, 0, 255);
}
}
`;
const style = document.createElement('style');
style.textContent = cssText;
document.head.append(style);

const box = createElement('div', {
className: 'mq-box'
}, [createText('orientation')]);
BODY.appendChild(box);

await nextFrames(2);
await snapshot();

await resizeViewport(800, 400);
expect(getComputedStyle(box).backgroundColor).toBe('rgb(0, 128, 0)');

await resizeViewport(400, 800);
expect(getComputedStyle(box).backgroundColor).toBe('rgb(0, 0, 255)');

await resizeViewport(-1, -1);
});
});
67 changes: 67 additions & 0 deletions integration_tests/specs/css/css-mediaqueries/prefers-contrast.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
describe('MediaQuery prefers-contrast', () => {
// WPT: css/mediaqueries/prefers-contrast.html
it('applies prefers-contrast media rules', async () => {
const cssText = `
.mq-box {
width: 120px;
height: 120px;
background-color: rgb(255, 0, 0);
}

@media (prefers-contrast: more) {
.mq-box {
background-color: rgb(0, 128, 0);
}
}

@media (prefers-contrast: less) {
.mq-box {
background-color: rgb(0, 0, 255);
}
}

@media (prefers-contrast: custom) {
.mq-box {
background-color: rgb(255, 165, 0);
}
}

@media (prefers-contrast: no-preference) {
.mq-box {
background-color: rgb(128, 128, 128);
}
}
`;
const style = document.createElement('style');
style.textContent = cssText;
document.head.append(style);

const box = createElement('div', {
className: 'mq-box'
}, [createText('prefers-contrast')]);
BODY.appendChild(box);

await nextFrames(2);
await snapshot();

const more = window.matchMedia('(prefers-contrast: more)').matches;
const less = window.matchMedia('(prefers-contrast: less)').matches;
const custom = window.matchMedia('(prefers-contrast: custom)').matches;
const noPref = window.matchMedia('(prefers-contrast: no-preference)').matches;
const booleanContext = window.matchMedia('(prefers-contrast)').matches;

const matches = [more, less, custom, noPref].filter(Boolean);
expect(matches.length).toBe(1);
expect(noPref).toBe(!booleanContext);

const expected = more
? 'rgb(0, 128, 0)'
: less
? 'rgb(0, 0, 255)'
: custom
? 'rgb(255, 165, 0)'
: 'rgb(128, 128, 128)';

expect(getComputedStyle(box).backgroundColor).toBe(expected);
});
});
Loading
Loading