From 5fd784c17bc1075d7cad30bc560f8aa3b5fed444 Mon Sep 17 00:00:00 2001 From: Liad Yosef Date: Tue, 10 Dec 2024 00:47:00 +0200 Subject: [PATCH 1/8] feat(editor): add responsive utils --- editor/src/components/canvas/canvas-types.ts | 8 + .../src/components/canvas/responsive-types.ts | 39 ++++ .../canvas/responsive-utils.spec.ts | 168 ++++++++++++++ .../src/components/canvas/responsive-utils.ts | 210 ++++++++++++++++++ 4 files changed, 425 insertions(+) create mode 100644 editor/src/components/canvas/responsive-types.ts create mode 100644 editor/src/components/canvas/responsive-utils.spec.ts create mode 100644 editor/src/components/canvas/responsive-utils.ts diff --git a/editor/src/components/canvas/canvas-types.ts b/editor/src/components/canvas/canvas-types.ts index 06f6f0f5220e..787c04380f36 100644 --- a/editor/src/components/canvas/canvas-types.ts +++ b/editor/src/components/canvas/canvas-types.ts @@ -30,6 +30,7 @@ import type { CSSPadding, FlexDirection, } from '../inspector/common/css-utils' +import type { ScreenSize } from './responsive-types' export const CanvasContainerID = 'canvas-container' export const SceneContainerName = 'scene' @@ -557,6 +558,13 @@ interface ParsedCSSStyleProperty { value: T } +type StyleHoverModifier = { type: 'hover' } +export type StyleMediaSizeModifier = { + type: 'media-size' + size: ScreenSize +} +export type StyleModifier = StyleHoverModifier | StyleMediaSizeModifier + export type CSSStyleProperty = | CSSStylePropertyNotFound | CSSStylePropertyNotParsable diff --git a/editor/src/components/canvas/responsive-types.ts b/editor/src/components/canvas/responsive-types.ts new file mode 100644 index 000000000000..c1e025a6f8fc --- /dev/null +++ b/editor/src/components/canvas/responsive-types.ts @@ -0,0 +1,39 @@ +import type { Identifier, Dimension } from 'css-tree' +import type { CSSNumber } from '../inspector/common/css-utils' +// @media (min-width: 100px) and (max-width: 200em) => { min: { value: 100, unit: 'px' }, max: { value: 200, unit: 'em' } } +export type ScreenSize = { + min?: CSSNumber + max?: CSSNumber +} + +export interface MediaQuery { + type: 'MediaQuery' + loc: null + modifier: null + mediaType: null + condition?: { + type: 'Condition' + loc: null + kind: 'media' + children: Array + } +} + +export interface FeatureRange { + type: 'FeatureRange' + loc: null + kind: 'media' + left?: Dimension | Identifier + leftComparison: '<' | '>' + middle: Dimension | Identifier + rightComparison: '<' | '>' + right?: Dimension | Identifier +} + +export interface Feature { + type: 'Feature' + loc: null + kind: 'media' + name: 'min-width' | 'max-width' + value?: Dimension +} diff --git a/editor/src/components/canvas/responsive-utils.spec.ts b/editor/src/components/canvas/responsive-utils.spec.ts new file mode 100644 index 000000000000..a6c7f426a362 --- /dev/null +++ b/editor/src/components/canvas/responsive-utils.spec.ts @@ -0,0 +1,168 @@ +import * as csstree from 'css-tree' +import { mediaQueryToScreenSize, selectValueByBreakpoint } from './responsive-utils' +import type { ScreenSize, MediaQuery } from './responsive-types' +import { extractScreenSizeFromCss } from './responsive-utils' +import type { StyleMediaSizeModifier } from './canvas-types' + +describe('mediaQueryToScreenSize', () => { + it('converts simple screen size queries', () => { + const testCases: { input: string; expected: ScreenSize }[] = [ + { + input: '@media (100px 100px)', + expected: { min: { value: 100, unit: 'px' } }, + }, + ] + testCases.forEach((testCase) => { + csstree.walk(csstree.parse(testCase.input), (node) => { + if (node.type === 'MediaQuery') { + const result = mediaQueryToScreenSize(node as unknown as MediaQuery) + expect(result).toEqual(testCase.expected) + } + }) + }) + }) +}) + +describe('extractScreenSizeFromCss', () => { + beforeEach(() => { + // Clear the cache before each test + ;(extractScreenSizeFromCss as any).screenSizeCache?.clear() + }) + + it('extracts screen size from simple media query', () => { + const css = '@media (min-width: 100px) and (max-width: 500px)' + const result = extractScreenSizeFromCss(css) + expect(result).toEqual({ + min: { value: 100, unit: 'px' }, + max: { value: 500, unit: 'px' }, + }) + }) + + it('returns null for invalid media query', () => { + const css = 'not-a-media-query' + const result = extractScreenSizeFromCss(css) + expect(result).toBeNull() + }) + + it('uses cache for repeated calls with same CSS', () => { + const css = '@media (min-width: 100px)' + + // First call + const result1 = extractScreenSizeFromCss(css) + // Second call - should return same object reference + const result2 = extractScreenSizeFromCss(css) + + expect(result1).toBe(result2) // Use toBe for reference equality + expect(result1).toEqual({ + min: { value: 100, unit: 'px' }, + }) + }) + + it('caches null results', () => { + const css = 'invalid-css' + + // First call + const result1 = extractScreenSizeFromCss(css) + // Second call - should return same null reference + const result2 = extractScreenSizeFromCss(css) + + expect(result1).toBe(result2) + expect(result1).toBeNull() + }) + + it('handles different CSS strings independently in cache', () => { + const css1 = '@media (min-width: 100px)' + const css2 = '@media (max-width: 500px)' + + // First string + const result1a = extractScreenSizeFromCss(css1) + const result1b = extractScreenSizeFromCss(css1) + expect(result1a).toBe(result1b) + expect(result1a).toEqual({ + min: { value: 100, unit: 'px' }, + }) + + // Second string + const result2a = extractScreenSizeFromCss(css2) + const result2b = extractScreenSizeFromCss(css2) + expect(result2a).toBe(result2b) + expect(result2a).toEqual({ + max: { value: 500, unit: 'px' }, + }) + + // Different strings should have different references + expect(result1a).not.toBe(result2a) + }) +}) + +describe('selectValueByBreakpoint', () => { + const variants: { value: string; modifiers?: StyleMediaSizeModifier[] }[] = [ + { + value: 'b', + modifiers: [{ type: 'media-size', size: { min: { value: 200, unit: 'px' } } }], + }, + { + value: 'a', + modifiers: [{ type: 'media-size', size: { min: { value: 100, unit: 'px' } } }], + }, + { + value: 'c', + modifiers: [{ type: 'media-size', size: { min: { value: 20, unit: 'em' } } }], + }, + { value: 'd' }, + ] + it('selects the correct value', () => { + expect(selectValueByBreakpoint(variants, 150)).toEqual({ + value: 'a', + modifiers: [{ type: 'media-size', size: { min: { value: 100, unit: 'px' } } }], + }) + }) + it('select the closest value', () => { + expect(selectValueByBreakpoint(variants, 250)).toEqual({ + value: 'b', + modifiers: [{ type: 'media-size', size: { min: { value: 200, unit: 'px' } } }], + }) + }) + it('converts em to px', () => { + expect(selectValueByBreakpoint(variants, 350)).toEqual({ + value: 'c', + modifiers: [{ type: 'media-size', size: { min: { value: 20, unit: 'em' } } }], + }) + }) + it('selects the first value if no breakpoint is matched', () => { + expect(selectValueByBreakpoint(variants, 50)).toEqual({ value: 'd' }) + }) + it('selects null if no matching breakpoint and no default value', () => { + expect(selectValueByBreakpoint(variants.slice(0, 2), 50)).toBeNull() + }) + it('selects default value if no media modifiers', () => { + expect( + selectValueByBreakpoint( + [ + { + value: 'a', + modifiers: [{ type: 'hover' }], + }, + { value: 'c' }, + ], + 50, + ), + ).toEqual({ value: 'c' }) + }) +}) diff --git a/editor/src/components/canvas/responsive-utils.ts b/editor/src/components/canvas/responsive-utils.ts new file mode 100644 index 000000000000..0b6ded8b6266 --- /dev/null +++ b/editor/src/components/canvas/responsive-utils.ts @@ -0,0 +1,210 @@ +import type { Feature, FeatureRange, MediaQuery, ScreenSize } from './responsive-types' +import * as csstree from 'css-tree' +import type { StyleMediaSizeModifier, StyleModifier } from './canvas-types' +import { type CSSNumber, type CSSNumberUnit, cssNumber } from '../inspector/common/css-utils' +import { memoize } from 'src/core/shared/memoize' + +/** + * Extracts the screen size from a CSS string, for example: + * `@media (min-width: 100px)` -> { min: {value: 100, unit: 'px'} } + * `@media (20px < width < 50em)` -> { min: {value: 20, unit: 'px'}, max: {value: 50, unit: 'em'} } + */ +export const extractScreenSizeFromCss = memoize((css: string): ScreenSize | null => { + const mediaQuery = extractMediaQueryFromCss(css) + return mediaQuery == null ? null : mediaQueryToScreenSize(mediaQuery) +}) + +function extractFromFeatureRange(featureRange: FeatureRange): { + leftValue: CSSNumber | null + rightValue: CSSNumber | null + leftComparison: '<' | '>' | null + rightComparison: '<' | '>' | null +} | null { + // (100px < width < 500px) OR (500px > width > 100px) OR (100px > width) OR (500px < width) + if (featureRange?.middle?.type === 'Identifier' && featureRange.middle.name === 'width') { + const leftValue = + featureRange.left?.type === 'Dimension' + ? cssNumber(Number(featureRange.left.value), featureRange.left.unit as CSSNumberUnit) + : null + + const rightValue = + featureRange.right?.type === 'Dimension' + ? cssNumber(Number(featureRange.right.value), featureRange.right.unit as CSSNumberUnit) + : null + + return { + leftValue: leftValue, + rightValue: rightValue, + leftComparison: featureRange.leftComparison, + rightComparison: featureRange.rightComparison, + } + } + // (width > 100px) OR (width < 500px) + if (featureRange?.left?.type === 'Identifier' && featureRange.left.name === 'width') { + const rightValue = + featureRange.middle?.type === 'Dimension' + ? cssNumber(Number(featureRange.middle.value), featureRange.middle.unit as CSSNumberUnit) + : null + // this is not a mistake, since we normalize the "width" to be in the middle + const rightComparison = featureRange.leftComparison + + return { + leftValue: null, + leftComparison: null, + rightValue: rightValue, + rightComparison: rightComparison, + } + } + return null +} + +export function mediaQueryToScreenSize(mediaQuery: MediaQuery): ScreenSize { + const result: ScreenSize = {} + + if (mediaQuery.condition?.type === 'Condition') { + // 1. Handle FeatureRange case + const featureRanges = mediaQuery.condition.children.filter( + (child): child is FeatureRange => child.type === 'FeatureRange', + ) as Array + + featureRanges.forEach((featureRange) => { + const rangeData = extractFromFeatureRange(featureRange) + if (rangeData == null) { + return + } + const { leftValue, rightValue, leftComparison, rightComparison } = rangeData + if (leftValue != null) { + if (leftComparison === '<') { + result.min = leftValue + } else { + result.max = leftValue + } + } + if (rightValue != null) { + if (rightComparison === '<') { + result.max = rightValue + } else { + result.min = rightValue + } + } + }) + + // 2. Handle Feature case (min-width/max-width) + const features = mediaQuery.condition.children.filter( + (child): child is Feature => child.type === 'Feature', + ) + features.forEach((feature) => { + if (feature.value?.type === 'Dimension') { + if (feature.name === 'min-width') { + result.min = cssNumber(Number(feature.value.value), feature.value.unit as CSSNumberUnit) + } else if (feature.name === 'max-width') { + result.max = cssNumber(Number(feature.value.value), feature.value.unit as CSSNumberUnit) + } + } + }) + } + + return result +} + +function extractMediaQueryFromCss(css: string): MediaQuery | null { + let result: MediaQuery | null = null + csstree.walk(csstree.parse(css), (node) => { + if (node.type === 'MediaQuery') { + result = node as unknown as MediaQuery + } + }) + return result +} + +function getMediaModifier( + modifiers: StyleModifier[] | undefined | null, +): StyleMediaSizeModifier | null { + return (modifiers ?? []).filter( + (modifier): modifier is StyleMediaSizeModifier => modifier.type === 'media-size', + )[0] +} + +export function selectValueByBreakpoint( + parsedVariants: T[], + sceneWidthInPx?: number, +): T | null { + const relevantModifiers = parsedVariants.filter((variant) => { + // 1. filter out variants that don't have media modifiers, but keep variants with no modifiers at all + if (variant.modifiers == null || variant.modifiers.length === 0) { + return true + } + const mediaModifier = getMediaModifier(variant.modifiers) + if (mediaModifier == null) { + // this means it only has other modifiers + return false + } + + if (sceneWidthInPx == null) { + // filter out variants that require a scene width + return false + } + + // 2. check that it has at least one media modifier that satisfies the current scene width + const maxSizeInPx = cssNumberAsPx(mediaModifier.size.max) + const minSizeInPx = cssNumberAsPx(mediaModifier.size.min) + + // if it has only max + if (maxSizeInPx != null && minSizeInPx == null && sceneWidthInPx <= maxSizeInPx) { + return true + } + + // if it has only min + if (maxSizeInPx == null && minSizeInPx != null && sceneWidthInPx >= minSizeInPx) { + return true + } + + // if it has both max and min + if ( + maxSizeInPx != null && + minSizeInPx != null && + sceneWidthInPx >= minSizeInPx && + sceneWidthInPx <= maxSizeInPx + ) { + return true + } + return false + }) + let chosen: T | null = null + for (const variant of relevantModifiers) { + const chosenMediaModifier = getMediaModifier(chosen?.modifiers) + const variantMediaModifier = getMediaModifier(variant.modifiers) + if (variantMediaModifier == null) { + if (chosenMediaModifier == null) { + // if we have nothing chosen then we'll take the base value + chosen = variant + } + continue + } + if (chosenMediaModifier == null) { + chosen = variant + continue + } + // find the closest media modifier + const minSizeInPx = cssNumberAsPx(variantMediaModifier.size.min) + const chosenMinSizeInPx = cssNumberAsPx(chosenMediaModifier.size.min) + if (minSizeInPx != null && (chosenMinSizeInPx == null || minSizeInPx > chosenMinSizeInPx)) { + chosen = variant + } + const maxSizeInPx = cssNumberAsPx(variantMediaModifier.size.max) + const chosenMaxSizeInPx = cssNumberAsPx(chosenMediaModifier.size.max) + if (maxSizeInPx != null && (chosenMaxSizeInPx == null || maxSizeInPx < chosenMaxSizeInPx)) { + chosen = variant + } + } + if (chosen == null) { + return null + } + return chosen +} + +// TODO: get this value from the Scene +const EM_TO_PX_RATIO = 16 +export function cssNumberAsPx(value: CSSNumber | null | undefined): number | null { + return value == null ? null : value.unit === 'em' ? value.value * EM_TO_PX_RATIO : value.value +} From 9fa2ed0834836dcc16e16cc1ecdf9378f17dabcd Mon Sep 17 00:00:00 2001 From: Liad Yosef Date: Tue, 10 Dec 2024 00:55:15 +0200 Subject: [PATCH 2/8] fix tests --- .../canvas/responsive-utils.spec.ts | 75 +++++++++---------- 1 file changed, 35 insertions(+), 40 deletions(-) diff --git a/editor/src/components/canvas/responsive-utils.spec.ts b/editor/src/components/canvas/responsive-utils.spec.ts index a6c7f426a362..6032a0285901 100644 --- a/editor/src/components/canvas/responsive-utils.spec.ts +++ b/editor/src/components/canvas/responsive-utils.spec.ts @@ -4,47 +4,7 @@ import type { ScreenSize, MediaQuery } from './responsive-types' import { extractScreenSizeFromCss } from './responsive-utils' import type { StyleMediaSizeModifier } from './canvas-types' -describe('mediaQueryToScreenSize', () => { - it('converts simple screen size queries', () => { - const testCases: { input: string; expected: ScreenSize }[] = [ - { - input: '@media (100px 100px)', - expected: { min: { value: 100, unit: 'px' } }, - }, - ] - testCases.forEach((testCase) => { - csstree.walk(csstree.parse(testCase.input), (node) => { - if (node.type === 'MediaQuery') { - const result = mediaQueryToScreenSize(node as unknown as MediaQuery) - expect(result).toEqual(testCase.expected) - } - }) - }) - }) -}) - describe('extractScreenSizeFromCss', () => { - beforeEach(() => { - // Clear the cache before each test - ;(extractScreenSizeFromCss as any).screenSizeCache?.clear() - }) - it('extracts screen size from simple media query', () => { const css = '@media (min-width: 100px) and (max-width: 500px)' const result = extractScreenSizeFromCss(css) @@ -166,3 +126,38 @@ describe('selectValueByBreakpoint', () => { ).toEqual({ value: 'c' }) }) }) + +describe('mediaQueryToScreenSize', () => { + it('converts simple screen size queries', () => { + const testCases: { input: string; expected: ScreenSize }[] = [ + { + input: '@media (100px 100px)', + expected: { min: { value: 100, unit: 'px' } }, + }, + ] + testCases.forEach((testCase) => { + csstree.walk(csstree.parse(testCase.input), (node) => { + if (node.type === 'MediaQuery') { + const result = mediaQueryToScreenSize(node as unknown as MediaQuery) + expect(result).toEqual(testCase.expected) + } + }) + }) + }) +}) From 190a52ceced375f8242c89e2c62d7ca593235869 Mon Sep 17 00:00:00 2001 From: Liad Yosef Date: Tue, 10 Dec 2024 00:58:37 +0200 Subject: [PATCH 3/8] rename --- editor/src/components/canvas/responsive-utils.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/editor/src/components/canvas/responsive-utils.ts b/editor/src/components/canvas/responsive-utils.ts index 0b6ded8b6266..db878d92114d 100644 --- a/editor/src/components/canvas/responsive-utils.ts +++ b/editor/src/components/canvas/responsive-utils.ts @@ -10,7 +10,7 @@ import { memoize } from 'src/core/shared/memoize' * `@media (20px < width < 50em)` -> { min: {value: 20, unit: 'px'}, max: {value: 50, unit: 'em'} } */ export const extractScreenSizeFromCss = memoize((css: string): ScreenSize | null => { - const mediaQuery = extractMediaQueryFromCss(css) + const mediaQuery = parseMediaQueryFromCss(css) return mediaQuery == null ? null : mediaQueryToScreenSize(mediaQuery) }) @@ -107,7 +107,7 @@ export function mediaQueryToScreenSize(mediaQuery: MediaQuery): ScreenSize { return result } -function extractMediaQueryFromCss(css: string): MediaQuery | null { +function parseMediaQueryFromCss(css: string): MediaQuery | null { let result: MediaQuery | null = null csstree.walk(csstree.parse(css), (node) => { if (node.type === 'MediaQuery') { From efaa2c269d1e3707b7fdb51344f39eb907f46630 Mon Sep 17 00:00:00 2001 From: Liad Yosef Date: Tue, 10 Dec 2024 00:59:59 +0200 Subject: [PATCH 4/8] import --- editor/src/components/canvas/responsive-utils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/editor/src/components/canvas/responsive-utils.ts b/editor/src/components/canvas/responsive-utils.ts index db878d92114d..52349915b1b6 100644 --- a/editor/src/components/canvas/responsive-utils.ts +++ b/editor/src/components/canvas/responsive-utils.ts @@ -2,7 +2,7 @@ import type { Feature, FeatureRange, MediaQuery, ScreenSize } from './responsive import * as csstree from 'css-tree' import type { StyleMediaSizeModifier, StyleModifier } from './canvas-types' import { type CSSNumber, type CSSNumberUnit, cssNumber } from '../inspector/common/css-utils' -import { memoize } from 'src/core/shared/memoize' +import { memoize } from '../../core/shared/memoize' /** * Extracts the screen size from a CSS string, for example: From e2ea5ebb80a2a7bd6b64f328aec94a6d19e80b99 Mon Sep 17 00:00:00 2001 From: Liad Yosef Date: Tue, 10 Dec 2024 01:01:01 +0200 Subject: [PATCH 5/8] remove unneeded test --- .../src/components/canvas/responsive-utils.spec.ts | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/editor/src/components/canvas/responsive-utils.spec.ts b/editor/src/components/canvas/responsive-utils.spec.ts index 6032a0285901..7c662d017f4f 100644 --- a/editor/src/components/canvas/responsive-utils.spec.ts +++ b/editor/src/components/canvas/responsive-utils.spec.ts @@ -34,18 +34,6 @@ describe('extractScreenSizeFromCss', () => { }) }) - it('caches null results', () => { - const css = 'invalid-css' - - // First call - const result1 = extractScreenSizeFromCss(css) - // Second call - should return same null reference - const result2 = extractScreenSizeFromCss(css) - - expect(result1).toBe(result2) - expect(result1).toBeNull() - }) - it('handles different CSS strings independently in cache', () => { const css1 = '@media (min-width: 100px)' const css2 = '@media (max-width: 500px)' From b087759efa7d01657cdbda496670d40e9deffcfa Mon Sep 17 00:00:00 2001 From: Liad Yosef Date: Tue, 10 Dec 2024 01:14:04 +0200 Subject: [PATCH 6/8] tests --- .../canvas/responsive-utils.spec.ts | 73 ++++++++++++------- 1 file changed, 46 insertions(+), 27 deletions(-) diff --git a/editor/src/components/canvas/responsive-utils.spec.ts b/editor/src/components/canvas/responsive-utils.spec.ts index 7c662d017f4f..75d9e5dec5b8 100644 --- a/editor/src/components/canvas/responsive-utils.spec.ts +++ b/editor/src/components/canvas/responsive-utils.spec.ts @@ -62,40 +62,59 @@ describe('extractScreenSizeFromCss', () => { describe('selectValueByBreakpoint', () => { const variants: { value: string; modifiers?: StyleMediaSizeModifier[] }[] = [ { - value: 'b', + value: 'Desktop Value', modifiers: [{ type: 'media-size', size: { min: { value: 200, unit: 'px' } } }], }, { - value: 'a', + value: 'Tablet Value', modifiers: [{ type: 'media-size', size: { min: { value: 100, unit: 'px' } } }], }, { - value: 'c', + value: 'Extra Large Value', modifiers: [{ type: 'media-size', size: { min: { value: 20, unit: 'em' } } }], }, - { value: 'd' }, + { + value: 'Ranged Value', + modifiers: [ + { + type: 'media-size', + size: { min: { value: 80, unit: 'px' }, max: { value: 90, unit: 'px' } }, + }, + ], + }, + { + value: 'Mobile Value', + modifiers: [{ type: 'media-size', size: { min: { value: 60, unit: 'px' } } }], + }, + { value: 'Default Value' }, ] - it('selects the correct value', () => { - expect(selectValueByBreakpoint(variants, 150)).toEqual({ - value: 'a', - modifiers: [{ type: 'media-size', size: { min: { value: 100, unit: 'px' } } }], - }) - }) - it('select the closest value', () => { - expect(selectValueByBreakpoint(variants, 250)).toEqual({ - value: 'b', - modifiers: [{ type: 'media-size', size: { min: { value: 200, unit: 'px' } } }], - }) - }) - it('converts em to px', () => { - expect(selectValueByBreakpoint(variants, 350)).toEqual({ - value: 'c', - modifiers: [{ type: 'media-size', size: { min: { value: 20, unit: 'em' } } }], + const tests: { title: string; screenSize: number; expected: string }[] = [ + { title: 'selects the correct value', screenSize: 150, expected: 'Tablet Value' }, + { title: 'select the closest value', screenSize: 250, expected: 'Desktop Value' }, + { title: 'converts em to px', screenSize: 350, expected: 'Extra Large Value' }, + { + title: 'selects the default value if no breakpoint is matched', + screenSize: 50, + expected: 'Default Value', + }, + { + title: 'selects the ranged value if the screen size is within the range', + screenSize: 85, + expected: 'Ranged Value', + }, + { + title: 'selects the mobile value if the screen size is outside the ranged values', + screenSize: 95, + expected: 'Mobile Value', + }, + ] as const + + tests.forEach((test) => { + it(`${test.title}`, () => { + expect(selectValueByBreakpoint(variants, test.screenSize)?.value).toEqual(test.expected) }) }) - it('selects the first value if no breakpoint is matched', () => { - expect(selectValueByBreakpoint(variants, 50)).toEqual({ value: 'd' }) - }) + it('selects null if no matching breakpoint and no default value', () => { expect(selectValueByBreakpoint(variants.slice(0, 2), 50)).toBeNull() }) @@ -104,14 +123,14 @@ describe('selectValueByBreakpoint', () => { selectValueByBreakpoint( [ { - value: 'a', + value: 'Hover Value', modifiers: [{ type: 'hover' }], }, - { value: 'c' }, + { value: 'Default Value' }, ], 50, - ), - ).toEqual({ value: 'c' }) + )?.value, + ).toEqual('Default Value') }) }) From d82deec2092d6ea636f7b4af6b75ed43bbe94f42 Mon Sep 17 00:00:00 2001 From: Liad Yosef Date: Tue, 10 Dec 2024 01:20:48 +0200 Subject: [PATCH 7/8] tests --- .../canvas/responsive-utils.spec.ts | 36 +++++++++++-------- 1 file changed, 21 insertions(+), 15 deletions(-) diff --git a/editor/src/components/canvas/responsive-utils.spec.ts b/editor/src/components/canvas/responsive-utils.spec.ts index 75d9e5dec5b8..6e659a310024 100644 --- a/editor/src/components/canvas/responsive-utils.spec.ts +++ b/editor/src/components/canvas/responsive-utils.spec.ts @@ -2,7 +2,7 @@ import * as csstree from 'css-tree' import { mediaQueryToScreenSize, selectValueByBreakpoint } from './responsive-utils' import type { ScreenSize, MediaQuery } from './responsive-types' import { extractScreenSizeFromCss } from './responsive-utils' -import type { StyleMediaSizeModifier } from './canvas-types' +import type { StyleMediaSizeModifier, StyleModifier } from './canvas-types' describe('extractScreenSizeFromCss', () => { it('extracts screen size from simple media query', () => { @@ -60,7 +60,7 @@ describe('extractScreenSizeFromCss', () => { }) describe('selectValueByBreakpoint', () => { - const variants: { value: string; modifiers?: StyleMediaSizeModifier[] }[] = [ + const variants: { value: string; modifiers?: StyleModifier[] }[] = [ { value: 'Desktop Value', modifiers: [{ type: 'media-size', size: { min: { value: 200, unit: 'px' } } }], @@ -116,21 +116,27 @@ describe('selectValueByBreakpoint', () => { }) it('selects null if no matching breakpoint and no default value', () => { - expect(selectValueByBreakpoint(variants.slice(0, 2), 50)).toBeNull() + const largeVariants: { value: string; modifiers?: StyleModifier[] }[] = [ + { + value: 'Desktop Value', + modifiers: [{ type: 'media-size', size: { min: { value: 200, unit: 'px' } } }], + }, + { + value: 'Tablet Value', + modifiers: [{ type: 'media-size', size: { min: { value: 100, unit: 'px' } } }], + }, + ] + expect(selectValueByBreakpoint(largeVariants, 50)).toBeNull() }) it('selects default value if no media modifiers', () => { - expect( - selectValueByBreakpoint( - [ - { - value: 'Hover Value', - modifiers: [{ type: 'hover' }], - }, - { value: 'Default Value' }, - ], - 50, - )?.value, - ).toEqual('Default Value') + const noMediaVariants: { value: string; modifiers?: StyleModifier[] }[] = [ + { + value: 'Hover Value', + modifiers: [{ type: 'hover' }], + }, + { value: 'Default Value' }, + ] + expect(selectValueByBreakpoint(noMediaVariants, 50)?.value).toEqual('Default Value') }) }) From 553634acc186fd6d21fde4e5488c26f2608edc7e Mon Sep 17 00:00:00 2001 From: Liad Yosef Date: Tue, 10 Dec 2024 01:21:33 +0200 Subject: [PATCH 8/8] clean imports --- editor/src/components/canvas/responsive-utils.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/editor/src/components/canvas/responsive-utils.spec.ts b/editor/src/components/canvas/responsive-utils.spec.ts index 6e659a310024..dcbcc0c434c1 100644 --- a/editor/src/components/canvas/responsive-utils.spec.ts +++ b/editor/src/components/canvas/responsive-utils.spec.ts @@ -2,7 +2,7 @@ import * as csstree from 'css-tree' import { mediaQueryToScreenSize, selectValueByBreakpoint } from './responsive-utils' import type { ScreenSize, MediaQuery } from './responsive-types' import { extractScreenSizeFromCss } from './responsive-utils' -import type { StyleMediaSizeModifier, StyleModifier } from './canvas-types' +import type { StyleModifier } from './canvas-types' describe('extractScreenSizeFromCss', () => { it('extracts screen size from simple media query', () => {