From 3ab7522b0b1bec0bf74f4cf18446b5f698dccc50 Mon Sep 17 00:00:00 2001
From: Federico Ruggi <1081051+ruggi@users.noreply.github.com>
Date: Mon, 15 Jul 2024 16:52:37 +0200
Subject: [PATCH] Support area names in grid templates (#6080)
**Problem:**
Parsed CSS grid templates should support area names.
**Fix:**
Parse area names along with the CSS number for grid templates. The label
is stored in the number itself, so we can reuse it later (e.g. for
targeting via other style props).
To make it more apparent, I added the area name under the resize
handles, but it's definitely not a requirement (although I think it
could/should be shown somewhere!).
https://github.com/user-attachments/assets/5aba975d-3b24-48be-8d69-c19f774bb351
Fixes #6079
---
.../canvas/controls/grid-controls.tsx | 5 ++
.../store/store-deep-equality-instances.ts | 4 +-
.../inspector/common/css-utils.spec.ts | 32 +++++++++++
.../components/inspector/common/css-utils.ts | 57 +++++++++++++++++--
4 files changed, 91 insertions(+), 7 deletions(-)
diff --git a/editor/src/components/canvas/controls/grid-controls.tsx b/editor/src/components/canvas/controls/grid-controls.tsx
index 960b10bbe3c8..e4ab421f7280 100644
--- a/editor/src/components/canvas/controls/grid-controls.tsx
+++ b/editor/src/components/canvas/controls/grid-controls.tsx
@@ -229,6 +229,7 @@ export const GridResizingControl = React.memo((props: GridResizingControlProps)
justifyContent: 'center',
cursor: gridEdgeToCSSCursor(props.axis === 'column' ? 'column-start' : 'row-start'),
fontSize: 8,
+ position: 'relative',
}}
css={{
opacity: resizing ? 1 : 0.5,
@@ -239,6 +240,10 @@ export const GridResizingControl = React.memo((props: GridResizingControlProps)
onMouseDown={mouseDownHandler}
>
{props.axis === 'row' ? '↕' : '↔'}
+ {when(
+ props.dimension.areaName != null,
+ {props.dimension.areaName},
+ )}
{when(
resizing,
diff --git a/editor/src/components/editor/store/store-deep-equality-instances.ts b/editor/src/components/editor/store/store-deep-equality-instances.ts
index 7c0227f6db6d..68a7ab75d6e3 100644
--- a/editor/src/components/editor/store/store-deep-equality-instances.ts
+++ b/editor/src/components/editor/store/store-deep-equality-instances.ts
@@ -1970,11 +1970,13 @@ export const CSSNumberKeepDeepEquality: KeepDeepEqualityCall = combin
)
export const GridCSSNumberKeepDeepEquality: KeepDeepEqualityCall =
- combine2EqualityCalls(
+ combine3EqualityCalls(
(cssNum) => cssNum.value,
createCallWithTripleEquals(),
(cssNum) => cssNum.unit,
nullableDeepEquality(createCallWithTripleEquals()),
+ (cssNum) => cssNum.areaName,
+ nullableDeepEquality(StringKeepDeepEquality),
gridCSSNumber,
)
diff --git a/editor/src/components/inspector/common/css-utils.spec.ts b/editor/src/components/inspector/common/css-utils.spec.ts
index ae3151c0e63e..cc0fb96037fa 100644
--- a/editor/src/components/inspector/common/css-utils.spec.ts
+++ b/editor/src/components/inspector/common/css-utils.spec.ts
@@ -71,6 +71,7 @@ import {
RegExpLibrary,
toggleSimple,
toggleStylePropPath,
+ tokenizeGridTemplate,
} from './css-utils'
describe('toggleStyleProp', () => {
@@ -1807,3 +1808,34 @@ describe('printBackgroundSize', () => {
`)
})
})
+
+describe('tokenizeGridTemplate', () => {
+ it('tokenizes the grid template strings (no units)', async () => {
+ expect(tokenizeGridTemplate('123 456 78 9')).toEqual(['123', '456', '78', '9'])
+ })
+ it('tokenizes the grid template strings (with units)', async () => {
+ expect(tokenizeGridTemplate('123 456px 78 9rem')).toEqual(['123', '456px', '78', '9rem'])
+ })
+ it('tokenizes the grid template strings (with some area names)', async () => {
+ expect(tokenizeGridTemplate('[foo] 123 456px 78 9rem')).toEqual([
+ '[foo] 123',
+ '456px',
+ '78',
+ '9rem',
+ ])
+ expect(tokenizeGridTemplate('123 [foo]456px 78 [bar] 9rem')).toEqual([
+ '123',
+ '[foo] 456px',
+ '78',
+ '[bar] 9rem',
+ ])
+ })
+ it('tokenizes the grid template strings (with all area names)', async () => {
+ expect(tokenizeGridTemplate('[foo] 123 [bar]456px [baz] 78 [QUX]9rem')).toEqual([
+ '[foo] 123',
+ '[bar] 456px',
+ '[baz] 78',
+ '[QUX] 9rem',
+ ])
+ })
+})
diff --git a/editor/src/components/inspector/common/css-utils.ts b/editor/src/components/inspector/common/css-utils.ts
index 4e00df8c85a0..5d063f951951 100644
--- a/editor/src/components/inspector/common/css-utils.ts
+++ b/editor/src/components/inspector/common/css-utils.ts
@@ -589,12 +589,18 @@ const GridCSSNumberUnits: Array = [...LengthUnits, ...Resolut
export interface GridCSSNumber {
value: number
unit: GridCSSNumberUnit | null
+ areaName: string | null
}
-export function gridCSSNumber(value: number, unit: GridCSSNumberUnit | null): GridCSSNumber {
+export function gridCSSNumber(
+ value: number,
+ unit: GridCSSNumberUnit | null,
+ areaName: string | null,
+): GridCSSNumber {
return {
- value,
- unit,
+ value: value,
+ unit: unit,
+ areaName: areaName,
}
}
@@ -762,7 +768,8 @@ export function printArrayCSSNumber(array: Array): string {
return array
.map((dimension) => {
const printed = printCSSNumber(dimension, null)
- return typeof printed === 'string' ? printed : `${printed}`
+ const areaName = dimension.areaName != null ? `[${dimension.areaName}] ` : ''
+ return `${areaName}${printed}`
})
.join(' ')
}
@@ -834,13 +841,30 @@ export const parseCSSUnitlessAsNumber = (input: unknown): Either
}
}
+const gridCSSTemplateNumberRegex = /^\[(.+)\]\s*(.+)$/
+
export function parseToCSSGridNumber(input: unknown): Either {
+ function getParts() {
+ if (typeof input === 'string') {
+ const match = input.match(gridCSSTemplateNumberRegex)
+ if (match != null) {
+ return {
+ areaName: match[1],
+ inputToParse: match[2],
+ }
+ }
+ }
+ return { areaName: null, inputToParse: input }
+ }
+ const { areaName, inputToParse } = getParts()
+
return mapEither((value) => {
return {
value: value.value,
unit: value.unit as GridCSSNumberUnit | null,
+ areaName: areaName,
}
- }, parseCSSGrid(input))
+ }, parseCSSGrid(inputToParse))
}
export const parseCSSNumber = (
@@ -887,6 +911,27 @@ export function parseGridRange(input: unknown): Either {
}
}
+const reGridAreaNameBrackets = /^\[.+\]$/
+
+export function tokenizeGridTemplate(str: string): string[] {
+ let tokens: string[] = []
+ let parts = str.replace(/\]/g, '] ').split(/\s+/)
+
+ while (parts.length > 0) {
+ const part = parts.shift()?.trim()
+ if (part == null) {
+ break
+ }
+ if (part.match(reGridAreaNameBrackets) != null && parts.length > 0) {
+ const withAreaName = `${part} ${parts.shift()}`
+ tokens.push(withAreaName)
+ } else {
+ tokens.push(part)
+ }
+ }
+ return tokens
+}
+
export function parseGridAutoOrTemplateBase(
input: unknown,
): Either {
@@ -895,7 +940,7 @@ export function parseGridAutoOrTemplateBase(
return leftMapEither(descriptionParseError, result)
}
if (typeof input === 'string') {
- const parsedCSSArray = parseCSSArray([numberParse])(input.split(/ +/))
+ const parsedCSSArray = parseCSSArray([numberParse])(tokenizeGridTemplate(input))
return bimapEither(
(error) => {
if (error.type === 'DESCRIPTION_PARSE_ERROR') {