From 8952076bcc5a89c1911981af08e80fc986394c8f Mon Sep 17 00:00:00 2001 From: Adnan Asani Date: Tue, 20 Apr 2021 14:19:28 +0200 Subject: [PATCH 1/3] refactor(buttons): use TypeScript (#1856) * refactor(flat-button): to use TypeScript, use readme generator * refactor(icon-button): to TypeScript, use readme generator * chore: remove required-if * refactor(primary-button): convert to TypeScript, use generated docs * refactor(secondary-icon-button): convert to TypeSript, use generate script for readme * chore: changeset * chore: update changeset with more description * refactor(secondary-button): migrate to TypeScript * chore: regenerate-readmes --- .changeset/five-impalas-impress.md | 10 + .changeset/sour-pillows-build.md | 5 + .../src/accessible-button.tsx | 2 +- .../components/buttons/flat-button/README.md | 80 +++++--- .../flat-button/docs/additional-info.md | 6 + .../buttons/flat-button/docs/usage-example.js | 15 ++ .../flat-button/{index.js => index.ts} | 0 .../buttons/flat-button/package.json | 3 +- .../flat-button/src/flat-button.styles.ts | 30 +++ .../src/{flat-button.js => flat-button.tsx} | 116 +++++++----- .../flat-button/src/{index.js => index.ts} | 0 .../src/{version.js => version.ts} | 0 .../components/buttons/icon-button/README.md | 92 +++++---- .../icon-button/docs/additional-info.md | 4 + .../buttons/icon-button/docs/usage-example.js | 13 ++ .../icon-button/{index.js => index.ts} | 0 .../buttons/icon-button/package.json | 5 +- .../buttons/icon-button/src/icon-button.js | 148 --------------- .../icon-button/src/icon-button.story.js | 5 +- ...button.styles.js => icon-button.styles.ts} | 45 ++++- .../buttons/icon-button/src/icon-button.tsx | 142 ++++++++++++++ .../icon-button/src/{index.js => index.ts} | 0 .../src/{version.js => version.ts} | 0 .../buttons/link-button/package.json | 3 +- .../buttons/primary-button/README.md | 90 ++++++--- .../primary-button/docs/additional-info.md | 7 + .../primary-button/docs/usage-example.js | 14 ++ .../primary-button/{index.js => index.ts} | 0 .../buttons/primary-button/package.json | 5 +- .../primary-button/src/{index.js => index.ts} | 0 .../primary-button/src/primary-button.js | 90 --------- ...ton.styles.js => primary-button.styles.ts} | 10 +- .../primary-button/src/primary-button.tsx | 130 +++++++++++++ .../src/{version.js => version.ts} | 0 .../secondary-button/{index.js => index.ts} | 0 .../buttons/secondary-button/package.json | 3 +- .../src/{index.js => index.ts} | 0 .../secondary-button/src/secondary-button.js | 173 ----------------- .../src/secondary-button.spec.js | 25 +-- .../secondary-button/src/secondary-button.tsx | 177 ++++++++++++++++++ .../src/{version.js => version.ts} | 0 .../buttons/secondary-icon-button/README.md | 84 ++++++--- .../docs/additional-info.md | 19 ++ .../docs/usage-example.js | 13 ++ .../{index.js => index.ts} | 0 .../secondary-icon-button/package.json | 5 +- .../src/{index.js => index.ts} | 0 .../src/secondary-icon-button.js | 55 ------ ...les.js => secondary-icon-button.styles.ts} | 17 +- .../src/secondary-icon-button.tsx | 83 ++++++++ .../src/{version.js => version.ts} | 0 51 files changed, 1059 insertions(+), 665 deletions(-) create mode 100644 .changeset/five-impalas-impress.md create mode 100644 .changeset/sour-pillows-build.md create mode 100644 packages/components/buttons/flat-button/docs/additional-info.md create mode 100644 packages/components/buttons/flat-button/docs/usage-example.js rename packages/components/buttons/flat-button/{index.js => index.ts} (100%) create mode 100644 packages/components/buttons/flat-button/src/flat-button.styles.ts rename packages/components/buttons/flat-button/src/{flat-button.js => flat-button.tsx} (61%) rename packages/components/buttons/flat-button/src/{index.js => index.ts} (100%) rename packages/components/buttons/flat-button/src/{version.js => version.ts} (100%) create mode 100644 packages/components/buttons/icon-button/docs/additional-info.md create mode 100644 packages/components/buttons/icon-button/docs/usage-example.js rename packages/components/buttons/icon-button/{index.js => index.ts} (100%) delete mode 100644 packages/components/buttons/icon-button/src/icon-button.js rename packages/components/buttons/icon-button/src/{icon-button.styles.js => icon-button.styles.ts} (80%) create mode 100644 packages/components/buttons/icon-button/src/icon-button.tsx rename packages/components/buttons/icon-button/src/{index.js => index.ts} (100%) rename packages/components/buttons/icon-button/src/{version.js => version.ts} (100%) create mode 100644 packages/components/buttons/primary-button/docs/additional-info.md create mode 100644 packages/components/buttons/primary-button/docs/usage-example.js rename packages/components/buttons/primary-button/{index.js => index.ts} (100%) rename packages/components/buttons/primary-button/src/{index.js => index.ts} (100%) delete mode 100644 packages/components/buttons/primary-button/src/primary-button.js rename packages/components/buttons/primary-button/src/{primary-button.styles.js => primary-button.styles.ts} (91%) create mode 100644 packages/components/buttons/primary-button/src/primary-button.tsx rename packages/components/buttons/primary-button/src/{version.js => version.ts} (100%) rename packages/components/buttons/secondary-button/{index.js => index.ts} (100%) rename packages/components/buttons/secondary-button/src/{index.js => index.ts} (100%) delete mode 100644 packages/components/buttons/secondary-button/src/secondary-button.js create mode 100644 packages/components/buttons/secondary-button/src/secondary-button.tsx rename packages/components/buttons/secondary-button/src/{version.js => version.ts} (100%) create mode 100644 packages/components/buttons/secondary-icon-button/docs/additional-info.md create mode 100644 packages/components/buttons/secondary-icon-button/docs/usage-example.js rename packages/components/buttons/secondary-icon-button/{index.js => index.ts} (100%) rename packages/components/buttons/secondary-icon-button/src/{index.js => index.ts} (100%) delete mode 100644 packages/components/buttons/secondary-icon-button/src/secondary-icon-button.js rename packages/components/buttons/secondary-icon-button/src/{secondary-icon-button.styles.js => secondary-icon-button.styles.ts} (72%) create mode 100644 packages/components/buttons/secondary-icon-button/src/secondary-icon-button.tsx rename packages/components/buttons/secondary-icon-button/src/{version.js => version.ts} (100%) diff --git a/.changeset/five-impalas-impress.md b/.changeset/five-impalas-impress.md new file mode 100644 index 0000000000..2c1b177202 --- /dev/null +++ b/.changeset/five-impalas-impress.md @@ -0,0 +1,10 @@ +--- +'@commercetools-uikit/accessible-button': patch +'@commercetools-uikit/flat-button': patch +'@commercetools-uikit/icon-button': patch +'@commercetools-uikit/primary-button': patch +'@commercetools-uikit/secondary-button': patch +'@commercetools-uikit/secondary-icon-button': patch +--- + +Migrate to `TypeScript` and migrate docs to README generator. diff --git a/.changeset/sour-pillows-build.md b/.changeset/sour-pillows-build.md new file mode 100644 index 0000000000..308745241b --- /dev/null +++ b/.changeset/sour-pillows-build.md @@ -0,0 +1,5 @@ +--- +'@commercetools-uikit/accessible-button': patch +--- + +Update `buttonAttributes` to support `unknown` type. diff --git a/packages/components/buttons/accessible-button/src/accessible-button.tsx b/packages/components/buttons/accessible-button/src/accessible-button.tsx index 5818a8a320..25ecacbc78 100644 --- a/packages/components/buttons/accessible-button/src/accessible-button.tsx +++ b/packages/components/buttons/accessible-button/src/accessible-button.tsx @@ -75,7 +75,7 @@ type TAccessibleButtonProps = { /** * Any HTML attributes to be forwarded to the HTML element. */ - buttonAttributes?: Record; + buttonAttributes?: Record; }; const defaultProps: Pick< diff --git a/packages/components/buttons/flat-button/README.md b/packages/components/buttons/flat-button/README.md index e855c34e40..cea7459d25 100644 --- a/packages/components/buttons/flat-button/README.md +++ b/packages/components/buttons/flat-button/README.md @@ -1,44 +1,78 @@ -# Buttons: Flat Button + + + +# FlatButton ## Description Flat buttons are minimal and a flat variation of primary and secondary buttons. +## Installation + +``` +yarn add @commercetools-uikit/flat-button +``` + +``` +npm --save install @commercetools-uikit/flat-button +``` + +Additionally install the peer dependencies (if not present) + +``` +yarn add react +``` + +``` +npm --save install react +``` + ## Usage -```js +```jsx +import React from 'react'; import FlatButton from '@commercetools-uikit/flat-button'; +import { InformationIcon } from '@commercetools-uikit/icons'; -} - label="A label text" - onClick={() => alert('Button clicked')} - isDisabled={false} -/>; -``` +const Example = () => ( + } + label="A label text" + onClick={() => alert('Button clicked')} + isDisabled={false} + /> +); -iconClass label url onClick +export default Example; +``` ## Properties -| Props | Type | Required | Values | Default | Description | -| -------------- | --------------------- | :------: | --------------------------------- | --------- | -------------------------------------------------------------------------------------------------------------------------------------------- | -| `tone` | `oneOf` | - | `primary`, `secondary`, `iverted` | `primary` | - | -| `type` | `string` | - | `submit`, `reset`, `button` | `button` | Used as the HTML `type` attribute. | -| `label` | `string` | ✅ | - | - | Should describe what the button is for | -| `onClick` | `func` | ✅ | - | - | What the button will trigger when clicked | -| `icon` | `element` | - | - | - | The icon of the button | -| `iconPosition` | `oneOf` | - | `left`, `right` | `left` | The position of the icon | -| `isDisabled` | `boolean` | - | - | - | Tells when the button should present a disabled state | -| `as` | `string` or `element` | - | - | - | You may pass in a string like "a" to have the button render as an anchor tag instead. Or you could pass in a React Component, like a `Link`. | +| Props | Type | Required | Default | Description | +| -------------- | ----------------------------------------------------------------------- | :------: | ----------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `as` | `union`
Possible values:
`string , ElementType` | | | You may pass in a string like "a" to have the button render as an anchor tag instead.
Or you could pass in a React Component, like a `Link`. | +| `tone` | `union`
Possible values:
`'primary' , 'secondary' , 'inverted'` | | `'primary'` | Indicates the color scheme of button | +| `type` | `union`
Possible values:
`'submit' , 'reset' , 'button'` | | `'button'` | Used as the HTML `type` attribute. | +| `label` | `string` | ✅ | | Should describe what the button is for | +| `onClick` | `Function`
[See signature.](#signature-onClick) | | | Handler when the button is clicked
Signature: (event: MouseEvent\ void | +| `icon` | `ReactReactElement` | | | The icon of the button | +| `iconPosition` | `union`
Possible values:
`'left' , 'right'` | | `'left'` | The position of the icon | +| `isDisabled` | `boolean` | | `false` | Determines if the button is disabled.
Note that this influences the `tone` and `onClick` will not be triggered in this state. | + +## Signatures -The component further forwards all valid HTML attributes to the underlying `button` component. +### Signature `onClick` + +```ts +( + event: MouseEvent | KeyboardEvent +) => void +``` ## Where to use Main Functions and use cases are: - Secondary or primary action _example: clear filters_ - - Expand/Collapse list of fields _example: product attributes_ diff --git a/packages/components/buttons/flat-button/docs/additional-info.md b/packages/components/buttons/flat-button/docs/additional-info.md new file mode 100644 index 0000000000..d9796b0477 --- /dev/null +++ b/packages/components/buttons/flat-button/docs/additional-info.md @@ -0,0 +1,6 @@ +## Where to use + +Main Functions and use cases are: + +- Secondary or primary action _example: clear filters_ +- Expand/Collapse list of fields _example: product attributes_ diff --git a/packages/components/buttons/flat-button/docs/usage-example.js b/packages/components/buttons/flat-button/docs/usage-example.js new file mode 100644 index 0000000000..56f304333f --- /dev/null +++ b/packages/components/buttons/flat-button/docs/usage-example.js @@ -0,0 +1,15 @@ +import React from 'react'; +import FlatButton from '@commercetools-uikit/flat-button'; +import { InformationIcon } from '@commercetools-uikit/icons'; + +const Example = () => ( + } + label="A label text" + onClick={() => alert('Button clicked')} + isDisabled={false} + /> +); + +export default Example; diff --git a/packages/components/buttons/flat-button/index.js b/packages/components/buttons/flat-button/index.ts similarity index 100% rename from packages/components/buttons/flat-button/index.js rename to packages/components/buttons/flat-button/index.ts diff --git a/packages/components/buttons/flat-button/package.json b/packages/components/buttons/flat-button/package.json index 5ccc1c957b..b573258253 100644 --- a/packages/components/buttons/flat-button/package.json +++ b/packages/components/buttons/flat-button/package.json @@ -34,8 +34,7 @@ "@emotion/styled": "^11.0.0", "common-tags": "1.8.0", "lodash": "4.17.20", - "prop-types": "15.7.2", - "react-required-if": "1.0.3" + "prop-types": "15.7.2" }, "devDependencies": { "react": "17.0.1" diff --git a/packages/components/buttons/flat-button/src/flat-button.styles.ts b/packages/components/buttons/flat-button/src/flat-button.styles.ts new file mode 100644 index 0000000000..fa28a4c206 --- /dev/null +++ b/packages/components/buttons/flat-button/src/flat-button.styles.ts @@ -0,0 +1,30 @@ +import type { TFlatButtonProps, TExtendedTheme } from './flat-button'; + +export const getButtonIconColor = ( + props: Pick +) => { + if (props.isDisabled) return 'neutral60'; + else if (props.tone === 'primary') return 'primary'; + else if (props.tone === 'secondary') return 'solid'; + else if (props.tone === 'inverted') return 'surface'; + return 'solid'; +}; + +export const getTextColor = ( + tone: TFlatButtonProps['tone'], + isHover: boolean = false, + overwrittenVars: TExtendedTheme +): string => { + switch (tone) { + case 'primary': + return isHover + ? overwrittenVars.colorPrimary25 + : overwrittenVars.colorPrimary; + case 'secondary': + return overwrittenVars.colorSolid; + case 'inverted': + return overwrittenVars.fontColorForTextWhenInverted; + default: + return 'inherit'; + } +}; diff --git a/packages/components/buttons/flat-button/src/flat-button.js b/packages/components/buttons/flat-button/src/flat-button.tsx similarity index 61% rename from packages/components/buttons/flat-button/src/flat-button.js rename to packages/components/buttons/flat-button/src/flat-button.tsx index a760bf2551..23559ff38e 100644 --- a/packages/components/buttons/flat-button/src/flat-button.js +++ b/packages/components/buttons/flat-button/src/flat-button.tsx @@ -1,29 +1,80 @@ -import React from 'react'; -import PropTypes from 'prop-types'; +import type { Theme } from '@emotion/react'; +import React, { ElementType, MouseEvent, KeyboardEvent } from 'react'; import { css, useTheme } from '@emotion/react'; import omit from 'lodash/omit'; -import requiredIf from 'react-required-if'; import { customProperties as vars } from '@commercetools-uikit/design-system'; import { filterInvalidAttributes } from '@commercetools-uikit/utils'; import Text from '@commercetools-uikit/text'; import AccessibleButton from '@commercetools-uikit/accessible-button'; +import { getTextColor, getButtonIconColor } from './flat-button.styles'; const propsToOmit = ['type']; -const ButtonIcon = (props) => { - if (!props.icon) return null; +export type TExtendedTheme = Theme & { + [key: string]: string; +}; +export type TFlatButtonProps = { + /** + * You may pass in a string like "a" to have the button render as an anchor tag instead. + *
+ * Or you could pass in a React Component, like a `Link`. + */ + as?: string | ElementType; + /** + * Indicates the color scheme of button + */ + tone?: 'primary' | 'secondary' | 'inverted'; + /** + * Used as the HTML `type` attribute. + */ + type?: 'submit' | 'reset' | 'button'; + /** + * Should describe what the button is for + */ + label: string; + /** + * Handler when the button is clicked + *
+ * Signature: (event: MouseEvent void + */ + onClick?: ( + event: MouseEvent | KeyboardEvent + ) => void; + /** + * The icon of the button + */ + icon?: React.ReactElement; + /** + * The position of the icon + */ + iconPosition?: 'left' | 'right'; + /** + * Determines if the button is disabled. + *
+ * Note that this influences the `tone` and `onClick` will not be triggered in this state. + */ + isDisabled?: boolean; +}; - let iconColor = 'solid'; - if (props.isDisabled) iconColor = 'neutral60'; - else if (props.tone === 'primary') iconColor = 'primary'; - else if (props.tone === 'secondary') iconColor = 'solid'; - else if (props.tone === 'inverted') iconColor = 'surface'; +const defaultProps: Pick< + TFlatButtonProps, + 'tone' | 'isDisabled' | 'type' | 'iconPosition' +> = { + tone: 'primary', + type: 'button', + iconPosition: 'left', + isDisabled: false, +}; +const ButtonIcon = ( + props: Pick +) => { + if (!props.icon) return null; + const iconColor = getButtonIconColor(props); const Icon = React.cloneElement(props.icon, { size: 'medium', color: iconColor, }); - if (props.as && props.as !== 'button') { return ( { return Icon; }; ButtonIcon.displayName = 'ButtonIcon'; -ButtonIcon.propTypes = { - icon: PropTypes.element, - tone: PropTypes.oneOf(['primary', 'secondary', 'inverted']), - isDisabled: PropTypes.bool, -}; -const getTextColor = (tone, isHover = false, overwrittenVars) => { - switch (tone) { - case 'primary': - return isHover - ? overwrittenVars.colorPrimary25 - : overwrittenVars.colorPrimary; - case 'secondary': - return overwrittenVars.colorSolid; - case 'inverted': - return overwrittenVars.fontColorForTextWhenInverted; - default: - return 'inherit'; - } -}; - -export const FlatButton = (props) => { +const FlatButton = (props: TFlatButtonProps) => { const dataProps = { 'data-track-component': 'FlatButton', ...filterInvalidAttributes(omit(props, propsToOmit)), @@ -67,11 +98,13 @@ export const FlatButton = (props) => { // we fall back to `isDisabled` disabled: props.isDisabled, }; + const theme = useTheme(); - const overwrittenVars = { + const overwrittenVars: TExtendedTheme = { ...vars, ...theme, }; + return ( { }; FlatButton.displayName = 'FlatButton'; -FlatButton.propTypes = { - as: PropTypes.oneOfType([PropTypes.string, PropTypes.elementType]), - tone: PropTypes.oneOf(['primary', 'secondary', 'inverted']), - type: PropTypes.oneOf(['submit', 'reset', 'button']), - label: PropTypes.string.isRequired, - onClick: requiredIf(PropTypes.func, (props) => !props.as), - icon: PropTypes.element, - iconPosition: PropTypes.oneOf(['left', 'right']), - isDisabled: PropTypes.bool, -}; -FlatButton.defaultProps = { - tone: 'primary', - type: 'button', - iconPosition: 'left', - isDisabled: false, -}; +FlatButton.defaultProps = defaultProps; export default FlatButton; diff --git a/packages/components/buttons/flat-button/src/index.js b/packages/components/buttons/flat-button/src/index.ts similarity index 100% rename from packages/components/buttons/flat-button/src/index.js rename to packages/components/buttons/flat-button/src/index.ts diff --git a/packages/components/buttons/flat-button/src/version.js b/packages/components/buttons/flat-button/src/version.ts similarity index 100% rename from packages/components/buttons/flat-button/src/version.js rename to packages/components/buttons/flat-button/src/version.ts diff --git a/packages/components/buttons/icon-button/README.md b/packages/components/buttons/icon-button/README.md index 8d3c8186c8..10331af150 100644 --- a/packages/components/buttons/icon-button/README.md +++ b/packages/components/buttons/icon-button/README.md @@ -1,49 +1,77 @@ -# Buttons: Icon Button + + + +# IconButton ## Description -Icon Buttons are "icon-only" buttons. They trigger an action when clicked -(`onClick` prop). You must also pass a label for accessibility reasons. +Icon buttons are icon-only buttons. They trigger an action when clicked (\`onClick\` prop). You must also pass a label for accessibility reasons. + +## Installation + +``` +yarn add @commercetools-uikit/icon-button +``` + +``` +npm --save install @commercetools-uikit/icon-button +``` + +Additionally install the peer dependencies (if not present) + +``` +yarn add react +``` + +``` +npm --save install react +``` ## Usage -```js +```jsx +import React from 'react'; import IconButton from '@commercetools-uikit/icon-button'; +import { InformationIcon } from '@commercetools-uikit/icons'; -} - label="Alerts a message" - onClick={() => alert('Button clicked')} -/>; +const Example = () => ( + } + label="A label text" + onClick={() => alert('Button clicked')} + /> +); + +export default Example; ``` ## Properties -| Props | Type | Required | Values | Default | Description | -| ---------------- | --------------------- | :------: | --------------------------- | ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ | -| `type` | `string` | - | `submit`, `reset`, `button` | `button` | Used as the HTML `type` attribute. | -| `label` | `string` | ✅ | - | - | Should describe what the button does, for accessibility purposes (screen-reader users) | -| `icon` | `node` | - | - | - | Likely an `Icon` component | -| `isToggleButton` | `bool` | - | - | `false` | If this is active, it means the button will persist in an "active" state when toggled (see `isToggled`), and back to normal state when untoggled | -| `isToggled` | `bool` | - | - | - | Tells when the button should present a toggled state. It does not have any effect when `isToggleButton` is false | -| `isDisabled` | `bool` | - | - | - | Tells when the button should present a disabled state | -| `onClick` | `func` | ✅ | - | - | What the button will trigger when clicked | -| `shape` | `oneOf` | - | `round`, `square` | `round` | The container shape of the button | -| `size` | `oneOf` | - | `big`, `medium`, `small` | `big` | - | -| `theme` | `oneOf` | - | `default` | `info`, `primary` | The component may have a theme only if `isToggleButton` is true | -| `as` | `string` or `element` | - | - | - | You may pass in a string like "a" to have the button render as an anchor tag instead. Or you could pass in a React Component, like a `Link`. | - -The component further forwards all valid HTML attributes to the underlying `button` component. +| Props | Type | Required | Default | Description | +| ---------------- | ----------------------------------------------------------------- | :------: | ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | +| `as` | `union`
Possible values:
`string , ElementType` | | | an `ElementType`.
You may pass in a string like "a" to have the button render as an anchor tag instead. | +| `type` | `union`
Possible values:
`'button' , 'reset' , 'submit'` | | `'button'` | Used as the HTML type attribute. | +| `label` | `string` | ✅ | | Should describe what the button does, for accessibility purposes (screen-reader users) | +| `icon` | `ReactReactElement` | | | an component | +| `isToggleButton` | `boolean` | | `false` | If this is active, it means the button will persist in an "active" state when toggled (see `isToggled`), and back to normal state when untoggled. | +| `isToggled` | `boolean` | | | Tells when the button should present a toggled state. It does not have any effect when `isToggleButton` is `false`. | +| `isDisabled` | `boolean` | | | Tells when the button should present a disabled state. | +| `onClick` | `Function`
[See signature.](#signature-onClick) | | | Handler when the button is clicked
Signature: (event: MouseEvent\ void | +| `shape` | `union`
Possible values:
`'round' , 'square'` | | `'round'` | The container shape of the button. | +| `theme` | `union`
Possible values:
`'default' , 'primary' , 'info'` | | `'default'` | The component may have a theme only if `isToggleButton` is `true` | +| `size` | `union`
Possible values:
`'small' , 'medium' , 'big'` | | `'big'` | | -## Where to use +## Signatures -Main Functions and use cases are: +### Signature `onClick` -- Secondary action _example: Delete product_ - -- Minimize effect _example: Reordering table_ +```ts +( + event: MouseEvent | KeyboardEvent +) => void +``` -- Highlight actions _example: Master variant, set default Shipping billing - address_ +### Examples in the Merchant Center -- Save space _example: Manage custom views_ +- Secondary action example: Delete product +- Minimize effect example: Reordering table diff --git a/packages/components/buttons/icon-button/docs/additional-info.md b/packages/components/buttons/icon-button/docs/additional-info.md new file mode 100644 index 0000000000..b89e18414e --- /dev/null +++ b/packages/components/buttons/icon-button/docs/additional-info.md @@ -0,0 +1,4 @@ +### Examples in the Merchant Center + +- Secondary action example: Delete product +- Minimize effect example: Reordering table diff --git a/packages/components/buttons/icon-button/docs/usage-example.js b/packages/components/buttons/icon-button/docs/usage-example.js new file mode 100644 index 0000000000..f6947fdd04 --- /dev/null +++ b/packages/components/buttons/icon-button/docs/usage-example.js @@ -0,0 +1,13 @@ +import React from 'react'; +import IconButton from '@commercetools-uikit/icon-button'; +import { InformationIcon } from '@commercetools-uikit/icons'; + +const Example = () => ( + } + label="A label text" + onClick={() => alert('Button clicked')} + /> +); + +export default Example; diff --git a/packages/components/buttons/icon-button/index.js b/packages/components/buttons/icon-button/index.ts similarity index 100% rename from packages/components/buttons/icon-button/index.js rename to packages/components/buttons/icon-button/index.ts diff --git a/packages/components/buttons/icon-button/package.json b/packages/components/buttons/icon-button/package.json index 0ff7f6f7ad..6ad6ce5455 100644 --- a/packages/components/buttons/icon-button/package.json +++ b/packages/components/buttons/icon-button/package.json @@ -1,6 +1,6 @@ { "name": "@commercetools-uikit/icon-button", - "description": "Icon buttons are icon-only buttons.", + "description": "Icon buttons are icon-only buttons. They trigger an action when clicked (`onClick` prop). You must also pass a label for accessibility reasons.", "version": "12.0.0", "bugs": "https://github.com/commercetools/ui-kit/issues", "repository": { @@ -34,8 +34,7 @@ "@emotion/styled": "^11.0.0", "common-tags": "1.8.0", "lodash": "4.17.20", - "prop-types": "15.7.2", - "react-required-if": "1.0.3" + "prop-types": "15.7.2" }, "devDependencies": { "react": "17.0.1" diff --git a/packages/components/buttons/icon-button/src/icon-button.js b/packages/components/buttons/icon-button/src/icon-button.js deleted file mode 100644 index 89f180fd15..0000000000 --- a/packages/components/buttons/icon-button/src/icon-button.js +++ /dev/null @@ -1,148 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import { oneLine } from 'common-tags'; -import isNil from 'lodash/isNil'; -import { css } from '@emotion/react'; -import { customProperties as vars } from '@commercetools-uikit/design-system'; -import { filterInvalidAttributes } from '@commercetools-uikit/utils'; -import AccessibleButton from '@commercetools-uikit/accessible-button'; -import { - getStateStyles, - getShapeStyles, - getSizeStyles, - getThemeStyles, - getHoverStyles, -} from './icon-button.styles'; - -// Gets the color which the icon should have based on context of button's state/cursor behavior -const getIconThemeColor = (props) => { - const isActive = props.isToggleButton && props.isToggled; - - // if button has a theme, icon should be white when hovering/clicking - if (props.theme !== 'default' && isActive) { - if (props.isDisabled) { - return 'neutral60'; - } - return 'surface'; - } - - // if button is disabled, icon should be neutral60 - if (props.isDisabled) return 'neutral60'; - // if button is not disabled nor has a theme, return icon's default color - return props.icon.props.theme; -}; - -export const IconButton = (props) => { - const attributes = { - 'data-track-component': 'IconButton', - ...filterInvalidAttributes(props), - }; - - const isActive = props.isToggleButton && props.isToggled; - - return ( - - {props.icon && - React.cloneElement(props.icon, { - size: props.size, - color: getIconThemeColor(props), - })} - - ); -}; - -IconButton.propTypes = { - as: PropTypes.oneOfType([PropTypes.string, PropTypes.elementType]), - type: (props, propName, componentName, ...rest) => { - // the type defaults to `button`, so we don't need to handle undefined - if (props.as && props.type !== 'button') { - throw new Error( - oneLine` - ${componentName}: "${propName}" does not have any effect when - "as" is set. - ` - ); - } - return PropTypes.oneOf(['submit', 'reset', 'button'])( - props, - propName, - componentName, - ...rest - ); - }, - label: PropTypes.string.isRequired, - icon: PropTypes.node, - isToggleButton: PropTypes.bool.isRequired, - isToggled(props, propName, componentName, ...rest) { - if (props.isToggleButton) { - return PropTypes.bool.isRequired(props, propName, componentName, ...rest); - } - if (!isNil(props[propName])) - return new Error( - `Invalid prop \`${propName}\` supplied to \`${componentName}\`. \`${propName}\` does not have any effect when the button is not a toggle button.` - ); - return PropTypes.bool(props, propName, componentName, ...rest); - }, - isDisabled: PropTypes.bool, - /* FIXME: onClick should be required. - There are still some places where we can't pass it yet. - Check the spreadsheet to see where; - */ - onClick: PropTypes.func, - shape: PropTypes.oneOf(['round', 'square']), - size: PropTypes.oneOf(['small', 'medium', 'big']), - theme(props, propName, componentName, ...rest) { - if (props[propName] !== 'default' && !props.isToggleButton) { - return new Error( - `Invalid prop \`${propName}\` supplied to \`${componentName}\`. Only toggle buttons may have a theme.` - ); - } - return PropTypes.oneOf(['default', 'primary', 'info'])( - props, - propName, - componentName, - ...rest - ); - }, -}; - -IconButton.defaultProps = { - type: 'button', - theme: 'default', - size: 'big', - shape: 'round', - isToggleButton: false, -}; - -IconButton.displayName = 'IconButton'; - -export default IconButton; diff --git a/packages/components/buttons/icon-button/src/icon-button.story.js b/packages/components/buttons/icon-button/src/icon-button.story.js index 3b93b9e2a2..9580dd465b 100644 --- a/packages/components/buttons/icon-button/src/icon-button.story.js +++ b/packages/components/buttons/icon-button/src/icon-button.story.js @@ -1,4 +1,5 @@ import React from 'react'; +import PropTypes from 'prop-types'; import { MemoryRouter, Link } from 'react-router-dom'; import { storiesOf } from '@storybook/react'; import { action } from '@storybook/addon-actions'; @@ -36,8 +37,8 @@ storiesOf('Components|Buttons', module) )} onClick={action('onClick')} label={text('label', 'Accessibility text')} - isToggleButton={boolean('isToggleButton', false)} - isToggled={boolean('isToggled', false)} + isToggleButton={boolean('isToggleButton', true)} + isToggled={boolean('isToggled', true)} isDisabled={boolean('isDisabled?', false)} /> diff --git a/packages/components/buttons/icon-button/src/icon-button.styles.js b/packages/components/buttons/icon-button/src/icon-button.styles.ts similarity index 80% rename from packages/components/buttons/icon-button/src/icon-button.styles.js rename to packages/components/buttons/icon-button/src/icon-button.styles.ts index b99b8cb074..80b122929e 100644 --- a/packages/components/buttons/icon-button/src/icon-button.styles.js +++ b/packages/components/buttons/icon-button/src/icon-button.styles.ts @@ -1,6 +1,7 @@ import { warning } from '@commercetools-uikit/utils'; import { customProperties as vars } from '@commercetools-uikit/design-system'; import { css } from '@emotion/react'; +import type { TIconButtonProps } from './icon-button'; const buttonSizes = { small: '16px', @@ -8,7 +9,34 @@ const buttonSizes = { big: '32px', }; -const getStateStyles = (isDisabled, isActive, theme) => { +// Gets the color which the icon should have based on context of button's state/cursor behavior +const getIconThemeColor = ( + props: Pick< + TIconButtonProps, + 'isToggleButton' | 'isToggled' | 'theme' | 'isDisabled' | 'icon' + > +) => { + const isActive = props.isToggleButton && props.isToggled; + + // if button has a theme, icon should be white when hovering/clicking + if (props.theme !== 'default' && isActive) { + if (props.isDisabled) { + return 'neutral60'; + } + return 'surface'; + } + + // if button is disabled, icon should be neutral60 + if (props.isDisabled) return 'neutral60'; + // if button is not disabled nor has a theme, return icon's default color + return props.icon?.props.theme; +}; + +const getStateStyles = ( + isDisabled: TIconButtonProps['isDisabled'], + isActive: boolean, + theme: TIconButtonProps['theme'] +) => { if (isDisabled) { const disabledStyle = css` background-color: ${vars.colorAccent98}; @@ -113,7 +141,10 @@ const getStateStyles = (isDisabled, isActive, theme) => { `; }; -const getShapeStyles = (shape, size) => { +const getShapeStyles = ( + shape: TIconButtonProps['shape'], + size: TIconButtonProps['size'] +) => { switch (shape) { case 'round': return css` @@ -140,7 +171,7 @@ const getShapeStyles = (shape, size) => { return css``; } }; -const getSizeStyles = (size) => { +const getSizeStyles = (size: TIconButtonProps['size']) => { switch (size) { case 'small': return css` @@ -161,7 +192,7 @@ const getSizeStyles = (size) => { return css``; } }; -const getThemeStyles = (theme) => { +const getThemeStyles = (theme: TIconButtonProps['theme']) => { if (!theme) return css``; if (theme === 'default') return css``; @@ -193,7 +224,10 @@ const getThemeStyles = (theme) => { } }; -const getHoverStyles = (isDisabled, theme) => { +const getHoverStyles = ( + isDisabled: TIconButtonProps['isDisabled'], + theme: TIconButtonProps['theme'] +) => { if (theme === 'default' || isDisabled) return css``; return css` @@ -211,4 +245,5 @@ export { getShapeStyles, getSizeStyles, getThemeStyles, + getIconThemeColor, }; diff --git a/packages/components/buttons/icon-button/src/icon-button.tsx b/packages/components/buttons/icon-button/src/icon-button.tsx new file mode 100644 index 0000000000..090c8239b1 --- /dev/null +++ b/packages/components/buttons/icon-button/src/icon-button.tsx @@ -0,0 +1,142 @@ +import React, { ElementType, MouseEvent, KeyboardEvent } from 'react'; +import { css } from '@emotion/react'; +import isNil from 'lodash/isNil'; +import { customProperties as vars } from '@commercetools-uikit/design-system'; +import { filterInvalidAttributes, warning } from '@commercetools-uikit/utils'; +import AccessibleButton from '@commercetools-uikit/accessible-button'; +import { + getStateStyles, + getShapeStyles, + getSizeStyles, + getThemeStyles, + getHoverStyles, + getIconThemeColor, +} from './icon-button.styles'; + +export type TIconButtonProps = { + /** + * an `ElementType`.
+ * You may pass in a string like "a" to have the button render as an anchor tag instead. + */ + as?: string | ElementType; + /** + * Used as the HTML type attribute. + */ + type?: 'button' | 'reset' | 'submit'; + /** + * Should describe what the button does, for accessibility purposes (screen-reader users) + */ + label: string; + /** + * an component + */ + icon?: React.ReactElement; + /** + * If this is active, it means the button will persist in an "active" state when toggled (see `isToggled`), and back to normal state when untoggled. + */ + isToggleButton?: boolean; + /** + * Tells when the button should present a toggled state. It does not have any effect when `isToggleButton` is `false`. + */ + isToggled?: boolean; + /** + * Tells when the button should present a disabled state. + */ + isDisabled?: boolean; + /** + * Handler when the button is clicked + *
+ * Signature: (event: MouseEvent void + */ + onClick?: ( + event: MouseEvent | KeyboardEvent + ) => void; + /** + * The container shape of the button. + */ + shape?: 'round' | 'square'; + /** + * The component may have a theme only if `isToggleButton` is `true` + */ + theme?: 'default' | 'primary' | 'info'; + size?: 'small' | 'medium' | 'big'; +}; + +const defaultProps: Pick< + TIconButtonProps, + 'type' | 'theme' | 'size' | 'shape' | 'isToggleButton' +> = { + type: 'button', + theme: 'default', + size: 'big', + shape: 'round', + isToggleButton: false, +}; + +const IconButton = (props: TIconButtonProps) => { + if (props.isToggleButton) { + warning( + !isNil(props.isToggled), + '`IconButton`: `isToggled` is required when `isToggleButton` is provided.' + ); + } + // the type defaults to `button`, so we don't need to handle undefined + warning( + !(props.as && props.type !== 'button'), + 'IconButton`: "type" does not have any effect when "as" is set.' + ); + warning( + !(props.theme !== 'default' && !props.isToggleButton), + `Invalid prop \`theme\` supplied to \`IconButton\`. Only toggle buttons may have a theme.` + ); + + const attributes = { + 'data-track-component': 'IconButton', + ...filterInvalidAttributes(props), + }; + const isActive = Boolean(props.isToggleButton && props.isToggled); + + return ( + + {props.icon && + React.cloneElement(props.icon, { + size: props.size, + color: getIconThemeColor(props), + })} + + ); +}; + +IconButton.defaultProps = defaultProps; +IconButton.displayName = 'IconButton'; + +export default IconButton; diff --git a/packages/components/buttons/icon-button/src/index.js b/packages/components/buttons/icon-button/src/index.ts similarity index 100% rename from packages/components/buttons/icon-button/src/index.js rename to packages/components/buttons/icon-button/src/index.ts diff --git a/packages/components/buttons/icon-button/src/version.js b/packages/components/buttons/icon-button/src/version.ts similarity index 100% rename from packages/components/buttons/icon-button/src/version.js rename to packages/components/buttons/icon-button/src/version.ts diff --git a/packages/components/buttons/link-button/package.json b/packages/components/buttons/link-button/package.json index f78b07b92b..a5efaa31e2 100644 --- a/packages/components/buttons/link-button/package.json +++ b/packages/components/buttons/link-button/package.json @@ -34,8 +34,7 @@ "@emotion/styled": "^11.0.0", "common-tags": "1.8.0", "lodash": "4.17.20", - "prop-types": "15.7.2", - "react-required-if": "1.0.3" + "prop-types": "15.7.2" }, "devDependencies": { "react": "17.0.1", diff --git a/packages/components/buttons/primary-button/README.md b/packages/components/buttons/primary-button/README.md index fd345b19ea..c85a65efa1 100644 --- a/packages/components/buttons/primary-button/README.md +++ b/packages/components/buttons/primary-button/README.md @@ -1,44 +1,80 @@ -# Buttons: Primary Button + + + +# PrimaryButton ## Description -Primary buttons are used for a primary action on a page. You must also pass a -label for accessibility reasons. +Primary buttons are used for a primary action on a page. You must also pass a label for accessibility reasons. + +## Installation + +``` +yarn add @commercetools-uikit/primary-button +``` + +``` +npm --save install @commercetools-uikit/primary-button +``` + +Additionally install the peer dependencies (if not present) + +``` +yarn add react +``` + +``` +npm --save install react +``` ## Usage -```js +```jsx +import React from 'react'; import PrimaryButton from '@commercetools-uikit/primary-button'; +import { InformationIcon } from '@commercetools-uikit/icons'; -} - label="Alerts a message" - onClick={() => alert('Button clicked')} -/>; +const Example = () => ( + } + label="A label text" + onClick={() => alert('Button clicked')} + isDisabled={false} + /> +); + +export default Example; ``` ## Properties -| Props | Type | Required | Values | Default | Description | -| ------------------ | --------------------- | :------: | --------------------------- | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------ | -| `type` | `oneOf` | - | `submit`, `reset`, `button` | `button` | Used as the HTML `type` attribute. | -| `label` | `string` | ✅ | - | - | Should describe what the button does, for accessibility purposes (screen-reader users) | -| `buttonAttributes` | `object` | - | - | - | Allows setting custom attributes on the underlying button html element | -| `iconLeft` | `node` | ✅ | - | - | The left icon displayed within the button | -| `isToggleButton` | `bool` | ✅ | - | `false` | If this is active, it means the button will persist in an "active" state when toggled (see `isToggled`), and back to normal state when untoggled | -| `isToggled` | `bool` | - | - | - | Tells when the button should present a toggled state. It does not have any effect when `isToggleButton` is false | -| `isDisabled` | `bool` | - | - | - | Tells when the button should present a disabled state | -| `onClick` | `func` | ✅ | - | - | What the button will trigger when clicked | -| `size` | `oneOf` | - | `big`, `small` | `big` | - | -| `tone` | `oneOf` | - | `urgent`, `primary` | `primary` | The component may have a theme only if `isToggleButton` is true | -| `as` | `string` or `element` | - | - | - | You may pass in a string like "a" to have the button render as an anchor tag instead. Or you could pass in a React Component, like a `Link`. | +| Props | Type | Required | Default | Description | +| ---------------- | ---------------------------------------------------------------- | :------: | ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------ | +| `as` | `union`
Possible values:
`string , ElementType` | | | an `ElementType`.
You may pass in a string like "a" to have the button render as an anchor tag instead. | +| `type` | `union`
Possible values:
`'button' , 'reset' , 'submit'` | | `'button'` | Used as the HTML type attribute. | +| `label` | `string` | ✅ | | Should describe what the button does, for accessibility purposes (screen-reader users) | +| `iconLeft` | `ReactReactElement` | | | The left icon displayed within the button. | +| `isToggleButton` | `boolean` | | `false` | If this is active, it means the button will persist in an "active" state when toggled (see `isToggled`), and back to normal state when untoggled | +| `isToggled` | `boolean` | | | Tells when the button should present a toggled state. It does not have any effect when `isToggleButton` is `false`. | +| `isDisabled` | `boolean` | | | Tells when the button should present a disabled state. | +| `onClick` | `Function`
[See signature.](#signature-onClick) | | | Handler when the button is clicked.
Required when `as` is `undefined` | +| `size` | `union`
Possible values:
`'small' , 'big'` | | `'big'` | The component may have a theme only if `isToggleButton` is `true` | +| `tone` | `union`
Possible values:
`'urgent' , 'primary'` | | `'primary'` | | -The component further forwards all valid HTML attributes to the underlying `button` component. +## Signatures -Main Functions and use cases are: +### Signature `onClick` -- Primary action _example: Save changes_ +```ts +( + event: MouseEvent | KeyboardEvent +) => void +``` + +The component further forwards all valid HTML attributes to the underlying `button` component. -- Affirming affects _example: Submit a form_ +### Examples in the Merchant Center -- Attracting attention _example: Add a discount rule_ +- Primary action: Save changes +- Affirming affects: Submit a form +- Attracting attention: Add a discount rule diff --git a/packages/components/buttons/primary-button/docs/additional-info.md b/packages/components/buttons/primary-button/docs/additional-info.md new file mode 100644 index 0000000000..89717432e2 --- /dev/null +++ b/packages/components/buttons/primary-button/docs/additional-info.md @@ -0,0 +1,7 @@ +The component further forwards all valid HTML attributes to the underlying `button` component. + +### Examples in the Merchant Center + +- Primary action: Save changes +- Affirming affects: Submit a form +- Attracting attention: Add a discount rule diff --git a/packages/components/buttons/primary-button/docs/usage-example.js b/packages/components/buttons/primary-button/docs/usage-example.js new file mode 100644 index 0000000000..0a58e98b3c --- /dev/null +++ b/packages/components/buttons/primary-button/docs/usage-example.js @@ -0,0 +1,14 @@ +import React from 'react'; +import PrimaryButton from '@commercetools-uikit/primary-button'; +import { InformationIcon } from '@commercetools-uikit/icons'; + +const Example = () => ( + } + label="A label text" + onClick={() => alert('Button clicked')} + isDisabled={false} + /> +); + +export default Example; diff --git a/packages/components/buttons/primary-button/index.js b/packages/components/buttons/primary-button/index.ts similarity index 100% rename from packages/components/buttons/primary-button/index.js rename to packages/components/buttons/primary-button/index.ts diff --git a/packages/components/buttons/primary-button/package.json b/packages/components/buttons/primary-button/package.json index 96c91ca76c..59273b73b5 100644 --- a/packages/components/buttons/primary-button/package.json +++ b/packages/components/buttons/primary-button/package.json @@ -1,6 +1,6 @@ { "name": "@commercetools-uikit/primary-button", - "description": "Primary buttons are used for a primary action on a page.", + "description": "Primary buttons are used for a primary action on a page. You must also pass a label for accessibility reasons.", "version": "12.0.0", "bugs": "https://github.com/commercetools/ui-kit/issues", "repository": { @@ -34,8 +34,7 @@ "@emotion/styled": "^11.0.0", "common-tags": "1.8.0", "lodash": "4.17.20", - "prop-types": "15.7.2", - "react-required-if": "1.0.3" + "prop-types": "15.7.2" }, "devDependencies": { "react": "17.0.1" diff --git a/packages/components/buttons/primary-button/src/index.js b/packages/components/buttons/primary-button/src/index.ts similarity index 100% rename from packages/components/buttons/primary-button/src/index.js rename to packages/components/buttons/primary-button/src/index.ts diff --git a/packages/components/buttons/primary-button/src/primary-button.js b/packages/components/buttons/primary-button/src/primary-button.js deleted file mode 100644 index de1c1c0444..0000000000 --- a/packages/components/buttons/primary-button/src/primary-button.js +++ /dev/null @@ -1,90 +0,0 @@ -import PropTypes from 'prop-types'; -import React from 'react'; -import isNil from 'lodash/isNil'; -import omit from 'lodash/omit'; -import requiredIf from 'react-required-if'; -import { css } from '@emotion/react'; -import Inline from '@commercetools-uikit/spacings-inline'; -import { customProperties as vars } from '@commercetools-uikit/design-system'; -import { filterInvalidAttributes } from '@commercetools-uikit/utils'; -import AccessibleButton from '@commercetools-uikit/accessible-button'; -import { getButtonStyles } from './primary-button.styles'; - -const propsToOmit = ['type']; - -const PrimaryButton = (props) => { - const dataProps = { - 'data-track-component': 'PrimaryButton', - ...filterInvalidAttributes(omit(props, propsToOmit)), - // if there is a divergence between `isDisabled` and `disabled`, - // we fall back to `isDisabled` - disabled: props.isDisabled, - }; - - const isActive = props.isToggleButton && props.isToggled; - return ( - - - {Boolean(props.iconLeft) && ( - - {React.cloneElement(props.iconLeft, { - color: props.isDisabled ? 'neutral60' : 'surface', - size: props.size === 'small' ? 'medium' : 'big', - })} - - )} - {props.label} - - - ); -}; - -PrimaryButton.propTypes = { - as: PropTypes.oneOfType([PropTypes.string, PropTypes.elementType]), - type: PropTypes.oneOf(['submit', 'reset', 'button']), - label: PropTypes.string.isRequired, - // eslint-disable-next-line react/no-unused-prop-types - buttonAttributes: PropTypes.object, - iconLeft: PropTypes.node, - isToggleButton: PropTypes.bool.isRequired, - isToggled(props, propName, componentName, ...rest) { - if (props.isToggleButton) { - return PropTypes.bool.isRequired(props, propName, componentName, ...rest); - } - if (!isNil(props[propName])) - return new Error( - `Invalid prop \`${propName}\` supplied to \`${componentName}\`. \`${propName}\` does not have any effect when the button is not a toggle button.` - ); - return PropTypes.bool(props, propName, componentName, ...rest); - }, - isDisabled: PropTypes.bool, - onClick: requiredIf(PropTypes.func, (props) => !props.as), - size: PropTypes.oneOf(['big', 'small']), - tone: PropTypes.oneOf(['urgent', 'primary']), -}; -PrimaryButton.defaultProps = { - type: 'button', - size: 'big', - isToggleButton: false, - tone: 'primary', -}; -PrimaryButton.displayName = 'PrimaryButton'; - -export default PrimaryButton; diff --git a/packages/components/buttons/primary-button/src/primary-button.styles.js b/packages/components/buttons/primary-button/src/primary-button.styles.ts similarity index 91% rename from packages/components/buttons/primary-button/src/primary-button.styles.js rename to packages/components/buttons/primary-button/src/primary-button.styles.ts index 3f5fea427b..a2827a4467 100644 --- a/packages/components/buttons/primary-button/src/primary-button.styles.js +++ b/packages/components/buttons/primary-button/src/primary-button.styles.ts @@ -1,8 +1,9 @@ /* eslint-disable import/prefer-default-export */ import { css } from '@emotion/react'; import { customProperties as vars } from '@commercetools-uikit/design-system'; +import type { TPrimaryButtonProps } from './primary-button'; -const getSizeStyles = (size) => { +const getSizeStyles = (size: TPrimaryButtonProps['size']) => { switch (size) { case 'small': return css` @@ -23,7 +24,12 @@ const getSizeStyles = (size) => { } }; -const getButtonStyles = (isDisabled, isActive, tone, size) => { +const getButtonStyles = ( + isDisabled: TPrimaryButtonProps['isDisabled'], + isActive: boolean, + tone: TPrimaryButtonProps['tone'], + size: TPrimaryButtonProps['size'] +) => { const baseStyles = css` align-items: center; color: ${vars.colorSurface}; diff --git a/packages/components/buttons/primary-button/src/primary-button.tsx b/packages/components/buttons/primary-button/src/primary-button.tsx new file mode 100644 index 0000000000..6c6d3a69b6 --- /dev/null +++ b/packages/components/buttons/primary-button/src/primary-button.tsx @@ -0,0 +1,130 @@ +import React, { ElementType, MouseEvent, KeyboardEvent } from 'react'; +import isNil from 'lodash/isNil'; +import omit from 'lodash/omit'; +import { css } from '@emotion/react'; +import Inline from '@commercetools-uikit/spacings-inline'; +import { customProperties as vars } from '@commercetools-uikit/design-system'; +import { filterInvalidAttributes, warning } from '@commercetools-uikit/utils'; +import AccessibleButton from '@commercetools-uikit/accessible-button'; +import { getButtonStyles } from './primary-button.styles'; + +const propsToOmit = ['type']; + +export type TPrimaryButtonProps = { + /** + * an `ElementType`.
+ * You may pass in a string like "a" to have the button render as an anchor tag instead. + */ + as?: string | ElementType; + /** + * Used as the HTML type attribute. + */ + type?: 'button' | 'reset' | 'submit'; + /** + * Should describe what the button does, for accessibility purposes (screen-reader users) + */ + label: string; + /** + * The left icon displayed within the button. + */ + iconLeft?: React.ReactElement; + /** + * If this is active, it means the button will persist in an "active" state when toggled (see `isToggled`), and back to normal state when untoggled + */ + isToggleButton?: boolean; + /** + * Tells when the button should present a toggled state. It does not have any effect when `isToggleButton` is `false`. + */ + isToggled?: boolean; + /** + * Tells when the button should present a disabled state. + */ + isDisabled?: boolean; + /** + * Handler when the button is clicked. + *
+ * Required when `as` is `undefined` + */ + onClick?: ( + event: MouseEvent | KeyboardEvent + ) => void; + /** + * The component may have a theme only if `isToggleButton` is `true` + */ + size?: 'small' | 'big'; + tone?: 'urgent' | 'primary'; +}; + +const defaultProps: Pick< + TPrimaryButtonProps, + 'type' | 'tone' | 'size' | 'isToggleButton' +> = { + type: 'button', + size: 'big', + isToggleButton: false, + tone: 'primary', +}; + +const PrimaryButton = (props: TPrimaryButtonProps) => { + const dataProps = { + 'data-track-component': 'PrimaryButton', + ...filterInvalidAttributes(omit(props, propsToOmit)), + // if there is a divergence between `isDisabled` and `disabled`, + // we fall back to `isDisabled` + disabled: props.isDisabled, + }; + + if (!isNil(props.as)) { + warning( + props.onClick, + 'PrimaryButton: `onClick` is required when `as` is not provided.' + ); + } + + if (props.isToggleButton) { + warning( + !isNil(props.isToggled), + '`PrimaryButton`: `isToggled` is required when `isToggleButton` is provided.' + ); + } + + const isActive = Boolean(props.isToggleButton && props.isToggled); + return ( + + + {Boolean(props.iconLeft) && ( + + {props.iconLeft && + React.cloneElement(props.iconLeft, { + color: props.isDisabled ? 'neutral60' : 'surface', + size: props.size === 'small' ? 'medium' : 'big', + })} + + )} + {props.label} + + + ); +}; + +PrimaryButton.defaultProps = defaultProps; +PrimaryButton.displayName = 'PrimaryButton'; + +export default PrimaryButton; diff --git a/packages/components/buttons/primary-button/src/version.js b/packages/components/buttons/primary-button/src/version.ts similarity index 100% rename from packages/components/buttons/primary-button/src/version.js rename to packages/components/buttons/primary-button/src/version.ts diff --git a/packages/components/buttons/secondary-button/index.js b/packages/components/buttons/secondary-button/index.ts similarity index 100% rename from packages/components/buttons/secondary-button/index.js rename to packages/components/buttons/secondary-button/index.ts diff --git a/packages/components/buttons/secondary-button/package.json b/packages/components/buttons/secondary-button/package.json index 12b5fefe67..897bc8c209 100644 --- a/packages/components/buttons/secondary-button/package.json +++ b/packages/components/buttons/secondary-button/package.json @@ -34,8 +34,7 @@ "@emotion/styled": "^11.0.0", "common-tags": "1.8.0", "lodash": "4.17.20", - "prop-types": "15.7.2", - "react-required-if": "1.0.3" + "prop-types": "15.7.2" }, "devDependencies": { "react": "17.0.1", diff --git a/packages/components/buttons/secondary-button/src/index.js b/packages/components/buttons/secondary-button/src/index.ts similarity index 100% rename from packages/components/buttons/secondary-button/src/index.js rename to packages/components/buttons/secondary-button/src/index.ts diff --git a/packages/components/buttons/secondary-button/src/secondary-button.js b/packages/components/buttons/secondary-button/src/secondary-button.js deleted file mode 100644 index 93bc7c7aa0..0000000000 --- a/packages/components/buttons/secondary-button/src/secondary-button.js +++ /dev/null @@ -1,173 +0,0 @@ -import PropTypes from 'prop-types'; -import React from 'react'; -import { oneLine } from 'common-tags'; -import { Link } from 'react-router-dom'; -import isNil from 'lodash/isNil'; -import requiredIf from 'react-required-if'; -import { css } from '@emotion/react'; -import { customProperties as vars } from '@commercetools-uikit/design-system'; -import Inline from '@commercetools-uikit/spacings-inline'; -import { filterInvalidAttributes } from '@commercetools-uikit/utils'; -import AccessibleButton from '@commercetools-uikit/accessible-button'; -import { getStateStyles, getThemeStyles } from './secondary-button.styles'; - -// Gets the color which the icon should have based on context of button's state/cursor behavior -export const getIconColor = (props) => { - const isActive = props.isToggleButton && props.isToggled; - // if button has a theme, icon should be the same color as the theme on active state - if (props.theme !== 'default' && isActive && !props.isDisabled) return 'info'; // returns the passed in theme without overwriting - // if button is disabled, icon should be grey - if (props.isDisabled) return 'neutral60'; - // if button is not disabled nor has a theme, return icon's default color - return props.iconLeft.props.color; -}; - -export const SecondaryButton = (props) => { - const isActive = props.isToggleButton && props.isToggled; - const shouldUseLinkTag = !props.isDisabled && Boolean(props.to); - const buttonAttributes = { - 'data-track-component': 'SecondaryButton', - ...filterInvalidAttributes(props), - ...(shouldUseLinkTag ? { to: props.to } : {}), - }; - - const containerStyles = [ - css` - display: inline-flex; - background-color: ${vars.colorSurface}; - border-radius: ${vars.borderRadius6}; - box-shadow: ${vars.shadow7}; - color: ${vars.colorSolid}; - font-size: ${vars.fontSizeDefault}; - transition: background-color ${vars.transitionLinear80Ms}, - box-shadow ${vars.transitionEaseinout150Ms}; - `, - getStateStyles(props.isDisabled, isActive, props.theme), - getThemeStyles(props.theme), - ]; - - return ( - - - {Boolean(props.iconLeft) && ( - - {React.cloneElement(props.iconLeft, { - color: getIconColor(props), - })} - - )} - {props.label} - - - ); -}; - -SecondaryButton.propTypes = { - label: PropTypes.string.isRequired, - iconLeft: PropTypes.node, - isToggleButton: PropTypes.bool.isRequired, - isToggled(props, propName, componentName, ...rest) { - if (props.isToggleButton) { - return PropTypes.bool.isRequired(props, propName, componentName, ...rest); - } - if (!isNil(props[propName])) - return new Error( - `Invalid prop \`${propName}\` supplied to \`${componentName}\`. \`${propName}\` does not have any effect when the button is not a toggle button.` - ); - return PropTypes.bool(props, propName, componentName, ...rest); - }, - theme(props, propName, componentName, ...rest) { - if (props[propName] !== 'default' && !props.isToggleButton) { - return new Error( - `Invalid prop \`${propName}\` supplied to \`${componentName}\`. Only toggle buttons may have a theme.` - ); - } - return PropTypes.oneOf(['default', 'info'])( - props, - propName, - componentName, - ...rest - ); - }, - isDisabled: PropTypes.bool, - type: (props, propName, componentName, ...rest) => { - if (props.as && props.type !== 'button') { - throw new Error( - oneLine` - ${componentName}: "${propName}" does not have any effect when - "as" is set. - ` - ); - } - return PropTypes.oneOf(['submit', 'reset', 'button'])( - props, - propName, - componentName, - ...rest - ); - }, - - onClick: requiredIf(PropTypes.func, (props) => { - return !props.to && !props.as; - }), - as: PropTypes.oneOfType([PropTypes.string, PropTypes.elementType]), - to(props, propName, componentName, ...rest) { - if (props[propName]) { - if (!props.as) { - return new Error(oneLine` - Invalid prop "${propName}" supplied to "${componentName}". - "${propName}" does not have any effect when "as" is not defined`); - } - return PropTypes.oneOfType([ - PropTypes.string, - PropTypes.shape({ - pathname: PropTypes.string.isRequired, - search: PropTypes.string, - query: PropTypes.objectOf(PropTypes.string), - }), - ])(props, propName, componentName, ...rest); - } - return PropTypes.oneOfType([ - PropTypes.string, - PropTypes.shape({ - pathname: PropTypes.string.isRequired, - search: PropTypes.string, - query: PropTypes.objectOf(PropTypes.string), - }), - ])(props, propName, componentName, ...rest); - }, -}; - -SecondaryButton.defaultProps = { - type: 'button', - theme: 'default', - isToggleButton: false, -}; - -SecondaryButton.displayName = 'SecondaryButton'; - -export default SecondaryButton; diff --git a/packages/components/buttons/secondary-button/src/secondary-button.spec.js b/packages/components/buttons/secondary-button/src/secondary-button.spec.js index 92962f3e29..5f8cabf1b5 100644 --- a/packages/components/buttons/secondary-button/src/secondary-button.spec.js +++ b/packages/components/buttons/secondary-button/src/secondary-button.spec.js @@ -1,6 +1,7 @@ import React from 'react'; import { Link } from 'react-router-dom'; import { PlusBoldIcon } from '@commercetools-uikit/icons'; +import { warning } from '@commercetools-uikit/utils'; import { screen, render, @@ -9,6 +10,11 @@ import { } from '../../../../../test/test-utils'; import SecondaryButton from './secondary-button'; +jest.mock('@commercetools-uikit/utils', () => ({ + ...jest.requireActual('@commercetools-uikit/utils'), + warning: jest.fn(), +})); + const createTestProps = (custom) => ({ label: 'Add', iconLeft: , @@ -72,23 +78,12 @@ describe('rendering', () => { expect(screen.getByLabelText('Add')).toHaveAttribute('type', 'reset'); }); }); - describe('when using `to` without using `as`', () => { - /* eslint-disable no-console */ - let log; - beforeEach(() => { - log = console.error; - console.error = jest.fn(); - }); - afterEach(() => { - console.error = log; - }); + describe('when using `to` and using `as`', () => { it('should warn', () => { render(); - expect(console.error).toHaveBeenCalledWith( - expect.stringMatching(/Warning/), - 'prop', - expect.stringMatching(/Invalid prop \"to\" supplied to(.*)/), - expect.any(String) + expect(warning).toHaveBeenCalledWith( + false, + 'Invalid prop "to" supplied to "SecondaryButton". "to" does not have any effect when "as" is not defined.' ); }); }); diff --git a/packages/components/buttons/secondary-button/src/secondary-button.tsx b/packages/components/buttons/secondary-button/src/secondary-button.tsx new file mode 100644 index 0000000000..e900664605 --- /dev/null +++ b/packages/components/buttons/secondary-button/src/secondary-button.tsx @@ -0,0 +1,177 @@ +import React, { + ReactElement, + KeyboardEvent, + MouseEvent, + ElementType, +} from 'react'; +import { Link } from 'react-router-dom'; +import isNil from 'lodash/isNil'; +import { css } from '@emotion/react'; +import { customProperties as vars } from '@commercetools-uikit/design-system'; +import Inline from '@commercetools-uikit/spacings-inline'; +import { filterInvalidAttributes, warning } from '@commercetools-uikit/utils'; +import AccessibleButton from '@commercetools-uikit/accessible-button'; +import { getStateStyles, getThemeStyles } from './secondary-button.styles'; + +export type TSecondaryButtonProps = { + /** + * an `ElementType`.
+ * You may pass in a string like "a" to have the button render as an anchor tag instead. + */ + as?: string | ElementType; + /** + * Used as the HTML type attribute. + */ + type?: 'button' | 'reset' | 'submit'; + /** + * Should describe what the button does, for accessibility purposes (screen-reader users) + */ + label: string; + /** + * The left icon displayed within the button. + */ + iconLeft?: ReactElement; + /** + * If this is active, it means the button will persist in an "active" state when toggled (see `isToggled`), and back to normal state when untoggled + */ + isToggleButton?: boolean; + /** + * Tells when the button should present a toggled state. It does not have any effect when `isToggleButton` is `false`. + */ + isToggled?: boolean; + /** + * Tells when the button should present a disabled state. + */ + isDisabled?: boolean; + /** + * Handler when the button is clicked. + *
+ * Required when `as` is `undefined` + */ + onClick?: ( + event: MouseEvent | KeyboardEvent + ) => void; + + theme?: 'default' | 'info'; // TODO consider renaming this to `tone` + to?: string; +}; + +// Gets the color which the icon sho√uld have based on context of button's state/cursor behavior +export const getIconColor = ( + props: Pick< + TSecondaryButtonProps, + 'isToggleButton' | 'isToggled' | 'theme' | 'isDisabled' | 'iconLeft' + > & { + isActive?: boolean; + } +) => { + const isActive = props.isToggleButton && props.isToggled; + // if button has a theme, icon should be the same color as the theme on active state + if (props.theme !== 'default' && isActive && !props.isDisabled) return 'info'; // returns the passed in theme without overwriting + // if button is disabled, icon should be grey + if (props.isDisabled) return 'neutral60'; + // if button is not disabled nor has a theme, return icon's default color + return props.iconLeft?.props.color; +}; + +const defaultProps: Pick< + TSecondaryButtonProps, + 'type' | 'theme' | 'isToggleButton' +> = { + type: 'button', + theme: 'default', + isToggleButton: false, +}; + +export const SecondaryButton = (props: TSecondaryButtonProps) => { + const isActive = Boolean(props.isToggleButton && props.isToggled); + const shouldUseLinkTag = !props.isDisabled && Boolean(props.to); + const buttonAttributes = { + 'data-track-component': 'SecondaryButton', + ...filterInvalidAttributes(props), + ...(shouldUseLinkTag ? { to: props.to } : {}), + }; + + warning( + !(props.theme !== 'default' && !props.isToggleButton), + `Invalid prop \`theme\` supplied to \`SecondaryButton\`. Only toggle buttons may have a theme.` + ); + + warning( + !(props.as && props.type !== 'button'), + 'SecondaryButton: "type" does not have any effect when "as" is set.' + ); + + if (isNil(props.to) && isNil(props.as)) { + warning( + typeof props.onClick === 'function', + 'SecondaryButton: "onClick" is required when "to" and "as" are not defined.' + ); + } + + if (!isNil(props.to)) { + warning( + !isNil(props.as), + 'Invalid prop "to" supplied to "SecondaryButton". "to" does not have any effect when "as" is not defined.' + ); + } + + const containerStyles = [ + css` + display: flex; + align-items: center; + padding: 0 ${vars.spacingM}; + height: ${vars.bigButtonHeight}; + `, + css` + display: inline-flex; + background-color: ${vars.colorSurface}; + border-radius: ${vars.borderRadius6}; + box-shadow: ${vars.shadow7}; + color: ${vars.colorSolid}; + font-size: ${vars.fontSizeDefault}; + transition: background-color ${vars.transitionLinear80Ms}, + box-shadow ${vars.transitionEaseinout150Ms}; + `, + getStateStyles(props.isDisabled, isActive, props.theme), + getThemeStyles(props.theme), + ]; + + return ( + + + {Boolean(props.iconLeft) && ( + + {props.iconLeft && + React.cloneElement(props.iconLeft, { + color: getIconColor(props), + })} + + )} + {props.label} + + + ); +}; + +SecondaryButton.displayName = 'SecondaryButton'; +SecondaryButton.defaultProps = defaultProps; + +export default SecondaryButton; diff --git a/packages/components/buttons/secondary-button/src/version.js b/packages/components/buttons/secondary-button/src/version.ts similarity index 100% rename from packages/components/buttons/secondary-button/src/version.js rename to packages/components/buttons/secondary-button/src/version.ts diff --git a/packages/components/buttons/secondary-icon-button/README.md b/packages/components/buttons/secondary-icon-button/README.md index 88db78d8e9..0a5f200fe0 100644 --- a/packages/components/buttons/secondary-icon-button/README.md +++ b/packages/components/buttons/secondary-icon-button/README.md @@ -1,34 +1,71 @@ -# Buttons: SecondaryIconButton + + + +# SecondaryIconButton ## Description -Secondary Icon Buttons are "icon-only" buttons and a restricted version of the -``. They trigger an action when clicked (`onClick` prop). You must -also pass a label for accessibility reasons. +Secondary Icon Buttons are "icon-only" buttons and a restricted version of the \`\\`. They trigger an action when clicked (\`onClick\` prop). You must also pass a label for accessibility reasons. + +## Installation + +``` +yarn add @commercetools-uikit/secondary-icon-button +``` + +``` +npm --save install @commercetools-uikit/secondary-icon-button +``` + +Additionally install the peer dependencies (if not present) + +``` +yarn add react +``` + +``` +npm --save install react +``` ## Usage -```js +```jsx +import React from 'react'; import SecondaryIconButton from '@commercetools-uikit/secondary-icon-button'; +import { InformationIcon } from '@commercetools-uikit/icons'; -} - label="Next" - onClick={() => alert('Button clicked')} -/>; +const Example = () => ( + } + label="A label text" + onClick={() => alert('Button clicked')} + /> +); + +export default Example; ``` ## Properties -| Props | Type | Required | Values | Default | Description | -| ------------ | --------------------- | :------: | --------------------------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------- | -| `type` | `string` | - | `submit`, `reset`, `button` | `button` | Used as the HTML `type` attribute. | -| `label` | `string` | ✅ | - | - | Should describe what the button does, for accessibility purposes (screen-reader users) | -| `icon` | `node` | ✅ | - | - | An `Icon` component | -| `isDisabled` | `bool` | - | - | `false` | Tells when the button should present a disabled state | -| `onClick` | `func` | ✅ | - | - | What the button will trigger when clicked | -| `color` | `oneOf` | - | `solid`, `primary` | `solid` | Sets the color of the icon | -| `as` | `string` or `element` | - | - | - | You may pass in a string like "a" to have the button render as an anchor tag instead. Or you could pass in a React Component, like a `Link`. | +| Props | Type | Required | Default | Description | +| ------------ | ---------------------------------------------------------------- | :------: | ---------- | ------------------------------------------------------------------------------------------------------------------ | +| `as` | `union`
Possible values:
`string , ElementType` | | | an `ElementType`.
You may pass in a string like "a" to have the button render as an anchor tag instead. | +| `type` | `union`
Possible values:
`'submit' , 'reset' , 'button'` | | `'button'` | Used as the HTML type attribute. | +| `icon` | `ReactReactElement` | | | An component. | +| `color` | `union`
Possible values:
`'solid' , 'primary'` | | `'solid'` | Sets the color of the icon | +| `label` | `string` | ✅ | | Should describe what the button does, for accessibility purposes (screen-reader users) | +| `isDisabled` | `boolean` | | `false` | Tells when the button should present a disabled state | +| `onClick` | `Function`
[See signature.](#signature-onClick) | | | Handler when the button is clicked.
This is required if `as` is not defined. | + +## Signatures + +### Signature `onClick` + +```ts +( + event: MouseEvent | KeyboardEvent +) => void +``` The component further forwards all valid HTML attributes to the underlying `button` component. @@ -36,7 +73,7 @@ The component further forwards all valid HTML attributes to the underlying `butt The size of the button should be adjusted directly on the passed `Icon` component. Example: -```js +```jsx } label="Next" @@ -44,9 +81,8 @@ The size of the button should be adjusted directly on the passed `Icon` componen /> ``` -## Where to use +## Examples in the Merchant Center -Mostly in all places where you just need a "clickable" icon, without the complex -behaviours of the `IconButton` +Mostly in all places where you just need a "clickable" icon, without the complex behaviors of the `IconButton`. -- Pagination list _example: Go to next page_ +- Pagination list: Go to next page diff --git a/packages/components/buttons/secondary-icon-button/docs/additional-info.md b/packages/components/buttons/secondary-icon-button/docs/additional-info.md new file mode 100644 index 0000000000..5d935a1806 --- /dev/null +++ b/packages/components/buttons/secondary-icon-button/docs/additional-info.md @@ -0,0 +1,19 @@ +The component further forwards all valid HTML attributes to the underlying `button` component. + +## Note + +The size of the button should be adjusted directly on the passed `Icon` component. Example: + +```jsx +} + label="Next" + onClick={() => alert('Button clicked')} +/> +``` + +## Examples in the Merchant Center + +Mostly in all places where you just need a "clickable" icon, without the complex behaviors of the `IconButton`. + +- Pagination list: Go to next page diff --git a/packages/components/buttons/secondary-icon-button/docs/usage-example.js b/packages/components/buttons/secondary-icon-button/docs/usage-example.js new file mode 100644 index 0000000000..23c004eae8 --- /dev/null +++ b/packages/components/buttons/secondary-icon-button/docs/usage-example.js @@ -0,0 +1,13 @@ +import React from 'react'; +import SecondaryIconButton from '@commercetools-uikit/secondary-icon-button'; +import { InformationIcon } from '@commercetools-uikit/icons'; + +const Example = () => ( + } + label="A label text" + onClick={() => alert('Button clicked')} + /> +); + +export default Example; diff --git a/packages/components/buttons/secondary-icon-button/index.js b/packages/components/buttons/secondary-icon-button/index.ts similarity index 100% rename from packages/components/buttons/secondary-icon-button/index.js rename to packages/components/buttons/secondary-icon-button/index.ts diff --git a/packages/components/buttons/secondary-icon-button/package.json b/packages/components/buttons/secondary-icon-button/package.json index 51a601655f..f72a6bbf28 100644 --- a/packages/components/buttons/secondary-icon-button/package.json +++ b/packages/components/buttons/secondary-icon-button/package.json @@ -1,6 +1,6 @@ { "name": "@commercetools-uikit/secondary-icon-button", - "description": "Secondary icon buttons are a simpler version of icon buttons.", + "description": "Secondary Icon Buttons are \"icon-only\" buttons and a restricted version of the ``. They trigger an action when clicked (`onClick` prop). You must also pass a label for accessibility reasons.", "version": "12.0.0", "bugs": "https://github.com/commercetools/ui-kit/issues", "repository": { @@ -34,8 +34,7 @@ "@emotion/styled": "^11.0.0", "common-tags": "1.8.0", "lodash": "4.17.20", - "prop-types": "15.7.2", - "react-required-if": "1.0.3" + "prop-types": "15.7.2" }, "devDependencies": { "react": "17.0.1" diff --git a/packages/components/buttons/secondary-icon-button/src/index.js b/packages/components/buttons/secondary-icon-button/src/index.ts similarity index 100% rename from packages/components/buttons/secondary-icon-button/src/index.js rename to packages/components/buttons/secondary-icon-button/src/index.ts diff --git a/packages/components/buttons/secondary-icon-button/src/secondary-icon-button.js b/packages/components/buttons/secondary-icon-button/src/secondary-icon-button.js deleted file mode 100644 index 443fc11b7c..0000000000 --- a/packages/components/buttons/secondary-icon-button/src/secondary-icon-button.js +++ /dev/null @@ -1,55 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import { useTheme } from '@emotion/react'; -import omit from 'lodash/omit'; -import requiredIf from 'react-required-if'; -import { filterInvalidAttributes } from '@commercetools-uikit/utils'; -import AccessibleButton from '@commercetools-uikit/accessible-button'; -import { getBaseStyles } from './secondary-icon-button.styles'; - -const propsToOmit = ['type']; - -export const SecondaryIconButton = (props) => { - const buttonAttributes = { - ...filterInvalidAttributes(omit(props, propsToOmit)), - 'data-track-component': 'SecondaryIconButton', - // if there is a divergence between `isDisabled` and `disabled`, - // we fall back to `isDisabled` - disabled: props.isDisabled, - }; - const theme = useTheme(); - return ( - - {props.icon} - - ); -}; - -SecondaryIconButton.displayName = 'SecondaryIconButton'; - -SecondaryIconButton.propTypes = { - as: PropTypes.oneOfType([PropTypes.string, PropTypes.elementType]), - type: PropTypes.oneOf(['submit', 'reset', 'button']), - icon: PropTypes.element.isRequired, - // eslint-disable-next-line react/no-unused-prop-types - color: PropTypes.oneOf(['solid', 'primary']), - label: PropTypes.string.isRequired, - onClick: requiredIf(PropTypes.func, (props) => !props.as), - isDisabled: PropTypes.bool, -}; - -SecondaryIconButton.defaultProps = { - color: 'solid', - type: 'button', - isDisabled: false, -}; - -export default SecondaryIconButton; diff --git a/packages/components/buttons/secondary-icon-button/src/secondary-icon-button.styles.js b/packages/components/buttons/secondary-icon-button/src/secondary-icon-button.styles.ts similarity index 72% rename from packages/components/buttons/secondary-icon-button/src/secondary-icon-button.styles.js rename to packages/components/buttons/secondary-icon-button/src/secondary-icon-button.styles.ts index ca619cddc7..91170ad915 100644 --- a/packages/components/buttons/secondary-icon-button/src/secondary-icon-button.styles.js +++ b/packages/components/buttons/secondary-icon-button/src/secondary-icon-button.styles.ts @@ -1,7 +1,13 @@ +import type { Theme } from '@emotion/react'; import { css } from '@emotion/react'; import { customProperties as vars } from '@commercetools-uikit/design-system'; +import type { TSecondaryButtonProps } from './secondary-icon-button'; -const getDisabledStyle = (overwrittenVars) => { +type TExtendedTheme = { + [key: string]: string; +} & Theme; + +const getDisabledStyle = (overwrittenVars: TExtendedTheme) => { /* By using the css 'disabled' selector directly, we don't need additional logic to check the isDisabled prop */ return css` &:disabled svg * { @@ -10,7 +16,10 @@ const getDisabledStyle = (overwrittenVars) => { `; }; -const getColorStyle = (props, overwrittenVars) => { +const getColorStyle = ( + props: Pick, + overwrittenVars: TExtendedTheme +) => { switch (props.color) { case 'solid': return css` @@ -41,8 +50,8 @@ const getColorStyle = (props, overwrittenVars) => { } }; -const getBaseStyles = (props, theme) => { - const overwrittenVars = { +const getBaseStyles = (props: TSecondaryButtonProps, theme: Theme) => { + const overwrittenVars: TExtendedTheme = { ...vars, ...theme, }; diff --git a/packages/components/buttons/secondary-icon-button/src/secondary-icon-button.tsx b/packages/components/buttons/secondary-icon-button/src/secondary-icon-button.tsx new file mode 100644 index 0000000000..c544a9d491 --- /dev/null +++ b/packages/components/buttons/secondary-icon-button/src/secondary-icon-button.tsx @@ -0,0 +1,83 @@ +import React, { ElementType, MouseEvent, KeyboardEvent } from 'react'; +import { useTheme } from '@emotion/react'; +import omit from 'lodash/omit'; +import { warning, filterInvalidAttributes } from '@commercetools-uikit/utils'; +import AccessibleButton from '@commercetools-uikit/accessible-button'; +import { getBaseStyles } from './secondary-icon-button.styles'; + +const propsToOmit = ['type']; + +export type TSecondaryButtonProps = { + /** + * an `ElementType`.
+ * You may pass in a string like "a" to have the button render as an anchor tag instead. + */ + as?: string | ElementType; + /** + * Used as the HTML type attribute. + */ + type?: 'submit' | 'reset' | 'button'; + /** + * An component. + */ + icon?: React.ReactElement; + /** + * Sets the color of the icon + */ + color?: 'solid' | 'primary'; // used in `getBaseStyles` + /** + * Should describe what the button does, for accessibility purposes (screen-reader users) + */ + label: string; + /** + * Tells when the button should present a disabled state + */ + isDisabled?: boolean; + /** + * Handler when the button is clicked. + *
+ * This is required if `as` is not defined. + */ + onClick?: ( + event: MouseEvent | KeyboardEvent + ) => void; +}; + +const SecondaryIconButton = (props: TSecondaryButtonProps) => { + if (!Boolean(props.as)) { + warning( + typeof props.onClick === 'function', + 'SecondaryIconButton: "onClick" is required when "as" is not defined.' + ); + } + const buttonAttributes = { + ...filterInvalidAttributes(omit(props, propsToOmit)), + 'data-track-component': 'SecondaryIconButton', + // if there is a divergence between `isDisabled` and `disabled`, + // we fall back to `isDisabled` + disabled: props.isDisabled, + }; + const theme = useTheme(); + return ( + + {props.icon} + + ); +}; + +SecondaryIconButton.displayName = 'SecondaryIconButton'; +SecondaryIconButton.defaultProps = { + color: 'solid', + type: 'button', + isDisabled: false, +}; + +export default SecondaryIconButton; diff --git a/packages/components/buttons/secondary-icon-button/src/version.js b/packages/components/buttons/secondary-icon-button/src/version.ts similarity index 100% rename from packages/components/buttons/secondary-icon-button/src/version.js rename to packages/components/buttons/secondary-icon-button/src/version.ts From 26f97e42ca9d8941b033d112e791342c8ed759ab Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 22 Apr 2021 11:18:16 +0200 Subject: [PATCH 2/3] fix(deps): update dependency @commercetools-frontend/eslint-config-mc-app to v19 (#1878) Co-authored-by: Renovate Bot --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 10ee7ec0ec..7dcb97230a 100644 --- a/package.json +++ b/package.json @@ -67,7 +67,7 @@ "@changesets/changelog-github": "0.3.0", "@changesets/cli": "2.14.1", "@commercetools-frontend/babel-preset-mc-app": "18.5.6", - "@commercetools-frontend/eslint-config-mc-app": "18.5.4", + "@commercetools-frontend/eslint-config-mc-app": "19.0.1", "@commercetools/github-labels": "1.1.0", "@commitlint/cli": "12.0.1", "@commitlint/config-conventional": "12.0.1", diff --git a/yarn.lock b/yarn.lock index 08e8990525..fa9d401b67 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1437,10 +1437,10 @@ babel-plugin-transform-react-remove-prop-types "0.4.24" core-js "3.9.1" -"@commercetools-frontend/eslint-config-mc-app@18.5.4": - version "18.5.4" - resolved "https://registry.yarnpkg.com/@commercetools-frontend/eslint-config-mc-app/-/eslint-config-mc-app-18.5.4.tgz#9024c014ce7026b13967a939b57d7bd517095ddd" - integrity sha512-uErnMIAMyw7XonqlYKXTn6SKvZW1uCHAjEZgVE0aT75XfRuMmXs3t1XbtsziSiABkocTbM2iw6YOigZ3KQ0Iag== +"@commercetools-frontend/eslint-config-mc-app@19.0.1": + version "19.0.1" + resolved "https://registry.yarnpkg.com/@commercetools-frontend/eslint-config-mc-app/-/eslint-config-mc-app-19.0.1.tgz#b6cb4eb702972053a8460ddd6490dab3f0c80940" + integrity sha512-XonvFiv6W+hqMdR+zjCiVeceNJg8m+nn5vDtfae1DhryDkZXVXF7DWrR752EfOx4y1yUMBpRqmwBA5WETA3mUQ== dependencies: "@typescript-eslint/eslint-plugin" "^4.14.0" "@typescript-eslint/parser" "^4.14.0" From bf7749675fff92cfdf5bde489ffe5b96361091c1 Mon Sep 17 00:00:00 2001 From: Adnan Asani Date: Thu, 22 Apr 2021 14:46:26 +0200 Subject: [PATCH 3/3] refactor(components/field-errors): migrate to TypeScript (#1874) * refactor(components/field-errors): migrate to TypeScript * refactor(field-errors): remove type parameter --- .changeset/nasty-hats-fold.md | 5 ++ packages/components/field-errors/README.md | 26 +++++++--- .../field-errors/{index.js => index.ts} | 0 .../src/{field-errors.js => field-errors.tsx} | 52 +++++++++---------- .../field-errors/src/{index.js => index.ts} | 0 .../src/{messages.js => messages.ts} | 0 .../src/{version.js => version.ts} | 0 7 files changed, 49 insertions(+), 34 deletions(-) create mode 100644 .changeset/nasty-hats-fold.md rename packages/components/field-errors/{index.js => index.ts} (100%) rename packages/components/field-errors/src/{field-errors.js => field-errors.tsx} (89%) rename packages/components/field-errors/src/{index.js => index.ts} (100%) rename packages/components/field-errors/src/{messages.js => messages.ts} (100%) rename packages/components/field-errors/src/{version.js => version.ts} (100%) diff --git a/.changeset/nasty-hats-fold.md b/.changeset/nasty-hats-fold.md new file mode 100644 index 0000000000..c0a2478b3d --- /dev/null +++ b/.changeset/nasty-hats-fold.md @@ -0,0 +1,5 @@ +--- +'@commercetools-uikit/field-errors': patch +--- + +Migrate `` to TypeScript diff --git a/packages/components/field-errors/README.md b/packages/components/field-errors/README.md index 36f0b8e7b1..2c3785821f 100644 --- a/packages/components/field-errors/README.md +++ b/packages/components/field-errors/README.md @@ -72,12 +72,26 @@ export default Example; ## Properties -| Props | Type | Required | Default | Description | -| -------------------- | -------- | :------: | ------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `errors` | `object` | | | List of errors. Only entries with truthy values will count as active errors. | -| `isVisible` | `bool` | | | `true` when the error messages should be rendered. Usually you'd pass in a `touched` state of fields. | -| `renderError` | `func` | | | Function which gets called with each error key (from the `errors` prop) and may render an error message or return `null` to hand the error handling off to `renderDefaultError`.
Signature: `(key, error) => React.node` | -| `renderDefaultError` | `func` | | | Function which gets called with each error key (from the `errors` prop) for which `renderError` returned `null`. It may render an error message or return `null` to hand the error handling off to `FieldError`s built-in error handling.
Signature: `(key, error) => React.node` | +| Props | Type | Required | Default | Description | +| -------------------- | -------------------------------------------------------------- | :------: | ------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `errors` | `Record` | ✅ | | List of errors. Only entries with truthy values will count as active errors. | +| `isVisible` | `boolean` | | | `true` when the error messages should be rendered. Usually you'd pass in a `touched` state of fields. | +| `renderError` | `Function`
[See signature.](#signature-renderError) | | | Function which gets called with each error key (from the `errors` prop) and may render an error message or return `null` to hand the error handling off to `renderDefaultError`. | +| `renderDefaultError` | `Function`
[See signature.](#signature-renderDefaultError) | | | Function which gets called with each error key (from the `errors` prop) for which `renderError` returned `null`. It may render an error message or return `null` to hand the error handling off to `FieldError`s built-in error handling. | + +## Signatures + +### Signature `renderError` + +```ts +(key: string, error?: boolean) => React.ReactNode; +``` + +### Signature `renderDefaultError` + +```ts +(key: string, error?: boolean) => React.ReactNode; +``` ## Static properties diff --git a/packages/components/field-errors/index.js b/packages/components/field-errors/index.ts similarity index 100% rename from packages/components/field-errors/index.js rename to packages/components/field-errors/index.ts diff --git a/packages/components/field-errors/src/field-errors.js b/packages/components/field-errors/src/field-errors.tsx similarity index 89% rename from packages/components/field-errors/src/field-errors.js rename to packages/components/field-errors/src/field-errors.tsx index 9a7f582644..b180716223 100644 --- a/packages/components/field-errors/src/field-errors.js +++ b/packages/components/field-errors/src/field-errors.tsx @@ -1,12 +1,33 @@ import React from 'react'; -import PropTypes from 'prop-types'; import { FormattedMessage } from 'react-intl'; import { ErrorMessage } from '@commercetools-uikit/messages'; import messages from './messages'; -const isObject = (obj) => typeof obj === 'object'; +const isObject = (obj: unknown): boolean => typeof obj === 'object'; -const FieldErrors = (props) => { +type TErrorRenderer = (key: string, error?: boolean) => React.ReactNode; + +type TFieldErrorsProps = { + /** + * List of errors. Only entries with truthy values will count as active errors. + */ + errors: Record; + /** + * `true` when the error messages should be rendered. Usually you'd pass in a `touched` state of fields. + */ + isVisible?: boolean; + /** + * Function which gets called with each error key (from the `errors` prop) and may render an error message or return `null` to hand the error handling off to `renderDefaultError`. + */ + renderError?: TErrorRenderer; + /** + * Function which gets called with each error key (from the `errors` prop) for which `renderError` returned `null`. + * It may render an error message or return `null` to hand the error handling off to `FieldError`s built-in error handling. + */ + renderDefaultError?: TErrorRenderer; +}; + +const FieldErrors = (props: TFieldErrorsProps) => { if (!props.isVisible) return null; if (!isObject(props.errors)) return null; @@ -63,31 +84,6 @@ const FieldErrors = (props) => { }; FieldErrors.displayName = 'FieldErrors'; - -FieldErrors.propTypes = { - /** - * List of errors. Only entries with truthy values will count as active errors. - */ - errors: PropTypes.object, - /** - * `true` when the error messages should be rendered. Usually you'd pass in a `touched` state of fields. - */ - isVisible: PropTypes.bool, - /** - * Function which gets called with each error key (from the `errors` prop) and may render an error message or return `null` to hand the error handling off to `renderDefaultError`. - *
- * Signature: `(key, error) => React.node` - */ - renderError: PropTypes.func, - /** - * Function which gets called with each error key (from the `errors` prop) for which `renderError` returned `null`. - * It may render an error message or return `null` to hand the error handling off to `FieldError`s built-in error handling. - *
- * Signature: `(key, error) => React.node` - */ - renderDefaultError: PropTypes.func, -}; - FieldErrors.errorTypes = { MISSING: 'missing', NEGATIVE: 'negative', diff --git a/packages/components/field-errors/src/index.js b/packages/components/field-errors/src/index.ts similarity index 100% rename from packages/components/field-errors/src/index.js rename to packages/components/field-errors/src/index.ts diff --git a/packages/components/field-errors/src/messages.js b/packages/components/field-errors/src/messages.ts similarity index 100% rename from packages/components/field-errors/src/messages.js rename to packages/components/field-errors/src/messages.ts diff --git a/packages/components/field-errors/src/version.js b/packages/components/field-errors/src/version.ts similarity index 100% rename from packages/components/field-errors/src/version.js rename to packages/components/field-errors/src/version.ts