grommet/grommet

View on GitHub
src/js/utils/styles.js

Summary

Maintainability
F
2 wks
Test Coverage
import { css } from 'styled-components';
import { backgroundStyle } from './background';
import { normalizeColor } from './colors';
import { getBreakpointStyle } from './responsive';
import { breakpointStyle, parseMetricToNum } from './mixins';

export const baseStyle = css`
  font-family: ${(props) => props.theme.global.font.family};
  font-size: ${(props) => props.theme.global.font.size};
  line-height: ${(props) => props.theme.global.font.height};
  font-weight: ${(props) => props.theme.global.font.weight};
  /* check if prop is defined in the theme*/
  ${(props) =>
    props.theme.global.font.variant &&
    `
    font-variant:${props.theme.global.font.variant};
  `}
  ${(props) =>
    !props.plain && backgroundStyle(props.theme.baseBackground, props.theme)}
  box-sizing: border-box;
  -webkit-text-size-adjust: 100%;
  -ms-text-size-adjust: 100%;
  -moz-osx-font-smoothing: grayscale;
  -webkit-font-smoothing: antialiased;
`;

export const controlBorderStyle = css`
  border: ${(props) => props.theme.global.control.border.width} solid
    ${(props) =>
      normalizeColor(
        props.theme.global.control.border.color || 'border',
        props.theme,
      )};
  border-radius: ${(props) => props.theme.global.control.border.radius};
`;

export const edgeStyle = (
  kind,
  data,
  responsive,
  responsiveBreakpoint,
  theme,
) => {
  const breakpoint =
    responsiveBreakpoint && theme.global.breakpoints[responsiveBreakpoint];

  if (typeof data === 'string') {
    return css`
      ${kind}: ${theme.global.edgeSize[data] || data};
      ${responsive && breakpoint
        ? breakpointStyle(
            breakpoint,
            `
        ${kind}: ${breakpoint.edgeSize[data] || data};
      `,
          )
        : ''};
    `;
  }
  const result = [];

  const { horizontal, vertical, top, bottom, left, right } = data;

  // if horizontal and vertical are equal OR all sides are equal,
  // we can just return a single css value such as padding: 12px
  // instead of breaking out by sides.
  const horizontalVerticalEqual =
    horizontal && vertical && horizontal === vertical;
  const allSidesEqual =
    top && bottom && left && right && ((top === bottom) === left) === right;

  if (horizontalVerticalEqual || allSidesEqual) {
    // since the values will be the same between vertical & horizontal OR
    // left, right, top, & bottom, we can just choose one
    const value = horizontalVerticalEqual ? horizontal : top;
    return css`
      ${kind}: ${theme.global.edgeSize[value] || value};
      ${responsive && breakpoint
        ? breakpointStyle(
            breakpoint,
            `
        ${kind}: ${breakpoint.edgeSize[value] || value};
      `,
          )
        : ''};
    `;
  }

  if (horizontal) {
    result.push(css`
      ${kind}-left: ${theme.global.edgeSize[horizontal] || horizontal};
      ${kind}-right: ${theme.global.edgeSize[horizontal] || horizontal};
      ${responsive && breakpoint
        ? breakpointStyle(
            breakpoint,
            `
          ${kind}-left: ${breakpoint.edgeSize[horizontal] || horizontal};
          ${kind}-right: ${breakpoint.edgeSize[horizontal] || horizontal};
        `,
          )
        : ''};
    `);
  }
  if (vertical) {
    result.push(css`
      ${kind}-top: ${theme.global.edgeSize[vertical] || vertical};
      ${kind}-bottom: ${theme.global.edgeSize[vertical] || vertical};
      ${responsive && breakpoint
        ? breakpointStyle(
            breakpoint,
            `
          ${kind}-top: ${breakpoint.edgeSize[vertical] || vertical};
          ${kind}-bottom: ${breakpoint.edgeSize[vertical] || vertical};
        `,
          )
        : ''};
    `);
  }
  if (top) {
    result.push(css`
      ${kind}-top: ${theme.global.edgeSize[top] || top};
      ${responsive && breakpoint
        ? breakpointStyle(
            breakpoint,
            `
          ${kind}-top: ${breakpoint.edgeSize[top] || top};
        `,
          )
        : ''};
    `);
  }
  if (bottom) {
    result.push(css`
      ${kind}-bottom: ${theme.global.edgeSize[bottom] || bottom};
      ${responsive && breakpoint
        ? breakpointStyle(
            breakpoint,
            `
          ${kind}-bottom: ${breakpoint.edgeSize[bottom] || bottom};
        `,
          )
        : ''};
    `);
  }
  if (left) {
    result.push(css`
      ${kind}-left: ${theme.global.edgeSize[left] || left};
      ${responsive && breakpoint
        ? breakpointStyle(
            breakpoint,
            `
          ${kind}-left: ${breakpoint.edgeSize[left] || left};
        `,
          )
        : ''};
    `);
  }
  if (right) {
    result.push(css`
      ${kind}-right: ${theme.global.edgeSize[right] || right};
      ${responsive && breakpoint
        ? breakpointStyle(
            breakpoint,
            `
          ${kind}-right: ${breakpoint.edgeSize[right] || right};
        `,
          )
        : ''};
    `);
  }
  if (data.start) {
    result.push(css`
      ${kind}-inline-start: ${theme.global.edgeSize[data.start] || data.start};
      ${responsive && breakpoint
        ? breakpointStyle(
            breakpoint,
            `
          ${kind}-inline-start: ${
              breakpoint.edgeSize[data.start] || data.start
            };
        `,
          )
        : ''};
    `);
  }
  if (data.end) {
    result.push(css`
      ${kind}-inline-end: ${theme.global.edgeSize[data.end] || data.end};
      ${responsive && breakpoint
        ? breakpointStyle(
            breakpoint,
            `
          ${kind}-inline-end: ${breakpoint.edgeSize[data.end] || data.end};
        `,
          )
        : ''};
    `);
  }

  return result;
};

