From b6c8d1b17ef00fc06b1a8dd34f5312c0f7d75362 Mon Sep 17 00:00:00 2001
From: Jess Telford <jess.telford@shopify.com>
Date: Fri, 15 Sep 2023 18:27:14 +1000
Subject: [PATCH] Add support for non-responsive values to `Grid`'s `gap`,
 `columns`, and `areas` props.

---
 .changeset/slimy-donuts-report.md             |   5 +
 polaris-react/src/components/Grid/Grid.scss   |  72 +++------
 polaris-react/src/components/Grid/Grid.tsx    |  53 +++----
 .../components/Grid/components/Cell/Cell.scss |  46 +-----
 .../components/Grid/components/Cell/Cell.tsx  |  21 +--
 .../Grid/components/Cell/tests/Cell.test.tsx  |   8 +-
 .../src/components/Grid/tests/Grid.test.tsx   | 146 +++++++++++-------
 polaris-react/src/utilities/css.ts            |  17 +-
 8 files changed, 151 insertions(+), 217 deletions(-)
 create mode 100644 .changeset/slimy-donuts-report.md

diff --git a/.changeset/slimy-donuts-report.md b/.changeset/slimy-donuts-report.md
new file mode 100644
index 00000000000..3aa224c29a8
--- /dev/null
+++ b/.changeset/slimy-donuts-report.md
@@ -0,0 +1,5 @@
+---
+'@shopify/polaris': minor
+---
+
+Added support for non-responsive values to `Grid`'s `gap`, `columns`, and `areas` props.
diff --git a/polaris-react/src/components/Grid/Grid.scss b/polaris-react/src/components/Grid/Grid.scss
index 2a29a0f880a..e384d7345c2 100644
--- a/polaris-react/src/components/Grid/Grid.scss
+++ b/polaris-react/src/components/Grid/Grid.scss
@@ -1,60 +1,24 @@
 @import '../../styles/common';
 
 .Grid {
-  // Remap custom properties as mobile first fallbacks for grid-template-areas and grid-template-columns
-  // stylelint-disable -- Polaris component custom properties
-  --pc-grid-areas-xs: initial;
-  --pc-grid-areas-sm: var(--pc-grid-areas-xs);
-  --pc-grid-areas-md: var(--pc-grid-areas-sm);
-  --pc-grid-areas-lg: var(--pc-grid-areas-md);
-  --pc-grid-areas-xl: var(--pc-grid-areas-lg);
-  --pc-grid-columns-xs: 6;
-  --pc-grid-columns-sm: var(--pc-grid-columns-xs);
-  --pc-grid-columns-md: var(--pc-grid-columns-sm);
-  --pc-grid-columns-lg: 12;
-  --pc-grid-columns-xl: var(--pc-grid-columns-lg);
-  // stylelint-enable
   display: grid;
-  // stylelint-disable-next-line -- generated by polaris-migrator DO NOT COPY
-  gap: var(--pc-grid-gap-xs, var(--p-space-400));
-  // stylelint-disable-next-line -- generated by polaris-migrator DO NOT COPY
-  grid-template-areas: var(--pc-grid-areas-xs);
-  // stylelint-disable-next-line -- generated by polaris-migrator DO NOT COPY
-  grid-template-columns: repeat(var(--pc-grid-columns-xs), minmax(0, 1fr));
-
-  @media #{$p-breakpoints-sm-up} {
-    // stylelint-disable-next-line -- generated by polaris-migrator DO NOT COPY
-    gap: var(--pc-grid-gap-sm, var(--p-space-400));
-    // stylelint-disable-next-line -- generated by polaris-migrator DO NOT COPY
-    grid-template-areas: var(--pc-grid-areas-sm);
-    // stylelint-disable-next-line -- generated by polaris-migrator DO NOT COPY
-    grid-template-columns: repeat(var(--pc-grid-columns-sm), minmax(0, 1fr));
-  }
 
