diff --git a/packages/cloud-cognitive/CHANGELOG.md b/packages/cloud-cognitive/CHANGELOG.md
index 50c193efc3..d195a7513a 100644
--- a/packages/cloud-cognitive/CHANGELOG.md
+++ b/packages/cloud-cognitive/CHANGELOG.md
@@ -3,6 +3,14 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
+## [0.39.11](https://github.com/carbon-design-system/ibm-cloud-cognitive/compare/@carbon/ibm-cloud-cognitive@0.39.10...@carbon/ibm-cloud-cognitive@0.39.11) (2021-05-25)
+
+**Note:** Version bump only for package @carbon/ibm-cloud-cognitive
+
+
+
+
+
## [0.39.10](https://github.com/carbon-design-system/ibm-cloud-cognitive/compare/@carbon/ibm-cloud-cognitive@0.39.9...@carbon/ibm-cloud-cognitive@0.39.10) (2021-05-24)
diff --git a/packages/cloud-cognitive/package.json b/packages/cloud-cognitive/package.json
index 7994e455d0..f3ae032be3 100644
--- a/packages/cloud-cognitive/package.json
+++ b/packages/cloud-cognitive/package.json
@@ -1,7 +1,7 @@
{
"name": "@carbon/ibm-cloud-cognitive",
"description": "Carbon for Cloud & Cognitive UI components",
- "version": "0.39.10",
+ "version": "0.39.11",
"license": "Apache-2.0",
"main": "lib/index.js",
"module": "es/index.js",
diff --git a/packages/cloud-cognitive/src/components/ActionBar/ActionBar.js b/packages/cloud-cognitive/src/components/ActionBar/ActionBar.js
index 64addac92c..7bdb6a200d 100644
--- a/packages/cloud-cognitive/src/components/ActionBar/ActionBar.js
+++ b/packages/cloud-cognitive/src/components/ActionBar/ActionBar.js
@@ -223,7 +223,7 @@ ActionBar.propTypes = {
PropTypes.arrayOf(PropTypes.element),
PropTypes.element,
]),
- "See documentation on the 'actions' property."
+ 'See documentation on the `actions` prop.'
), // expects action bar item as array or in fragment,
/**
* className
diff --git a/packages/cloud-cognitive/src/components/ActionBar/ActionBar.test.js b/packages/cloud-cognitive/src/components/ActionBar/ActionBar.test.js
index a07545da4f..840da8ad4e 100644
--- a/packages/cloud-cognitive/src/components/ActionBar/ActionBar.test.js
+++ b/packages/cloud-cognitive/src/components/ActionBar/ActionBar.test.js
@@ -32,7 +32,7 @@ const ActionBarChildren = (
);
// eslint-disable-next-line react/prop-types
-const TestActionBar = ({ width, children, ...rest }) => {
+const TestActionBar = ({ width, children = null, ...rest }) => {
return (
{children}
@@ -83,7 +83,7 @@ describe(ActionBar.displayName, () => {
screen.getByText(/Action 10/);
expect(warn).toBeCalledWith(
- "The prop 'children' of 'ActionBar' has been deprecated and will soon be removed. See documentation on the 'actions' property."
+ 'The prop `children` of `ActionBar` has been deprecated and will soon be removed. See documentation on the `actions` prop.'
);
warn.mockRestore(); // Remove mock
diff --git a/packages/cloud-cognitive/src/components/ActionSet/ActionSet.js b/packages/cloud-cognitive/src/components/ActionSet/ActionSet.js
index 03c3f00271..fd195c77b6 100644
--- a/packages/cloud-cognitive/src/components/ActionSet/ActionSet.js
+++ b/packages/cloud-cognitive/src/components/ActionSet/ActionSet.js
@@ -12,6 +12,7 @@ import React from 'react';
import PropTypes from 'prop-types';
import cx from 'classnames';
import { pkg } from '../../settings';
+import { allPropTypes } from '../../global/js/utils/props-helper';
// Carbon and package components we use.
import { Button, ButtonSet, InlineLoading } from 'carbon-components-react';
@@ -54,6 +55,8 @@ const ActionSetButton = React.forwardRef(
)
);
+ActionSetButton.displayName = 'ActionSetButton';
+
ActionSetButton.propTypes = {
...Button.PropTypes,
kind: PropTypes.oneOf(['ghost', 'secondary', 'primary']),
@@ -94,7 +97,7 @@ export const ActionSet = React.forwardRef(
const buttons = (actions && actions.slice?.(0)) || [];
// We stack the buttons in a xs/sm set, or if there are three or more in a md set.
- const stack = willStack(size, buttons.length);
+ const stacking = willStack(size, buttons.length);
// order the actions with ghost buttons first and primary buttons last
// (and the opposite way if we're stacking)
@@ -103,11 +106,11 @@ export const ActionSet = React.forwardRef(
const kind2 = action2.kind || defaultKind;
return kind1 === 'ghost' || kind2 === 'primary'
- ? stack
+ ? stacking
? 1
: -1
: kind1 === 'primary' || kind2 === 'ghost'
- ? stack
+ ? stacking
? -1
: 1
: 0;
@@ -123,17 +126,17 @@ export const ActionSet = React.forwardRef(
blockClass,
className,
{
- [`${blockClass}--row-single`]: !stack && buttons.length === 1,
- [`${blockClass}--row-double`]: !stack && buttons.length === 2,
- [`${blockClass}--row-triple`]: !stack && buttons.length === 3,
- [`${blockClass}--row-quadruple`]: !stack && buttons.length >= 4,
- [`${blockClass}--stack`]: stack,
+ [`${blockClass}--row-single`]: !stacking && buttons.length === 1,
+ [`${blockClass}--row-double`]: !stacking && buttons.length === 2,
+ [`${blockClass}--row-triple`]: !stacking && buttons.length === 3,
+ [`${blockClass}--row-quadruple`]: !stacking && buttons.length >= 4,
+ [`${blockClass}--stacking`]: stacking,
},
`${blockClass}--${size}`
)}
ref={ref}
role="presentation"
- stacked={stack}>
+ stacked={stacking}>
{buttons.map((action, index) => (
(props, propName, componentName) => {
- const prop = props[propName];
+/**
+ * A validator function to help validate the actions supplied for a particular
+ * size of component. When the size is xs or sm, or md with three actions, the
+ * buttons will be stacked and a maximum of three buttons is applied with no
+ * ghosts unless the ghost is the only button. Otherwise a maximum of four
+ * buttons with a maximum of one ghost is applied. In either case, a maximum
+ * of one primary button is allowed.
+ * @param sizeFn An optional function which will be passed all the props and
+ * returns the size that the component should be treated as being: if not
+ * provided, a 'size' prop is used to determine the size of the component.
+ * @returns null if the actions meet the requirements, or an Error object with
+ * an explanatory message.
+ */
+ActionSet.validateActions = (sizeFn) => (
+ props,
+ propName,
+ componentName,
+ location,
+ propFullName
+) => {
+ const name = propFullName || propName;
+ const prop = props[name];
const actions = prop && prop?.length;
+ const problems = [];
if (actions > 0) {
const size = sizeFn ? sizeFn(props) : props.size;
- const stack = willStack(size, prop.length);
+ const stacking = willStack(size, actions);
const countActions = (kind) =>
prop.filter((action) => (action.kind || defaultKind) === kind).length;
- const check = (iftrue, problem) => {
- if (iftrue)
- throw new Error(`Property '${propName}' is invalid: ${problem}`);
- };
+ const primaryActions = countActions('primary');
+ const secondaryActions = countActions('secondary');
+ const ghostActions = countActions('ghost');
- check(
- stack && actions > 3,
- `you cannot have more than three actions in this size of ${componentName}.`
- );
+ stacking &&
+ actions > 3 &&
+ problems.push(
+ `you cannot have more than three actions in this size of ${componentName}`
+ );
- check(
- actions > 4,
- `you cannot have more than four actions in a ${componentName}.`
- );
+ actions > 4 &&
+ problems.push(
+ `you cannot have more than four actions in a ${componentName}`
+ );
- const primaryActions = countActions('primary');
- check(
- primaryActions > 1,
- `you cannot have more than one 'primary' action in a ${componentName}.`
- );
+ primaryActions > 1 &&
+ problems.push(
+ `you cannot have more than one 'primary' action in a ${componentName}`
+ );
- const ghostActions = countActions('ghost');
- check(
- ghostActions > 1,
- `you cannot have more than one 'ghost' action in a ${componentName}.`
- );
+ ghostActions > 1 &&
+ problems.push(
+ `you cannot have more than one 'ghost' action in a ${componentName}`
+ );
- check(
- stack && actions > 1 && ghostActions > 0,
- `you cannot have a 'ghost' button in conjunction with other action types in this size of ${componentName}. Try using a 'secondary' button instead.`
- );
+ stacking &&
+ actions > 1 &&
+ ghostActions > 0 &&
+ problems.push(
+ `you cannot have a 'ghost' button in conjunction with other action types in this size of ${componentName}`
+ );
- const secondaryActions = countActions('secondary');
- check(
- prop.length > primaryActions + secondaryActions + ghostActions,
- `you can only have 'primary', 'secondary' and 'ghost' buttons in a ${componentName}.`
- );
+ actions > primaryActions + secondaryActions + ghostActions &&
+ problems.push(
+ `you can only have 'primary', 'secondary' and 'ghost' buttons in a ${componentName}`
+ );
}
- return null;
+ return problems.length > 0
+ ? new Error(
+ `Invalid ${location} \`${name}\` supplied to \`${componentName}\`: ${problems.join(
+ ', and '
+ )}.`
+ )
+ : null;
};
ActionSet.propTypes = {
@@ -223,7 +243,7 @@ ActionSet.propTypes = {
*
* See https://react.carbondesignsystem.com/?path=/docs/components-button--default#component-api
*/
- actions: PropTypes.oneOfType([
+ actions: allPropTypes([
ActionSet.validateActions(),
PropTypes.arrayOf(
PropTypes.shape({
diff --git a/packages/cloud-cognitive/src/components/ActionSet/ActionSet.test.js b/packages/cloud-cognitive/src/components/ActionSet/ActionSet.test.js
index c1fa019cb0..e21045c455 100644
--- a/packages/cloud-cognitive/src/components/ActionSet/ActionSet.test.js
+++ b/packages/cloud-cognitive/src/components/ActionSet/ActionSet.test.js
@@ -81,6 +81,19 @@ describe(componentName, () => {
expect(buttons[1].textContent).toEqual(label1);
});
+ it('rejects too many buttons using the custom validator', () => {
+ const error = jest.spyOn(console, 'error').mockImplementation(() => {});
+ render(
+
+ );
+ expect(error).toBeCalledWith(
+ expect.stringContaining('`actions` supplied to `ActionSet`: you cannot')
+ );
+ error.mockRestore();
+ });
+
it('applies className to an action button', () => {
render();
expect(getByRoleAndLabel('button', label1)).toHaveClass(className);
@@ -131,7 +144,7 @@ describe(componentName, () => {
});
});
-const v = (size, props, propName, componentName) => () =>
+const v = (size, props, propName, componentName) =>
ActionSet.validateActions(() => size)(props, propName, componentName);
const prop = `prop-${uuidv4()}`;
@@ -166,70 +179,80 @@ const props = {
describe(`${componentName}.validateActions`, () => {
it('rejects more than three actions for an extra small size', () => {
- expect(v('xs', props[1], prop, componentName)).not.toThrow();
- expect(v('xs', props[2], prop, componentName)).not.toThrow();
- expect(v('xs', props[3], prop, componentName)).not.toThrow();
- expect(v('xs', props[4], prop, componentName)).toThrow();
+ expect(v('xs', props[1], prop, componentName)).toBeNull();
+ expect(v('xs', props[2], prop, componentName)).toBeNull();
+ expect(v('xs', props[3], prop, componentName)).toBeNull();
+ expect(v('xs', props[4], prop, componentName)).toBeInstanceOf(Error);
});
it('rejects more than three actions for a small size', () => {
- expect(v('sm', props[1], prop, componentName)).not.toThrow();
- expect(v('sm', props[2], prop, componentName)).not.toThrow();
- expect(v('sm', props[3], prop, componentName)).not.toThrow();
- expect(v('sm', props[4], prop, componentName)).toThrow();
+ expect(v('sm', props[1], prop, componentName)).toBeNull();
+ expect(v('sm', props[2], prop, componentName)).toBeNull();
+ expect(v('sm', props[3], prop, componentName)).toBeNull();
+ expect(v('sm', props[4], prop, componentName)).toBeInstanceOf(Error);
});
it('rejects more than three actions for a medium size', () => {
- expect(v('md', props[1], prop, componentName)).not.toThrow();
- expect(v('md', props[2], prop, componentName)).not.toThrow();
- expect(v('md', props[3], prop, componentName)).not.toThrow();
- expect(v('md', props[4], prop, componentName)).toThrow();
+ expect(v('md', props[1], prop, componentName)).toBeNull();
+ expect(v('md', props[2], prop, componentName)).toBeNull();
+ expect(v('md', props[3], prop, componentName)).toBeNull();
+ expect(v('md', props[4], prop, componentName)).toBeInstanceOf(Error);
});
it('rejects more than four actions for a large size', () => {
- expect(v('lg', props[1], prop, componentName)).not.toThrow();
- expect(v('lg', props[2], prop, componentName)).not.toThrow();
- expect(v('lg', props[3], prop, componentName)).not.toThrow();
- expect(v('lg', props[4], prop, componentName)).not.toThrow();
- expect(v('lg', props[5], prop, componentName)).toThrow();
+ expect(v('lg', props[1], prop, componentName)).toBeNull();
+ expect(v('lg', props[2], prop, componentName)).toBeNull();
+ expect(v('lg', props[3], prop, componentName)).toBeNull();
+ expect(v('lg', props[4], prop, componentName)).toBeNull();
+ expect(v('lg', props[5], prop, componentName)).toBeInstanceOf(Error);
});
it('rejects more than four actions for a max size', () => {
- expect(v('max', props[1], prop, componentName)).not.toThrow();
- expect(v('max', props[2], prop, componentName)).not.toThrow();
- expect(v('max', props[3], prop, componentName)).not.toThrow();
- expect(v('max', props[4], prop, componentName)).not.toThrow();
- expect(v('max', props[5], prop, componentName)).toThrow();
+ expect(v('max', props[1], prop, componentName)).toBeNull();
+ expect(v('max', props[2], prop, componentName)).toBeNull();
+ expect(v('max', props[3], prop, componentName)).toBeNull();
+ expect(v('max', props[4], prop, componentName)).toBeNull();
+ expect(v('max', props[5], prop, componentName)).toBeInstanceOf(Error);
});
it('rejects more than one primary kind', () => {
- expect(v('md', props.primary, prop, componentName)).not.toThrow();
- expect(v('md', props.twoPrimaries, prop, componentName)).toThrow();
+ expect(v('md', props.primary, prop, componentName)).toBeNull();
+ expect(v('md', props.twoPrimaries, prop, componentName)).toBeInstanceOf(
+ Error
+ );
});
it('rejects more than one ghost kind', () => {
- expect(v('md', props.ghost, prop, componentName)).not.toThrow();
- expect(v('md', props.twoGhosts, prop, componentName)).toThrow();
+ expect(v('md', props.ghost, prop, componentName)).toBeNull();
+ expect(v('md', props.twoGhosts, prop, componentName)).toBeInstanceOf(Error);
});
it('rejects ghost kind with other kinds for extra small, small, medium size', () => {
- expect(v('xs', props.psg, prop, componentName)).toThrow();
- expect(v('sm', props.psg, prop, componentName)).toThrow();
- expect(v('md', props.psg, prop, componentName)).toThrow();
- expect(v('lg', props.psg, prop, componentName)).not.toThrow();
- expect(v('max', props.psg, prop, componentName)).not.toThrow();
+ expect(v('xs', props.psg, prop, componentName)).toBeInstanceOf(Error);
+ expect(v('sm', props.psg, prop, componentName)).toBeInstanceOf(Error);
+ expect(v('md', props.psg, prop, componentName)).toBeInstanceOf(Error);
+ expect(v('lg', props.psg, prop, componentName)).toBeNull();
+ expect(v('max', props.psg, prop, componentName)).toBeNull();
});
it('rejects any kind other than primary, secondary, ghost', () => {
- expect(v('md', props.primary, prop, componentName)).not.toThrow();
- expect(v('md', props.secondary, prop, componentName)).not.toThrow();
- expect(v('md', props.danger, prop, componentName)).toThrow();
- expect(v('md', props.ghost, prop, componentName)).not.toThrow();
- expect(v('md', props.dangerPrimary, prop, componentName)).toThrow();
- expect(v('md', props.dangerGhost, prop, componentName)).toThrow();
- expect(v('md', props.dangerTertiary, prop, componentName)).toThrow();
- expect(v('md', props.tertiary, prop, componentName)).toThrow();
- expect(v('md', props.twoPrimaries, prop, componentName)).toThrow();
- expect(v('md', props.twoGhosts, prop, componentName)).toThrow();
+ expect(v('md', props.primary, prop, componentName)).toBeNull();
+ expect(v('md', props.secondary, prop, componentName)).toBeNull();
+ expect(v('md', props.danger, prop, componentName)).toBeInstanceOf(Error);
+ expect(v('md', props.ghost, prop, componentName)).toBeNull();
+ expect(v('md', props.dangerPrimary, prop, componentName)).toBeInstanceOf(
+ Error
+ );
+ expect(v('md', props.dangerGhost, prop, componentName)).toBeInstanceOf(
+ Error
+ );
+ expect(v('md', props.dangerTertiary, prop, componentName)).toBeInstanceOf(
+ Error
+ );
+ expect(v('md', props.tertiary, prop, componentName)).toBeInstanceOf(Error);
+ expect(v('md', props.twoPrimaries, prop, componentName)).toBeInstanceOf(
+ Error
+ );
+ expect(v('md', props.twoGhosts, prop, componentName)).toBeInstanceOf(Error);
});
});
diff --git a/packages/cloud-cognitive/src/components/ButtonSetWithOverflow/ButtonSetWithOverflow.js b/packages/cloud-cognitive/src/components/ButtonSetWithOverflow/ButtonSetWithOverflow.js
index a352815802..da69d5a313 100644
--- a/packages/cloud-cognitive/src/components/ButtonSetWithOverflow/ButtonSetWithOverflow.js
+++ b/packages/cloud-cognitive/src/components/ButtonSetWithOverflow/ButtonSetWithOverflow.js
@@ -192,7 +192,7 @@ ButtonSetWithOverflow.propTypes = {
PropTypes.arrayOf(PropTypes.element),
PropTypes.element,
]),
- "See documentation on the 'buttons' property."
+ 'See documentation on the `buttons` property.'
), // expects action bar item as array or in fragment,
/**
* className
diff --git a/packages/cloud-cognitive/src/components/ButtonSetWithOverflow/ButtonSetWithOverflow.test.js b/packages/cloud-cognitive/src/components/ButtonSetWithOverflow/ButtonSetWithOverflow.test.js
index 11a049cb68..6087a5fd37 100644
--- a/packages/cloud-cognitive/src/components/ButtonSetWithOverflow/ButtonSetWithOverflow.test.js
+++ b/packages/cloud-cognitive/src/components/ButtonSetWithOverflow/ButtonSetWithOverflow.test.js
@@ -89,7 +89,7 @@ describe(ButtonSetWithOverflow.displayName, () => {
});
expect(warn).toBeCalledWith(
- "The prop 'children' of 'ButtonSetWithOverflow' has been deprecated and will soon be removed. See documentation on the 'buttons' property."
+ 'The prop `children` of `ButtonSetWithOverflow` has been deprecated and will soon be removed. See documentation on the `buttons` property.'
);
userEvent.click(action1);
diff --git a/packages/cloud-cognitive/src/components/PageHeader/PageHeader.js b/packages/cloud-cognitive/src/components/PageHeader/PageHeader.js
index ad3af63163..8073a44226 100644
--- a/packages/cloud-cognitive/src/components/PageHeader/PageHeader.js
+++ b/packages/cloud-cognitive/src/components/PageHeader/PageHeader.js
@@ -924,7 +924,7 @@ PageHeader.propTypes = {
*/
titleIcon: deprecateProp(
PropTypes.oneOfType([PropTypes.func, PropTypes.object]),
- 'Deprecated. Use title prop shape instead.'
+ 'Use `title` prop shape instead.'
),
};
diff --git a/packages/cloud-cognitive/src/components/PageHeader/PageHeader.test.js b/packages/cloud-cognitive/src/components/PageHeader/PageHeader.test.js
index e2dbe8732c..cff0cb54dc 100644
--- a/packages/cloud-cognitive/src/components/PageHeader/PageHeader.test.js
+++ b/packages/cloud-cognitive/src/components/PageHeader/PageHeader.test.js
@@ -339,7 +339,7 @@ describe('PageHeader', () => {
render();
expect(warn).toBeCalledWith(
- "The usage of prop 'actionBarItems' of 'PageHeader' has been changed and you should update. Expects an array of objects with the following properties: iconDescription, renderIcon and onClick."
+ 'The usage of the prop `actionBarItems` of `PageHeader` has been changed and support for the old usage will soon be removed. Expects an array of objects with the following properties: iconDescription, renderIcon and onClick.'
);
warn.mockRestore(); // Remove mock
@@ -355,7 +355,7 @@ describe('PageHeader', () => {
});
expect(warn).toBeCalledWith(
- "The usage of prop 'pageActions' of 'PageHeader' has been changed and you should update. Expects an array of objects with the following properties: label and onClick."
+ 'The usage of the prop `pageActions` of `PageHeader` has been changed and support for the old usage will soon be removed. Expects an array of objects with the following properties: label and onClick.'
);
warn.mockRestore(); // Remove mock
@@ -371,7 +371,7 @@ describe('PageHeader', () => {
});
expect(warn).toBeCalledWith(
- "The usage of prop 'pageActions' of 'PageHeader' has been changed and you should update. Expects an array of objects with the following properties: label and onClick."
+ 'The usage of the prop `pageActions` of `PageHeader` has been changed and support for the old usage will soon be removed. Expects an array of objects with the following properties: label and onClick.'
);
warn.mockRestore(); // Remove mock
@@ -479,7 +479,7 @@ describe('PageHeader', () => {
render();
expect(warn).toBeCalledWith(
- "The prop 'titleIcon' of 'PageHeader' has been deprecated and will soon be removed. Deprecated. Use title prop shape instead."
+ 'The prop `titleIcon` of `PageHeader` has been deprecated and will soon be removed. Use `title` prop shape instead.'
);
screen.getByText(titleString, { selector: `.${blockClass}__title span` });
diff --git a/packages/cloud-cognitive/src/components/SidePanel/SidePanel.js b/packages/cloud-cognitive/src/components/SidePanel/SidePanel.js
index 97506ba531..b1f9bf0c1d 100644
--- a/packages/cloud-cognitive/src/components/SidePanel/SidePanel.js
+++ b/packages/cloud-cognitive/src/components/SidePanel/SidePanel.js
@@ -14,6 +14,7 @@ import cx from 'classnames';
import ReactResizeDetector from 'react-resize-detector';
import wrapFocus from '../../global/js/utils/wrapFocus';
import { pkg } from '../../settings';
+import { allPropTypes } from '../../global/js/utils/props-helper';
import { SIDE_PANEL_SIZES } from './constants';
// Carbon and package components we use.
@@ -576,7 +577,7 @@ SidePanel.propTypes = {
*
* See https://react.carbondesignsystem.com/?path=/docs/components-button--default#component-api
*/
- actions: PropTypes.oneOfType([
+ actions: allPropTypes([
ActionSet.validateActions(),
PropTypes.arrayOf(
PropTypes.shape({
diff --git a/packages/cloud-cognitive/src/components/SidePanel/SidePanel.test.js b/packages/cloud-cognitive/src/components/SidePanel/SidePanel.test.js
index 86e2700b0d..11f8fa4fd0 100644
--- a/packages/cloud-cognitive/src/components/SidePanel/SidePanel.test.js
+++ b/packages/cloud-cognitive/src/components/SidePanel/SidePanel.test.js
@@ -271,6 +271,23 @@ describe('SidePanel', () => {
).toBeTruthy();
});
+ it('rejects too many buttons using the custom validator', () => {
+ const error = jest.spyOn(console, 'error').mockImplementation(() => {});
+ renderSidePanel({
+ actions: [
+ { kind: 'primary' },
+ { kind: 'primary' },
+ { kind: 'ghost' },
+ { kind: 'ghost' },
+ { kind: 'danger' },
+ ],
+ });
+ expect(error).toBeCalledWith(
+ expect.stringContaining('`actions` supplied to `SidePanel`: you cannot')
+ );
+ error.mockRestore();
+ });
+
it('should render navigation button', () => {
const { container } = renderSidePanel({
currentStep: 1,
diff --git a/packages/cloud-cognitive/src/components/Tearsheet/Tearsheet.js b/packages/cloud-cognitive/src/components/Tearsheet/Tearsheet.js
index 7189741668..556bd499c6 100644
--- a/packages/cloud-cognitive/src/components/Tearsheet/Tearsheet.js
+++ b/packages/cloud-cognitive/src/components/Tearsheet/Tearsheet.js
@@ -11,7 +11,7 @@ import React from 'react';
// Other standard imports.
import PropTypes from 'prop-types';
import { pkg } from '../../settings';
-import { prepareProps } from '../../global/js/utils/props-helper';
+import { allPropTypes, prepareProps } from '../../global/js/utils/props-helper';
// Carbon and package components we use.
import { Button } from 'carbon-components-react';
@@ -66,7 +66,7 @@ Tearsheet.propTypes = {
*
* See https://react.carbondesignsystem.com/?path=/docs/components-button--default#component-api
*/
- actions: PropTypes.oneOfType([
+ actions: allPropTypes([
ActionSet.validateActions(() => 'max'),
PropTypes.arrayOf(
PropTypes.shape({
diff --git a/packages/cloud-cognitive/src/components/Tearsheet/Tearsheet.test.js b/packages/cloud-cognitive/src/components/Tearsheet/Tearsheet.test.js
index f99791009a..63203f39e1 100644
--- a/packages/cloud-cognitive/src/components/Tearsheet/Tearsheet.test.js
+++ b/packages/cloud-cognitive/src/components/Tearsheet/Tearsheet.test.js
@@ -29,6 +29,13 @@ const actions = [
{ kind: 'secondary', onClick, label: 'Cancel' },
{ onClick, label: createButton },
];
+const badactions = [
+ { kind: 'primary' },
+ { kind: 'primary' },
+ { kind: 'ghost' },
+ { kind: 'ghost' },
+ { kind: 'danger' },
+];
const childFragment = `Main ${uuidv4()} content`;
const children = {childFragment}
;
const className = `class-${uuidv4()}`;
@@ -98,6 +105,15 @@ const commonTests = (Ts, name) => {
expect(onClick).toHaveBeenCalledTimes(1);
});
+ it('rejects too many buttons using the custom validator', () => {
+ const error = jest.spyOn(console, 'error').mockImplementation(() => {});
+ render();
+ expect(error).toBeCalledWith(
+ expect.stringContaining(`\`actions\` supplied to \`${name}\`: you cannot`)
+ );
+ error.mockRestore();
+ });
+
it('renders children', () => {
render({children});
expect(document.querySelector(`.${blockClass}__main`)).not.toBeNull();
diff --git a/packages/cloud-cognitive/src/components/Tearsheet/TearsheetNarrow.js b/packages/cloud-cognitive/src/components/Tearsheet/TearsheetNarrow.js
index 83be579110..06211f1307 100644
--- a/packages/cloud-cognitive/src/components/Tearsheet/TearsheetNarrow.js
+++ b/packages/cloud-cognitive/src/components/Tearsheet/TearsheetNarrow.js
@@ -11,7 +11,7 @@ import React from 'react';
// Other standard imports.
import PropTypes from 'prop-types';
import { pkg } from '../../settings';
-import { prepareProps } from '../../global/js/utils/props-helper';
+import { allPropTypes, prepareProps } from '../../global/js/utils/props-helper';
// Carbon and package components we use.
import { Button } from 'carbon-components-react';
@@ -65,7 +65,7 @@ TearsheetNarrow.propTypes = {
*
* See https://react.carbondesignsystem.com/?path=/docs/components-button--default#component-api
*/
- actions: PropTypes.oneOfType([
+ actions: allPropTypes([
ActionSet.validateActions(() => 'lg'),
PropTypes.arrayOf(
PropTypes.shape({
diff --git a/packages/cloud-cognitive/src/global/js/utils/pconsole.js b/packages/cloud-cognitive/src/global/js/utils/pconsole.js
new file mode 100644
index 0000000000..59ed8e8268
--- /dev/null
+++ b/packages/cloud-cognitive/src/global/js/utils/pconsole.js
@@ -0,0 +1,22 @@
+//
+// Copyright IBM Corp. 2020, 2021
+//
+// This source code is licensed under the Apache-2.0 license found in the
+// LICENSE file in the root directory of this source tree.
+//
+
+export const isProduction = process.env.NODE_ENV === 'production';
+export const noop = () => undefined;
+export const shimIfProduction = (fn) => (isProduction ? noop : fn);
+export const log = shimIfProduction((...args) => console.log(...args));
+export const warn = shimIfProduction((...args) => console.warn(...args));
+export const error = shimIfProduction((...args) => console.error(...args));
+
+export default {
+ isProduction,
+ noop,
+ shimIfProduction,
+ log,
+ warn,
+ error,
+};
diff --git a/packages/cloud-cognitive/src/global/js/utils/props-helper.js b/packages/cloud-cognitive/src/global/js/utils/props-helper.js
index 6dd9409278..50ff911499 100644
--- a/packages/cloud-cognitive/src/global/js/utils/props-helper.js
+++ b/packages/cloud-cognitive/src/global/js/utils/props-helper.js
@@ -6,9 +6,9 @@
//
import React from 'react';
-import PropTypes from 'prop-types';
import unwrapIfFragment from './unwrap-if-fragment';
+import pconsole from './pconsole';
// helper functions for component props
@@ -55,50 +55,49 @@ export const prepareProps = (...values) => {
}, {});
};
-const deprecatePropInner = (validator, messageFunction, additionalInfo) => {
- const deprecatePropValidator = (
- props,
- name,
- componentName,
- location,
- propFullName
- ) => {
- if (props[name] !== null) {
- const info = additionalInfo ? ` ${additionalInfo}` : '';
- console.warn(
- messageFunction(location, propFullName || name, componentName, info)
- );
- }
-
- return null;
- };
+// A simple wrapper for a prop-types checker that issues a warning message if
+// the value being validated is not null/undefined.
+const deprecatePropInner = (message, validator, info) => (...args) => (
+ // args = [props, propName, componentName, location, propFullName, ...]
+ args[0][args[1]] &&
+ pconsole.warn(message(args[3], args[4] || args[1], args[2], info)),
+ validator(...args)
+);
- // first does the deprecation check and then calls original validator
- return (
- PropTypes.oneOfType([deprecatePropValidator]) ||
- PropTypes.oneOfType([validator])
- );
-};
-
-export const deprecatePropUsage = (validator, additionalInfo) => {
- return deprecatePropInner(
- validator,
- (location, name, componentName, info) => {
- return `The usage of ${location} '${name}' of '${componentName}' has been changed and you should update.${info}`;
- },
- additionalInfo
- );
-};
+/**
+ * A prop-types type checker that marks a particular usage of a prop as
+ * deprecated. This can be used to deprecate an option in a oneOfType checker,
+ * and the deprecated option(s) should be listed last so that the deprecation
+ * message is only reported if none of the other type options is matched.
+ * @param {} validator The prop-types validator for the prop usage as it should
+ * be if it weren't deprecated. If this validator produces type checking
+ * errors they will be reported as usual.
+ * @param {*} additionalInfo One or more sentences to be appended to the
+ * deprecation message to explain why the prop usage is deprecated and/or what
+ * should be used instead.
+ * @returns Any type checking error reported by the validator, or null.
+ */
+export const deprecatePropUsage = deprecatePropInner.bind(
+ undefined,
+ (location, propName, componentName, info) =>
+ `The usage of the ${location} \`${propName}\` of \`${componentName}\` has been changed and support for the old usage will soon be removed. ${info}`
+);
-export const deprecateProp = (validator, additionalInfo) => {
- return deprecatePropInner(
- validator,
- (location, name, componentName, info) => {
- return `The ${location} '${name}' of '${componentName}' has been deprecated and will soon be removed.${info}`;
- },
- additionalInfo
- );
-};
+/**
+ * A prop-types type checker that marks a prop as deprecated.
+ * @param {} validator The prop-types validator for the prop as it should be
+ * used if it weren't deprecated. If this validator produces type checking
+ * errors they will be reported as usual.
+ * @param {*} additionalInfo One or more sentences to be appended to the
+ * deprecation message to explain why the prop is deprecated and/or what should
+ * be used instead.
+ * @returns Any type checking error reported by the validator, or null.
+ */
+export const deprecateProp = deprecatePropInner.bind(
+ undefined,
+ (location, propName, componentName, info) =>
+ `The ${location} \`${propName}\` of \`${componentName}\` has been deprecated and will soon be removed. ${info}`
+);
/**
* Takes items as fragment, node or array
@@ -118,3 +117,69 @@ export const extractShapesArray = (items) => {
return Array.isArray(items) ? items : [];
};
+
+/**
+ * A prop-types validation function that takes an array of type checkers and
+ * requires prop values to satisfy all of the type checkers. This can be useful
+ * to combine custom validation functions with regular prop types, or for
+ * combining inherited prop-types from another component with tighter
+ * requirements.
+ *
+ * Examples:
+ *
+ * MyComponent.propTypes = {
+ *
+ * foo: allPropTypes([
+ * customValidationFunction,
+ * PropTypes.arrayOf(
+ * PropTypes.shape({
+ * text: PropType.string
+ * })
+ * )
+ * ]),
+ *
+ * kind: allPropTypes([
+ * Button.propTypes.kind,
+ * PropTypes.oneOf('primary', 'secondary')
+ * ]),
+ *
+ * }
+ */
+export const allPropTypes = pconsole.shimIfProduction((arrayOfTypeCheckers) => {
+ if (!Array.isArray(arrayOfTypeCheckers)) {
+ pconsole.error(
+ 'Warning: Invalid argument supplied to allPropTypes, expected an instance of array.'
+ );
+ return pconsole.noop;
+ }
+
+ for (let i = 0; i < arrayOfTypeCheckers.length; i++) {
+ if (typeof arrayOfTypeCheckers[i] !== 'function') {
+ pconsole.error(
+ `Invalid argument supplied to allPropTypes. Expected an array of check functions, but received ${arrayOfTypeCheckers[i]} at index ${i}.`
+ );
+ return pconsole.noop;
+ }
+ }
+
+ const checkType = (...args) => {
+ let error = null;
+ arrayOfTypeCheckers.some((checker) => (error = checker(...args)));
+ return error;
+ };
+
+ checkType.isRequired = (props, propName, comp, loc, propFullName, secret) => {
+ const prop = propFullName || propName;
+ return props[prop] == null
+ ? new Error(
+ `The ${loc} \`${prop}\` is marked as required in \`${
+ comp || '<>'
+ }\`, but its value is \`${
+ props[prop] === null ? 'null' : 'undefined'
+ }\`.`
+ )
+ : checkType(props, prop, comp, loc, propFullName, secret);
+ };
+
+ return checkType;
+});
diff --git a/packages/cloud-cognitive/src/global/js/utils/props-helper.test.js b/packages/cloud-cognitive/src/global/js/utils/props-helper.test.js
index be52fc6481..8b7a4038ea 100644
--- a/packages/cloud-cognitive/src/global/js/utils/props-helper.test.js
+++ b/packages/cloud-cognitive/src/global/js/utils/props-helper.test.js
@@ -1,11 +1,27 @@
-import { prepareProps } from './props-helper';
+//
+// Copyright IBM Corp. 2021, 2021
+//
+// This source code is licensed under the Apache-2.0 license found in the
+// LICENSE file in the root directory of this source tree.
+//
-const defaults = { a: 1, b: 2, c: 3, d: 4, e: 5, f: 6, g: 7, h: 8 };
-const props = { e: 9, f: 10, g: 11, h: 12, i: 13, j: 14, k: 15, l: 16 };
-const overrides = { c: 17, d: 18, g: 19, h: 20, k: 21, l: 22, o: 23, p: 24 };
-const block = ['b', 'd', 'f', 'h', 'n', 'p'];
+import React from 'react';
+import PropTypes from 'prop-types';
+import { render } from '@testing-library/react';
+
+import {
+ prepareProps,
+ allPropTypes,
+ deprecateProp,
+ deprecatePropUsage,
+} from './props-helper';
describe('prepareProps', () => {
+ const defaults = { a: 1, b: 2, c: 3, d: 4, e: 5, f: 6, g: 7, h: 8 };
+ const props = { e: 9, f: 10, g: 11, h: 12, i: 13, j: 14, k: 15, l: 16 };
+ const overrides = { c: 17, d: 18, g: 19, h: 20, k: 21, l: 22, o: 23, p: 24 };
+ const block = ['b', 'd', 'f', 'h', 'n', 'p'];
+
it('applies correct defaults and overrides and blocks values', () => {
const result = prepareProps(defaults, props, block, 'j', 'l', overrides);
@@ -43,3 +59,87 @@ describe('prepareProps', () => {
expect(result.p).toEqual(24);
});
});
+
+describe('deprecateProp and deprecatePropUsage', () => {
+ const Component = () => null;
+ Component.displayName = 'x';
+ Component.propTypes = {
+ a: deprecateProp(PropTypes.string, 'Explanation 1.'),
+ b: PropTypes.string,
+ c: PropTypes.oneOfType([
+ PropTypes.string,
+ deprecatePropUsage(PropTypes.number, 'Explanation 2.'),
+ ]),
+ };
+
+ it('reports prop deprecated when deprecated prop is used', () => {
+ const warn = jest.spyOn(console, 'warn').mockImplementation(() => {});
+ render();
+ expect(warn).toBeCalledWith(
+ 'The prop `a` of `x` has been deprecated and will soon be removed. Explanation 1.'
+ );
+ warn.mockRestore();
+ });
+
+ it('does not report prop deprecated when non-deprecated prop is used', () => {
+ const warn = jest.spyOn(console, 'warn').mockImplementation(() => {});
+ render();
+ expect(warn).not.toBeCalled();
+ warn.mockRestore();
+ });
+
+ it('reports prop usage deprecated when deprecated usage is used', () => {
+ const warn = jest.spyOn(console, 'warn').mockImplementation(() => {});
+ render();
+ expect(warn).toBeCalledWith(
+ 'The usage of the prop `c` of `x` has been changed and support for the old usage will soon be removed. Explanation 2.'
+ );
+ warn.mockRestore();
+ });
+
+ it('does not report prop usage deprecated when non-deprecated usage is used', () => {
+ const warn = jest.spyOn(console, 'warn').mockImplementation(() => {});
+ render();
+ expect(warn).not.toBeCalled();
+ warn.mockRestore();
+ });
+});
+
+describe('allPropTypes', () => {
+ const e1 = new Error();
+ const e2 = new Error(
+ 'The prop `b` is marked as required in `Component X`, but its value is `null`.'
+ );
+ const e3 = new Error(
+ 'The prop `c` is marked as required in `Component X`, but its value is `undefined`.'
+ );
+ const p = () => null;
+ const f = () => e1;
+ const props = { a: '42', b: null };
+ const args1 = [props, 'a', 'Component X', 'prop'];
+ const args2 = [props, 'b', 'Component X', 'prop'];
+ const args3 = [props, 'c', 'Component X', 'prop'];
+
+ it('returns null only if all supplied type checkers return null', () => {
+ expect(allPropTypes([f, f, f, f])(...args1)).toEqual(e1);
+ expect(allPropTypes([p, f, f, f])(...args1)).toEqual(e1);
+ expect(allPropTypes([p, p, f, f])(...args1)).toEqual(e1);
+ expect(allPropTypes([p, p, p, f])(...args1)).toEqual(e1);
+ expect(allPropTypes([p, p, p, p])(...args1)).toBeNull();
+ });
+
+ it('rejects null or undefined when marked required', () => {
+ expect(allPropTypes([f, f, f, f]).isRequired(...args1)).toEqual(e1);
+ expect(allPropTypes([f, f, f, f]).isRequired(...args2)).toEqual(e2);
+ expect(allPropTypes([f, f, f, f]).isRequired(...args3)).toEqual(e3);
+ expect(allPropTypes([p, f, f, f]).isRequired(...args1)).toEqual(e1);
+ expect(allPropTypes([p, f, f, f]).isRequired(...args2)).toEqual(e2);
+ expect(allPropTypes([f, f, f, f]).isRequired(...args3)).toEqual(e3);
+ expect(allPropTypes([p, p, p, f]).isRequired(...args1)).toEqual(e1);
+ expect(allPropTypes([p, p, p, f]).isRequired(...args2)).toEqual(e2);
+ expect(allPropTypes([f, f, f, f]).isRequired(...args3)).toEqual(e3);
+ expect(allPropTypes([p, p, p, p]).isRequired(...args1)).toBeNull();
+ expect(allPropTypes([p, p, p, p]).isRequired(...args2)).toEqual(e2);
+ expect(allPropTypes([f, f, f, f]).isRequired(...args3)).toEqual(e3);
+ });
+});
diff --git a/packages/core/CHANGELOG.md b/packages/core/CHANGELOG.md
index 7cd33d9e6a..3a54d1d7f9 100644
--- a/packages/core/CHANGELOG.md
+++ b/packages/core/CHANGELOG.md
@@ -3,6 +3,14 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
+## [0.8.174](https://github.com/carbon-design-system/ibm-cloud-cognitive/compare/@carbon/ibm-cloud-cognitive-core@0.8.173...@carbon/ibm-cloud-cognitive-core@0.8.174) (2021-05-25)
+
+**Note:** Version bump only for package @carbon/ibm-cloud-cognitive-core
+
+
+
+
+
## [0.8.173](https://github.com/carbon-design-system/ibm-cloud-cognitive/compare/@carbon/ibm-cloud-cognitive-core@0.8.172...@carbon/ibm-cloud-cognitive-core@0.8.173) (2021-05-24)
**Note:** Version bump only for package @carbon/ibm-cloud-cognitive-core
diff --git a/packages/core/package.json b/packages/core/package.json
index 47a38ba49f..a29f57ed1a 100644
--- a/packages/core/package.json
+++ b/packages/core/package.json
@@ -1,7 +1,7 @@
{
"name": "@carbon/ibm-cloud-cognitive-core",
"private": true,
- "version": "0.8.173",
+ "version": "0.8.174",
"license": "Apache-2.0",
"main": "scripts/build.js",
"repository": {
@@ -25,7 +25,7 @@
},
"devDependencies": {
"@carbon/grid": "10.24.0",
- "@carbon/ibm-cloud-cognitive": "^0.39.10",
+ "@carbon/ibm-cloud-cognitive": "^0.39.11",
"@carbon/icons-react": "10.29.0",
"@carbon/import-once": "10.5.0",
"@carbon/layout": "10.22.0",
diff --git a/packages/experimental/CHANGELOG.md b/packages/experimental/CHANGELOG.md
index 6186d833a0..7e3698abd9 100644
--- a/packages/experimental/CHANGELOG.md
+++ b/packages/experimental/CHANGELOG.md
@@ -3,6 +3,14 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
+## [0.34.56](https://github.com/carbon-design-system/ibm-cloud-cognitive/compare/@carbon/ibm-cloud-cognitive-experimental@0.34.55...@carbon/ibm-cloud-cognitive-experimental@0.34.56) (2021-05-25)
+
+**Note:** Version bump only for package @carbon/ibm-cloud-cognitive-experimental
+
+
+
+
+
## [0.34.55](https://github.com/carbon-design-system/ibm-cloud-cognitive/compare/@carbon/ibm-cloud-cognitive-experimental@0.34.54...@carbon/ibm-cloud-cognitive-experimental@0.34.55) (2021-05-24)
**Note:** Version bump only for package @carbon/ibm-cloud-cognitive-experimental
diff --git a/packages/experimental/package.json b/packages/experimental/package.json
index e80a24d182..4bd8aeac33 100644
--- a/packages/experimental/package.json
+++ b/packages/experimental/package.json
@@ -3,7 +3,7 @@
"private-note": "no longer published, package deprecated in favor of @carbon/ibm-cloud-cognitive with feature flags",
"private": true,
"description": "Carbon for Cloud & Cognitive UI components",
- "version": "0.34.55",
+ "version": "0.34.56",
"license": "Apache-2.0",
"main": "lib/index.js",
"module": "es/index.js",
@@ -50,7 +50,7 @@
},
"dependencies": {
"@babel/runtime": "^7.13.10",
- "@carbon/ibm-cloud-cognitive": "^0.39.10",
+ "@carbon/ibm-cloud-cognitive": "^0.39.11",
"@carbon/telemetry": "^0.0.0-alpha.6",
"react-resize-detector": "^6.0.0"
},