export const fillStyle = (fillProp) => {
  if (fillProp === 'horizontal') {
    return 'width: 100%;';
  }
  if (fillProp === 'vertical') {
    return 'height: 100%;';
  }
  if (fillProp) {
    return `
      width: 100%;
      height: 100%;
    `;
  }
  return undefined;
};

const focusStyles = (props, { forceOutline, justBorder } = {}) => {
  const {
    theme: {
      global: { focus },
    },
  } = props;
  if (!focus || (forceOutline && !focus.outline)) {
    const color = normalizeColor('focus', props.theme);
    if (color) return `outline: 2px solid ${color};`;
    return ''; // native
  }
  if (focus.outline && (!focus.border || !justBorder)) {
    if (typeof focus.outline === 'object') {
      const color = normalizeColor(focus.outline.color || 'focus', props.theme);
      const size = focus.outline.size || '2px';
      return `
        outline-offset: 0px;
        outline: ${size} solid ${color};
      `;
    }
    return `outline: ${focus.outline};`;
  }
  if (focus.shadow && (!focus.border || !justBorder)) {
    if (typeof focus.shadow === 'object') {
      const color = normalizeColor(
        // If there is a focus.border.color, use that for shadow too.
        // This is for backwards compatibility in v2.
        (focus.border && focus.border.color) || focus.shadow.color || 'focus',
        props.theme,
      );
      const size = focus.shadow.size || '2px'; // backwards compatible default
      return `
        outline: none;
        box-shadow: 0 0 ${size} ${size} ${color};
      `;
    }
    return `
      outline: none;
      box-shadow: ${focus.shadow};
    `;
  }
  if (focus.border) {
    const color = normalizeColor(focus.border.color || 'focus', props.theme);
    return `
      outline: none;
      border-color: ${color};
    `;
  }
  return ''; // defensive
};

