diff --git a/polaris-react/src/components/Bleed/Bleed.scss b/polaris-react/src/components/Bleed/Bleed.scss new file mode 100644 index 00000000000..d46b8317b6b --- /dev/null +++ b/polaris-react/src/components/Bleed/Bleed.scss @@ -0,0 +1,10 @@ +@import '../../styles/common'; + +.Bleed { + /* stylelint-disable declaration-block-no-redundant-longhand-properties */ + margin-bottom: calc(-1 * var(--pc-bleed-margin-bottom)); + margin-left: calc(-1 * var(--pc-bleed-margin-left)); + margin-right: calc(-1 * var(--pc-bleed-margin-right)); + margin-top: calc(-1 * var(--pc-bleed-margin-top)); + /* stylelint-enable declaration-block-no-redundant-longhand-properties */ +} diff --git a/polaris-react/src/components/Bleed/Bleed.stories.tsx b/polaris-react/src/components/Bleed/Bleed.stories.tsx new file mode 100644 index 00000000000..a01edd02630 --- /dev/null +++ b/polaris-react/src/components/Bleed/Bleed.stories.tsx @@ -0,0 +1,64 @@ +import React from 'react'; +import type {ComponentMeta} from '@storybook/react'; +import {Bleed, Box} from '@shopify/polaris'; + +export default { + component: Bleed, +} as ComponentMeta; + +const styles = { + background: 'var(--p-surface-neutral-subdued-dark)', + borderRadius: 'var(--p-border-radius-05)', + padding: 'var(--p-space-4)', + height: 'var(--p-space-12)', +}; + +export function Default() { + return ( + + +
+ + + ); +} + +export function WithVerticalDirection() { + return ( + + +
+ + + ); +} + +export function WithHorizontalDirection() { + return ( + + +
+ + + ); +} + +export function WithSpecificDirection() { + return ( + + +
+ + + ); +} + +export function WithAllDirection() { + return ( + + +
+ + + ); +} diff --git a/polaris-react/src/components/Bleed/Bleed.tsx b/polaris-react/src/components/Bleed/Bleed.tsx new file mode 100644 index 00000000000..4745cca36d4 --- /dev/null +++ b/polaris-react/src/components/Bleed/Bleed.tsx @@ -0,0 +1,95 @@ +import React from 'react'; +import type {spacing} from '@shopify/polaris-tokens'; + +import {sanitizeCustomProperties} from '../../utilities/css'; + +import styles from './Bleed.scss'; + +type SpacingTokenName = keyof typeof spacing; + +// TODO: Bring this logic into tokens +type SpacingTokenScale = SpacingTokenName extends `space-${infer Scale}` + ? Scale + : never; + +interface Spacing { + bottom: SpacingTokenScale; + left: SpacingTokenScale; + right: SpacingTokenScale; + top: SpacingTokenScale; +} + +export interface BleedProps { + /** Elements to display inside tile */ + children: React.ReactNode; + spacing?: SpacingTokenScale; + horizontal?: SpacingTokenScale; + vertical?: SpacingTokenScale; + top?: SpacingTokenScale; + bottom?: SpacingTokenScale; + left?: SpacingTokenScale; + right?: SpacingTokenScale; +} + +export const Bleed = ({ + spacing, + horizontal, + vertical, + top, + bottom, + left, + right, + children, +}: BleedProps) => { + const getNegativeMargins = (direction: string) => { + const xAxis = ['left', 'right']; + const yAxis = ['top', 'bottom']; + + const directionValues: {[key: string]: string | undefined} = { + top, + bottom, + left, + right, + horizontal, + vertical, + }; + + if (directionValues[direction]) { + return directionValues[direction]; + } else if (!yAxis.includes(direction) && horizontal) { + return directionValues.horizontal; + } else if (!xAxis.includes(direction) && vertical) { + return directionValues.vertical; + } else { + return spacing; + } + }; + + const negativeMargins = { + top: getNegativeMargins('top'), + left: getNegativeMargins('left'), + right: getNegativeMargins('right'), + bottom: getNegativeMargins('bottom'), + } as Spacing; + + const style = { + ...(negativeMargins.bottom + ? {'--pc-bleed-margin-bottom': `var(--p-space-${negativeMargins.bottom})`} + : undefined), + ...(negativeMargins.left + ? {'--pc-bleed-margin-left': `var(--p-space-${negativeMargins.left})`} + : undefined), + ...(negativeMargins.right + ? {'--pc-bleed-margin-right': `var(--p-space-${negativeMargins.right})`} + : undefined), + ...(negativeMargins.top + ? {'--pc-bleed-margin-top': `var(--p-space-${negativeMargins.top})`} + : undefined), + } as React.CSSProperties; + + return ( +
+ {children} +
+ ); +}; diff --git a/polaris-react/src/components/Bleed/index.ts b/polaris-react/src/components/Bleed/index.ts new file mode 100644 index 00000000000..58ccfa0647a --- /dev/null +++ b/polaris-react/src/components/Bleed/index.ts @@ -0,0 +1 @@ +export * from './Bleed'; diff --git a/polaris-react/src/components/Bleed/tests/Bleed.test.tsx b/polaris-react/src/components/Bleed/tests/Bleed.test.tsx new file mode 100644 index 00000000000..da2c480b410 --- /dev/null +++ b/polaris-react/src/components/Bleed/tests/Bleed.test.tsx @@ -0,0 +1,59 @@ +import React from 'react'; +import {mountWithApp} from 'tests/utilities'; + +import {Bleed} from '../Bleed'; + +const Children = () =>

This is a tile

; + +describe('', () => { + it('renders children', () => { + const bleed = mountWithApp( + + + , + ); + + expect(bleed).toContainReactComponent(Children); + }); + + it('does not render custom properties by default', () => { + const bleed = mountWithApp( + + + , + ); + + expect(bleed).toContainReactComponent('div', {style: undefined}); + }); + + it('only renders the custom property that matches the property passed in', () => { + const bleed = mountWithApp( + + + , + ); + + expect(bleed).toContainReactComponent('div', { + style: { + '--pc-bleed-margin-left': 'var(--p-space-2)', + } as React.CSSProperties, + }); + }); + + it('renders custom properties combined with any overrides if they are passed in', () => { + const bleed = mountWithApp( + + + , + ); + + expect(bleed).toContainReactComponent('div', { + style: { + '--pc-bleed-margin-bottom': 'var(--p-space-1)', + '--pc-bleed-margin-left': 'var(--p-space-2)', + '--pc-bleed-margin-right': 'var(--p-space-3)', + '--pc-bleed-margin-top': 'var(--p-space-1)', + } as React.CSSProperties, + }); + }); +}); diff --git a/polaris-react/src/index.ts b/polaris-react/src/index.ts index 9e4ca0483ec..2004514e998 100644 --- a/polaris-react/src/index.ts +++ b/polaris-react/src/index.ts @@ -72,6 +72,9 @@ export type { BannerHandles, } from './components/Banner'; +export {Bleed} from './components/Bleed'; +export type {BleedProps} from './components/Bleed'; + export {Box} from './components/Box'; export type {BoxProps} from './components/Box';