Skip to content

Commit ec2858b

Browse files
committed
Support orientation media query
1 parent 6dd85b5 commit ec2858b

File tree

1 file changed

+125
-22
lines changed

1 file changed

+125
-22
lines changed

src/media-query.test.ts

Lines changed: 125 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,12 @@ function* ParseInt() {
3232
return parseInt(stringValue, 10) * (isNegative ? -1 : 1);
3333
}
3434

35+
interface MatchMediaContext {
36+
mediaType: 'screen' | 'print';
37+
viewportWidth: number;
38+
viewportHeight: number;
39+
}
40+
3541
class ParsedMediaType {
3642
constructor(public readonly mediaType: 'screen' | 'print' | 'all') {}
3743

@@ -42,11 +48,32 @@ class ParsedMediaType {
4248

4349
static *Parser() {
4450
yield optionalWhitespace;
51+
yield optional(() => ['only', requiredWhitespace]);
4552
const mediaType: ParsedMediaType['mediaType'] = yield ['screen', 'print'];
4653
return new ParsedMediaType(mediaType);
4754
}
4855
}
4956

57+
class ParsedNotMediaType {
58+
constructor(public readonly mediaType: 'screen' | 'print' | 'all') {}
59+
60+
matches(context: { mediaType: 'screen' | 'print' }) {
61+
if (this.mediaType === 'all') return false;
62+
return this.mediaType !== context.mediaType;
63+
}
64+
65+
static *Parser() {
66+
yield optionalWhitespace;
67+
yield 'not';
68+
yield requiredWhitespace;
69+
const mediaType: ParsedNotMediaType['mediaType'] = yield [
70+
'screen',
71+
'print',
72+
];
73+
return new ParsedNotMediaType(mediaType);
74+
}
75+
}
76+
5077
class ParsedMinWidth {
5178
constructor(
5279
public readonly value: number,
@@ -72,18 +99,48 @@ class ParsedMinWidth {
7299
}
73100
}
74101

102+
/**
103+
https://www.w3.org/TR/mediaqueries-5/#orientation
104+
*/
105+
class ParsedOrientation {
106+
constructor(public readonly orientation: 'portrait' | 'landscape') {}
107+
108+
matches(context: { viewportWidth: number; viewportHeight: number }) {
109+
const calculated =
110+
context.viewportHeight >= context.viewportWidth
111+
? 'portrait'
112+
: 'landscape';
113+
return this.orientation === calculated;
114+
}
115+
116+
static *Parser() {
117+
yield optionalWhitespace;
118+
yield '(';
119+
yield 'orientation:';
120+
yield optionalWhitespace;
121+
const orientation: 'portrait' | 'landscape' = yield [
122+
'portrait',
123+
'landscape',
124+
];
125+
yield optionalWhitespace;
126+
yield ')';
127+
return new ParsedOrientation(orientation);
128+
}
129+
}
130+
75131
// See https://www.w3.org/TR/mediaqueries-5/#mq-syntax
76-
type ParsedMediaFeature = ParsedMinWidth;
132+
const parsedMediaFeature = [ParsedMinWidth.Parser, ParsedOrientation.Parser];
133+
const parsedMediaInParens = [...parsedMediaFeature];
134+
type ParsedMediaFeature = ParsedType<typeof parsedMediaFeature[-1]>;
77135
type ParsedMediaInParens = ParsedMediaFeature;
78-
const parsedMediaInParens = [ParsedMinWidth.Parser];
79136

80137
class ParsedMediaCondition {
81138
constructor(
82139
public readonly first: ParsedMediaFeature,
83140
public readonly conditions?: ParsedMediaAnds
84141
) {}
85142

86-
matches(context: { mediaType: 'screen' | 'print'; viewportWidth: number }) {
143+
matches(context: MatchMediaContext) {
87144
const base = this.first.matches(context);
88145
if (this.conditions) {
89146
return base && this.conditions.matches(context);
@@ -94,7 +151,7 @@ class ParsedMediaCondition {
94151

95152
static *Parser() {
96153
yield optionalWhitespace;
97-
const first: ParsedMediaFeature = yield [ParsedMinWidth.Parser];
154+
const first: ParsedMediaInParens = yield parsedMediaInParens;
98155
// const conditions: ParsedMediaAnds | undefined = yield optional(ParsedMediaAnds.Parser);
99156
const conditions: ParsedMediaAnds | '' = yield [ParsedMediaAnds.Parser, ''];
100157
if (conditions === '') {
@@ -108,7 +165,7 @@ class ParsedMediaCondition {
108165
class ParsedMediaAnds {
109166
constructor(public readonly list: ReadonlyArray<ParsedMediaInParens>) {}
110167

111-
matches(context: { mediaType: 'screen' | 'print'; viewportWidth: number }) {
168+
matches(context: MatchMediaContext) {
112169
return this.list.every((m) => m.matches(context));
113170
}
114171

@@ -130,19 +187,22 @@ class ParsedMediaAnds {
130187

131188
class ParsedMediaTypeThenConditionWithoutOr {
132189
constructor(
133-
public readonly mediaType: ParsedMediaType,
190+
public readonly mediaType: ParsedMediaType | ParsedNotMediaType,
134191
public readonly and: ReadonlyArray<ParsedMediaInParens>
135192
) {}
136193

137-
matches(context: { mediaType: 'screen' | 'print'; viewportWidth: number }) {
194+
matches(context: MatchMediaContext) {
138195
return (
139196
this.mediaType.matches(context) &&
140197
this.and.every((m) => m.matches(context))
141198
);
142199
}
143200

144201
static *Parser() {
145-
const mediaType: ParsedMediaType = yield ParsedMediaType.Parser;
202+
const mediaType: ParsedMediaType | ParsedNotMediaType = yield [
203+
ParsedMediaType.Parser,
204+
ParsedNotMediaType.Parser,
205+
];
146206

147207
const list: Array<ParsedMediaInParens> = [];
148208

@@ -176,10 +236,6 @@ class ParsedMediaQuery {
176236
}
177237
}
178238

179-
interface MatchMediaContext {
180-
mediaType: 'screen' | 'print';
181-
viewportWidth: number;
182-
}
183239
function matchMedia(context: MatchMediaContext, mediaQuery: string) {
184240
const parsed: ParseResult<ParsedMediaQuery> = parse(
185241
mediaQuery,
@@ -202,6 +258,15 @@ function matchMedia(context: MatchMediaContext, mediaQuery: string) {
202258
};
203259
}
204260

261+
test('screen', () => {
262+
const result = parse('screen', ParsedMediaQuery.Parser() as any);
263+
expect(result).toEqual({
264+
success: true,
265+
result: new ParsedMediaType('screen'),
266+
remaining: '',
267+
});
268+
});
269+
205270
test('(min-width: 480px)', () => {
206271
const result = parse('(min-width: 480px)', ParsedMediaQuery.Parser() as any);
207272
expect(result).toEqual({
@@ -211,11 +276,14 @@ test('(min-width: 480px)', () => {
211276
});
212277
});
213278

214-
test('screen', () => {
215-
const result = parse('screen', ParsedMediaQuery.Parser() as any);
279+
test('(orientation: landscape)', () => {
280+
const result = parse(
281+
'(orientation: landscape)',
282+
ParsedMediaQuery.Parser() as any
283+
);
216284
expect(result).toEqual({
217285
success: true,
218-
result: new ParsedMediaType('screen'),
286+
result: new ParsedOrientation('landscape'),
219287
remaining: '',
220288
});
221289
});
@@ -238,17 +306,19 @@ test('screen and (min-width: 480px)', () => {
238306
test('matchMedia()', () => {
239307
const screenSized = (viewportWidth: number, viewportHeight: number) =>
240308
({ mediaType: 'screen', viewportWidth, viewportHeight } as const);
241-
const print = { mediaType: 'print' } as const;
309+
const printSized = (viewportWidth: number, viewportHeight: number) =>
310+
({ mediaType: 'print', viewportWidth, viewportHeight } as const);
242311

243312
expect(matchMedia(screenSized(100, 100), 'screen').matches).toBe(true);
313+
expect(matchMedia(screenSized(100, 100), 'only screen').matches).toBe(true);
314+
expect(matchMedia(screenSized(100, 100), 'not screen').matches).toBe(false);
244315
expect(matchMedia(screenSized(100, 100), 'print').matches).toBe(false);
316+
expect(matchMedia(screenSized(100, 100), 'only print').matches).toBe(false);
245317

246-
expect(matchMedia({ ...print, viewportWidth: 100 }, 'screen').matches).toBe(
247-
false
248-
);
249-
expect(matchMedia({ ...print, viewportWidth: 100 }, 'print').matches).toBe(
250-
true
251-
);
318+
expect(matchMedia(printSized(100, 100), 'screen').matches).toBe(false);
319+
expect(matchMedia(printSized(100, 100), 'only screen').matches).toBe(false);
320+
expect(matchMedia(printSized(100, 100), 'print').matches).toBe(true);
321+
expect(matchMedia(printSized(100, 100), 'only print').matches).toBe(true);
252322

253323
expect(matchMedia(screenSized(478, 100), '(min-width: 480px)').matches).toBe(
254324
false
@@ -262,4 +332,37 @@ test('matchMedia()', () => {
262332
expect(matchMedia(screenSized(481, 100), '(min-width: 480px)').matches).toBe(
263333
true
264334
);
335+
336+
expect(
337+
matchMedia(screenSized(200, 100), '(orientation: landscape)').matches
338+
).toBe(true);
339+
expect(
340+
matchMedia(screenSized(200, 100), '(orientation: portrait)').matches
341+
).toBe(false);
342+
343+
expect(
344+
matchMedia(screenSized(100, 200), '(orientation: landscape)').matches
345+
).toBe(false);
346+
expect(
347+
matchMedia(screenSized(100, 200), '(orientation: portrait)').matches
348+
).toBe(true);
349+
350+
expect(
351+
matchMedia(screenSized(100, 100), '(orientation: landscape)').matches
352+
).toBe(false);
353+
expect(
354+
matchMedia(screenSized(100, 100), '(orientation: portrait)').matches
355+
).toBe(true);
356+
357+
expect(
358+
matchMedia(screenSized(481, 100), 'screen and (min-width: 480px)').matches
359+
).toBe(true);
360+
expect(
361+
matchMedia(screenSized(481, 100), 'only screen and (min-width: 480px)')
362+
.matches
363+
).toBe(true);
364+
expect(
365+
matchMedia(screenSized(481, 100), 'only screen and (min-width: 480px) and (orientation: landscape)')
366+
.matches
367+
).toBe(true);
265368
});

0 commit comments

Comments
 (0)