const unfocusStyles = (props, { forceOutline, justBorder } = {}) => {
  const {
    theme: {
      global: { focus },
    },
  } = props;
  if (!focus || (forceOutline && !focus.outline)) {
    const color = normalizeColor('focus', props.theme);
    if (color) return `outline: none;`;
    return ''; // native
  }
  if (focus.outline && (!focus.border || !justBorder)) {
    if (typeof focus.outline === 'object') {
      return `
        outline-offset: 0px;
        outline: none;
      `;
    }
    return `outline: none;`;
  }
  if (focus.shadow && (!focus.border || !justBorder)) {
    if (typeof focus.shadow === 'object') {
      return `
        outline: none;
        box-shadow: none;
      `;
    }
    return `
      outline: none;
      box-shadow: none;
    `;
  }
  if (focus.border) {
    return `
      outline: none;
      border-color: none;
    `;
  }
  return ''; // defensive
};

// focus also supports clickable elements inside svg
export const focusStyle = ({
  forceOutline,
  justBorder,
  skipSvgChildren,
} = {}) => css`
  ${(props) =>
    !skipSvgChildren &&
    `
  > circle,
  > ellipse,
  > line,
  > path,
  > polygon,
  > polyline,
  > rect {
    ${focusStyles(props)}
  }`}
  ${(props) => focusStyles(props, { forceOutline, justBorder })}
  ${!forceOutline &&
  `
  ::-moz-focus-inner {
    border: 0;
  }
  `}
`;

// This is placed next to focusStyle for easy maintainability
// of code since changes to focusStyle should be reflected in
// unfocusStyle as well.
// this function can be used to reset focus styles which is
// applicable when turning the focus ring off when using the mouse
// see https://nelo.is/writing/styling-better-focus-states/
export const unfocusStyle = ({
  forceOutline,
  justBorder,
  skipSvgChildren,
} = {}) => css`
  ${(props) =>
    !skipSvgChildren &&
    `
  > circle,
  > ellipse,
  > line,
  > path,
  > polygon,
  > polyline,
  > rect {
    ${unfocusStyles(props)}
  }`}
  ${(props) => unfocusStyles(props, { forceOutline, justBorder })}
  ${!forceOutline &&
  `
  ::-moz-focus-inner {
    border: 0;
  }
  `}
`;

// For backwards compatibility we need to add back the control border width.
// Based on how grommet was functioning prior to https://github.com/grommet/grommet/pull/3939,
// the padding was subtracting the border width from the theme value, but the
// placeholder was not. Because we're now placing the subtraction into the
// theme itself, we have to add back in the border width here.
// This is used for placeholder/icon in TextInput and MaskedInput.
const adjustPad = (props, value) =>
  `${
    parseMetricToNum(`${props.theme.global.edgeSize[value] || value}px`) +
    parseMetricToNum(`${props.theme.global.control.border.width}px`)
  }px`;

export const getInputPadBySide = (props, side) => {
  if (typeof props.theme.global.input.padding !== 'object') {
    const adjustedPad = adjustPad(props, props.theme.global.input.padding);
    return adjustedPad;
  }

  let orientation;
  if (side === 'left' || side === 'right') orientation = 'horizontal';
  else if (side === 'top' || side === 'bottom') orientation = 'vertical';
  else orientation = undefined;

  // if individual side isn't available, fallback to the
  // orientation if possible
  const pad =
    props.theme.global.input.padding[side] ||
    props.theme.global.input.padding[orientation];

  const adjustedPad = adjustPad(props, pad);

  return adjustedPad;
};

const placeholderColor = css`
  color: ${(props) =>
    normalizeColor(props.theme.global.colors.placeholder, props.theme)};
`;

const placeholderStyle = css`
  &::-webkit-input-placeholder {
    ${placeholderColor};
  }

  &::-moz-placeholder {
    ${placeholderColor};
  }

  &:-ms-input-placeholder {
    ${placeholderColor};
  }
`;

const inputSizeStyle = (props) => {
  const data = props.theme.text[props.size];

  if (!data) {
    return css`
      font-size: ${props.size};
    `;
  }

  return css`
    font-size: ${data.size};
    line-height: ${data.height};
  `;
};