-  @media #{$p-breakpoints-md-up} {
-    // stylelint-disable-next-line -- generated by polaris-migrator DO NOT COPY
-    gap: var(--pc-grid-gap-md, var(--p-space-400));
-    // stylelint-disable-next-line -- generated by polaris-migrator DO NOT COPY
-    grid-template-areas: var(--pc-grid-areas-md);
-    // stylelint-disable-next-line -- generated by polaris-migrator DO NOT COPY
-    grid-template-columns: repeat(var(--pc-grid-columns-md), minmax(0, 1fr));
-  }
-
-  @media #{$p-breakpoints-lg-up} {
-    // stylelint-disable-next-line -- generated by polaris-migrator DO NOT COPY
-    gap: var(--pc-grid-gap-lg, var(--p-space-400));
-    // stylelint-disable-next-line -- generated by polaris-migrator DO NOT COPY
-    grid-template-areas: var(--pc-grid-areas-lg);
-    // stylelint-disable-next-line -- generated by polaris-migrator DO NOT COPY
-    grid-template-columns: repeat(var(--pc-grid-columns-lg), minmax(0, 1fr));
-  }
-
-  @media #{$p-breakpoints-xl-up} {
-    // stylelint-disable-next-line -- generated by polaris-migrator DO NOT COPY
-    gap: var(--pc-grid-gap-xl, var(--p-space-400));
-    // stylelint-disable-next-line -- generated by polaris-migrator DO NOT COPY
-    grid-template-areas: var(--pc-grid-areas-xl);
-    // stylelint-disable-next-line -- generated by polaris-migrator DO NOT COPY
-    grid-template-columns: repeat(var(--pc-grid-columns-xl), minmax(0, 1fr));
-  }
+  @include responsive-props(
+    'grid',
+    'gap',
+    'gap',
+    $default: 'var(--p-space-400)'
+  );
+  @include responsive-props('grid', 'areas', 'grid-template-areas');
+  @include responsive-props(
+    'grid',
+    'columns',
+    '--pc-grid-template-columns',
+    $default: ('xs': 6, 'lg': 12)
+  );
+  // stylelint-disable-next-line -- generated by polaris-migrator DO NOT COPY
+  grid-template-columns: repeat(
+    var(--pc-grid-template-columns),
+    minmax(0, 1fr)
+  );
 }
diff --git a/polaris-react/src/components/Grid/Grid.tsx b/polaris-react/src/components/Grid/Grid.tsx
index 7d7f5c7f492..c0cbe7c0e41 100644
--- a/polaris-react/src/components/Grid/Grid.tsx
+++ b/polaris-react/src/components/Grid/Grid.tsx
@@ -1,21 +1,17 @@
 import React from 'react';
+import type {SpaceScale} from '@shopify/polaris-tokens';
+
+import {
+  getResponsiveProps,
+  getResponsiveValue,
+  mapResponsivePropValues,
+} from '../../utilities/css';
+import type {ResponsiveProp} from '../../utilities/css';
 
 import {Cell} from './components';
 import styles from './Grid.scss';
 
-type Breakpoints = 'xs' | 'sm' | 'md' | 'lg' | 'xl';
-
-type Areas = {
-  [Breakpoint in Breakpoints]?: string[];
-};
-
-type Columns = {
-  [Breakpoint in Breakpoints]?: number;
-};
-
-type Gap = {
-  [Breakpoint in Breakpoints]?: string;
-};
+type Area = string[];
 
 export interface GridProps {
   /**
@@ -24,32 +20,25 @@ export interface GridProps {
    * cells instead. See:
    * https://polaris.shopify.com/components/layout-and-structure
    */
-  areas?: Areas;
+  areas?: ResponsiveProp<Area>;
   /* Number of columns */
-  columns?: Columns;
+  columns?: ResponsiveProp<number>;
   /* Grid gap */
-  gap?: Gap;
+  gap?: ResponsiveProp<SpaceScale>;
   children?: React.ReactNode;
 }
