Repository: uber-web/baseui
Pull request #636 (2018-12-10 19:20:25)

feat(block): add CSS Grid properties
<!-- Thanks so much for your PR, your contribution is appreciated! ❤️ -->

#### Minor: Add CSS Grid Properties

Block component should support basic layout CSS properties. It already supported flexbox, this PR adds grid as well. Adding more new props doesn't scale all that well, but for now I guess it's ok.


CLAassistant: [![CLA assistant check](https://cla-assistant.io/pull/badge/not_signed)](https://cla-assistant.io/uber-web/baseui?pullRequest=636) <br/>Thank you for your submission, we really appreciate it. Like many open source projects, we ask that you sign our [Contributor License Agreement](https://cla-assistant.io/uber-web/baseui?pullRequest=636) before we can accept your contribution.<br/><sub>You have signed the CLA already but the status is still pending? Let us [recheck](https://cla-assistant.io/check/uber-web/baseui?pullRequest=636) it.</sub>

Patch:
diff --git a/src/block/README.md b/src/block/README.md
index 2abc75b98..7725f4baf 100644
--- a/src/block/README.md
+++ b/src/block/README.md
@@ -54,7 +54,25 @@ All other props are spread onto the base element.
 * `flexDirection?: [available values](https://developer.mozilla.org/en-US/docs/Web/CSS/flex-direction)`
 * `display?: [available values](https://developer.mozilla.org/en-US/docs/Web/CSS/display)`
 * `flex?: string`
+* `grid?: [available values](https://developer.mozilla.org/en-US/docs/Web/CSS/grid)`
+* `gridArea?: [available values](https://developer.mozilla.org/en-US/docs/Web/CSS/grid-area)`
+* `gridAutoColumns?: [available values](https://developer.mozilla.org/en-US/docs/Web/CSS/grid-auto-columns)`
+* `gridAutoFlow?: [available values](https://developer.mozilla.org/en-US/docs/Web/CSS/grid-auto-flow)`
+* `gridAutoRows?: [available values](https://developer.mozilla.org/en-US/docs/Web/CSS/grid-auto-rows)`
+* `gridColumn?: [available values](https://developer.mozilla.org/en-US/docs/Web/CSS/grid-column)`
+* `gridColumnEnd?: [available values](https://developer.mozilla.org/en-US/docs/Web/CSS/grid-column-end)`
+* `gridColumnGap?: [available values](https://developer.mozilla.org/en-US/docs/Web/CSS/grid-column-gap)`
+* `gridColumnStart?: [available values](https://developer.mozilla.org/en-US/docs/Web/CSS/grid-column-start)`
+* `gridGap?: [available values](https://developer.mozilla.org/en-US/docs/Web/CSS/grid-gap)`
+* `gridRow?: [available values](https://developer.mozilla.org/en-US/docs/Web/CSS/grid-row)`
+* `gridRowStart?: [available values](https://developer.mozilla.org/en-US/docs/Web/CSS/grid-row-start)`
+* `gridRowEnd?: [available values](https://developer.mozilla.org/en-US/docs/Web/CSS/grid-row-end)`
+* `gridTemplate?: [available values](https://developer.mozilla.org/en-US/docs/Web/CSS/grid-template)`
+* `gridTemplateAreas?: [available values](https://developer.mozilla.org/en-US/docs/Web/CSS/grid-template-areas)`
+* `gridTemplateColumns?: [available values](https://developer.mozilla.org/en-US/docs/Web/CSS/grid-template-columns)`
+* `gridTemplateRows?: [available values](https://developer.mozilla.org/en-US/docs/Web/CSS/grid-template-rows)`
 * `justifyContent?: [available values](https://developer.mozilla.org/en-US/docs/Web/CSS/justify-content)`
+* `justifyItems?: [available values](https://developer.mozilla.org/en-US/docs/Web/CSS/justify-items)`
 * `justifySelf?: [available values](https://developer.mozilla.org/en-US/docs/Web/CSS/justify-self)`
 * `position?: 'static' | 'absolute' | 'relative' | 'fixed' | 'sticky'`
 * `width?: string`
@@ -84,6 +102,9 @@ All other props are spread onto the base element.
   * Accepts all themeable sizing properties (`scale100`, etc.).
 * `paddingLeft?: string`
   * Accepts all themeable sizing properties (`scale100`, etc.).
+* `placeContent?: [available values](https://developer.mozilla.org/en-US/docs/Web/CSS/place-content)`
+* `placeItems?: [available values](https://developer.mozilla.org/en-US/docs/Web/CSS/place-items)`
+* `placeSelf?: [available values](https://developer.mozilla.org/en-US/docs/Web/CSS/place-self)`
 * `flexWrap?: boolean`
 * `left?: string`
 * `top?: string`
diff --git a/src/block/block.js b/src/block/block.js
index f6702fb79..01565ce76 100644
--- a/src/block/block.js
+++ b/src/block/block.js
@@ -24,7 +24,25 @@ function Block({
   flexDirection,
   display,
   flex,
+  grid,
+  gridArea,
+  gridAutoColumns,
+  gridAutoFlow,
+  gridAutoRows,
+  gridColumn,
+  gridColumnEnd,
+  gridColumnGap,
+  gridColumnStart,
+  gridGap,
+  gridRow,
+  gridRowStart,
+  gridRowEnd,
+  gridTemplate,
+  gridTemplateAreas,
+  gridTemplateColumns,
+  gridTemplateRows,
   justifyContent,
+  justifyItems,
   justifySelf,
   position,
   width,
@@ -44,6 +62,9 @@ function Block({
   paddingRight,
   paddingBottom,
   paddingLeft,
+  placeContent,
+  placeItems,
+  placeSelf,
   flexWrap,
   left,
   top,
@@ -69,7 +90,25 @@ function Block({
       $flexDirection={flexDirection}
       $display={display}
       $flex={flex}
+      $grid={grid}
+      $gridArea={gridArea}
+      $gridAutoColumns={gridAutoColumns}
+      $gridAutoFlow={gridAutoFlow}
+      $gridAutoRows={gridAutoRows}
+      $gridColumn={gridColumn}
+      $gridColumnEnd={gridColumnEnd}
+      $gridColumnGap={gridColumnGap}
+      $gridColumnStart={gridColumnStart}
+      $gridGap={gridGap}
+      $gridRow={gridRow}
+      $gridRowStart={gridRowStart}
+      $gridRowEnd={gridRowEnd}
+      $gridTemplate={gridTemplate}
+      $gridTemplateAreas={gridTemplateAreas}
+      $gridTemplateColumns={gridTemplateColumns}
+      $gridTemplateRows={gridTemplateRows}
       $justifyContent={justifyContent}
+      $justifyItems={justifyItems}
       $justifySelf={justifySelf}
       $position={position}
       $width={width}
@@ -89,6 +128,9 @@ function Block({
       $paddingRight={paddingRight}
       $paddingBottom={paddingBottom}
       $paddingLeft={paddingLeft}
+      $placeContent={placeContent}
+      $placeItems={placeItems}
+      $placeSelf={placeSelf}
       $flexWrap={flexWrap}
       $left={left}
       $top={top}
diff --git a/src/block/examples-list.js b/src/block/examples-list.js
index 6a257c027..727f3b089 100644
--- a/src/block/examples-list.js
+++ b/src/block/examples-list.js
@@ -12,5 +12,6 @@ module.exports = {
   DEFAULT: 'Themable colors',
   FONTS: 'Themable typography styles',
   FLEXBOX: 'Flexbox layout',
+  GRID: 'Grid layout',
   OVERRIDES: 'Applying styles via overrides',
 };
diff --git a/src/block/examples.js b/src/block/examples.js
index 2c0b6a0e6..6b19d02e5 100644
--- a/src/block/examples.js
+++ b/src/block/examples.js
@@ -10,6 +10,7 @@ LICENSE file in the root directory of this source tree.
 import React from 'react';
 import {Block} from './index.js';
 import examples from './examples-list.js';
+import {styled} from '../styles/index.js';
 
 export default {
   [examples.DEFAULT]: function Story1() {
@@ -90,6 +91,32 @@ export default {
       </Block>
     );
   },
+  [examples.GRID]: function Story4() {
+    const Inner = styled('div', ({$theme}) => ({
+      display: 'flex',
+      alignItems: 'center',
+      justifyContent: 'center',
+      backgroundColor: $theme.colors.mono300,
+      border: `1px solid ${$theme.colors.mono900}`,
+      height: '50px',
+      width: '50px',
+    }));
+    return (
+      <Block
+        display="grid"
+        gridTemplateColumns="repeat(3,1fr)"
+        justifyItems="center"
+        gridGap="scale1000"
+      >
+        <Inner>1</Inner>
+        <Inner>2</Inner>
+        <Inner>3</Inner>
+        <Inner>4</Inner>
+        <Inner>5</Inner>
+        <Inner>6</Inner>
+      </Block>
+    );
+  },
   [examples.OVERRIDES]: function Story5() {
     return (
       <Block
diff --git a/src/block/styled-components.js b/src/block/styled-components.js
index 67d224d4d..21b40e76c 100644
--- a/src/block/styled-components.js
+++ b/src/block/styled-components.js
@@ -23,7 +23,25 @@ export const StyledBlock = styled(
     $flexDirection,
     $display,
     $flex,
+    $grid,
+    $gridArea,
+    $gridAutoColumns,
+    $gridAutoFlow,
+    $gridAutoRows,
+    $gridColumn,
+    $gridColumnEnd,
+    $gridColumnGap,
+    $gridColumnStart,
+    $gridGap,
+    $gridRow,
+    $gridRowStart,
+    $gridRowEnd,
+    $gridTemplate,
+    $gridTemplateAreas,
+    $gridTemplateColumns,
+    $gridTemplateRows,
     $justifyContent,
+    $justifyItems,
     $justifySelf,
     $position,
     $width,
@@ -43,6 +61,9 @@ export const StyledBlock = styled(
     $paddingRight,
     $paddingBottom,
     $paddingLeft,
+    $placeContent,
+    $placeItems,
+    $placeSelf,
     $flexWrap,
     $left,
     $top,
@@ -90,10 +111,82 @@ export const StyledBlock = styled(
       styles.flex = $flex;
     }
 
+    if ($grid !== undefined) {
+      styles.grid = $grid;
+    }
+
+    if ($gridArea !== undefined) {
+      styles.gridArea = $gridArea;
+    }
+
+    if ($gridAutoColumns !== undefined) {
+      styles.gridAutoColumns = $gridAutoColumns;
+    }
+
+    if ($gridAutoFlow !== undefined) {
+      styles.gridAutoFlow = $gridAutoFlow;
+    }
+
+    if ($gridAutoRows !== undefined) {
+      styles.gridAutoRows = $gridAutoRows;
+    }
+
+    if ($gridColumn !== undefined) {
+      styles.gridColumn = $gridColumn;
+    }
+
+    if ($gridColumnEnd !== undefined) {
+      styles.gridColumnEnd = $gridColumnEnd;
+    }
+
+    if ($gridColumnGap !== undefined) {
+      styles.gridColumnGap = sizing[$gridColumnGap] || $gridColumnGap;
+    }
+
+    if ($gridColumnStart !== undefined) {
+      styles.gridColumnStart = $gridColumnStart;
+    }
+
+    if ($gridGap !== undefined) {
+      styles.gridGap = sizing[$gridGap] || $gridGap;
+    }
+
+    if ($gridRow !== undefined) {
+      styles.gridRow = $gridRow;
+    }
+
+    if ($gridRowStart !== undefined) {
+      styles.gridRowStart = $gridRowStart;
+    }
+
+    if ($gridRowEnd !== undefined) {
+      styles.gridRowEnd = $gridRowEnd;
+    }
+
+    if ($gridTemplate !== undefined) {
+      styles.gridTemplate = $gridTemplate;
+    }
+
+    if ($gridTemplateAreas !== undefined) {
+      styles.gridTemplateAreas = $gridTemplateAreas;
+    }
+
+    if ($gridTemplateColumns !== undefined) {
+      styles.gridTemplateColumns = $gridTemplateColumns;
+    }
+
+    if ($gridTemplateRows !== undefined) {
+      styles.gridTemplateRows = $gridTemplateRows;
+    }
+
     if ($justifyContent !== undefined) {
       styles.justifyContent = $justifyContent;
     }
 
+    if ($justifyItems !== undefined) {
+      styles.justifyItems = $justifyItems;
+    }
+
     if ($justifySelf !== undefined) {
       styles.justifySelf = $justifySelf;
     }
@@ -174,6 +267,18 @@ export const StyledBlock = styled(
       styles.paddingLeft = sizing[$paddingLeft] || $paddingLeft;
     }
 
+    if ($placeContent !== undefined) {
+      styles.placeContent = $placeContent;
+    }
+
+    if ($placeItems !== undefined) {
+      styles.placeItems = $placeItems;
+    }
+
+    if ($placeSelf !== undefined) {
+      styles.placeSelf = $placeSelf;
+    }
+
     if ($flexWrap !== undefined) {
       styles.flexWrap = 'wrap';
     }
diff --git a/src/block/types.js b/src/block/types.js
index b6cb70c88..ca363971b 100644
--- a/src/block/types.js
+++ b/src/block/types.js
@@ -126,6 +126,16 @@ type DisplayT =
   | 'initial'
   | 'unset';
 
+type GridAutoFlowT =
+  | 'row'
+  | 'column'
+  | 'dense'
+  | 'row dense'
+  | 'column dense'
+  | 'inherit'
+  | 'initial'
+  | 'unset';
+
 type JustifyContentT =
   | 'center'
   | 'start'
@@ -144,6 +154,32 @@ type JustifyContentT =
   | 'initial'
   | 'unset';
 
+type JustifyItemsT =
+  /* Basic keywords */
+  | 'auto'
+  | 'normal'
+  | 'stretch'
+  | 'center'
+  | 'start'
+  | 'end'
+  | 'flex-start'
+  | 'flex-end'
+  | 'self-start'
+  | 'self-end'
+  | 'left'
+  | 'right'
+  | 'baseline'
+  | 'first baseline'
+  | 'last baseline'
+  | 'safe center'
+  | 'unsafe center'
+  | 'legacy right'
+  | 'legacy left'
+  | 'legacy center'
+  | 'inherit'
+  | 'initial'
+  | 'unset';
+
 type JustifySelfT =
   | 'auto'
   | 'normal'
@@ -191,7 +227,25 @@ export type BlockPropsT = {
   flexDirection?: FlexDirectionT,
   display?: DisplayT,
   flex?: string,
+  grid?: string,
+  gridArea?: string,
+  gridAutoColumns?: string,
+  gridAutoFlow?: GridAutoFlowT,
+  gridAutoRows?: string,
+  gridColumn?: string,
+  gridColumnEnd?: string,
+  gridColumnGap?: string,
+  gridColumnStart?: string,
+  gridGap?: string,
+  gridRow?: string,
+  gridRowStart?: string,
+  gridRowEnd?: string,
+  gridTemplate?: string,
+  gridTemplateAreas?: string,
+  gridTemplateColumns?: string,
+  gridTemplateRows?: string,
   justifyContent?: JustifyContentT,
+  justifyItems?: JustifyItemsT,
   justifySelf?: JustifySelfT,
   position?: PositionT,
   width?: string,
@@ -211,6 +265,9 @@ export type BlockPropsT = {
   paddingRight?: string,
   paddingBottom?: string,
   paddingLeft?: string,
+  placeContent?: string,
+  placeItems?: string,
+  placeSelf?: string,
   flexWrap?: boolean,
   left?: string,
   top?: string,
@@ -229,7 +286,25 @@ export type StyledBlockPropsT = {
   $flexDirection?: FlexDirectionT,
   $display?: DisplayT,
   $flex?: string,
+  $grid?: string,
+  $gridArea?: string,
+  $gridAutoColumns?: string,
+  $gridAutoFlow?: GridAutoFlowT,
+  $gridAutoRows?: string,
+  $gridColumn?: string,
+  $gridColumnEnd?: string,
+  $gridColumnGap?: string,
+  $gridColumnStart?: string,
+  $gridGap?: string,
+  $gridRow?: string,
+  $gridRowStart?: string,
+  $gridRowEnd?: string,
+  $gridTemplate?: string,
+  $gridTemplateAreas?: string,
+  $gridTemplateColumns?: string,
+  $gridTemplateRows?: string,
   $justifyContent?: JustifyContentT,
+  $justifyItems?: JustifyItemsT,
   $justifySelf?: JustifySelfT,
   $position?: PositionT,
   $width?: string,
@@ -249,6 +324,9 @@ export type StyledBlockPropsT = {
   $paddingRight?: string,
   $paddingBottom?: string,
   $paddingLeft?: string,
+  $placeContent?: string,
+  $placeItems?: string,
+  $placeSelf?: string,
   $flexWrap?: boolean,
   $left?: string,
   $top?: string,
diff --git a/src/popover/stateful-container.js b/src/popover/stateful-container.js
index 67c86de1d..150b20f7a 100644
--- a/src/popover/stateful-container.js
+++ b/src/popover/stateful-container.js
@@ -74,12 +74,18 @@ class StatefulContainer extends React.Component<
     this.internalSetState(STATE_CHANGE_TYPE.open, {
       isOpen: true,
     });
+    if (this.props.onOpen) {
+      this.props.onOpen();
+    }
   }
 
   close() {
     this.internalSetState(STATE_CHANGE_TYPE.close, {
       isOpen: false,
     });
+    if (this.props.onClose) {
+      this.props.onClose();
+    }
   }
 
   internalSetState(type: StateChangeTypeT, changes: StateT) {


Test patch:
diff --git a/src/block/__tests__/block.test.js b/src/block/__tests__/block.test.js
index e56e9651e..7d9847d2f 100644
--- a/src/block/__tests__/block.test.js
+++ b/src/block/__tests__/block.test.js
@@ -77,6 +77,134 @@ describe('Block', () => {
     });
   });
 
+  it('renders grid style if provided', () => {
+    expect(
+      retrieveStyles(<Block grid="100px / 200px">test</Block>),
+    ).toMatchObject({
+      grid: '100px / 200px',
+    });
+  });
+
+  it('renders gridArea style if provided', () => {
+    expect(retrieveStyles(<Block gridArea="auto">test</Block>)).toMatchObject({
+      gridArea: 'auto',
+    });
+  });
+
+  it('renders gridAutoColumns style if provided', () => {
+    expect(
+      retrieveStyles(<Block gridAutoColumns="min-content">test</Block>),
+    ).toMatchObject({
+      gridAutoColumns: 'min-content',
+    });
+  });
+
+  it('renders gridAutoFlow style if provided', () => {
+    expect(
+      retrieveStyles(<Block gridAutoFlow="row dense">test</Block>),
+    ).toMatchObject({
+      gridAutoFlow: 'row dense',
+    });
+  });
+
+  it('renders gridAutoRows style if provided', () => {
+    expect(
+      retrieveStyles(<Block gridAutoRows="20cm">test</Block>),
+    ).toMatchObject({
+      gridAutoRows: '20cm',
+    });
+  });
+
+  it('renders gridColumn style if provided', () => {
+    expect(
+      retrieveStyles(<Block gridColumn="1 / 3">test</Block>),
+    ).toMatchObject({
+      gridColumn: '1 / 3',
+    });
+  });
+
+  it('renders gridColumnEnd style if provided', () => {
+    expect(
+      retrieveStyles(<Block gridColumnEnd="span 3">test</Block>),
+    ).toMatchObject({
+      gridColumnEnd: 'span 3',
+    });
+  });
+
+  it('renders gridColumnGap style if provided', () => {
+    expect(
+      retrieveStyles(<Block gridColumnGap="scale800">test</Block>),
+    ).toMatchObject({
+      gridColumnGap: '$theme.sizing.scale800',
+    });
+  });
+
+  it('renders gridColumnStart style if provided', () => {
+    expect(
+      retrieveStyles(<Block gridColumnStart="span 2">test</Block>),
+    ).toMatchObject({
+      gridColumnStart: 'span 2',
+    });
+  });
+
+  it('renders gridGap style if provided', () => {
+    expect(retrieveStyles(<Block gridGap="50px">test</Block>)).toMatchObject({
+      gridGap: '50px',
+    });
+  });
+
+  it('renders gridRow style if provided', () => {
+    expect(retrieveStyles(<Block gridRow="1">test</Block>)).toMatchObject({
+      gridRow: '1',
+    });
+  });
+
+  it('renders gridRowStart style if provided', () => {
+    expect(retrieveStyles(<Block gridRowStart="-1">test</Block>)).toMatchObject(
+      {
+        gridRowStart: '-1',
+      },
+    );
+  });
+
+  it('renders gridRowEnd style if provided', () => {
+    expect(retrieveStyles(<Block gridRowEnd="2">test</Block>)).toMatchObject({
+      gridRowEnd: '2',
+    });
+  });
+
+  it('renders gridTemplate style if provided', () => {
+    expect(
+      retrieveStyles(<Block gridTemplate="1fr / 1fr">test</Block>),
+    ).toMatchObject({
+      gridTemplate: '1fr / 1fr',
+    });
+  });
+
+  it('renders gridTemplateAreas style if provided', () => {
+    expect(
+      retrieveStyles(<Block gridTemplateAreas="inherit">test</Block>),
+    ).toMatchObject({
+      gridTemplateAreas: 'inherit',
+    });
+  });
+
+  it('renders gridTemplateColumns style if provided', () => {
+    expect(
+      retrieveStyles(<Block gridTemplateColumns="1fr 60px">test</Block>),
+    ).toMatchObject({
+      gridTemplateColumns: '1fr 60px',
+    });
+  });
+
+  it('renders gridTemplateRows style if provided', () => {
+    expect(
+      retrieveStyles(<Block gridTemplateRows="auto">test</Block>),
+    ).toMatchObject({
+      gridTemplateRows: 'auto',
+    });
+  });
+
   it('renders justifyContent style if provided', () => {
     expect(
       retrieveStyles(<Block justifyContent="start">test</Block>),
@@ -85,6 +213,14 @@ describe('Block', () => {
     });
   });
 
+  it('renders justifyItems style if provided', () => {
+    expect(
+      retrieveStyles(<Block justifyItems="center">test</Block>),
+    ).toMatchObject({
+      justifyItems: 'center',
+    });
+  });
+
   it('renders justifySelf style if provided', () => {
     expect(
       retrieveStyles(<Block justifySelf="start">test</Block>),
@@ -255,6 +391,30 @@ describe('Block', () => {
     });
   });
 
+  it('renders placeContent style if provided', () => {
+    expect(
+      retrieveStyles(<Block placeContent="end center">test</Block>),
+    ).toMatchObject({
+      placeContent: 'end center',
+    });
+  });
+
+  it('renders placeItems style if provided', () => {
+    expect(
+      retrieveStyles(<Block placeItems="end center">test</Block>),
+    ).toMatchObject({
+      placeItems: 'end center',
+    });
+  });
+
+  it('renders placeSelf style if provided', () => {
+    expect(
+      retrieveStyles(<Block placeSelf="center start">test</Block>),
+    ).toMatchObject({
+      placeSelf: 'center start',
+    });
+  });
+
   it('renders flexWrap style if provided', () => {
     expect(retrieveStyles(<Block flexWrap>test</Block>)).toMatchObject({
       flexWrap: 'wrap',
diff --git a/src/popover/__tests__/stateful-container.test.js b/src/popover/__tests__/stateful-container.test.js
index 333740021..0631efb03 100644
--- a/src/popover/__tests__/stateful-container.test.js
+++ b/src/popover/__tests__/stateful-container.test.js
@@ -233,6 +233,29 @@ describe('StatefulPopoverContainer', () => {
     expect(component).toHaveState('isOpen', true);
   });
 
+  test('onOpen/onClose callbacks', () => {
+    const props = {
+      onOpen: jest.fn(),
+      onClose: jest.fn(),
+    };
+    const children = jest.fn();
+
+    const component = shallow(
+      <StatefulContainer {...props}>{children}</StatefulContainer>,
+    );
+
+    component.instance().onMouseEnter();
+    expect(props.onOpen).toHaveBeenCalledTimes(1);
+    expect(props.onClose).toHaveBeenCalledTimes(0);
+
+    props.onOpen.mockClear();
+    props.onClose.mockClear();
+
+    component.instance().onMouseLeave();
+    expect(props.onOpen).toHaveBeenCalledTimes(0);
+    expect(props.onClose).toHaveBeenCalledTimes(1);
+  });
+
   test('null stateReducer', () => {
     const props = {
       content: jest.fn(),