export const inputStyle = css`
  box-sizing: border-box;
  ${(props) =>
    `font-size: ${
      props.theme.global.input.font.size
        ? props.theme.text[props.theme.global.input.font.size]?.size ||
          props.theme.global.input.font.size
        : 'inherit'
    };`}
  font-family: inherit;
  border: none;
  -webkit-appearance: none;
  background: transparent;
  color: inherit;
  width: 100%;
  ${(props) =>
    props.theme.global.input.font.height &&
    `line-height: ${props.theme.global.input.font.height};`}
  ${(props) =>
    props.theme.global.input.padding &&
    typeof props.theme.global.input.padding !== 'object'
      ? // On a breaking change release, this condition could be removed and
        // just the edgeStyle could remain. Currently, this is needed for
        // backwards compatibility since we are placing the calculation in
        // base.js
        `padding: ${
          parseMetricToNum(
            props.theme.global.edgeSize[props.theme.global.input.padding] ||
              props.theme.global.input.padding,
          ) - parseMetricToNum(props.theme.global.control.border.width)
        }px;`
      : edgeStyle(
          'padding',
          props.theme.global.input.padding,
          props.responsive,
          props.theme.box.responsiveBreakpoint,
          props.theme,
        )}
  ${(props) =>
    // for backwards compatibility, check if props.theme.global.input.weight
    (props.theme.global.input.weight || props.theme.global.input.font.weight) &&
    css`
      font-weight: ${props.theme.global.input.weight ||
      props.theme.global.input.font.weight};
    `} margin: 0;
  ${(props) => props.size && inputSizeStyle(props)}
  &:focus {
    ${(props) => (!props.plain || props.focusIndicator) && focusStyle()};
  }
  ${controlBorderStyle}
  ${placeholderStyle}

  ::-webkit-search-decoration {
    -webkit-appearance: none;
  }

  &::-moz-focus-inner {
    border: none;
    outline: none;
  }

  &:-moz-placeholder, // FF 18-
  &::-moz-placeholder {
    // FF 19+
    opacity: 1;
  }

  ${(props) => props.theme.global.input.extend}
`;

// Apply padding on input to create space for icon.
// When theme.icon.matchSize is true, the space for the
// icon should equal the icon dimension + 12px (edgeSize.medium)
// to ensure there is reasonable space between the icon and value or placeholder
export const inputPadForIcon = css`
  ${(props) => {
    const pad = props.theme?.icon?.matchSize
      ? `${
          parseMetricToNum(props.theme.icon?.size?.[props?.size || 'medium']) +
          parseMetricToNum(props.theme.global.edgeSize.medium)
        }px`
      : props.theme.global.edgeSize.large;

    return props.reverse ? `padding-right: ${pad};` : `padding-left: ${pad};`;
  }}
`;

export const overflowStyle = (overflowProp) => {
  if (typeof overflowProp === 'string') {
    return css`
      overflow: ${overflowProp};
    `;
  }

  return css`
    ${overflowProp.horizontal &&
    `overflow-x: ${overflowProp.horizontal};`} ${overflowProp.vertical &&
    `overflow-y: ${overflowProp.vertical};`};
  `;
};

const ALIGN_SELF_MAP = {
  center: 'center',
  end: 'flex-end',
  start: 'flex-start',
  stretch: 'stretch',
  baseline: 'baseline',
};

export const genericStyles = css`
  ${(props) =>
    props.alignSelf && `align-self: ${ALIGN_SELF_MAP[props.alignSelf]};`}
  ${(props) => props.gridArea && `grid-area: ${props.gridArea};`}
  ${(props) =>
    props.margin &&
    props.theme.global &&
    edgeStyle(
      'margin',
      props.margin,
      props.responsive,
      props.theme.global.edgeSize.responsiveBreakpoint,
      props.theme,
    )}
`;

export const disabledStyle = (componentStyle) => css`
  opacity: ${(props) =>
    componentStyle || props.theme.global.control.disabled.opacity};
  cursor: default;
`;

export const sizeStyle = (name, value, theme) => css`
  ${name}: ${theme.global.size[value] || value};
`;

export const plainInputStyle = css`
  outline: none;
  border: none;
`;