+
 export const Grid: React.FunctionComponent<GridProps> & {
   Cell: typeof Cell;
 } = function Grid({gap, areas, children, columns}: GridProps) {
   const style = {
-    '--pc-grid-gap-xs': gap?.xs,
-    '--pc-grid-gap-sm': gap?.sm,
-    '--pc-grid-gap-md': gap?.md,
-    '--pc-grid-gap-lg': gap?.lg,
-    '--pc-grid-gap-xl': gap?.xl,
-    '--pc-grid-columns-xs': columns?.xs,
-    '--pc-grid-columns-sm': columns?.sm,
-    '--pc-grid-columns-md': columns?.md,
-    '--pc-grid-columns-lg': columns?.lg,
-    '--pc-grid-columns-xl': columns?.xl,
-    '--pc-grid-areas-xs': formatAreas(areas?.xs),
-    '--pc-grid-areas-sm': formatAreas(areas?.sm),
-    '--pc-grid-areas-md': formatAreas(areas?.md),
-    '--pc-grid-areas-lg': formatAreas(areas?.lg),
-    '--pc-grid-areas-xl': formatAreas(areas?.xl),
+    ...getResponsiveProps('grid', 'gap', 'space', gap),
+    ...getResponsiveValue('grid', 'columns', columns),
+    ...getResponsiveValue(
+      'grid',
+      'areas',
+      mapResponsivePropValues(areas, formatAreas),
+    ),
   } as React.CSSProperties;
 
   return (
@@ -59,7 +48,7 @@ export const Grid: React.FunctionComponent<GridProps> & {
   );
 };
 
-export function formatAreas(areas?: string[]) {
+export function formatAreas(areas?: Area) {
   if (!areas) return;
   return `'${areas?.join(`' '`)}'`;
 }
diff --git a/polaris-react/src/components/Grid/components/Cell/Cell.scss b/polaris-react/src/components/Grid/components/Cell/Cell.scss
index a2746d9105f..ae8343c0b91 100644
--- a/polaris-react/src/components/Grid/components/Cell/Cell.scss
+++ b/polaris-react/src/components/Grid/components/Cell/Cell.scss
@@ -1,52 +1,10 @@
 @import '../../../../styles/common';
 
 .Cell {
+  @include responsive-props('grid-cell', 'row', 'grid-row');
+  @include responsive-props('grid-cell', 'column', 'grid-column');
   // Remap custom properties as mobile first fallbacks for grid-row and grid-column
-  // stylelint-disable -- Polaris component custom properties
-  --pc-row-xs: initial;
-  --pc-row-sm: var(--pc-row-xs);
-  --pc-row-md: var(--pc-row-sm);
-  --pc-row-lg: var(--pc-row-md);
-  --pc-row-xl: var(--pc-row-lg);
-  --pc-column-xs: initial;
-  --pc-column-sm: var(--pc-column-xs);
-  --pc-column-md: var(--pc-column-sm);
-  --pc-column-lg: var(--pc-column-md);
-  --pc-column-xl: var(--pc-column-lg);
-  // stylelint-enable
   min-width: 0;
-  // stylelint-disable-next-line -- generated by polaris-migrator DO NOT COPY
-  grid-row: var(--pc-row-xs);
-  // stylelint-disable-next-line -- generated by polaris-migrator DO NOT COPY
-  grid-column: var(--pc-column-xs);
-
-  @media #{$p-breakpoints-sm-up} {
-    // stylelint-disable-next-line -- generated by polaris-migrator DO NOT COPY
-    grid-row: var(--pc-row-sm);
-    // stylelint-disable-next-line -- generated by polaris-migrator DO NOT COPY
-    grid-column: var(--pc-column-sm);
-  }
-
-  @media #{$p-breakpoints-md-up} {
-    // stylelint-disable-next-line -- generated by polaris-migrator DO NOT COPY
-    grid-row: var(--pc-row-md);
-    // stylelint-disable-next-line -- generated by polaris-migrator DO NOT COPY
-    grid-column: var(--pc-column-md);
-  }
-
-  @media #{$p-breakpoints-lg-up} {
-    // stylelint-disable-next-line -- generated by polaris-migrator DO NOT COPY
-    grid-row: var(--pc-row-lg);
-    // stylelint-disable-next-line -- generated by polaris-migrator DO NOT COPY
-    grid-column: var(--pc-column-lg);
-  }
-
-  @media #{$p-breakpoints-xl-up} {
-    // stylelint-disable-next-line -- generated by polaris-migrator DO NOT COPY
-    grid-row: var(--pc-row-xl);
-    // stylelint-disable-next-line -- generated by polaris-migrator DO NOT COPY
-    grid-column: var(--pc-column-xl);
-  }
 }
 
 @for $i from 1 through 6 {
diff --git a/polaris-react/src/components/Grid/components/Cell/Cell.tsx b/polaris-react/src/components/Grid/components/Cell/Cell.tsx
index 802c9c48b89..266d2a55e56 100644
--- a/polaris-react/src/components/Grid/components/Cell/Cell.tsx
+++ b/polaris-react/src/components/Grid/components/Cell/Cell.tsx
@@ -1,14 +1,11 @@
 import React from 'react';
 
-import {classNames} from '../../../../utilities/css';
+import {classNames, getResponsiveValue} from '../../../../utilities/css';
+import type {ResponsiveProp} from '../../../../utilities/css';
 
 import styles from './Cell.scss';
 
-type Breakpoints = 'xs' | 'sm' | 'md' | 'lg' | 'xl';
-
-type Cell = {
-  [Breakpoint in Breakpoints]?: string;
-};
+type Cell = ResponsiveProp;
 
 interface Columns {
   /** Number of columns the section should span on extra small screens */
@@ -54,16 +51,8 @@ export function Cell({
 
   const style = {
     gridArea,
-    '--pc-column-xs': column?.xs,
-    '--pc-column-sm': column?.sm,
-    '--pc-column-md': column?.md,
-    '--pc-column-lg': column?.lg,
-    '--pc-column-xl': column?.xl,
-    '--pc-row-xs': row?.xs,
-    '--pc-row-sm': row?.sm,
-    '--pc-row-md': row?.md,
-    '--pc-row-lg': row?.lg,
-    '--pc-row-xl': row?.xl,
+    ...getResponsiveValue('grid-cell', 'column', column),
+    ...getResponsiveValue('grid-cell', 'row', row),
   };
 
   return (
diff --git a/polaris-react/src/components/Grid/components/Cell/tests/Cell.test.tsx b/polaris-react/src/components/Grid/components/Cell/tests/Cell.test.tsx
index 462fd0c5c2c..60cf997be94 100644
--- a/polaris-react/src/components/Grid/components/Cell/tests/Cell.test.tsx
+++ b/polaris-react/src/components/Grid/components/Cell/tests/Cell.test.tsx
@@ -37,8 +37,8 @@ describe('<Cell />', () => {
 
     expect(cell).toContainReactComponent('div', {
       style: {
-        '--pc-column-xs': '2 / span 1',
-        '--pc-column-lg': 'span 12',
+        '--pc-grid-cell-column-xs': '2 / span 1',
+        '--pc-grid-cell-column-lg': 'span 12',
       } as React.CSSProperties,
     });
   });
@@ -50,8 +50,8 @@ describe('<Cell />', () => {
 
     expect(cell).toContainReactComponent('div', {
       style: {
-        '--pc-row-xs': '2 / span 3',
-        '--pc-row-lg': '1 / span 2',
+        '--pc-grid-cell-row-xs': '2 / span 3',
+        '--pc-grid-cell-row-lg': '1 / span 2',
       } as React.CSSProperties,
     });
   });
diff --git a/polaris-react/src/components/Grid/tests/Grid.test.tsx b/polaris-react/src/components/Grid/tests/Grid.test.tsx
index 038c84f47a0..73d0e92cac2 100644
--- a/polaris-react/src/components/Grid/tests/Grid.test.tsx
+++ b/polaris-react/src/components/Grid/tests/Grid.test.tsx
@@ -21,67 +21,103 @@ describe('<Grid />', () => {
   const lgAreas = ['lg1', 'lg2', 'lg3'];
   const xlAreas = ['xl1', 'xl2', 'xl3'];
 
-  it('applies grid-template-areas as custom properties', () => {
-    const grid = mountWithApp(
-      <Grid
-        areas={{
-          xs: xsAreas,
-          sm: smAreas,
-          md: mdAreas,
-          lg: lgAreas,
-          xl: xlAreas,
-        }}
-      />,
-    );
-
-    expect(grid).toContainReactComponent('div', {
-      style: {
-        '--pc-grid-areas-xs': formatAreas(xsAreas),
-        '--pc-grid-areas-sm': formatAreas(smAreas),
-        '--pc-grid-areas-md': formatAreas(mdAreas),
-        '--pc-grid-areas-lg': formatAreas(lgAreas),
-        '--pc-grid-areas-xl': formatAreas(xlAreas),
-      } as React.CSSProperties,
+  describe('applies grid-template-areas as custom properties', () => {
+    it('responsively', () => {
+      const grid = mountWithApp(
+        <Grid
+          areas={{
+            xs: xsAreas,
+            sm: smAreas,
+            md: mdAreas,
+            lg: lgAreas,
+            xl: xlAreas,
+          }}
+        />,
+      );
+
+      expect(grid).toContainReactComponent('div', {
+        style: {
+          '--pc-grid-areas-xs': formatAreas(xsAreas),
+          '--pc-grid-areas-sm': formatAreas(smAreas),
+          '--pc-grid-areas-md': formatAreas(mdAreas),
+          '--pc-grid-areas-lg': formatAreas(lgAreas),
+          '--pc-grid-areas-xl': formatAreas(xlAreas),
+        } as React.CSSProperties,
+      });
+    });
+
+    it('non-responsively', () => {
+      const grid = mountWithApp(<Grid areas={xsAreas} />);
+
+      expect(grid).toContainReactComponent('div', {
+        style: {
+          '--pc-grid-areas-xs': formatAreas(xsAreas),
+        } as React.CSSProperties,
+      });
     });
   });
 
-  it('renders inline custom properties for custom columns', () => {
-    const grid = mountWithApp(
-      <Grid columns={{xs: 1, sm: 3, md: 7, lg: 12, xl: 12}} />,
-    );
-
-    expect(grid).toContainReactComponent('div', {
-      style: {
-        '--pc-grid-columns-xs': 1,
-        '--pc-grid-columns-sm': 3,
-        '--pc-grid-columns-md': 7,
-        '--pc-grid-columns-lg': 12,
-        '--pc-grid-columns-xl': 12,
-      } as React.CSSProperties,
+  describe('renders inline custom properties for custom columns', () => {
+    it('responsively', () => {
+      const grid = mountWithApp(
+        <Grid columns={{xs: 1, sm: 3, md: 7, lg: 12, xl: 12}} />,
+      );
+
+      expect(grid).toContainReactComponent('div', {
+        style: {
+          '--pc-grid-columns-xs': 1,
+          '--pc-grid-columns-sm': 3,
+          '--pc-grid-columns-md': 7,
+          '--pc-grid-columns-lg': 12,
+          '--pc-grid-columns-xl': 12,
+        } as React.CSSProperties,
+      });
+    });
+
+    it('non-responsively', () => {
+      const grid = mountWithApp(<Grid columns={3} />);
+
+      expect(grid).toContainReactComponent('div', {
+        style: {
+          '--pc-grid-columns-xs': 3,
+        } as React.CSSProperties,
+      });
     });
   });
 
-  it('renders inline custom properties for custom gap', () => {
-    const grid = mountWithApp(
-      <Grid
-        gap={{
-          xs: 'var(--p-space-100)',
-          sm: 'var(--p-space-100)',
-          md: 'var(--p-space-200)',
-          lg: 'var(--p-space-400)',
-          xl: 'var(--p-space-400)',
-        }}
-      />,
-    );
-
-    expect(grid).toContainReactComponent('div', {
-      style: {
-        '--pc-grid-gap-xs': 'var(--p-space-100)',
-        '--pc-grid-gap-sm': 'var(--p-space-100)',
-        '--pc-grid-gap-md': 'var(--p-space-200)',
-        '--pc-grid-gap-lg': 'var(--p-space-400)',
-        '--pc-grid-gap-xl': 'var(--p-space-400)',
-      } as React.CSSProperties,
+  describe('renders inline custom properties for custom gap', () => {
+    it('responsively', () => {
+      const grid = mountWithApp(
+        <Grid
+          gap={{
+            xs: '100',
+            sm: '100',
+            md: '200',
+            lg: '400',
+            xl: '400',
+          }}
+        />,
+      );
+
+      expect(grid).toContainReactComponent('div', {
+        style: {
+          '--pc-grid-gap-xs': 'var(--p-space-100)',
+          '--pc-grid-gap-sm': 'var(--p-space-100)',
+          '--pc-grid-gap-md': 'var(--p-space-200)',
+          '--pc-grid-gap-lg': 'var(--p-space-400)',
+          '--pc-grid-gap-xl': 'var(--p-space-400)',
+        } as React.CSSProperties,
+      });
+    });
+
+    it('non-responsively', () => {
+      const grid = mountWithApp(<Grid gap="300" />);
+
+      expect(grid).toContainReactComponent('div', {
+        style: {
+          '--pc-grid-gap-xs': 'var(--p-space-300)',
+        } as React.CSSProperties,
+      });
     });
   });
 
diff --git a/polaris-react/src/utilities/css.ts b/polaris-react/src/utilities/css.ts
index 0a7380fa7bc..131a8731568 100644
--- a/polaris-react/src/utilities/css.ts
+++ b/polaris-react/src/utilities/css.ts
@@ -45,19 +45,12 @@ export function createPolarisCSSVar<T extends string | number = string>(
   tokenSubgroup: string,
   tokenValue: T,
 ): PolarisCSSVar {
-  // For backwards compatibility with `Grid` and `Grid.Cell`, accept already
-  // formed var()'s using either polaris or polaris component custom properties.
+  // `Grid`'s `gap` prop used to allow passing fully formed var() functions as
+  // the value. This is no longer supported in v12+.
   if (typeof tokenValue === 'string' && tokenValue.startsWith('var(')) {
-    if (
-      !tokenValue.startsWith(`var(--p-${tokenSubgroup}-`) &&
-      !tokenValue.startsWith(`var(--pc-${tokenSubgroup}-`)
-    ) {
-      throw new Error(
-        `"${tokenValue}" is not from the ${tokenSubgroup} token group.`,
-      );
-    }
-
-    return tokenValue as PolarisCSSVar;
+    throw new Error(
+      `"${tokenValue}" is not from the ${tokenSubgroup} token group.`,
+    );
   }
 
   // NOTE: All our token values today are either strings or numbers, so