All files / packages/design-system/ui/shared/helpers index.jsx

31.57% Statements 24/76
36.36% Branches 12/33
23.8% Functions 5/21
30.43% Lines 21/69

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305                                60x 60x 60x   111x     2317x 2317x   2317x   2317x 2317x   2323x           126x     833x                         830x     124x           2x               126x       10x       60x                                                                                                                                                                                                                                       60x                                                 4x                 4x                                                                                                                                                        
// Copyright (c) 2015-present, salesforce.com, inc. All rights reserved
// Licensed under BSD 3-Clause - see LICENSE.txt or git.io/sfdc-license
 
import React from 'react';
import range from 'lodash.range';
import startCase from 'lodash.startcase';
import times from 'lodash.times';
import upperFirst from 'lodash.upperfirst';
import IsDependentOn from './prop-types/is-dependent-on';
import CannotBeSetWith from './prop-types/cannot-be-set-with';
import StoryWrapper from '../../../shared/components/StoryWrapper';
 
import StoryFrame from '../../../shared/components/StoryFrame';
import DocsPage from '../../../.storybook/components/DocsPage';
 
// SHIM Lodash because it caches in node_modules and generates id's that are always incrementing
const uniqueId = (() => {
  const PREFIXES = {};
  let idCounter = 0;
  const newCounter = (prefix) => {
    PREFIXES[prefix] = 0;
  };
  const incCounter = (prefix) => {
    PREFIXES[prefix] = PREFIXES[prefix] + 1;
    return PREFIXES[prefix];
  };
  const initCounterForPrefix = (prefix) => (PREFIXES[prefix] != null ? PREFIXES[prefix] : newCounter(prefix));
  const addToPrefix = (prefix) => {
    initCounterForPrefix(prefix);
    return prefix + incCounter(prefix);
  };
  return (prefix) => (prefix ? addToPrefix(prefix) : idCounter++);
})();
 
// Get a component example object from the exported states and examples objects
// To retrieve a specific property value from the object, pass in its key as an argument
export const getDisplayExampleById = (collection, id, key) => {
  Iif (
    !(
      Array.isArray(collection) &&
      collection.every((o) => o.hasOwnProperty('id') && React.isValidElement(o.element))
    )
  )
    throw new Error(
      `${JSON.stringify(
        collection,
        null,
        2,
      )} has broken schema that requires an array of example/state objects with 'id' and 'element' properties`,
    );
 
  // if an ID was provided then search for it in the collection
  if (id !== undefined) {
    const elementObj = collection.filter((example) => example.id === id);
 
    if (elementObj && elementObj[0]) {
      return elementObj[0][key] || elementObj[0];
    } else {
      throw new Error(`No display element with id "${id}" found`);
    }
  } else {
    // if no ID was provided then we simply return the first item of the array
    return collection[0][key];
  }
};
 
// Get a component example in doc block from the exported states and examples objects
export const getDisplayElementById = (collection, id) => {
  // if collection is an array, continue as expected
  if (Array.isArray(collection)) {
    return getDisplayExampleById(collection, id, 'element');
  }
 
  // if collection is not an array simply return it (probably a React element)
  return collection;
};
 
// Get a component example's styles in doc block from the exported states and examples objects
export const getDemoStylesById = (collection, id) => getDisplayExampleById(collection, id, 'demoStyles');
 
export const capitalize = (str) => {
  if (typeof str === 'string') {
    return str.replace(/^\w/, (c) => c.toUpperCase());
  } else {
    return '';
  }
};
 
function union(setA, setB) {
  let _union = new Set(setA);
  for (let elem of setB) {
    _union.add(elem);
  }
  return _union;
}
 
/**
 * @desc Get all contexts for examples for a single component
 * @param object $object - the object to get contexts from
 * @return array - array of example contexts
 */
export const getExampleContexts = (object) => {
  let contexts = [];
 
  object.map((group) => {
    const groupSet = new Set(
      group.map((example) => (example.context === undefined ? 'kitchen' : example.context)),
    );
    contexts = [...union(groupSet, new Set(contexts))];
  });
 
  return contexts;
};
 
/**
 * @desc Get all examples for a single component by type
 * @param object $object - the object to check types against
 * @param array $types - the type of examples you want
 * @return array - array of component examples based parameters
 */