// CSS for this sub-object in the theme
export const kindPartStyles = (obj, theme, colorValue) => {
  const styles = [];
  if (obj.padding || obj.pad) {
    // button uses `padding` but other components use Grommet `pad`
    const pad = obj.padding || obj.pad;
    if (pad.vertical || pad.horizontal)
      styles.push(
        `padding: ${theme.global.edgeSize[pad.vertical] || pad.vertical || 0} ${
          theme.global.edgeSize[pad.horizontal] || pad.horizontal || 0
        };`,
      );
    else styles.push(`padding: ${theme.global.edgeSize[pad] || pad || 0};`);
  }
  if (obj.background)
    styles.push(
      backgroundStyle(
        colorValue || obj.background,
        theme,
        obj.color ||
          (Object.prototype.hasOwnProperty.call(obj, 'color') &&
          obj.color === undefined
            ? false
            : undefined),
      ),
    );
  else if (obj.color)
    styles.push(`color: ${normalizeColor(obj.color, theme)};`);
  if (obj.border) {
    if (obj.border.width)
      styles.push(css`
        border-style: solid;
        border-width: ${obj.border.width};
      `);
    if (obj.border.color)
      styles.push(css`
        border-color: ${normalizeColor(
          (!obj.background && colorValue) || obj.border.color || 'border',
          theme,
        )};
      `);
    if (obj.border.radius)
      styles.push(css`
        border-radius: ${obj.border.radius};
      `);
  } else if (obj.border === false) styles.push('border: none;');
  if (colorValue && !obj.border && !obj.background)
    styles.push(`color: ${normalizeColor(colorValue, theme)};`);
  if (obj.font) {
    if (obj.font.size) {
      styles.push(
        `font-size: ${theme.text[obj.font.size].size || obj.font.size};`,
      );
    }
    if (obj.font.height) {
      styles.push(`line-height: ${obj.font.height};`);
    }
    if (obj.font.weight) {
      styles.push(`font-weight: ${obj.font.weight};`);
    }
  }
  if (obj.opacity) {
    const opacity =
      obj.opacity === true
        ? theme.global.opacity.medium
        : theme.global.opacity[obj.opacity] || obj.opacity;
    styles.push(`opacity: ${opacity};`);
  }
  if (obj.extend) styles.push(obj.extend);
  return styles;
};

const ROUND_MAP = {
  full: '100%',
};

export const roundStyle = (data, responsive, theme) => {
  const breakpoint = getBreakpointStyle(theme, theme.box.responsiveBreakpoint);
  const styles = [];
  if (typeof data === 'object') {
    const size =
      ROUND_MAP[data.size] ||
      theme.global.edgeSize[data.size || 'medium'] ||
      data.size;
    const responsiveSize =
      responsive &&
      breakpoint &&
      breakpoint.edgeSize[data.size] &&
      (breakpoint.edgeSize[data.size] || data.size);
    if (data.corner === 'top') {
      styles.push(css`
        border-top-left-radius: ${size};
        border-top-right-radius: ${size};
      `);
      if (responsiveSize) {
        styles.push(
          breakpointStyle(
            breakpoint,
            `
          border-top-left-radius: ${responsiveSize};
          border-top-right-radius: ${responsiveSize};
        `,
          ),
        );
      }
    } else if (data.corner === 'bottom') {
      styles.push(css`
        border-bottom-left-radius: ${size};
        border-bottom-right-radius: ${size};
      `);
      if (responsiveSize) {
        styles.push(
          breakpointStyle(
            breakpoint,
            `
          border-bottom-left-radius: ${responsiveSize};
          border-bottom-right-radius: ${responsiveSize};
        `,
          ),
        );
      }
    } else if (data.corner === 'left') {
      styles.push(css`
        border-top-left-radius: ${size};
        border-bottom-left-radius: ${size};
      `);
      if (responsiveSize) {
        styles.push(
          breakpointStyle(
            breakpoint,
            `
          border-top-left-radius: ${responsiveSize};
          border-bottom-left-radius: ${responsiveSize};
        `,
          ),
        );
      }
    } else if (data.corner === 'right') {
      styles.push(css`
        border-top-right-radius: ${size};
        border-bottom-right-radius: ${size};
      `);
      if (responsiveSize) {
        styles.push(
          breakpointStyle(
            breakpoint,
            `
          border-top-right-radius: ${responsiveSize};
          border-bottom-right-radius: ${responsiveSize};
        `,
          ),
        );
      }
    } else if (data.corner) {
      styles.push(css`
        border-${data.corner}-radius: ${size};
      `);
      if (responsiveSize) {
        styles.push(
          breakpointStyle(
            breakpoint,
            `
          border-${data.corner}-radius: ${responsiveSize};
        `,
          ),
        );
      }
    } else {
      styles.push(css`
        border-radius: ${size};
      `);
      if (responsiveSize) {
        styles.push(
          breakpointStyle(
            breakpoint,
            `
          border-radius: ${responsiveSize};
        `,
          ),
        );
      }
    }
  } else {
    const size = data === true ? 'medium' : data;
    styles.push(css`
      border-radius: ${ROUND_MAP[size] || theme.global.edgeSize[size] || size};
    `);
    const responsiveSize =
      responsive && breakpoint && breakpoint.edgeSize[size];
    if (responsiveSize) {
      styles.push(
        breakpointStyle(
          breakpoint,
          `
        border-radius: ${responsiveSize};
      `,
        ),
      );
    }
  }
  return styles;
};

const TEXT_ALIGN_MAP = {
  center: 'center',
  end: 'right',
  justify: 'justify',
  start: 'left',
};

export const textAlignStyle = css`
  text-align: ${(props) => TEXT_ALIGN_MAP[props.textAlign]};
`;

const ALIGN_ITEMS_MAP = {
  baseline: 'baseline',
  center: 'center',
  end: 'flex-end',
  start: 'flex-start',
  stretch: 'stretch',
};

export const alignStyle = css`
  align-items: ${(props) => ALIGN_ITEMS_MAP[props.align] ?? props.align};
`;

const ALIGN_CONTENT_MAP = {
  around: 'space-around',
  baseline: 'baseline',
  between: 'space-between',
  center: 'center',
  evenly: 'space-evenly',
  end: 'flex-end',
  start: 'flex-start',
  stretch: 'stretch',
};

export const alignContentStyle = css`
  align-content: ${(props) =>
    ALIGN_CONTENT_MAP[props.alignContent] ?? props.alignContent};
`;
const getSize = (theme, size) => theme.global.size[size] || size;

const widthObjectStyle = (width, theme) => {
  const result = [];
  if (width.max)
    result.push(
      css`
        max-width: ${getSize(theme, width.max)};
      `,
    );
  if (width.min)
    result.push(
      css`
        min-width: ${getSize(theme, width.min)};
      `,
    );
  if (width.width)
    result.push(
      css`
        width: ${getSize(theme, width.width)};
      `,
    );
  return result;
};

const widthStringStyle = (width, theme) =>
  css`
    width: ${getSize(theme, width)};
  `;

export const widthStyle = (width, theme) =>
  typeof width === 'object'
    ? widthObjectStyle(width, theme)
    : widthStringStyle(width, theme);

const heightObjectStyle = (height, theme) => {
  const result = [];
  if (height.max)
    result.push(
      css`
        max-height: ${getSize(theme, height.max)};
      `,
    );
  if (height.min)
    result.push(
      css`
        min-height: ${getSize(theme, height.min)};
      `,
    );
  // backwards compatibile
  if (height.width)
    result.push(
      css`
        height: ${getSize(theme, height.height)};
      `,
    );
  if (height.height)
    result.push(
      css`
        height: ${getSize(theme, height.height)};
      `,
    );
  return result;
};

const heightStringStyle = (height, theme) =>
  css`
    height: ${getSize(theme, height)};
  `;

export const heightStyle = (height, theme) =>
  typeof height === 'object'
    ? heightObjectStyle(height, theme)
    : heightStringStyle(height, theme);