export const getDisplayCollectionsByType = (object, types) => {
  let collection = [];
  if (Array.isArray(types)) {
    types.map((type) => {
      if (object.hasOwnProperty(type)) {
        if (Array.isArray(object[type])) {
          object[type].map((element) => {
            const { id, demoStyles, storybookStyles, label, demoProps, deprecated, context } = element;
            return collection.push({
              id: id,
              component: getDisplayElementById(object[type], id),
              label,
              demoStyles,
              storybookStyles,
              demoProps,
              deprecated,
              context,
            });
          });
        } else {
          collection.push({
            label: 'Default',
            component: object[type],
          });
        }
      }
    });
  } else {
    throw new Error(`Expected "${types}" to be an array`);
  }
  return collection;
};
 
/**
 * @desc Get all examples for multiple components by type
 * @param array $array - the components to check types against
 * @param array $types - the type of examples you want
 * @return array - array of component examples based on parameters
 */
export const getAllDisplayCollectionsByType = (array, types) => {
  let collection = [];
  if (Array.isArray(array)) {
    array.map((element) => collection.push(getDisplayCollectionsByType(element, types)));
  } else {
    throw new Error(`Expected "${array}" to be an array`);
  }
  return collection;
};
 
/**
 * @desc Get the StoryWrapper for a demo-styled example as a Story decorator
 * @param object $example - the example object being Story-ified
 * @return object - decorator object to pass to storiesOf.add
 */
export const getStoryWrapperDecorator = (example) => {
  const { demoStyles, demoProps } = example;
  return demoStyles || demoProps
    ? {
        decorators: [
          (storyFn) => (
            <StoryWrapper styles={demoStyles} {...demoProps}>
              {storyFn()}
            </StoryWrapper>
          ),
        ],
      }
    : null;
};
 
/**
 * @desc Get the parameters for a story with consideration for example wrapper decorator
 * @param {...Object} $paramObjects - the params to pass to story as Objects
 * @return object - parameter object to pass to storiesOf.add
 */
export const getExampleStoryParams = (...paramObjects) =>
  paramObjects.reduce((prev, next) => {
    if (prev.decorators && next.decorators) next.decorators = [...prev.decorators, ...next.decorators];
    return { ...prev, ...next };
  }, {});
 
/**
 * @desc Return the nubbin position class
 * @param {string} position Accepts declarative location e.g. "top left", 'bottom right', 'top', etc
 * @return Classname as a string
 */
export const getNubbinClass = (position) => {
  const nubbinPositionArray = position.split(' ');
  const nubbinComputedClass = 'slds-nubbin_' + nubbinPositionArray.join('-');
  return nubbinComputedClass;
};
 
/**
 * @desc Factory for creating custom React prop-types
 * @param {boolean} isRequired Sets the prop-type to required
 * @param {function} callback Prop-type validation logic
 */
export const createCustomPropType = (isRequired, callback) => {
  // The factory returns a custom prop type
  return function (props, propName, componentName) {
    const prop = props[propName];
    if (prop == null) {
      // Prop is missing
      if (isRequired) {
        // Prop is required but wasn't specified. Throw an error.
        throw new Error();
      }
      // Prop is optional. Do nothing.
    } else {
      callback(props, propName, componentName);
    }
  };
};
 
/**
 *
 */
export const generateStories = (
  patternName,
  examples,
  collections,
  Docs,
  options = {
    defaultDemoStyles: '',
    isFullBleed: false,
    isViewport: false,
    eyes: undefined,
  },
) => {
  // retrieve examples by type
  const kitchenSink = getAllDisplayCollectionsByType(examples, collections);
 
  // retrieve contexts defined in examples definition files
  const contexts = getExampleContexts(kitchenSink);
 
  return contexts.map((context) => {
    return {
      name: `${capitalize(context)} Sink`,
      render: () =>
        kitchenSink.map((element, idx) => {
          const kitchen = element.filter((el) =>
            context === 'kitchen'
              ? el.context === context || el.context === undefined
              : el.context === context,
          );
 
          return kitchen.map(({ label, component, storybookStyles, demoStyles }) => {
            // if storybookStyles is a boolean true then we use the same styles defined in demoStyles
            let storyStyles;
            if (storybookStyles === true) {
              storyStyles = demoStyles || '';
            } else {
              storyStyles = storybookStyles || '';
            }
            storyStyles = options.defaultDemoStyles + storyStyles;
 
            return (
              <StoryFrame
                component={component}
                label={label}
                key={`${context}-sink-${label}-${idx}`}
                styles={storyStyles}
                isFullBleed={options.isFullBleed}
                isViewport={options.isViewport}
              />
            );
          });
        }),
      docs: {
        page: () => <DocsPage title={patternName} Docs={Docs} />,
      },
      eyes: options.eyes,
    };
  });
};
 
export default {
  range,
  startCase,
  times,
  uniqueId,
  upperFirst,
};
 
export { IsDependentOn, CannotBeSetWith };