diff --git a/.storybook/main.js b/.storybook/main.js index 0fb4e15cbb..886ea2f138 100644 --- a/.storybook/main.js +++ b/.storybook/main.js @@ -30,6 +30,18 @@ module.exports = { // https://storybook.js.org/docs/react/configure/overview#configure-story-loading stories: isTesting ? ['../packages/*/src/**/*.stories.e2e.@(js|jsx)'] - : ['../packages/*/src/**/*.stories.@(js|jsx)'], - addons: ['@storybook/preset-create-react-app', 'storybook-addon-jsx'], + : [ + '../docs/**/*.stories.mdx', + '../packages/*/src/**/*.stories.@(js|jsx)', + ], + addons: [ + '@storybook/preset-create-react-app', + '@storybook/addon-essentials', + { + name: '@storybook/addon-storysource', + options: { loaderOptions: { injectDecorator: false } }, + }, + 'storybook-addon-jsx', + '@storybook/addon-a11y', + ], } diff --git a/.storybook/manager-head.html b/.storybook/manager-head.html new file mode 100644 index 0000000000..8182f775d5 --- /dev/null +++ b/.storybook/manager-head.html @@ -0,0 +1,13 @@ + + + + + + + diff --git a/.storybook/manager.js b/.storybook/manager.js new file mode 100644 index 0000000000..f121dd1529 --- /dev/null +++ b/.storybook/manager.js @@ -0,0 +1,6 @@ +import { addons } from '@storybook/addons' +import theme from './theme' + +addons.setConfig({ + theme: theme +}) diff --git a/.storybook/preview-head.html b/.storybook/preview-head.html new file mode 100644 index 0000000000..c6c27fa527 --- /dev/null +++ b/.storybook/preview-head.html @@ -0,0 +1,3 @@ + + + diff --git a/.storybook/preview.js b/.storybook/preview.js index e7f5b87880..bbbf905fb5 100644 --- a/.storybook/preview.js +++ b/.storybook/preview.js @@ -2,6 +2,16 @@ import '@fontsource/roboto/latin.css' import React, { Fragment } from 'react' import { jsxDecorator } from 'storybook-addon-jsx' import { addDecorator, addParameters } from '@storybook/react' +import '@storybook/addon-console' +import { + Title, + Subtitle, + Description, + Primary, + ArgsTable, + Stories, + PRIMARY_STORY, +} from '@storybook/addon-docs/blocks' import { CssReset } from '@dhis2/ui-core' // Enable storybook jsx visualization @@ -32,16 +42,51 @@ addDecorator(Component => ( )) -/** - * Sort all our stories alphabetically - * - * See: https://storybook.js.org/docs/configurations/options-parameter/#sorting-stories - */ addParameters({ options: { - storySort: (a, b) => - a[1].kind === b[1].kind - ? 0 - : a[1].id.localeCompare(b[1].id, undefined, { numeric: true }), + storySort: { + // Manually sort top content + order: [ + 'About This Documentation', + ['For readers', 'For maintainers'], + 'Using UI', + [ + 'Getting Started', + 'Troubleshooting', + 'Advanced Usage', + 'Recipes', + ], + ], + // Then sort the rest alphabetically + method: 'alphabetical', + }, + }, + docs: { + // Customize docs page layout (in order to rename 'Stories' section) + page: () => ( + <> + + <Subtitle /> + <Description /> + <Primary /> + <ArgsTable story={PRIMARY_STORY} /> + <Stories title="Examples" /> + </> + ), + }, + jsx: { + filterProps: val => val !== undefined, + showDefaultProps: false, + functionValue: fn => fn.name, + tabStop: 4, + maxInlineAttributesLineLength: 80, + }, + // A11y addon config + a11y: { + // the target DOM element + element: '#root', + // execution mode for the addon + manual: false, }, + controls: { hideNoControlsWarning: true, expanded: true }, }) diff --git a/.storybook/static/dhis2-icon-rgb-positive.png b/.storybook/static/dhis2-icon-rgb-positive.png new file mode 100644 index 0000000000..b9ab6ba518 Binary files /dev/null and b/.storybook/static/dhis2-icon-rgb-positive.png differ diff --git a/.storybook/static/favicon.ico b/.storybook/static/favicon.ico new file mode 100644 index 0000000000..ab6e2ef70d Binary files /dev/null and b/.storybook/static/favicon.ico differ diff --git a/.storybook/static/manager.css b/.storybook/static/manager.css new file mode 100644 index 0000000000..583dcd9eed --- /dev/null +++ b/.storybook/static/manager.css @@ -0,0 +1,5 @@ +/* Styles to customize storybook manager UI (search, sidebar, etc) go here */ + +.sidebar-subheading { + color: #4a5768 !important; +} diff --git a/.storybook/static/preview.css b/.storybook/static/preview.css new file mode 100644 index 0000000000..bbdd8bb839 --- /dev/null +++ b/.storybook/static/preview.css @@ -0,0 +1,9 @@ +/* Styles for storybook docs or anything else in the 'preview' frame go here */ + +.sbdocs.sbdocs-a:hover { + text-decoration: underline; +} + +.sbdocs-expandable { + color: #004D40 !important; /* teal800 */ +} diff --git a/.storybook/theme.js b/.storybook/theme.js new file mode 100644 index 0000000000..c849ad5039 --- /dev/null +++ b/.storybook/theme.js @@ -0,0 +1,91 @@ +import { create } from '@storybook/theming/create' +import { colors } from '@dhis2/ui-constants' + +/** + * See here for an example of these values: + * https://github.com/storybookjs/storybook/blob/master/lib/theming/src/themes/light.ts + */ +export default create({ + /** Main theme ('light' or 'dark') */ + base: 'light', + + /** + * Color palette + */ + /** Not sure where this is applied */ + // colorPrimary: '#FF4785', // Storybook coral + + /** + * '4-square' icon in sidebar, sidebar highlight color, link color, + * toolbar icon color (when hovered), focused input highlight + */ + colorSecondary: colors.teal600, + + /** + * UI + */ + /** Background of side bar & rest of page: */ + appBg: colors.grey050, + + /** Background of preview & docs ('app') frame: */ + // appContentBg: 'white', + + /** Color for 'glowing' placeholders for loading stories AND lines in app frame: */ + // appBorderColor: 'red', + + /** Border radius of app frame */ + // appBorderRadius: 4, + + /** + * Typography --- + */ + /** Affects text in sidebar */ + // fontBase: '"Georgia", serif', + // fontCode: 'monospace', + + /** + * Text colors + */ + /** Color for text in sidebar and addons */ + // textColor: 'blue', + + /** Not sure where this applies */ + // textInverseColor: 'green', // 'rgba(255,255,255,0.9)', + + /** Text in search bar */ + // textMutedColor: 'purple', + + /** + * Toolbar default and active colors --- + */ + /** Color of text & icons in toolbars (addons and top bar) when unselected */ + // barTextColor: 'purple', + + /** Text color and underline of selected tab in toolbars (e.g. Canvas, Controls) */ + barSelectedColor: colors.teal600, + + /** Background color of toolbars (addons and top bar) */ + // barBg: 'yellow', + + /** + * Form colors + */ + /** Mostly self-explanatory */ + // inputBg: "red", + + /** Border while not focused - when focused, uses 'colorSecondary' */ + // inputBorder: 'purple', + + // inputTextColor: 'green', + // inputBorderRadius: 0, + + /** + * Brand + */ + brandTitle: 'DHIS2 UI', + // TODO: Better place to send this? + brandUrl: 'https://www.dhis2.org', + // TODO: Serve this statically? + brandImage: + 'https://raw.githubusercontent.com/dhis2/dhis2-identity/master/web/Logo/Default/dhis2-logo-rgb-positive.svg', +}) diff --git a/docs/advanced-usage.stories.mdx b/docs/advanced-usage.stories.mdx new file mode 100644 index 0000000000..2ffa3f98b2 --- /dev/null +++ b/docs/advanced-usage.stories.mdx @@ -0,0 +1,6 @@ +import { Meta, Description } from '@storybook/addon-docs/blocks' +import AdvancedUsage from './advanced-usage.md' + +<Meta title="Using UI/Advanced Usage" /> + +<Description>{AdvancedUsage}</Description> diff --git a/docs/getting-started.stories.mdx b/docs/getting-started.stories.mdx new file mode 100644 index 0000000000..64b6e3c6a1 --- /dev/null +++ b/docs/getting-started.stories.mdx @@ -0,0 +1,6 @@ +import { Meta, Description } from '@storybook/addon-docs/blocks' +import GettingStarted from './getting-started.md' + +<Meta title="Using UI/Getting Started" /> + +<Description>{GettingStarted}</Description> diff --git a/docs/recipes/transer-infinite-loading-all-options-selected.md b/docs/recipes/transfer-infinite-loading-all-options-selected.md similarity index 100% rename from docs/recipes/transer-infinite-loading-all-options-selected.md rename to docs/recipes/transfer-infinite-loading-all-options-selected.md diff --git a/docs/recipes/transfer-infinite-loading-all-options-selected.stories.mdx b/docs/recipes/transfer-infinite-loading-all-options-selected.stories.mdx new file mode 100644 index 0000000000..738e519d1f --- /dev/null +++ b/docs/recipes/transfer-infinite-loading-all-options-selected.stories.mdx @@ -0,0 +1,6 @@ +import { Meta, Description } from '@storybook/addon-docs/blocks' +import markdown from './transfer-infinite-loading-all-options-selected.md' + +<Meta title="Using UI/Recipes/Transfer: Infinite Loading, All Options Selected" /> + +<Description>{markdown}</Description> diff --git a/docs/tips-for-maintaining-storybook-documentation.stories.mdx b/docs/tips-for-maintaining-storybook-documentation.stories.mdx new file mode 100644 index 0000000000..631277f65b --- /dev/null +++ b/docs/tips-for-maintaining-storybook-documentation.stories.mdx @@ -0,0 +1,353 @@ +import { Meta } from '@storybook/addon-docs/blocks' + +<Meta title="About This Documentation/For maintainers" /> + +# Tips for Maintaining Storybook Documentation + +The Docs Page for a component (visible at the Docs tab in the toolbar above) is automatically generated from stories, the component's prop types, and JSDoc comments above the component and its props. + +This page assumes some knowledge of Storybook. + +[Storybook documentation](https://storybook.js.org/docs/react/get-started/introduction) + +## Organizing components + +In a story file, you can choose the path and name of the stories page by adding the `title` property to the default export, e.g. + +```js +export default { + component: DropdownButton, + title: 'Actions/Buttons/Dropdown Button', +} +``` + +This adds the component to the 'Buttons' folder in the 'Actions' section and names its stories 'Dropdown Button.' + +Sections, folders, and component names in storybook can include spaces and don't need to match the React component name exactly. + +See more about naming and organization at [Naming Components and Hierarchy](https://storybook.js.org/docs/react/writing-stories/naming-components-and-hierarchy). + +## Making stories + +There are a few ways you can make sure the stories generate the best documentation for the Docs Page. + +### Getting the args table + +Make sure you add the `component` property to the default export of the stories file (see default export example above) - this is how storybook knows to extract the component's props for the args table. + +### Controls and `args` + +To take advantage of Controls and enable 'dynamic' JSX-style code snippets in the Docs Page, make sure you use the '[Component Story Format](https://storybook.js.org/docs/react/api/csf)' and the `args` syntax. For an individual story, that looks like this: + +```js +export const DefaultStory = args => <Button {...args} /> +``` + +The Controls addon will automatically set up the props control table, which will dynamically change the component props via the `args`. There are options to customize the controls for the purposes of complex components or some prop types - see more in the 'Making sure propTypes are correctly documented' section below. + +Note that you don't need to use a template to take advantage of `args`. + +### Demonstrating callback behavior + +The **Actions** addon in the Canvas tab (with the addition of the [**Console** addon](https://storybook.js.org/addons/@storybook/addon-console)) displays messages sent to the console, including `log`s, `error`s and `warning`s, _but notably not `info`s._ + +Feel free to add console messages that demonstrate callback behavior, especially their function signatures. + +Example: + +```js +const onClick = (...args) => console.log('onClick args:', ...args) + +export const Button = args => <Button {...args} /> +Button.args = { onClick: onClick } +``` + +Simply passing `console.log` to callbacks like `onClick` is probably enough. + +The Actions addon adds some slightly different tools for demonstrating callbacks, but the added utility is not really worth using a new system for callback logging. + +### Customising source code snippets + +Make sure the source code snippets for demos gives the user useful information. There are a few ways to show source code snippets in the Docs Page: + +1. 'Dynamic', JSX-like rendering of the story. This is the default if the story uses `args`, and updates in response to changes in the Args Table. +2. A literal, nearly character-for-character expression of the story. This is the fallback if the story does not use `args`, and is the same as the code snippet shown in the 'Story' addon in the Canvas tab. This option does not dynamically update with the Args Table, but _does_ show accessory code like hooks that is useful to explain how a component requires a ref or state. +3. A hand-written snippet. It will receive syntax highlighting when shown on the Docs Page. + +To use #2 for a story that uses `args`, set `parameters.docs.source.type` to `'code'`: + +```js +export const StatefulStory = () => {...} +StatefulStory.parameters = { docs: { source: { type: 'code' } } } +``` + +This can be done at the story or component level. + +If neither of the auto-generated code snippets provide meaningful examples, use a handwritten snippet by setting `parameters.docs.source.code` to the string that you would like to appear in the code snippet: + +```js +export const CustomCodeSnippet = () => {} +CustomCodeSnippet.parameters = { + docs: { + source: { + code: ` +const ComponentDemo = () => { + const ref = useRef() + + return <ExampleComponent ref={ref} /> +} +`, + }, + }, +} +``` + +Read more about source code blocks at [Storybook: Doc Blocks - Source](https://storybook.js.org/docs/react/writing-docs/doc-blocks#source) + +### Reusing a story template + +To keep stories concise and DRY, you can reuse a story template (that uses the `args` syntax) and set the relevant props on each story using `StoryName.args = { ... }`. That looks like this: + +```js +// The story template +const Template = args => <Button {...args} /> + +// Stories to render +export const Primary = Template.bind({}) // Makes a copy of the template +Primary.args = { primary: true } + +export const PrimaryDense = Template.bind({}) +PrimaryDense.args = { ...Primary.args, dense: true } +``` + +Note that templates may not be the best solution in some circumstances, for example when component composition varies between stories. In these cases, you should still see if you can apply `{...args}` props to one or more components in the story. + +`args` can be composed for reuse by spreading the args from one story into another that shares those args, for example: + +```js +export const Primary = Template.bind({}) +Primary.args = { + primary: true, + label: 'Primary', + name: 'primaryName', + value: 'primaryValue', +} + +export const PrimaryDense = Template.bind({}) +PrimaryDense.args = { ...Primary.args, dense: true } +``` + +Here is [some more information about templates](https://storybook.js.org/docs/react/writing-stories/introduction#using-args) from the storybook documentation. + +### Templates with state or refs (or other hooks) + +Templates can add some complexity with wrappers or hooks that might get used repeatedly. + +See this example from the Popper component: + +```js +// Popper.stories.js +const Template = args => { + const ref = useRef(null) + + return ( + <div className="box" style={boxStyle}> + <div + className="reference-element" + style={referenceElementStyle} + ref={ref} + > + Reference Element + </div> + <Popper {...args} reference={ref}> + <div style={popperStyle}>{args.placement}</div> + </Popper> + </div> + ) +} +``` + +### Stories for multiple components + +Applying `args` or a template for a story with multiple components may not be straightforward, and controls may not work completely. See '[Stories for multiple components](https://storybook.js.org/docs/react/workflows/stories-for-multiple-components)' for detailed recommendations. + +Here is also an easy, 'good-enough' solution for variations on a single component: + +Apply `{...args}` to each one, then hard-code the prop of interest after the args. The hard-coded prop will take precedent over `args` and won't be controllable with the Controls addon, but all other props will be. + +```js +export const ButtonSizes = args => ( + <> + <Button {...args} small /> + <Button {...args} /> + <Button {...args} large /> + </> +) +ButtonSizes.args = { onClick: console.log } +``` + +## Annotating a component + +There are several ways to add descriptions to a component and its props for the Docs Page. Each of these accepts markdown syntax inside the strings or comments. + +By default, a JSDoc above the component definition becomes the component description on the Docs Page, and JSDoc comments above individual props become descriptions of those props in the args table (on both the Docs Page and in the Controls addon on the Canvas tab). Since the components in this library have pre-existing JSDoc comments with a particular syntax, these comments have been overridden with descriptions in the stories file. In the future, this may change back to comments above the component as the source. + +Descriptions can be added to the component and for each story (except the primary story on the Docs Page), and a subtitle can be added below the main story title. + +To add a subtitle and a component description (that overrides the auto-generated description from a JSDoc above the component definition), use `parameters` in the default export: + +```js +// This format is used to take advantage of multiline markdown syntax. +// Make sure it's not indentend and that backticks for code formatting are escaped. +const componentDescription = ` +Buttons are used for triggering actions. There are different types of buttons... + +\`\`\`js +import { Button } from '@dhis2/ui' +\`\`\` +` + +export default { + component: Button, + title: 'Actions/Buttons/Button', + parameters: { + componentSubtitle: 'Initiates an action', + docs: { description: { component: componentDescription } }, + }, +} +``` + +Adding a description to an individual story is similar to adding a component description. Where component descriptions use the `parameters.docs.description.component` property of the default export, story descriptions use the `parameters.docs.description.story` property of the story: + +```js +export const PrimaryButton = Template.bind({}) +PrimaryButton.args = { primary: true } +Primary.parameters = { + docs: { + description: { + story: + 'Used to highlight the most important/main action on a page.', + }, + }, +} +``` + +## Making sure propTypes are correctly documented + +Imported prop types (either from another file or from another library) are currently not interpreted correctly by react docgen under the hood. For this reason, it's best to use the React `prop-types` library for any props that will appear in user-facing stories. + +The workaround for imported props is to create a configuration object to define that prop's `argTypes` that decides what the row in the `args` table should look like, and what control should be used. In this library, mostly the `type` in the args table and the control type were configured, leaving descriptions to the JSDoc above the prop type definition. + +Read more about configuring the table at [Arg Types: Manual specification](https://storybook.js.org/docs/react/api/argtypes#manual-specification), and configuring controls at [Controls: Fully custom args](https://storybook.js.org/docs/react/essentials/controls#fully-custom-args). + +If a custom prop type is reused for multiple components, it makes sense to colocate the `argType` customisation object with the shared prop type. + +Here is an example for `buttonVariantPropType` from `shared-prop-types.js` in `ui-constants`: + +```js +// shared-prop-types.js + +export const buttonVariantPropType = propTypes.mutuallyExclusive( + ['primary', 'secondary', 'destructive'], + propTypes.bool +) + +export const buttonVariantArgType = { + // Table details + table: { + type: { + summary: 'bool', + detail: + "'primary', 'secondary', and 'destructive' are mutually exclusive props", + }, + }, + // What kind of prop control to use + control: { + type: 'boolean', + }, +} + +// Button.stories.js + +import { sharedPropTypes } from '@dhis2/ui-constants' + +export default { + component: Button, + // ... + argTypes: { + primary: { ...sharedPropTypes.buttonVariantArgType }, + secondary: { ...sharedPropTypes.buttonVariantArgType }, + destructive: { ...sharedPropTypes.buttonVariantArgType }, + // ... + }, +} +``` + +## Edge cases and workarounds + +There are several cases where bugs arise with Storybook and the docs page, and a few workarounds have been found. + +### Portal components + +Components that use React portals sometimes exhibit undesired behavior on the Docs Page, for example Layers or Modals stacking on top of each other and taking up the whole page. Other components like Popovers might not position correctly relative to their reference element. These issues don't appear on the Canvas tab. + +The above issues can be managed by rendering the stories in the Docs Page in `iframe`s instead of the default inline method. Do this by setting `parameters.docs.inlineStories` to `false` either at the component or the story level. + +Because the `iframe`s have a fixed height of 100px, the height should also be configured to something appropriate for the stories. + +Note that the iframes are _slow to render_ and can make the page unresponsive for a time if there are many rendered at once. It's best to render only one representative example on the Docs Page and leave the rest to view in the Canvas tab. To do this, disable all the stories by default using `parameters.docs.disable = true` at the component level, then reenable the representative example at the story level: + +```js +export default { + title: 'Moda', + component: Modal, + parameters: { + docs: { + // render in iframe + inlineStories: false, + iframeHeight: '500px', + // disable all stories on docs page by default + disable: true, + }, + }, +} + +export const RepresentativeExample = Template.bind({}) +// enable this story on the Docs Page +RepresentativeExample.parameters = { docs: { disable: false } } +``` + +**NB:** Controls on the props will not work for an `iframe` story on the Docs Page, but the Controls will work normally in the Canvas. It would be helpful to make a note for users so they know what to expect. + +#### Props extraction for portal components + +Make sure components return JSX - otherwise react docgen will not find them and not generate an args table. If a component returns a react portal, that portal can be wrapped in a React fragment to make the prop detection work. + +### Default props & default args + +There is a bug that manifests as a story throwing an error that looks like '[callback] is not a function'/'expected function but received string' or '[variable used in default props] is undefined' - this is due to a quirk with how default props are processed when using the `args` syntax. (See [this issue](https://github.com/storybookjs/storybook/issues/12098#issuecomment-758153653)) + +Providing actual (non-default) values in the stories enables the expected behavior. + +An easy workaround, therefore, is to set the component's default props as args for all the stories as default (specifying args for an individual story still takes precedence): + +```js +export default { + title: 'Forms/Transfer' + component: Transfer, + // configure default args for workaround + args: { ...Transfer.defaultProps }, +} +``` + +Note that the JSX addon which is configured to not show default props in code snippets conveniently prevents this workaround from spamming a component's code snippet with props. + +### Stories that use initial focus + +Components that have have a story to demonstrate an `initialFocus` prop cause an annoying behavior on the Docs Page. Whenever a control is changed, the whole page scrolls down to the initial focus story. For that reason, these stories should be disabled on the Docs Page by setting `parameters.docs.disable` to `true` on the Story: + +```js +export const InitialFocus = Template.bind({}) +InitialFocus.args = { initialFocus: true } +InitialFocus.parameters = { docs: { disable: true } } +``` diff --git a/docs/tips-for-using-this-documentation.stories.mdx b/docs/tips-for-using-this-documentation.stories.mdx new file mode 100644 index 0000000000..e3f152e468 --- /dev/null +++ b/docs/tips-for-using-this-documentation.stories.mdx @@ -0,0 +1,61 @@ +import { Meta } from '@storybook/addon-docs/blocks' + +<Meta title="About This Documentation/For readers" /> + +# Tips for Using This Documentation + +This documentation for the DHIS2 UI library is made using [Storybook](https://storybook.js.org/). Here is a quick tour and a few tips for making the most of the documentation in these pages. + +## 'Using UI' section + +In the **Using UI** section in the menu on the left, there are a few pages describing how to use the UI library. Start here to get a big-picture idea of how to use the library in your project. + +**Recipes** are longer-form descriptions of complex applications of UI components. + +These pages are the same as you would find on the previous doc site. + +## Components + +The other sections in the menu on the left describe the components available for use in the UI library. + +For each component, there are a number of demos to show off different variations and how they interact with props. Individual demos can be viewed in the **Canvas** tab of the toolbar above, or you can view a documentation page by clicking on the **Docs** tab. For a purely textual page like this one, the content on the Canvas and Docs tabs are the same. + +### Docs Page + +In the component's Docs tab (called the Docs Page from here on), you can find a description of the component and its usage, an interactive table of the props for that component, a list of demos, and viewable code snippets for each demo to help you integrate it into your project. + +#### Args table + +The args table shows each prop for that component, the prop type, the default value if there is one, a control for dynamically editing that prop in the primary demo on the page, and often a description of that prop. + +If that component is showing dynamic code snippets, changes to the props made by the args table will be reflected in those code snippets. + +Note that the control for the `children` prop only works with string literals surrounded by quotes: `""` + +For components with **subcomponents**, look out for tabs above the args table to look at a props table for each subcomponent. + +#### Code snippets + +You can find code snippets for the demos on the page by clicking on the 'Show code' button on the bottom right of a demo's container. + +Depending on the component, this code snippet will be dynamically generated from the component and its props as they have been configured by the props' controls, or it will be a reflection of the actual code written to create the demo. These are the same as the code snippets you will find in the **JSX** and **Story** tabs of the Canvas, respectively; see more comments about those in the 'Canvas' section below. + +#### Some exceptions + +A few components have unusual behaviors that don't work well on the Docs Page, and a few workarounds are made for them. In every case, you can find on the Docs Page a description of the component, its props, and at least one demo. Some of these unusual demos on the Docs Page won't update dynamically in response to the props controls. + +Note that all demos, displayed in the Docs Page or not, _work completely normally in the Canvas tab, including updating in response to props controls_. View the demos in the Canvas tab if they are not working on the Docs Page. + +### Canvas + +When viewing demos in the Canvas tab, there will be several tabs along the lower toolbar that provide tools to explore the component. + +In the **Controls** tab, you will see descriptions of the component's props and controls that will dynamically manipulate them. This is the same args table as in the Docs Page. + +In the **Actions** tab, you will see the results of callback functions that are triggered by the components. Console commands are logged in the Actions tab as well. + +In the **Story** tab, you will find the code that was written to create this demo with Storybook. This can be useful for more complex components that require hooks or state to control them, but sometimes can be unhelpful for simple components that are predominantly shaped by props. When you see `{...args}` added to the props of a component in the code here, that means it's receiving the dynamically-controlled props from **Controls**. + +In the **JSX** tab, you can find the dynamic JSX output from the rendered component. It updates in response to changing a component's props using Controls, so this is useful for configuring a component and then getting the exact snippet you need to copy and paste into your project. For some unusual components, the dynamically-rendered JSX is not as helpful as the code that's found in the Story tab. + +The **Accessibility** tab is intended for developers. It shows several accessibility tests for the component being displayed. diff --git a/docs/troubleshooting.stories.mdx b/docs/troubleshooting.stories.mdx new file mode 100644 index 0000000000..e2f93ee04b --- /dev/null +++ b/docs/troubleshooting.stories.mdx @@ -0,0 +1,6 @@ +import { Meta, Description } from '@storybook/addon-docs/blocks' +import Troubleshooting from './troubleshooting.md' + +<Meta title="Using UI/Troubleshooting" /> + +<Description>{Troubleshooting}</Description> diff --git a/package.json b/package.json index 002c18e659..e20f54d631 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,7 @@ "cy:server": "yarn build && STORYBOOK_TESTING=1 EXTEND_ESLINT=true start-storybook --port 5001 --quiet --ci", "cy:open": "d2-utils-cypress open --appStart 'yarn cy:server' --waitOn 'http-get://localhost:5001'", "cy:run": "d2-utils-cypress run --appStart 'yarn cy:server' --waitOn 'http-get://localhost:5001'", - "demo": "EXTEND_ESLINT=true build-storybook -o ./dist/demo --quiet", + "demo": "EXTEND_ESLINT=true build-storybook -o ./dist/demo --quiet -s .storybook/static", "docs": "d2-utils-docsite build ./docs -o ./dist --jsdoc ./packages/**/src --jsdoc-output-file api.md", "format": "yarn format:js && yarn format:text", "format:staged": "yarn format:js --staged && yarn format:text --staged", @@ -41,11 +41,12 @@ "lint:js": "d2-style js check", "lint:text": "d2-style text check", "start": "yarn build:icons && concurrently -n w: \"yarn:watch:*\" --kill-others", + "storybook:dev": "EXTEND_ESLINT=true start-storybook --port 5002 --quiet --ci -s .storybook/static --no-manager-cache", "test": "d2-app-scripts test", "watch:constants": "yarn build:constants --watch --dev", "watch:core": "yarn build:core --watch --dev", "watch:forms": "yarn build:forms --watch --dev", - "watch:storybook": "EXTEND_ESLINT=true start-storybook --port 5000 --quiet --ci", + "watch:storybook": "EXTEND_ESLINT=true start-storybook --port 5000 --quiet --ci -s .storybook/static", "watch:widgets": "yarn build:widgets --watch --dev" }, "devDependencies": { @@ -58,14 +59,18 @@ "@dhis2/cypress-plugins": "^5.1.0", "@dhis2/d2-i18n": "^1.0.6", "@fontsource/roboto": "^4.0.0", - "@storybook/addons": "^6.1.16", - "@storybook/channels": "^6.1.16", - "@storybook/components": "^6.1.16", - "@storybook/core-events": "^6.1.16", - "@storybook/node-logger": "^6.1.16", - "@storybook/preset-create-react-app": "^3.1.5", - "@storybook/react": "^6.1.16", - "@storybook/theming": "^6.1.16", + "@storybook/addon-a11y": "^6.1.20", + "@storybook/addon-console": "^1.2.3", + "@storybook/addon-essentials": "^6.1.20", + "@storybook/addon-storysource": "^6.1.20", + "@storybook/addons": "^6.1.20", + "@storybook/channels": "^6.1.20", + "@storybook/components": "^6.1.20", + "@storybook/core-events": "^6.1.20", + "@storybook/node-logger": "^6.1.20", + "@storybook/preset-create-react-app": "^3.1.6", + "@storybook/react": "^6.1.20", + "@storybook/theming": "^6.1.20", "@wertarbyte/react-props-md-table": "^1.1.1", "concurrently": "^5.3.0", "enzyme": "^3.11.0", @@ -74,7 +79,7 @@ "react-dev-utils": "^10.2.1", "react-docgen": "^5.3.0", "react-dom": "16.8", - "storybook-addon-jsx": "^7.3.4", + "storybook-addon-jsx": "^7.3.6", "styled-jsx": "^3.3.0" }, "d2": { diff --git a/packages/constants/src/shared-prop-types.js b/packages/constants/src/shared-prop-types.js index 00d7ca6871..59fe7d7074 100644 --- a/packages/constants/src/shared-prop-types.js +++ b/packages/constants/src/shared-prop-types.js @@ -14,6 +14,17 @@ export const statusPropType = propTypes.mutuallyExclusive( ['valid', 'warning', 'error'], propTypes.bool ) +// Exported for storybook +export const statusArgType = { + table: { + type: { + summary: 'bool', + detail: + "'valid', 'warning', and 'error' are mutually exclusive props", + }, + }, + control: { type: 'boolean' }, +} /** * Button variant propType @@ -24,6 +35,19 @@ export const buttonVariantPropType = propTypes.mutuallyExclusive( ['primary', 'secondary', 'destructive'], propTypes.bool ) +export const buttonVariantArgType = { + // No description because it should be set for the component description + table: { + type: { + summary: 'bool', + detail: + "'primary', 'secondary', and 'destructive' are mutually exclusive props", + }, + }, + control: { + type: 'boolean', + }, +} /** * Size variant propType @@ -34,6 +58,18 @@ export const sizePropType = propTypes.mutuallyExclusive( ['small', 'large'], propTypes.bool ) +export const sizeArgType = { + // No description because it should be set in the component description + table: { + type: { + summary: 'bool', + detail: "'small' and 'large' are mutually exclusive props", + }, + }, + control: { + type: 'boolean', + }, +} /** * Inside alignment props @@ -44,6 +80,18 @@ export const insideAlignmentPropType = propTypes.oneOf([ 'middle', 'bottom', ]) +export const insideAlignmentArgType = { + description: 'Inside alignment of the component', + table: { + type: { + summary: "'top' | 'middle' | 'bottom'", + }, + }, + control: { + type: 'select', + options: ['top', 'middle', 'bottom'], + }, +} /** * Placement properties against reference element @@ -56,7 +104,7 @@ export const popperPlacementPropType = propTypes.oneOf([ 'top', 'top-start', 'top-end', - 'bottom', // will be used as default + 'bottom', 'bottom-start', 'bottom-end', 'right', @@ -66,6 +114,35 @@ export const popperPlacementPropType = propTypes.oneOf([ 'left-start', 'left-end', ]) +export const popperPlacementArgType = { + description: 'Placement properties relative to reference element', + table: { + type: { + summary: 'string (one of several)', + detail: 'see options in menu', + }, + }, + control: { + type: 'select', + options: [ + 'auto', + 'auto-start', + 'auto-end', + 'top', + 'top-start', + 'top-end', + 'bottom', + 'bottom-start', + 'bottom-end', + 'right', + 'right-start', + 'right-end', + 'left', + 'left-start', + 'left-end', + ], + }, +} /** * Either a DOM node, React ref or a virtual element @@ -80,3 +157,12 @@ export const popperReferencePropType = propTypes.oneOfType([ // Virtual element propTypes.shape({ getBoundingClientRect: propTypes.func }), ]) +export const popperReferenceArgType = { + description: + 'A reference to the element to position against: either a DOM node, React ref, \ + or an instance of an element', + table: { + type: { summary: 'DOM node | React ref | Virtual element' }, + }, + control: { type: null }, +} diff --git a/packages/core/src/AlertBar/AlertBar.js b/packages/core/src/AlertBar/AlertBar.js index f05e3d2179..0b0017a900 100644 --- a/packages/core/src/AlertBar/AlertBar.js +++ b/packages/core/src/AlertBar/AlertBar.js @@ -1,5 +1,6 @@ import propTypes from '@dhis2/prop-types' import cx from 'classnames' +import PropTypes from 'prop-types' import React, { Component } from 'react' import { Actions, actionsPropType } from './Actions.js' import styles, { ANIMATION_TIME } from './AlertBar.styles.js' @@ -127,7 +128,7 @@ class AlertBar extends Component { const alertTypePropType = propTypes.mutuallyExclusive( ['success', 'warning', 'critical'], - propTypes.bool + PropTypes.bool ) AlertBar.defaultProps = { @@ -156,17 +157,25 @@ AlertBar.defaultProps = { * @prop {string} [dataTest] */ AlertBar.propTypes = { + /** An array of 0-2 action objects */ actions: actionsPropType, - children: propTypes.string, - className: propTypes.string, + /** The message string for the alert */ + children: PropTypes.string, + className: PropTypes.string, + /** Alert bars with `critical` will not autohide */ critical: alertTypePropType, - dataTest: propTypes.string, - duration: propTypes.number, + dataTest: PropTypes.string, + duration: PropTypes.number, + /** + * A specific icon to override the default icon in the bar. + * If `false` is provided, no icon will be shown. + */ icon: iconPropType, - permanent: propTypes.bool, + permanent: PropTypes.bool, success: alertTypePropType, + /** Alert bars with `warning` will not autohide */ warning: alertTypePropType, - onHidden: propTypes.func, + onHidden: PropTypes.func, } export { AlertBar } diff --git a/packages/core/src/AlertBar/AlertBar.stories.js b/packages/core/src/AlertBar/AlertBar.stories.js index 2e7c6843ff..5fb36c1554 100644 --- a/packages/core/src/AlertBar/AlertBar.stories.js +++ b/packages/core/src/AlertBar/AlertBar.stories.js @@ -1,94 +1,174 @@ -import { storiesOf } from '@storybook/react' import React from 'react' import { AttachFile } from '../Icons/index.js' import { AlertBar } from './AlertBar.js' +const subtitle = ` +A floating alert that informs the user about temporary information +in the context of the current screen. +` + +const description = ` +Alert bars notify a user of some information. There are different types of +alert bar for displaying different types of content. Use the alert bar type +that matches your content type and importance. Note that alert bar can be +ignored by the user, so they shouldn't be used for content that needs to +block an app flow, use a modal instead. + +Alert bars are always displayed at centered and fixed at the bottom of the +screen. Some types of alert bar dismiss after a set time, others must be +dismissed by the user. + +See specification: [Design System](https://github.com/dhis2/design-system/blob/master/molecules/alertbar.md) + +\`\`\`js +import { AlertBar } from '@dhis2/ui' +\`\`\` +` + const Wrapper = fn => ( - <div - className="alert-bars" - style={{ - width: '100%', - position: 'fixed', - bottom: 0, - left: 0, - paddingLeft: 16, - }} - > - {fn()} + <div style={{ height: '260px' }}> + <div + className="alert-bars" + style={{ + width: '100%', + position: 'fixed', + bottom: 0, + left: 0, + paddingLeft: 16, + }} + > + {fn()} + </div> </div> ) -storiesOf('AlertBar', module) - .addDecorator(Wrapper) - .add('Default', () => <AlertBar>Default - I will autohide</AlertBar>) - .add('States', () => ( - <React.Fragment> - <AlertBar permanent>Default (info)</AlertBar> - <AlertBar permanent success> - Success - </AlertBar> - <AlertBar permanent warning> - Warning - </AlertBar> - <AlertBar permanent critical> - Critical - </AlertBar> - </React.Fragment> - )) - .add('Auto hiding', () => ( - <React.Fragment> - <AlertBar permanent>Permanent never auto-hides</AlertBar> - <AlertBar warning>Warning never auto-hides</AlertBar> - <AlertBar critical>Critial never auto-hides</AlertBar> - <AlertBar - duration={2000} - onHidden={(payload, event) => { - console.log('onHidden payload', payload) - console.log('onHidden event', event) - }} - > - Custom duration, hides after 2s - </AlertBar> - <AlertBar - onHidden={(payload, event) => { - console.log('onHidden payload', payload) - console.log('onHidden event', event) - }} - > - Default auto-hides after 8s - </AlertBar> - </React.Fragment> - )) - .add('With actions', () => ( +const alertTypeArgType = { + table: { + type: { + summary: 'bool', + detail: + "'success', 'warning', and 'critical' are mutually exclusive props", + }, + }, + control: { + type: 'boolean', + }, +} +const iconArgType = { + table: { + type: { + summary: 'bool | element', + }, + }, +} +const actionsArgType = { + table: { + type: { + summary: '[{ label: string, onClick: func }]', + }, + }, +} + +export default { + title: 'Feedback/Alerts/Alert Bar', + component: AlertBar, + decorators: [Wrapper], + parameters: { + componentSubtitle: subtitle, + docs: { + description: { + component: description, + }, + }, + }, + argTypes: { + actions: { ...actionsArgType }, + critical: { ...alertTypeArgType }, + success: { ...alertTypeArgType }, + warning: { ...alertTypeArgType }, + icon: { ...iconArgType }, + }, +} + +export const Default = args => ( + <AlertBar {...args}>Default - I will autohide</AlertBar> +) + +export const States = () => ( + <React.Fragment> + <AlertBar permanent>Default (info)</AlertBar> + <AlertBar permanent success> + Success + </AlertBar> + <AlertBar permanent warning> + Warning + </AlertBar> + <AlertBar permanent critical> + Critical + </AlertBar> + </React.Fragment> +) + +export const AutoHiding = () => ( + <React.Fragment> + <AlertBar permanent>Permanent never auto-hides</AlertBar> + <AlertBar warning>Warning never auto-hides</AlertBar> + <AlertBar critical>Critial never auto-hides</AlertBar> + <AlertBar + duration={2000} + onHidden={(payload, event) => { + console.log('onHidden payload', payload) + console.log('onHidden event', event) + }} + > + Custom duration, hides after 2s + </AlertBar> <AlertBar - permanent - actions={[ - { label: 'Save', onClick: () => {} }, - { label: 'Cancel', onClick: () => {} }, - ]} + onHidden={(payload, event) => { + console.log('onHidden payload', payload) + console.log('onHidden event', event) + }} > - With Actions + Default auto-hides after 8s </AlertBar> - )) - .add('Icons', () => ( - <React.Fragment> - <AlertBar permanent>Default icon</AlertBar> - <AlertBar permanent icon={false}> - No icon - </AlertBar> - <AlertBar permanent icon={<AttachFile />}> - Custom icon - </AlertBar> - </React.Fragment> - )) - .add('Text overflow', () => ( - <React.Fragment> - <AlertBar permanent>Short text</AlertBar> - <AlertBar permanent> - If the alert bar gets a ver long text, it will grow to a maximum - of 600px and the text will overflow across several lines. If - there are multiple AlertBars in a stack, they will all grow to - the size of the widest sibling. - </AlertBar> - </React.Fragment> - )) + </React.Fragment> +) +AutoHiding.storyName = 'Auto hiding' + +export const WithActions = () => ( + <AlertBar + permanent + actions={[ + { label: 'Save', onClick: () => {} }, + { label: 'Cancel', onClick: () => {} }, + ]} + > + With Actions + </AlertBar> +) +WithActions.storyName = 'With actions' + +export const Icons = () => ( + <React.Fragment> + <AlertBar permanent>Default icon</AlertBar> + <AlertBar permanent icon={false}> + No icon + </AlertBar> + <AlertBar permanent icon={<AttachFile />}> + Custom icon + </AlertBar> + </React.Fragment> +) + +export const TextOverflow = () => ( + <React.Fragment> + <AlertBar permanent>Short text</AlertBar> + <AlertBar permanent> + If the alert bar gets a ver long text, it will grow to a maximum of + 600px and the text will overflow across several lines. If there are + multiple AlertBars in a stack, they will all grow to the size of the + widest sibling. + </AlertBar> + </React.Fragment> +) +TextOverflow.storyName = 'Text overflow' diff --git a/packages/core/src/AlertStack/AlertStack.js b/packages/core/src/AlertStack/AlertStack.js index 4add9a4059..5342a79bc5 100644 --- a/packages/core/src/AlertStack/AlertStack.js +++ b/packages/core/src/AlertStack/AlertStack.js @@ -1,6 +1,6 @@ -import propTypes from '@dhis2/prop-types' import { layers } from '@dhis2/ui-constants' import cx from 'classnames' +import PropTypes from 'prop-types' import React from 'react' import { createPortal } from 'react-dom' ;('') // TODO: https://github.com/jsdoc/jsdoc/issues/1718 @@ -12,30 +12,33 @@ import { createPortal } from 'react-dom' * @example import { AlertStack } from '@dhis2/ui-core' * @see Live demo: {@link /demo/?path=/story/alertstack--default|Storybook} */ -const AlertStack = ({ className, children, dataTest }) => - createPortal( - <div className={cx(className)} data-test={dataTest}> - {children} - <style jsx>{` - div { - position: fixed; - top: auto; - right: auto; - bottom: 0; - left: 50%; - transform: translateX(-50%); +export const AlertStack = ({ className, children, dataTest }) => ( + <> + {createPortal( + <div className={cx(className)} data-test={dataTest}> + {children} + <style jsx>{` + div { + position: fixed; + top: auto; + right: auto; + bottom: 0; + left: 50%; + transform: translateX(-50%); - z-index: ${layers.alert}; + z-index: ${layers.alert}; - display: flex; - flex-direction: column-reverse; + display: flex; + flex-direction: column-reverse; - pointer-events: none; - } - `}</style> - </div>, - document.body - ) + pointer-events: none; + } + `}</style> + </div>, + document.body + )} + </> +) AlertStack.defaultProps = { dataTest: 'dhis2-uicore-alertstack', @@ -49,9 +52,7 @@ AlertStack.defaultProps = { * @prop {string} [dataTest] */ AlertStack.propTypes = { - children: propTypes.arrayOf(propTypes.element), - className: propTypes.string, - dataTest: propTypes.string, + children: PropTypes.node, + className: PropTypes.string, + dataTest: PropTypes.string, } - -export { AlertStack } diff --git a/packages/core/src/AlertStack/AlertStack.stories.js b/packages/core/src/AlertStack/AlertStack.stories.js index c38ee2f783..946c4c2dce 100644 --- a/packages/core/src/AlertStack/AlertStack.stories.js +++ b/packages/core/src/AlertStack/AlertStack.stories.js @@ -1,10 +1,32 @@ -import { storiesOf } from '@storybook/react' import React from 'react' import { AlertBar } from '../index.js' import { AlertStack } from './AlertStack.js' -storiesOf('AlertStack', module).add('Default', () => ( - <AlertStack> +const description = ` +A container for Alert Bars. + +_**Note:** The demos on this page may be slow._ + +\`\`\`js +import { AlertStack } from '@dhis2/ui' +\`\`\` +` + +export default { + title: 'Feedback/Alerts/Alert Stack', + component: AlertStack, + // Use an iframe in docs to contain 'portal' + parameters: { + docs: { + inlineStories: false, + iframeHeight: '300px', + description: { component: description }, + }, + }, +} + +export const Default = args => ( + <AlertStack {...args}> <AlertBar permanent>First notification - I am at the bottom</AlertBar> <AlertBar permanent critical> Second notification @@ -16,4 +38,4 @@ storiesOf('AlertStack', module).add('Default', () => ( Fourth notification - I am at the top </AlertBar> </AlertStack> -)) +) diff --git a/packages/core/src/Box/Box.js b/packages/core/src/Box/Box.js index e53c5e36ed..9ff03a1989 100644 --- a/packages/core/src/Box/Box.js +++ b/packages/core/src/Box/Box.js @@ -1,4 +1,4 @@ -import propTypes from '@dhis2/prop-types' +import PropTypes from 'prop-types' import React from 'react' /** @@ -55,15 +55,15 @@ Box.defaultProps = { * @prop {string} [dataTest] */ Box.propTypes = { - children: propTypes.node, - className: propTypes.string, - dataTest: propTypes.string, - height: propTypes.string, - marginTop: propTypes.string, - maxHeight: propTypes.string, - maxWidth: propTypes.string, - minHeight: propTypes.string, - minWidth: propTypes.string, - overflow: propTypes.string, - width: propTypes.string, + children: PropTypes.node, + className: PropTypes.string, + dataTest: PropTypes.string, + height: PropTypes.string, + marginTop: PropTypes.string, + maxHeight: PropTypes.string, + maxWidth: PropTypes.string, + minHeight: PropTypes.string, + minWidth: PropTypes.string, + overflow: PropTypes.string, + width: PropTypes.string, } diff --git a/packages/core/src/Box/Box.stories.js b/packages/core/src/Box/Box.stories.js index a198d749d1..56ec0f9385 100644 --- a/packages/core/src/Box/Box.stories.js +++ b/packages/core/src/Box/Box.stories.js @@ -1,35 +1,58 @@ import React from 'react' import { Box } from './Box.js' +const description = ` +A box for creating some layout on the page. + +\`\`\`js +import { Box } from '@dhis2/ui' +\`\`\` +` + export default { - title: 'Box', + title: 'Layout/Box', component: Box, + parameters: { docs: { description: { component: description } } }, } -export const Default = () => <Box>I am a child in a Box.</Box> +const Template = args => <Box {...args} /> + +export const Default = Template.bind({}) +Default.args = { children: 'I am a child in a Box.' } -export const Height = () => <Box height="250px">I am a child in a Box.</Box> +export const Height = Template.bind({}) +Height.args = { + ...Default.args, + height: '250px', +} -export const MaxHeight = () => ( - <Box maxHeight="250px"> +export const MaxHeight = Template.bind({}) +MaxHeight.args = { + children: ( <p style={{ height: '500px' }}>I am a tall child in a low Box.</p> - </Box> -) + ), + maxHeight: '250px', +} -export const MinHeight = () => ( - <Box minHeight="50vh">I am a child in a Box.</Box> -) +export const MinHeight = Template.bind({}) +MinHeight.args = { ...Default.args, minHeight: '50vh' } -export const Width = () => <Box width="250px">I am a child in a Box.</Box> +export const Width = Template.bind({}) +Width.args = { ...Default.args, width: '250px' } -export const MinWidth = () => <Box minWidth="50vh">I am a child in a Box.</Box> +export const MinWidth = Template.bind({}) +MinWidth.args = { ...Default.args, minWidth: '50vh' } -export const MaxWidth = () => <Box maxWidth="50vh">I am a child in a Box.</Box> +export const MaxWidth = Template.bind({}) +MaxWidth.args = { ...Default.args, maxWidth: '50vh' } -export const Overflow = () => ( - <Box maxHeight="250px" overflow="scroll"> +export const Overflow = Template.bind({}) +Overflow.args = { + maxHeight: '250px', + overflow: 'scroll', + children: ( <p style={{ height: '500px' }}> I am a tall child in a low Box, and my parent clips me </p> - </Box> -) + ), +} diff --git a/packages/core/src/Button/Button.js b/packages/core/src/Button/Button.js index 77be554006..b3fb7fd0fc 100644 --- a/packages/core/src/Button/Button.js +++ b/packages/core/src/Button/Button.js @@ -1,6 +1,6 @@ -import propTypes from '@dhis2/prop-types' import { sharedPropTypes } from '@dhis2/ui-constants' import cx from 'classnames' +import PropTypes from 'prop-types' import React, { useEffect, useRef } from 'react' import styles from './Button.styles.js' @@ -112,23 +112,67 @@ Button.defaultProps = { * state */ Button.propTypes = { - children: propTypes.node, - className: propTypes.string, - dataTest: propTypes.string, + /** Component to render inside the button */ + children: PropTypes.node, + /** A className that will be passed to the `<button>` element */ + className: PropTypes.string, + /** + * A string that will be applied as a `data-test` attribute on the button element + * for identification during testing + */ + dataTest: PropTypes.string, + /** + * Indicates that the button makes potentially dangerous + * deletions or data changes. + * Mutually exclusive with `primary` and `secondary` props + */ destructive: sharedPropTypes.buttonVariantPropType, - disabled: propTypes.bool, - icon: propTypes.element, - initialFocus: propTypes.bool, + /** Applies a greyed-out appearance and makes the button non-interactive */ + disabled: PropTypes.bool, + /** An icon element to display inside the button */ + icon: PropTypes.element, + /** Use this variant to capture the initial focus on the page. */ + initialFocus: PropTypes.bool, + /** Makes the button large. Mutually exclusive with `small` */ large: sharedPropTypes.sizePropType, - name: propTypes.string, + /** + * Sets `name` attribute on button element. + * Gets passed as part of the first argument to callbacks (see `onClick`). + */ + name: PropTypes.string, + /** + * Applies 'primary' button appearance. + * Mutually exclusive with `destructive` and `secondary` props + */ primary: sharedPropTypes.buttonVariantPropType, + /** + * Applies 'secondary' button appearance. + * Mutually exclusive with `primary` and `destructive` props + */ secondary: sharedPropTypes.buttonVariantPropType, + /** Makes the button small. Mutually exclusive with `large` prop */ small: sharedPropTypes.sizePropType, - tabIndex: propTypes.string, - toggled: propTypes.bool, - type: propTypes.oneOf(['submit', 'reset', 'button']), - value: propTypes.string, - onBlur: propTypes.func, - onClick: propTypes.func, - onFocus: propTypes.func, + /** Tab index for focusing the button with a keyboard */ + tabIndex: PropTypes.string, + /** Changes appearance of button to an on/off state */ + toggled: PropTypes.bool, + /** Sets `type` attribute on `<button>` element */ + type: PropTypes.oneOf(['submit', 'reset', 'button']), + /** + * Value associated with the button. + * Gets passed as part of the first argument to callbacks (see `onClick`). + */ + value: PropTypes.string, + /** + * Callback to trigger on de-focus (blur). + * Called with same args as `onClick` + * */ + onBlur: PropTypes.func, + /** + * Callback to trigger on click. + * Called with args `({ value, name }, event)` + * */ + onClick: PropTypes.func, + /** Callback to trigger on focus. Called with same args as `onClick` */ + onFocus: PropTypes.func, } diff --git a/packages/core/src/Button/Button.stories.js b/packages/core/src/Button/Button.stories.js index 089605fba6..d8b1c7152b 100644 --- a/packages/core/src/Button/Button.stories.js +++ b/packages/core/src/Button/Button.stories.js @@ -1,183 +1,235 @@ +import { sharedPropTypes } from '@dhis2/ui-constants' import React from 'react' import { Button } from './Button.js' -const logger = ({ name, value }) => console.info(`${name}: ${value}`) +// Note: make sure 'fenced code blocks' are not indentend in this template string +const description = `Buttons are used for triggering actions. +There are different types of buttons in the design system which are intended +for different types of actions. + +\`\`\`js +import { Button } from '@dhis2/ui' +\`\`\`` + +const { buttonVariantArgType, sizeArgType } = sharedPropTypes export default { - title: 'Button', + title: 'Actions/Buttons/Button', component: Button, + parameters: { + componentSubtitle: 'Initiates an action', + docs: { + description: { + component: description, + }, + }, + }, + args: { + children: 'Label me!', + value: 'default', + onClick: logger, + }, + argTypes: { + primary: { ...buttonVariantArgType }, + secondary: { ...buttonVariantArgType }, + destructive: { ...buttonVariantArgType }, + small: { ...sizeArgType }, + large: { ...sizeArgType }, + }, } -export const Basic = () => ( - <Button name="Basic button" value="default" onClick={logger}> - Label me! - </Button> -) +const logger = ({ name, value }) => console.log(`${name}: ${value}`) -export const Primary = () => ( - <Button primary name="Primary button" value="default" onClick={logger}> - Label me! - </Button> +const DemoIcon = ( + <svg + height="24" + viewBox="0 0 24 24" + width="24" + xmlns="http://www.w3.org/2000/svg" + > + <path + d="m10.7071068 13.2928932c.3604839.360484.3882135.927715.0831886 1.3200062l-.0831886.0942074-5.2921068 5.2918932 2.585.001c.51283584 0 .93550716.3860402.99327227.8833789l.00672773.1166211c0 .5128358-.38604019.9355072-.88337887.9932723l-.11662113.0067277h-5l-.0193545-.0001861c-.02332655-.0004488-.04664039-.0017089-.06989557-.0037803l.08925007.0039664c-.05062028 0-.10036209-.0037612-.14896122-.0110193-.01698779-.0026088-.03441404-.0056829-.05176454-.0092208-.02202032-.0043997-.04371072-.0095935-.06511385-.0154809-.01562367-.0043767-.03101173-.0090077-.04630291-.0140171-.01965516-.0063844-.03943668-.0135776-.058916-.0213659-.01773713-.0070924-.03503998-.014575-.05216303-.0225694-.02066985-.0097032-.0410724-.0201205-.0610554-.0312024-.01211749-.006623-.02433616-.0137311-.0364318-.0211197-.0255662-.0157232-.05042194-.0324946-.07445055-.050318-.00744374-.0054399-.01468311-.010971-.02186305-.0166142-.0631594-.049624-.12042594-.1068905-.17019169-.1703222l.08010726.0903567c-.03539405-.0353941-.06758027-.0727812-.09655864-.1118002-.01784449-.0241759-.03461588-.0490316-.05026715-.0746464-.00746051-.0120471-.0145686-.0242658-.02139626-.0365981-.01087725-.0197682-.02129453-.0401707-.03101739-.060963-.00797473-.0170006-.01545736-.0343035-.02242829-.0517631-.00790975-.0197568-.015103-.0395383-.02167881-.0595996-.00481796-.0148851-.00944895-.0302731-.01370154-.0457434-.00601151-.0215565-.01120534-.0432469-.01567999-.0651989-.00346298-.0174188-.00653707-.0348451-.00914735-.0523272-.00160026-.010231-.00303174-.021012-.00429007-.0318458l-.00276132-.027371c-.00207143-.0232552-.00333152-.0465691-.00378026-.0698956l-.00018615-.0193545v-5c0-.5522847.44771525-1 1-1 .51283584 0 .93550716.3860402.99327227.8833789l.00672773.1166211v2.584l5.29289322-5.2911068c.39052429-.3905243 1.02368928-.3905243 1.41421358 0zm9.2928932-3.2928932v10h-10v-2h8v-8zm-6-6v2h-8v7h-2v-9zm7-2 .0193545.00018615c.0233265.00044874.0466404.00170883.0698956.00378026l-.0892501-.00396641c.0506203 0 .1003621.00376119.1489612.01101934.0169878.00260874.0344141.00568283.0517646.00922073.0220203.00439973.0437107.00959356.0651138.0154809.0156237.00437676.0310117.00900775.0463029.01401712.0196552.0063844.0394367.01357765.058916.02136587.0177371.00709246.03504.01457509.052163.0225694.0206699.00970328.0410724.02012056.0610555.03120241.0121174.00662306.0243361.01373115.0364318.02111968.0255662.01572325.0504219.03249464.0744505.05031806.0074437.00543993.0146831.01097097.021863.01661418.0631595.04962402.120426.10689056.1701917.17032223l-.0801072-.0903567c.035394.03539405.0675802.0727812.0965586.11180017.0178445.02417592.0346159.04903166.0502672.07464642.0074605.01204708.0145686.02426575.0213962.03659809.0108773.01976815.0212946.0401707.0310174.06096295.0079748.01700065.0154574.0343035.0224283.05176313.0079098.01975682.015103.03953834.0216788.05959961.004818.01488507.009449.03027313.0137016.04574344.0060115.02155649.0112053.04324689.0156799.06519887.003463.01741884.0065371.03484509.0091474.05232723.0016003.01023098.0030317.02101195.0042901.03184574l.0030256.03039033c.0015457.01796531.0026074.03596443.003185.05397618l.0005171.03225462v5c0 .55228475-.4477153 1-1 1-.5128358 0-.9355072-.38604019-.9932723-.88337887l-.0067277-.11662113v-2.586l-5.2928932 5.2931068c-.3905243.3905243-1.0236893.3905243-1.4142136 0-.3604839-.360484-.3882135-.92771504-.0831886-1.32000624l.0831886-.09420734 5.2911068-5.29289322h-2.584c-.5128358 0-.9355072-.38604019-.9932723-.88337887l-.0067277-.11662113c0-.51283584.3860402-.93550716.8833789-.99327227l.1166211-.00672773z" + fill="#inherit" + /> + </svg> ) -export const Secondary = () => ( - <Button secondary name="Secondary button" value="default" onClick={logger}> - Label me! - </Button> -) +const Template = args => <Button {...args} /> -export const Destructive = () => ( - <Button - destructive - name="Destructive button" - value="default" - onClick={logger} - > - Label me! - </Button> -) +export const Basic = Template.bind({}) +Basic.args = { + name: 'Basic button', +} -export const Disabled = () => ( +export const Primary = Template.bind({}) +Primary.args = { + primary: true, + name: 'Primary button', +} +Primary.parameters = { + docs: { + description: { + story: + 'Used to highlight the most important/main action on a page. \ + A "Save" button for a form page should be primary, for example. \ + Use sparingly, rarely should there be more than a single primary \ + button per page.', + }, + }, +} + +export const Secondary = Template.bind({}) +Secondary.args = { + secondary: true, + name: 'Secondary button', +} +Secondary.parameters = { + docs: { + description: { + story: + 'Used for passive actions, often as an alternative to the primary \ + action. If "Save" is primary, "Cancel" could be secondary. \ + Not intended to draw user attention. Do not use for the only \ + action on a page.', + }, + }, +} + +export const Destructive = Template.bind({}) +Destructive.args = { + destructive: true, + name: 'Destructive button', +} +Destructive.parameters = { + docs: { + description: { + story: + 'Used instead of a primary button when the main action is \ + destructive in nature. Used to highlight to the user the \ + seriousness of the action. \ + **Destructive buttons must only be used for destructive actions.**', + }, + }, +} + +export const Disabled = args => ( <> - <Button - disabled - name="Disabled button" - value="default" - onClick={logger} - > - Label me! - </Button> - <Button - primary - disabled - name="Disabled primary button" - value="default" - onClick={logger} - > - Label me! - </Button> - <Button - secondary - disabled - name="Disabled secondary button" - value="default" - onClick={logger} - > - Label me! - </Button> - <Button - destructive - disabled - name="Disabled destructive button" - value="default" - onClick={logger} - > - Label me! - </Button> + <Button name="Disabled button" {...args} /> + <Button primary name="Disabled primary button" {...args} /> + <Button secondary name="Disabled button" {...args} /> + <Button destructive name="Disabled button" {...args} /> </> ) +Disabled.args = { + disabled: true, +} +Disabled.parameters = { + docs: { + description: { + story: + "Use disabled buttons when an action is being prevented for some reason. \ + Always communicate to the user why the button can't be clicked. This can \ + be done through a tooltip on hover, or with supplementary text underneath \ + the button. Do not change the button label between disabled/enabled states.", + }, + }, +} -export const Small = () => ( - <Button small name="Small button" value="default" onClick={logger}> - Label me! - </Button> -) +export const Small = Template.bind({}) +Small.args = { + small: true, + name: 'Small button', +} +Small.parameters = { + docs: { + description: { + story: + 'Buttons are available in three sizes: `small`, `medium`, and `large`. \ + Medium is usually the correct choice. Use small buttons in an information-\ + dense ui.', + }, + }, +} -export const Large = () => ( - <Button large name="Large button" value="default" onClick={logger}> - Label me! - </Button> -) +export const Large = Template.bind({}) +Large.args = { + large: true, + name: 'Large button', +} +Large.parameters = { + docs: { + description: { + story: + 'Buttons are available in three sizes: `small`, `medium`, and `large`. \ + Medium is usually the correct choice. Large buttons can be used on very simple, \ + single-action pages.', + }, + }, +} -export const InitialFocus = () => ( - <Button initialFocus name="Focused button" value="default" onClick={logger}> - Label me! - </Button> -) +export const InitialFocus = Template.bind({}) +InitialFocus.args = { + initialFocus: true, + name: 'Focused button', +} +// When enabled, this story grabs focus every time a control is changed +// in the docs page. Disabled for better UX +InitialFocus.parameters = { docs: { disable: true } } -export const Icon = () => ( - <Button - name="Icon button" - value="default" - onClick={logger} - icon={ - <svg - height="24" - viewBox="0 0 24 24" - width="24" - xmlns="http://www.w3.org/2000/svg" - > - <path - d="m10.7071068 13.2928932c.3604839.360484.3882135.927715.0831886 1.3200062l-.0831886.0942074-5.2921068 5.2918932 2.585.001c.51283584 0 .93550716.3860402.99327227.8833789l.00672773.1166211c0 .5128358-.38604019.9355072-.88337887.9932723l-.11662113.0067277h-5l-.0193545-.0001861c-.02332655-.0004488-.04664039-.0017089-.06989557-.0037803l.08925007.0039664c-.05062028 0-.10036209-.0037612-.14896122-.0110193-.01698779-.0026088-.03441404-.0056829-.05176454-.0092208-.02202032-.0043997-.04371072-.0095935-.06511385-.0154809-.01562367-.0043767-.03101173-.0090077-.04630291-.0140171-.01965516-.0063844-.03943668-.0135776-.058916-.0213659-.01773713-.0070924-.03503998-.014575-.05216303-.0225694-.02066985-.0097032-.0410724-.0201205-.0610554-.0312024-.01211749-.006623-.02433616-.0137311-.0364318-.0211197-.0255662-.0157232-.05042194-.0324946-.07445055-.050318-.00744374-.0054399-.01468311-.010971-.02186305-.0166142-.0631594-.049624-.12042594-.1068905-.17019169-.1703222l.08010726.0903567c-.03539405-.0353941-.06758027-.0727812-.09655864-.1118002-.01784449-.0241759-.03461588-.0490316-.05026715-.0746464-.00746051-.0120471-.0145686-.0242658-.02139626-.0365981-.01087725-.0197682-.02129453-.0401707-.03101739-.060963-.00797473-.0170006-.01545736-.0343035-.02242829-.0517631-.00790975-.0197568-.015103-.0395383-.02167881-.0595996-.00481796-.0148851-.00944895-.0302731-.01370154-.0457434-.00601151-.0215565-.01120534-.0432469-.01567999-.0651989-.00346298-.0174188-.00653707-.0348451-.00914735-.0523272-.00160026-.010231-.00303174-.021012-.00429007-.0318458l-.00276132-.027371c-.00207143-.0232552-.00333152-.0465691-.00378026-.0698956l-.00018615-.0193545v-5c0-.5522847.44771525-1 1-1 .51283584 0 .93550716.3860402.99327227.8833789l.00672773.1166211v2.584l5.29289322-5.2911068c.39052429-.3905243 1.02368928-.3905243 1.41421358 0zm9.2928932-3.2928932v10h-10v-2h8v-8zm-6-6v2h-8v7h-2v-9zm7-2 .0193545.00018615c.0233265.00044874.0466404.00170883.0698956.00378026l-.0892501-.00396641c.0506203 0 .1003621.00376119.1489612.01101934.0169878.00260874.0344141.00568283.0517646.00922073.0220203.00439973.0437107.00959356.0651138.0154809.0156237.00437676.0310117.00900775.0463029.01401712.0196552.0063844.0394367.01357765.058916.02136587.0177371.00709246.03504.01457509.052163.0225694.0206699.00970328.0410724.02012056.0610555.03120241.0121174.00662306.0243361.01373115.0364318.02111968.0255662.01572325.0504219.03249464.0744505.05031806.0074437.00543993.0146831.01097097.021863.01661418.0631595.04962402.120426.10689056.1701917.17032223l-.0801072-.0903567c.035394.03539405.0675802.0727812.0965586.11180017.0178445.02417592.0346159.04903166.0502672.07464642.0074605.01204708.0145686.02426575.0213962.03659809.0108773.01976815.0212946.0401707.0310174.06096295.0079748.01700065.0154574.0343035.0224283.05176313.0079098.01975682.015103.03953834.0216788.05959961.004818.01488507.009449.03027313.0137016.04574344.0060115.02155649.0112053.04324689.0156799.06519887.003463.01741884.0065371.03484509.0091474.05232723.0016003.01023098.0030317.02101195.0042901.03184574l.0030256.03039033c.0015457.01796531.0026074.03596443.003185.05397618l.0005171.03225462v5c0 .55228475-.4477153 1-1 1-.5128358 0-.9355072-.38604019-.9932723-.88337887l-.0067277-.11662113v-2.586l-5.2928932 5.2931068c-.3905243.3905243-1.0236893.3905243-1.4142136 0-.3604839-.360484-.3882135-.92771504-.0831886-1.32000624l.0831886-.09420734 5.2911068-5.29289322h-2.584c-.5128358 0-.9355072-.38604019-.9932723-.88337887l-.0067277-.11662113c0-.51283584.3860402-.93550716.8833789-.99327227l.1166211-.00672773z" - fill="#6e7a8a" - /> - </svg> - } - /> -) -export const IconSmall = () => ( - <Button - name="Icon small button" - value="default" - onClick={logger} - small - icon={ - <svg - height="24" - viewBox="0 0 24 24" - width="24" - xmlns="http://www.w3.org/2000/svg" - > - <path - d="m10.7071068 13.2928932c.3604839.360484.3882135.927715.0831886 1.3200062l-.0831886.0942074-5.2921068 5.2918932 2.585.001c.51283584 0 .93550716.3860402.99327227.8833789l.00672773.1166211c0 .5128358-.38604019.9355072-.88337887.9932723l-.11662113.0067277h-5l-.0193545-.0001861c-.02332655-.0004488-.04664039-.0017089-.06989557-.0037803l.08925007.0039664c-.05062028 0-.10036209-.0037612-.14896122-.0110193-.01698779-.0026088-.03441404-.0056829-.05176454-.0092208-.02202032-.0043997-.04371072-.0095935-.06511385-.0154809-.01562367-.0043767-.03101173-.0090077-.04630291-.0140171-.01965516-.0063844-.03943668-.0135776-.058916-.0213659-.01773713-.0070924-.03503998-.014575-.05216303-.0225694-.02066985-.0097032-.0410724-.0201205-.0610554-.0312024-.01211749-.006623-.02433616-.0137311-.0364318-.0211197-.0255662-.0157232-.05042194-.0324946-.07445055-.050318-.00744374-.0054399-.01468311-.010971-.02186305-.0166142-.0631594-.049624-.12042594-.1068905-.17019169-.1703222l.08010726.0903567c-.03539405-.0353941-.06758027-.0727812-.09655864-.1118002-.01784449-.0241759-.03461588-.0490316-.05026715-.0746464-.00746051-.0120471-.0145686-.0242658-.02139626-.0365981-.01087725-.0197682-.02129453-.0401707-.03101739-.060963-.00797473-.0170006-.01545736-.0343035-.02242829-.0517631-.00790975-.0197568-.015103-.0395383-.02167881-.0595996-.00481796-.0148851-.00944895-.0302731-.01370154-.0457434-.00601151-.0215565-.01120534-.0432469-.01567999-.0651989-.00346298-.0174188-.00653707-.0348451-.00914735-.0523272-.00160026-.010231-.00303174-.021012-.00429007-.0318458l-.00276132-.027371c-.00207143-.0232552-.00333152-.0465691-.00378026-.0698956l-.00018615-.0193545v-5c0-.5522847.44771525-1 1-1 .51283584 0 .93550716.3860402.99327227.8833789l.00672773.1166211v2.584l5.29289322-5.2911068c.39052429-.3905243 1.02368928-.3905243 1.41421358 0zm9.2928932-3.2928932v10h-10v-2h8v-8zm-6-6v2h-8v7h-2v-9zm7-2 .0193545.00018615c.0233265.00044874.0466404.00170883.0698956.00378026l-.0892501-.00396641c.0506203 0 .1003621.00376119.1489612.01101934.0169878.00260874.0344141.00568283.0517646.00922073.0220203.00439973.0437107.00959356.0651138.0154809.0156237.00437676.0310117.00900775.0463029.01401712.0196552.0063844.0394367.01357765.058916.02136587.0177371.00709246.03504.01457509.052163.0225694.0206699.00970328.0410724.02012056.0610555.03120241.0121174.00662306.0243361.01373115.0364318.02111968.0255662.01572325.0504219.03249464.0744505.05031806.0074437.00543993.0146831.01097097.021863.01661418.0631595.04962402.120426.10689056.1701917.17032223l-.0801072-.0903567c.035394.03539405.0675802.0727812.0965586.11180017.0178445.02417592.0346159.04903166.0502672.07464642.0074605.01204708.0145686.02426575.0213962.03659809.0108773.01976815.0212946.0401707.0310174.06096295.0079748.01700065.0154574.0343035.0224283.05176313.0079098.01975682.015103.03953834.0216788.05959961.004818.01488507.009449.03027313.0137016.04574344.0060115.02155649.0112053.04324689.0156799.06519887.003463.01741884.0065371.03484509.0091474.05232723.0016003.01023098.0030317.02101195.0042901.03184574l.0030256.03039033c.0015457.01796531.0026074.03596443.003185.05397618l.0005171.03225462v5c0 .55228475-.4477153 1-1 1-.5128358 0-.9355072-.38604019-.9932723-.88337887l-.0067277-.11662113v-2.586l-5.2928932 5.2931068c-.3905243.3905243-1.0236893.3905243-1.4142136 0-.3604839-.360484-.3882135-.92771504-.0831886-1.32000624l.0831886-.09420734 5.2911068-5.29289322h-2.584c-.5128358 0-.9355072-.38604019-.9932723-.88337887l-.0067277-.11662113c0-.51283584.3860402-.93550716.8833789-.99327227l.1166211-.00672773z" - fill="#6e7a8a" - /> - </svg> - } - /> -) +export const Icon = Template.bind({}) +Icon.args = { + icon: DemoIcon, + name: 'Icon button', + children: null, +} +Icon.parameters = { + docs: { + description: { + story: + 'Icons can be included in Basic, Primary, Secondary and Destructive buttons. \ + Use an icon to supplement the text label. Remember that the user may not be \ + fluent in the working language, so an accompanying icon on an important action \ + can be a welcome addition. Buttons with icons only should be used for \ + supplementary actions and should include a text tooltip on hover.', + }, + }, +} -export const Toggled = () => ( - <Button - name="Toggled button" - value="default" - onClick={logger} - toggled - icon={ - <svg - height="24" - viewBox="0 0 24 24" - width="24" - xmlns="http://www.w3.org/2000/svg" - > - <path - d="m10.7071068 13.2928932c.3604839.360484.3882135.927715.0831886 1.3200062l-.0831886.0942074-5.2921068 5.2918932 2.585.001c.51283584 0 .93550716.3860402.99327227.8833789l.00672773.1166211c0 .5128358-.38604019.9355072-.88337887.9932723l-.11662113.0067277h-5l-.0193545-.0001861c-.02332655-.0004488-.04664039-.0017089-.06989557-.0037803l.08925007.0039664c-.05062028 0-.10036209-.0037612-.14896122-.0110193-.01698779-.0026088-.03441404-.0056829-.05176454-.0092208-.02202032-.0043997-.04371072-.0095935-.06511385-.0154809-.01562367-.0043767-.03101173-.0090077-.04630291-.0140171-.01965516-.0063844-.03943668-.0135776-.058916-.0213659-.01773713-.0070924-.03503998-.014575-.05216303-.0225694-.02066985-.0097032-.0410724-.0201205-.0610554-.0312024-.01211749-.006623-.02433616-.0137311-.0364318-.0211197-.0255662-.0157232-.05042194-.0324946-.07445055-.050318-.00744374-.0054399-.01468311-.010971-.02186305-.0166142-.0631594-.049624-.12042594-.1068905-.17019169-.1703222l.08010726.0903567c-.03539405-.0353941-.06758027-.0727812-.09655864-.1118002-.01784449-.0241759-.03461588-.0490316-.05026715-.0746464-.00746051-.0120471-.0145686-.0242658-.02139626-.0365981-.01087725-.0197682-.02129453-.0401707-.03101739-.060963-.00797473-.0170006-.01545736-.0343035-.02242829-.0517631-.00790975-.0197568-.015103-.0395383-.02167881-.0595996-.00481796-.0148851-.00944895-.0302731-.01370154-.0457434-.00601151-.0215565-.01120534-.0432469-.01567999-.0651989-.00346298-.0174188-.00653707-.0348451-.00914735-.0523272-.00160026-.010231-.00303174-.021012-.00429007-.0318458l-.00276132-.027371c-.00207143-.0232552-.00333152-.0465691-.00378026-.0698956l-.00018615-.0193545v-5c0-.5522847.44771525-1 1-1 .51283584 0 .93550716.3860402.99327227.8833789l.00672773.1166211v2.584l5.29289322-5.2911068c.39052429-.3905243 1.02368928-.3905243 1.41421358 0zm9.2928932-3.2928932v10h-10v-2h8v-8zm-6-6v2h-8v7h-2v-9zm7-2 .0193545.00018615c.0233265.00044874.0466404.00170883.0698956.00378026l-.0892501-.00396641c.0506203 0 .1003621.00376119.1489612.01101934.0169878.00260874.0344141.00568283.0517646.00922073.0220203.00439973.0437107.00959356.0651138.0154809.0156237.00437676.0310117.00900775.0463029.01401712.0196552.0063844.0394367.01357765.058916.02136587.0177371.00709246.03504.01457509.052163.0225694.0206699.00970328.0410724.02012056.0610555.03120241.0121174.00662306.0243361.01373115.0364318.02111968.0255662.01572325.0504219.03249464.0744505.05031806.0074437.00543993.0146831.01097097.021863.01661418.0631595.04962402.120426.10689056.1701917.17032223l-.0801072-.0903567c.035394.03539405.0675802.0727812.0965586.11180017.0178445.02417592.0346159.04903166.0502672.07464642.0074605.01204708.0145686.02426575.0213962.03659809.0108773.01976815.0212946.0401707.0310174.06096295.0079748.01700065.0154574.0343035.0224283.05176313.0079098.01975682.015103.03953834.0216788.05959961.004818.01488507.009449.03027313.0137016.04574344.0060115.02155649.0112053.04324689.0156799.06519887.003463.01741884.0065371.03484509.0091474.05232723.0016003.01023098.0030317.02101195.0042901.03184574l.0030256.03039033c.0015457.01796531.0026074.03596443.003185.05397618l.0005171.03225462v5c0 .55228475-.4477153 1-1 1-.5128358 0-.9355072-.38604019-.9932723-.88337887l-.0067277-.11662113v-2.586l-5.2928932 5.2931068c-.3905243.3905243-1.0236893.3905243-1.4142136 0-.3604839-.360484-.3882135-.92771504-.0831886-1.32000624l.0831886-.09420734 5.2911068-5.29289322h-2.584c-.5128358 0-.9355072-.38604019-.9932723-.88337887l-.0067277-.11662113c0-.51283584.3860402-.93550716.8833789-.99327227l.1166211-.00672773z" - fill="#inherit" - /> - </svg> - } - /> -) +export const IconSmall = Template.bind({}) +IconSmall.args = { + icon: DemoIcon, + small: true, + name: 'Icon small button', + children: null, +} -export const ToggledDisabled = () => ( - <Button - name="Toggled button" - value="default" - onClick={logger} - toggled - disabled - icon={ - <svg - height="24" - viewBox="0 0 24 24" - width="24" - xmlns="http://www.w3.org/2000/svg" - > - <path - d="m10.7071068 13.2928932c.3604839.360484.3882135.927715.0831886 1.3200062l-.0831886.0942074-5.2921068 5.2918932 2.585.001c.51283584 0 .93550716.3860402.99327227.8833789l.00672773.1166211c0 .5128358-.38604019.9355072-.88337887.9932723l-.11662113.0067277h-5l-.0193545-.0001861c-.02332655-.0004488-.04664039-.0017089-.06989557-.0037803l.08925007.0039664c-.05062028 0-.10036209-.0037612-.14896122-.0110193-.01698779-.0026088-.03441404-.0056829-.05176454-.0092208-.02202032-.0043997-.04371072-.0095935-.06511385-.0154809-.01562367-.0043767-.03101173-.0090077-.04630291-.0140171-.01965516-.0063844-.03943668-.0135776-.058916-.0213659-.01773713-.0070924-.03503998-.014575-.05216303-.0225694-.02066985-.0097032-.0410724-.0201205-.0610554-.0312024-.01211749-.006623-.02433616-.0137311-.0364318-.0211197-.0255662-.0157232-.05042194-.0324946-.07445055-.050318-.00744374-.0054399-.01468311-.010971-.02186305-.0166142-.0631594-.049624-.12042594-.1068905-.17019169-.1703222l.08010726.0903567c-.03539405-.0353941-.06758027-.0727812-.09655864-.1118002-.01784449-.0241759-.03461588-.0490316-.05026715-.0746464-.00746051-.0120471-.0145686-.0242658-.02139626-.0365981-.01087725-.0197682-.02129453-.0401707-.03101739-.060963-.00797473-.0170006-.01545736-.0343035-.02242829-.0517631-.00790975-.0197568-.015103-.0395383-.02167881-.0595996-.00481796-.0148851-.00944895-.0302731-.01370154-.0457434-.00601151-.0215565-.01120534-.0432469-.01567999-.0651989-.00346298-.0174188-.00653707-.0348451-.00914735-.0523272-.00160026-.010231-.00303174-.021012-.00429007-.0318458l-.00276132-.027371c-.00207143-.0232552-.00333152-.0465691-.00378026-.0698956l-.00018615-.0193545v-5c0-.5522847.44771525-1 1-1 .51283584 0 .93550716.3860402.99327227.8833789l.00672773.1166211v2.584l5.29289322-5.2911068c.39052429-.3905243 1.02368928-.3905243 1.41421358 0zm9.2928932-3.2928932v10h-10v-2h8v-8zm-6-6v2h-8v7h-2v-9zm7-2 .0193545.00018615c.0233265.00044874.0466404.00170883.0698956.00378026l-.0892501-.00396641c.0506203 0 .1003621.00376119.1489612.01101934.0169878.00260874.0344141.00568283.0517646.00922073.0220203.00439973.0437107.00959356.0651138.0154809.0156237.00437676.0310117.00900775.0463029.01401712.0196552.0063844.0394367.01357765.058916.02136587.0177371.00709246.03504.01457509.052163.0225694.0206699.00970328.0410724.02012056.0610555.03120241.0121174.00662306.0243361.01373115.0364318.02111968.0255662.01572325.0504219.03249464.0744505.05031806.0074437.00543993.0146831.01097097.021863.01661418.0631595.04962402.120426.10689056.1701917.17032223l-.0801072-.0903567c.035394.03539405.0675802.0727812.0965586.11180017.0178445.02417592.0346159.04903166.0502672.07464642.0074605.01204708.0145686.02426575.0213962.03659809.0108773.01976815.0212946.0401707.0310174.06096295.0079748.01700065.0154574.0343035.0224283.05176313.0079098.01975682.015103.03953834.0216788.05959961.004818.01488507.009449.03027313.0137016.04574344.0060115.02155649.0112053.04324689.0156799.06519887.003463.01741884.0065371.03484509.0091474.05232723.0016003.01023098.0030317.02101195.0042901.03184574l.0030256.03039033c.0015457.01796531.0026074.03596443.003185.05397618l.0005171.03225462v5c0 .55228475-.4477153 1-1 1-.5128358 0-.9355072-.38604019-.9932723-.88337887l-.0067277-.11662113v-2.586l-5.2928932 5.2931068c-.3905243.3905243-1.0236893.3905243-1.4142136 0-.3604839-.360484-.3882135-.92771504-.0831886-1.32000624l.0831886-.09420734 5.2911068-5.29289322h-2.584c-.5128358 0-.9355072-.38604019-.9932723-.88337887l-.0067277-.11662113c0-.51283584.3860402-.93550716.8833789-.99327227l.1166211-.00672773z" - fill="#inherit" - /> - </svg> - } - /> -) +export const Toggled = Template.bind({}) +Toggled.args = { + toggled: true, + icon: DemoIcon, + name: 'Toggled button', + children: null, +} +Toggled.parameters = { + docs: { + description: { + story: + 'A button can represent an on/off state using the toggle option. \ + Use a toggle button when the user can enable or disable an option and \ + a checkbox or switch is not suitable. This will most often be in the case of \ + a toolbar, such as bold or italic options in a text editing toolbar. \ + A toggle button in this example uses an icon and does not need text. \ + A text label should be provided in a tooltip on hover. The toggle option \ + is available for basic and secondary type buttons.', + }, + }, +} + +export const ToggledDisabled = Template.bind({}) +ToggledDisabled.args = { + toggled: true, + disabled: true, + icon: DemoIcon, + name: 'Toggled button', + children: null, +} diff --git a/packages/core/src/ButtonStrip/ButtonStrip.js b/packages/core/src/ButtonStrip/ButtonStrip.js index 88f4cf412d..4689167156 100644 --- a/packages/core/src/ButtonStrip/ButtonStrip.js +++ b/packages/core/src/ButtonStrip/ButtonStrip.js @@ -1,6 +1,7 @@ import propTypes from '@dhis2/prop-types' import { spacers } from '@dhis2/ui-constants' import cx from 'classnames' +import PropTypes from 'prop-types' import React, { Children } from 'react' ;('') // TODO: https://github.com/jsdoc/jsdoc/issues/1718 @@ -46,7 +47,7 @@ const ButtonStrip = ({ className, children, middle, end, dataTest }) => ( const alignmentPropType = propTypes.mutuallyExclusive( ['middle', 'end'], - propTypes.bool + PropTypes.bool ) ButtonStrip.defaultProps = { @@ -65,10 +66,12 @@ ButtonStrip.defaultProps = { * @prop {string} [dataTest] */ ButtonStrip.propTypes = { - children: propTypes.node, - className: propTypes.string, - dataTest: propTypes.string, + children: PropTypes.node, + className: PropTypes.string, + dataTest: PropTypes.string, + /** Horizontal alignment for buttons. Mutually exclusive with `middle` prop */ end: alignmentPropType, + /** Horizontal alignment. Mutually exclusive with `end` prop */ middle: alignmentPropType, } diff --git a/packages/core/src/ButtonStrip/ButtonStrip.stories.js b/packages/core/src/ButtonStrip/ButtonStrip.stories.js index b295574547..6b362e1852 100644 --- a/packages/core/src/ButtonStrip/ButtonStrip.stories.js +++ b/packages/core/src/ButtonStrip/ButtonStrip.stories.js @@ -1,12 +1,19 @@ -import { storiesOf } from '@storybook/react' import React from 'react' import { Button, SplitButton } from '../index.js' import { ButtonStrip } from './ButtonStrip.js' +const description = ` +A wrapper for buttons to add spacing and alignment. + +\`\`\`js +import { ButtonStrip } from '@dhis2/ui' +\`\`\` +` + const Wrapper = fn => ( <div style={{ - display: 'inline-block', + width: '100%', border: '1px solid #c4c9cc', padding: 8, }} @@ -15,30 +22,55 @@ const Wrapper = fn => ( </div> ) -storiesOf('ButtonStrip', module) - .addDecorator(Wrapper) - .add('Default', () => ( - <ButtonStrip> - <Button>Save</Button> - <Button>Save</Button> - <Button>Save</Button> - <Button>Save</Button> - <SplitButton>Label?</SplitButton> - </ButtonStrip> - )) - .add('Default - aligned middle', () => ( - <ButtonStrip middle> - <Button>Save</Button> - <Button>Save</Button> - <Button>Save</Button> - <Button>Save</Button> - </ButtonStrip> - )) - .add('Default - aligned right', () => ( - <ButtonStrip end> - <Button>Save</Button> - <Button>Save</Button> - <Button>Save</Button> - <Button>Save</Button> - </ButtonStrip> - )) +const alignmentArgType = { + table: { + type: { + summary: 'bool', + detail: "'middle' and 'end' are mutually exclusive props", + }, + }, + control: { type: 'boolean' }, +} + +export default { + title: 'Actions/Buttons/Button Strip', + component: ButtonStrip, + decorators: [Wrapper], + parameters: { docs: { description: { component: description } } }, + argTypes: { + middle: { ...alignmentArgType }, + end: { ...alignmentArgType }, + }, +} + +export const Default = args => ( + <ButtonStrip {...args}> + <Button>Save</Button> + <Button>Save</Button> + <Button>Save</Button> + <Button>Save</Button> + <SplitButton>Label?</SplitButton> + </ButtonStrip> +) + +export const DefaultAlignedMiddle = args => ( + <ButtonStrip {...args}> + <Button>Save</Button> + <Button>Save</Button> + <Button>Save</Button> + <Button>Save</Button> + </ButtonStrip> +) +DefaultAlignedMiddle.args = { middle: true } +DefaultAlignedMiddle.storyName = 'Default - aligned middle' + +export const DefaultAlignedRight = args => ( + <ButtonStrip {...args}> + <Button>Save</Button> + <Button>Save</Button> + <Button>Save</Button> + <Button>Save</Button> + </ButtonStrip> +) +DefaultAlignedRight.args = { end: true } +DefaultAlignedRight.storyName = 'Default - aligned right' diff --git a/packages/core/src/Card/Card.js b/packages/core/src/Card/Card.js index 766da51112..6a91632e99 100644 --- a/packages/core/src/Card/Card.js +++ b/packages/core/src/Card/Card.js @@ -1,6 +1,6 @@ -import propTypes from '@dhis2/prop-types' import { colors } from '@dhis2/ui-constants' import cx from 'classnames' +import PropTypes from 'prop-types' import React from 'react' ;('') // TODO: https://github.com/jsdoc/jsdoc/issues/1718 @@ -46,9 +46,9 @@ Card.defaultProps = { * @prop {string} [dataTest] */ Card.propTypes = { - children: propTypes.node, - className: propTypes.string, - dataTest: propTypes.string, + children: PropTypes.node, + className: PropTypes.string, + dataTest: PropTypes.string, } export { Card } diff --git a/packages/core/src/Card/Card.stories.js b/packages/core/src/Card/Card.stories.js index df1883d6f6..d19f323a18 100644 --- a/packages/core/src/Card/Card.stories.js +++ b/packages/core/src/Card/Card.stories.js @@ -1,11 +1,36 @@ -import { storiesOf } from '@storybook/react' import React from 'react' +import { Box } from '../Box/Box.js' import { Card } from './Card.js' -const Wrapper = fn => ( - <div style={{ width: '358px', height: '358px' }}>{fn()}</div> -) +const subtitle = ` +A card is a container element for grouping together +and separating blocks of content. +` + +const description = ` +Use a card where there is content that can be grouped together. +Cards are most often useful when this grouped content may be repeated, +for example with items on a dashboard, or different sections of patient +information displayed in a profile. + +Note that it requires a parent, like [Box](../?path=/docs/layout-box--default), to define its size. -storiesOf('Card', module) - .addDecorator(Wrapper) - .add('Default', () => <Card />) +\`\`\`js +import { Card } from '@dhis2/ui' +\`\`\` +` + +export default { + title: 'Layout/Card', + component: Card, + parameters: { + componentSubtitle: subtitle, + docs: { description: { component: description } }, + }, +} + +export const Default = args => ( + <Box width="358px" height="358px"> + <Card {...args} /> + </Box> +) diff --git a/packages/core/src/CenteredContent/CenteredContent.js b/packages/core/src/CenteredContent/CenteredContent.js index c00b641c6d..bc5add7c70 100644 --- a/packages/core/src/CenteredContent/CenteredContent.js +++ b/packages/core/src/CenteredContent/CenteredContent.js @@ -1,5 +1,5 @@ -import propTypes from '@dhis2/prop-types' import cx from 'classnames' +import PropTypes from 'prop-types' import React, { forwardRef } from 'react' ;('') // TODO: https://github.com/jsdoc/jsdoc/issues/1718 @@ -59,10 +59,11 @@ CenteredContent.defaultProps = { * @prop {string} [position=middle] One of `top`, `middle`, `bottom` */ CenteredContent.propTypes = { - children: propTypes.node, - className: propTypes.string, - dataTest: propTypes.string, - position: propTypes.oneOf(['top', 'middle', 'bottom']), + children: PropTypes.node, + className: PropTypes.string, + dataTest: PropTypes.string, + /** Vertical alignment */ + position: PropTypes.oneOf(['top', 'middle', 'bottom']), } export { CenteredContent } diff --git a/packages/core/src/CenteredContent/CenteredContent.stories.js b/packages/core/src/CenteredContent/CenteredContent.stories.js index bd2aa40d79..1a071a3e7c 100644 --- a/packages/core/src/CenteredContent/CenteredContent.stories.js +++ b/packages/core/src/CenteredContent/CenteredContent.stories.js @@ -1,25 +1,34 @@ import React from 'react' import { CenteredContent } from './CenteredContent.js' +const description = ` +Centers children horizontally, and by default, vertically. +Use the \`top\` or \`bottom\` props to change vertical alignment. + +\`\`\`js +import { CenteredContent } from '@dhis2/ui' +\`\`\` +` + +const Wrapper = story => <div style={{ height: '150px' }}>{story()}</div> + export default { - title: 'CenteredContent', + title: 'Layout/Centered Content', component: CenteredContent, + decorators: [Wrapper], + parameters: { docs: { description: { component: description } } }, } -export const Default = () => ( - <CenteredContent> +const Template = args => ( + <CenteredContent {...args}> <span>Center me</span> </CenteredContent> ) -export const Top = () => ( - <CenteredContent position="top"> - <span>Center me</span> - </CenteredContent> -) +export const Default = Template.bind({}) -export const Bottom = () => ( - <CenteredContent position="bottom"> - <span>Center me</span> - </CenteredContent> -) +export const Top = Template.bind({}) +Top.args = { position: 'top' } + +export const Bottom = Template.bind({}) +Bottom.args = { position: 'bottom' } diff --git a/packages/core/src/Checkbox/Checkbox.js b/packages/core/src/Checkbox/Checkbox.js index 9a5a63dc35..ebd46161c0 100644 --- a/packages/core/src/Checkbox/Checkbox.js +++ b/packages/core/src/Checkbox/Checkbox.js @@ -1,6 +1,7 @@ import propTypes from '@dhis2/prop-types' import { colors, theme, sharedPropTypes } from '@dhis2/ui-constants' import cx from 'classnames' +import PropTypes from 'prop-types' import React, { Component, createRef } from 'react' import { CheckboxRegular, CheckboxDense } from '../Icons/index.js' ;('') // TODO: https://github.com/jsdoc/jsdoc/issues/1718 @@ -207,27 +208,28 @@ Checkbox.defaultProps = { const uniqueOnStatePropType = propTypes.mutuallyExclusive( ['checked', 'indeterminate'], - propTypes.bool + PropTypes.bool ) Checkbox.propTypes = { checked: uniqueOnStatePropType, - className: propTypes.string, - dataTest: propTypes.string, - dense: propTypes.bool, - disabled: propTypes.bool, + className: PropTypes.string, + dataTest: PropTypes.string, + dense: PropTypes.bool, + disabled: PropTypes.bool, error: sharedPropTypes.statusPropType, indeterminate: uniqueOnStatePropType, - initialFocus: propTypes.bool, - label: propTypes.node, - name: propTypes.string, - tabIndex: propTypes.string, + initialFocus: PropTypes.bool, + label: PropTypes.node, + name: PropTypes.string, + tabIndex: PropTypes.string, valid: sharedPropTypes.statusPropType, - value: propTypes.string, + value: PropTypes.string, warning: sharedPropTypes.statusPropType, - onBlur: propTypes.func, - onChange: propTypes.func, - onFocus: propTypes.func, + onBlur: PropTypes.func, + /** Called with signature `(object, event)` */ + onChange: PropTypes.func, + onFocus: PropTypes.func, } export { Checkbox } diff --git a/packages/core/src/Checkbox/Checkbox.stories.js b/packages/core/src/Checkbox/Checkbox.stories.js index 2bef6eab40..d5be16e80a 100644 --- a/packages/core/src/Checkbox/Checkbox.stories.js +++ b/packages/core/src/Checkbox/Checkbox.stories.js @@ -1,7 +1,22 @@ -import { storiesOf } from '@storybook/react' +import { sharedPropTypes } from '@dhis2/ui-constants' import React from 'react' import { Checkbox } from './Checkbox.js' +const subtitle = + 'A checkbox is a control that allows a user to toggle an option.' + +const description = ` +Checkboxes are used when an option can be toggled on or off. Toggling a checkbox on (true) is always considered a positive action and should reflect a positive/true/on state. Multiple checkboxes can be used in a list where a user can toggle multiple elements. + +Do not use checkboxes in a list of several options where only a single option can be toggled, use [radio buttons](../?path=/docs/forms-radio-radio--default) here instead. + +If there are many options that need to select from, consider using a [select](../?path=/docs/forms-single-select-single-select--with-options) instead. + +\`\`\`js +import { Checkbox } from '@dhis2/ui' +\`\`\` +` + window.onChange = (payload, event) => { console.log('onClick payload', payload) console.log('onClick event', event) @@ -21,376 +36,142 @@ const onChange = (...args) => window.onChange(...args) const onFocus = (...args) => window.onFocus(...args) const onBlur = (...args) => window.onBlur(...args) -storiesOf('Checkbox', module) - // Regular - .add('Default', () => ( - <Checkbox - name="Ex" - label="Checkbox" - value="default" - onChange={onChange} - onFocus={onFocus} - onBlur={onBlur} - /> - )) - - .add('Focused unchecked', () => ( - <> - <Checkbox - initialFocus - name="Ex" - label="Checkbox" - value="default" - className="initially-focused" - onChange={onChange} - onFocus={onFocus} - onBlur={onBlur} - /> - <Checkbox - name="Ex2" - label="Checkbox" - value="default2" - className="initially-unfocused" - onChange={onChange} - onFocus={onFocus} - onBlur={onBlur} - /> - </> - )) - - .add('Focused checked', () => ( - <> - <Checkbox - initialFocus - checked - name="Ex" - label="Checkbox" - value="default" - onChange={onChange} - onFocus={onFocus} - onBlur={onBlur} - /> - <Checkbox - checked - name="Ex2" - label="Checkbox" - value="default2" - onChange={onChange} - onFocus={onFocus} - onBlur={onBlur} - /> - </> - )) - - .add('Checked', () => ( - <Checkbox - name="Ex" - label="Checkbox" - checked - value="checked" - onChange={onChange} - onFocus={onFocus} - onBlur={onBlur} - /> - )) +const defaultArgs = { name: 'Ex', label: 'Checkbox', onChange, onFocus, onBlur } + +const uniqueOnStateArgType = { + table: { + type: { + summary: 'bool', + detail: + "'checked' and 'indeterminate' are mutually exclusive props", + }, + }, + control: { type: 'boolean' }, +} - .add('Indeterminate', () => ( - <Checkbox - name="Ex" - label="Checkbox" - indeterminate - value="checked" - onChange={onChange} - onFocus={onFocus} - onBlur={onBlur} - /> - )) - - .add('Disabled', () => ( - <> - <Checkbox - name="Ex" - label="Checkbox" - disabled - value="disabled" - onChange={onChange} - onFocus={onFocus} - onBlur={onBlur} - /> - <Checkbox - name="Ex" - label="Checkbox" - disabled - checked - value="disabled" - onChange={onChange} - onFocus={onFocus} - onBlur={onBlur} - /> - </> - )) - - .add('Valid', () => ( - <> - <Checkbox - name="Ex" - label="Checkbox" - valid - value="valid" - onChange={onChange} - onFocus={onFocus} - onBlur={onBlur} - /> - <Checkbox - name="Ex" - label="Checkbox" - valid - checked - value="valid" - onChange={onChange} - onFocus={onFocus} - onBlur={onBlur} - /> - </> - )) - - .add('Warning', () => ( - <> - <Checkbox - name="Ex" - label="Checkbox" - warning - value="warning" - onChange={onChange} - onFocus={onFocus} - onBlur={onBlur} - /> - <Checkbox - name="Ex" - label="Checkbox" - warning - checked - value="warning" - onChange={onChange} - onFocus={onFocus} - onBlur={onBlur} - /> - </> - )) - - .add('Error', () => ( - <> - <Checkbox - name="Ex" - label="Checkbox" - error - value="error" - onChange={onChange} - onFocus={onFocus} - onBlur={onBlur} - /> - <Checkbox - name="Ex" - label="Checkbox" - error - checked - value="error" - onChange={onChange} - onFocus={onFocus} - onBlur={onBlur} - /> - </> - )) - - .add('Image label', () => ( - <Checkbox - name="Ex" - label={<img src="https://picsum.photos/id/82/200/100" />} - value="with-help" - onChange={onChange} - onFocus={onFocus} - onBlur={onBlur} - /> - )) +export default { + title: 'Forms/Checkbox/Checkbox', + component: Checkbox, + parameters: { + componentSubtitle: subtitle, + docs: { description: { component: description } }, + }, + // Sets default args on all stories unless overridden + args: { ...defaultArgs }, + argTypes: { + checked: { ...uniqueOnStateArgType }, + indeterminate: { ...uniqueOnStateArgType }, + valid: { ...sharedPropTypes.statusArgType }, + warning: { ...sharedPropTypes.statusArgType }, + error: { ...sharedPropTypes.statusArgType }, + }, +} - // Dense - .add('Default - Dense', () => ( - <Checkbox - dense - name="Ex" - label="Checkbox" - value="default" - onChange={onChange} - onFocus={onFocus} - onBlur={onBlur} - /> - )) +const Template = args => <Checkbox {...args} /> - .add('Focused unchecked - Dense', () => ( - <Checkbox - dense - initialFocus - name="Ex" - label="Checkbox" - value="default" - onChange={onChange} - onFocus={onFocus} - onBlur={onBlur} - /> - )) +const CheckedUncheckedTemplate = args => ( + <> + <Checkbox {...args} /> + <Checkbox checked {...args} /> + </> +) - .add('Focused checked - Dense', () => ( +export const Default = Template.bind({}) +Default.args = { value: 'default' } + +export const FocusedUnchecked = args => ( + <> <Checkbox - dense initialFocus - checked - name="Ex" - label="Checkbox" value="default" - onChange={onChange} - onFocus={onFocus} - onBlur={onBlur} + className="initially-focused" + {...args} /> - )) + <Checkbox value="default2" className="initially-unfocused" {...args} /> + </> +) +FocusedUnchecked.storyName = 'Focused unchecked' +/** + * 'initialFocus' causes docs page to scroll away every time a control is + * changed, so it's disabled + */ +FocusedUnchecked.parameters = { docs: { disable: true } } + +export const FocusedChecked = args => ( + <> + <Checkbox initialFocus checked value="default" {...args} /> + <Checkbox checked value="default2" {...args} /> + </> +) +FocusedChecked.storyName = 'Focused checked' +FocusedChecked.parameters = { docs: { disable: true } } + +export const Checked = Template.bind({}) +Checked.args = { checked: true, value: 'checked' } + +export const Indeterminate = Template.bind({}) +Indeterminate.args = { indeterminate: true, value: 'checked' } + +export const Disabled = CheckedUncheckedTemplate.bind({}) +Disabled.args = { disabled: true, value: 'disabled' } + +export const Valid = CheckedUncheckedTemplate.bind({}) +Valid.args = { valid: true, value: 'valid' } + +export const Warning = CheckedUncheckedTemplate.bind({}) +Warning.args = { warning: true, value: 'warning' } + +export const Error = CheckedUncheckedTemplate.bind({}) +Error.args = { error: true, value: 'error' } + +export const ImageLabel = Template.bind({}) +ImageLabel.args = { + label: <img src="https://picsum.photos/id/82/200/100" />, + value: 'with-help', +} - .add('Checked - Dense', () => ( - <Checkbox - dense - name="Ex" - label="Checkbox" - checked - value="checked" - onChange={onChange} - onFocus={onFocus} - onBlur={onBlur} - /> - )) +export const DefaultDense = Template.bind({}) +DefaultDense.args = { dense: true, value: 'default' } +DefaultDense.storyName = 'Default - Dense' - .add('Indeterminate - Dense', () => ( - <Checkbox - dense - name="Ex" - label="Checkbox" - indeterminate - value="checked" - onChange={onChange} - onFocus={onFocus} - onBlur={onBlur} - /> - )) - - .add('Disabled - Dense', () => ( - <> - <Checkbox - dense - name="Ex" - label="Checkbox" - disabled - value="disabled" - onChange={onChange} - onFocus={onFocus} - onBlur={onBlur} - /> - <Checkbox - dense - name="Ex" - label="Checkbox" - disabled - checked - value="disabled" - onChange={onChange} - onFocus={onFocus} - onBlur={onBlur} - /> - </> - )) - - .add('Valid - Dense', () => ( - <> - <Checkbox - dense - name="Ex" - label="Checkbox" - valid - value="valid" - onChange={onChange} - onFocus={onFocus} - onBlur={onBlur} - /> - <Checkbox - dense - name="Ex" - label="Checkbox" - valid - checked - value="valid" - onChange={onChange} - onFocus={onFocus} - onBlur={onBlur} - /> - </> - )) - - .add('Warning - Dense', () => ( - <> - <Checkbox - dense - name="Ex" - label="Checkbox" - warning - value="warning" - onChange={onChange} - onFocus={onFocus} - onBlur={onBlur} - /> - <Checkbox - dense - name="Ex" - label="Checkbox" - warning - checked - value="warning" - onChange={onChange} - onFocus={onFocus} - onBlur={onBlur} - /> - </> - )) - - .add('Error - Dense', () => ( - <> - <Checkbox - dense - name="Ex" - label="Checkbox" - error - value="error" - onChange={onChange} - onFocus={onFocus} - onBlur={onBlur} - /> - <Checkbox - dense - name="Ex" - label="Checkbox" - error - checked - value="error" - onChange={onChange} - onFocus={onFocus} - onBlur={onBlur} - /> - </> - )) - - .add('Image label - Dense', () => ( - <Checkbox - dense - name="Ex" - label={<img src="https://picsum.photos/id/82/200/100" />} - value="with-help" - onChange={onChange} - onFocus={onFocus} - onBlur={onBlur} - /> - )) +export const FocusedUncheckedDense = Template.bind({}) +FocusedUncheckedDense.args = { + dense: true, + initialFocus: true, + value: 'default', +} +FocusedUncheckedDense.storyName = 'Focused unchecked - Dense' +FocusedUncheckedDense.parameters = { docs: { disable: true } } + +export const FocusedCheckedDense = Template.bind({}) +FocusedCheckedDense.args = { ...FocusedUncheckedDense.args, checked: true } +FocusedCheckedDense.storyName = 'Focused checked - Dense' +FocusedCheckedDense.parameters = { docs: { disable: true } } + +export const CheckedDense = Template.bind({}) +CheckedDense.args = { dense: true, checked: true, value: 'checked' } +CheckedDense.storyName = 'Checked - Dense' + +export const IndeterminateDense = Template.bind({}) +IndeterminateDense.args = { dense: true, indeterminate: true, value: 'checked' } +IndeterminateDense.storyName = 'Indeterminate - Dense' + +export const DisabledDense = CheckedUncheckedTemplate.bind({}) +DisabledDense.args = { ...Disabled.args, dense: true } +DisabledDense.storyName = 'Disabled - Dense' + +export const ValidDense = CheckedUncheckedTemplate.bind({}) +ValidDense.args = { ...Valid.args, dense: true } +ValidDense.storyName = 'Valid - Dense' + +export const WarningDense = CheckedUncheckedTemplate.bind({}) +WarningDense.args = { ...Warning.args, dense: true } +WarningDense.storyName = 'Warning - Dense' + +export const ErrorDense = CheckedUncheckedTemplate.bind({}) +ErrorDense.args = { ...Error.args, dense: true } +ErrorDense.storyName = 'Error - Dense' + +export const ImageLabelDense = Template.bind({}) +ImageLabelDense.args = { ...ImageLabel.args, dense: true } +ImageLabelDense.storyName = 'Image label - Dense' diff --git a/packages/core/src/Chip/Chip.js b/packages/core/src/Chip/Chip.js index eee6d31453..7e7c72ce85 100644 --- a/packages/core/src/Chip/Chip.js +++ b/packages/core/src/Chip/Chip.js @@ -1,6 +1,6 @@ -import propTypes from '@dhis2/prop-types' import { colors, theme, spacers } from '@dhis2/ui-constants' import cx from 'classnames' +import PropTypes from 'prop-types' import React from 'react' import { Content } from './Content.js' import { Icon } from './Icon.js' @@ -124,17 +124,17 @@ Chip.defaultProps = { * @prop {string} [dataTest] */ Chip.propTypes = { - children: propTypes.string, - className: propTypes.string, - dataTest: propTypes.string, - dense: propTypes.bool, - disabled: propTypes.bool, - dragging: propTypes.bool, - icon: propTypes.element, - overflow: propTypes.bool, - selected: propTypes.bool, - onClick: propTypes.func, - onRemove: propTypes.func, + children: PropTypes.string, + className: PropTypes.string, + dataTest: PropTypes.string, + dense: PropTypes.bool, + disabled: PropTypes.bool, + dragging: PropTypes.bool, + icon: PropTypes.element, + overflow: PropTypes.bool, + selected: PropTypes.bool, + onClick: PropTypes.func, + onRemove: PropTypes.func, } export { Chip } diff --git a/packages/core/src/Chip/Chip.stories.js b/packages/core/src/Chip/Chip.stories.js index 3bb125f2c7..86e591fa4c 100644 --- a/packages/core/src/Chip/Chip.stories.js +++ b/packages/core/src/Chip/Chip.stories.js @@ -1,6 +1,26 @@ -import { storiesOf } from '@storybook/react' import React from 'react' -import Chip from './Chip.js' +import { Chip } from './Chip.js' + +const subtitle = `Chips are useful for displaying a selection of defined choices and filters to the user.` + +const description = ` +Chips are used to display a list of defined options, filters or views for a related view. Chips are always a secondary content to a main element, for example a data table or a dashboard. +See an example application of chips [here](https://github.com/dhis2/design-system/blob/master/atoms/chip.md#usage). + +#### Chip vs. Button + +Chips should only be used for filtering or selecting an option. Do not use filters to trigger actions such as 'Save', 'Exit' or 'Open'. Use a [button](../?path=/docs/actions-buttons-button--basic) to trigger actions. + +#### Displaying chips + +- Chips should be displayed in a horizontal list, where space permits. +- Do not stack single chips on top of one another if there is space to display them inline. +- Do not use horizontal scrolling to display a large number of chips. Always wrap chips onto a new line below. + +\`\`\`js +import { Chip } from '@dhis2/ui' +\`\`\` +` window.onClick = (payload, event) => { console.log('onClick payload', payload) @@ -15,40 +35,33 @@ window.onRemove = (payload, event) => { const onClick = (...args) => window.onClick(...args) const onRemove = (...args) => window.onRemove(...args) -storiesOf('Chip', module) - .add('Default', () => <Chip onClick={onClick}>Chippy</Chip>) - - .add('Selected', () => ( - <Chip selected onClick={onClick}> - Chipmunk - </Chip> - )) - - .add('Overflow', () => ( - <Chip selected onClick={onClick}> - A super long chip which should definitely truncate - </Chip> - )) - - .add('Removable', () => ( - <Chip onClick={onClick} onRemove={onRemove}> - Chipmunk - </Chip> - )) - - .add('Icon', () => ( - <Chip onClick={onClick} icon={<Globe />}> - With an icon - </Chip> - )) - - .add('Dense', () => <Chip dense>I am dense</Chip>) - - .add('Dense removeable', () => ( - <Chip dense onRemove={onRemove}> - Removeable and dense - </Chip> - )) +export default { + title: 'Actions/Chip', + component: Chip, + parameters: { + componentSubtitle: subtitle, + docs: { description: { component: description } }, + }, + args: { onClick }, +} + +const Template = args => <Chip {...args} /> + +export const Default = Template.bind({}) +Default.args = { children: 'Chippy' } + +export const Selected = Template.bind({}) +Selected.args = { selected: true, children: 'Chipmunk' } + +export const Overflow = Template.bind({}) +Overflow.args = { + overflow: true, + selected: true, + children: 'A super long chip which should definitely truncate', +} + +export const Removable = Template.bind({}) +Removable.args = { ...Default.args, onRemove } const Globe = () => ( <svg role="img" viewBox="0 0 24 24" height="100px" width="100px"> @@ -56,3 +69,16 @@ const Globe = () => ( <path d="M12.008 4.866c-2.914 0-5.377.679-6.995 2.11-.05-.088-.085-.163-.141-.253-.17-.273-.328-.583-.828-.89a2.699 2.699 0 0 0-1.014-.387 2.14 2.14 0 0 0-.47-.001l.099-.013c-.91 0-1.442.546-1.788 1.016-.344.47-.566.999-.71 1.577-.285 1.155-.289 2.651.81 3.767.575.588 1.251.863 1.863 1.027.118.032.223.04.336.063.399 1.892 1.58 3.492 3.158 4.381v.001h.002c.929.522 1.81.761 2.472 1.014.924.357 1.9.751 3.036.85l.06.006h.15c1.306 0 2.296-.527 3.152-.855.655-.25 1.54-.482 2.477-1.008a6.156 6.156 0 0 0 1.196-.88 6.759 6.759 0 0 0 1.967-3.45 3.913 3.913 0 0 0 2.19-1.15c1.098-1.115 1.094-2.611.809-3.766-.144-.578-.366-1.106-.71-1.577-.345-.47-.879-1.016-1.788-1.016l.1.013a2.14 2.14 0 0 0-.472.002 2.695 2.695 0 0 0-1.01.385c-.499.307-.658.616-.827.888-.055.089-.09.163-.139.249-1.617-1.426-4.076-2.103-6.985-2.103zm.023 1.37c3.557 0 6.013 1.065 7.057 2.97.63-.243 1.093-1.89 1.612-2.209.42-.26.665-.195.665-.195.95 0 1.93 2.797.712 4.032-.644.657-1.83.842-2.422.795-.045 1.906-1.155 3.626-2.624 4.45-.748.42-1.552.64-2.296.924-.921.352-1.786.763-2.662.763h-.092c-.872-.076-1.74-.407-2.662-.763-.744-.285-1.548-.51-2.296-.93-1.46-.824-2.571-2.54-2.616-4.445-.587.05-1.788-.132-2.436-.794-1.217-1.235-.238-4.032.712-4.032 0 0 .246-.064.666.195.521.321.987 1.984 1.62 2.214C6.013 7.3 8.47 6.236 12.032 6.236zm-3.876 2.33a3.053 3.053 0 1 0 0 6.105 3.053 3.053 0 0 0 0-6.105zm7.751 0a3.052 3.052 0 1 0 0 6.105 3.052 3.052 0 0 0 0-6.105zM8.591 10.28a1.357 1.357 0 0 1 0 2.713 1.356 1.356 0 0 1-1.313-1.694.57.57 0 0 0 1.098-.216.57.57 0 0 0-.547-.57 1.35 1.35 0 0 1 .762-.233zm6.888 0a1.357 1.357 0 0 1 0 2.713 1.356 1.356 0 0 1-1.314-1.694.57.57 0 1 0 .552-.785 1.35 1.35 0 0 1 .762-.234zM11.52 14.93c-.239.02-.377.146-.377.476 0 .21.138.365.378.365a.143.143 0 0 0 .033-.282c-.022-.005-.13-.044-.13-.136 0-.093 0-.125.183-.15.078-.012.116-.105.092-.18-.024-.075-.094-.1-.18-.093zm1.023 0c-.085-.006-.156.018-.18.093-.024.075.015.168.093.18.182.025.182.057.182.15 0 .092-.107.131-.13.136a.143.143 0 0 0 .033.282c.24 0 .379-.155.379-.365 0-.33-.139-.456-.377-.476z" /> </svg> ) + +export const Icon = Template.bind({}) +Icon.args = { icon: <Globe />, children: 'With an icon' } + +export const Dense = () => <Chip dense>I am dense</Chip> +Dense.args = { dense: true, children: 'I am dense' } + +export const DenseRemoveable = Template.bind({}) +DenseRemoveable.args = { + ...Dense.args, + ...Removable.args, + children: 'Removable and dense', +} diff --git a/packages/core/src/CircularLoader/CircularLoader.js b/packages/core/src/CircularLoader/CircularLoader.js index 1b87b40626..42a35d7352 100644 --- a/packages/core/src/CircularLoader/CircularLoader.js +++ b/packages/core/src/CircularLoader/CircularLoader.js @@ -1,6 +1,6 @@ -import propTypes from '@dhis2/prop-types' import { theme, spacers, sharedPropTypes } from '@dhis2/ui-constants' import cx from 'classnames' +import PropTypes from 'prop-types' import React from 'react' ;('') // TODO: https://github.com/jsdoc/jsdoc/issues/1718 @@ -101,8 +101,8 @@ CircularLoader.defaultProps = { * @prop {string} [dataTest] */ CircularLoader.propTypes = { - className: propTypes.string, - dataTest: propTypes.string, + className: PropTypes.string, + dataTest: PropTypes.string, large: sharedPropTypes.sizePropType, small: sharedPropTypes.sizePropType, } diff --git a/packages/core/src/CircularLoader/CircularLoader.stories.js b/packages/core/src/CircularLoader/CircularLoader.stories.js index a88fd01314..0cf36741d0 100644 --- a/packages/core/src/CircularLoader/CircularLoader.stories.js +++ b/packages/core/src/CircularLoader/CircularLoader.stories.js @@ -1,11 +1,38 @@ +import { sharedPropTypes } from '@dhis2/ui-constants' import React from 'react' import { CircularLoader } from './CircularLoader.js' +const subtitle = `Used to inform the user that an element or page is in a loading state` + +const description = ` +Use loading indicators whenever a component or application takes longer than 700ms to load. After this time a loader should be displayed so that the user can understand what is happening: loading is in progress. Consider that without a loading indicator a user would be unsure of their current status, so they are important UI elements. + +A circular loader is used when the loading time is uncertain and cannot be displayed as a percentage. A circular loader can spin many times, and each spin does not represent any amount of completion. + +\`\`\`js +import { CircularLoader } from '@dhis2/ui' +\`\`\` +` + export default { - title: 'CircularLoader', + title: 'Feedback/Loading Indicators/Circular Loader', component: CircularLoader, + parameters: { + componentSubtitle: subtitle, + docs: { description: { component: description } }, + }, + argTypes: { + small: { ...sharedPropTypes.sizeArgType }, + large: { ...sharedPropTypes.sizeArgType }, + }, } -export const Default = () => <CircularLoader /> -export const Small = () => <CircularLoader small /> -export const Large = () => <CircularLoader large /> +const Template = args => <CircularLoader {...args} /> + +export const Default = Template.bind({}) + +export const Small = Template.bind({}) +Small.args = { small: true } + +export const Large = Template.bind({}) +Large.args = { large: true } diff --git a/packages/core/src/ComponentCover/ComponentCover.js b/packages/core/src/ComponentCover/ComponentCover.js index 0d5ffd487f..15bd96d3d8 100644 --- a/packages/core/src/ComponentCover/ComponentCover.js +++ b/packages/core/src/ComponentCover/ComponentCover.js @@ -1,6 +1,6 @@ -import propTypes from '@dhis2/prop-types' import { layers } from '@dhis2/ui-constants' import cx from 'classnames' +import PropTypes from 'prop-types' import React from 'react' ;('') // TODO: https://github.com/jsdoc/jsdoc/issues/1718 @@ -61,11 +61,12 @@ ComponentCover.defaultProps = { * @prop {function} [onClick] */ ComponentCover.propTypes = { - children: propTypes.node, - className: propTypes.string, - dataTest: propTypes.string, - translucent: propTypes.bool, - onClick: propTypes.func, + children: PropTypes.node, + className: PropTypes.string, + dataTest: PropTypes.string, + /** Adds a semi-transparent background to the cover */ + translucent: PropTypes.bool, + onClick: PropTypes.func, } export { ComponentCover } diff --git a/packages/core/src/ComponentCover/ComponentCover.stories.e2e.js b/packages/core/src/ComponentCover/ComponentCover.stories.e2e.js index 38906edbc9..a367ab25be 100644 --- a/packages/core/src/ComponentCover/ComponentCover.stories.e2e.js +++ b/packages/core/src/ComponentCover/ComponentCover.stories.e2e.js @@ -8,9 +8,9 @@ export default { title: 'ComponentCover', component: ComponentCover, decorators: [ - storyFn => ( + story => ( <div> - {storyFn()} + {story()} <style jsx>{` div { width: 400px; diff --git a/packages/core/src/ComponentCover/ComponentCover.stories.js b/packages/core/src/ComponentCover/ComponentCover.stories.js index 2c9d82335c..1379d9eb91 100644 --- a/packages/core/src/ComponentCover/ComponentCover.stories.js +++ b/packages/core/src/ComponentCover/ComponentCover.stories.js @@ -2,62 +2,50 @@ import React from 'react' import { CircularLoader, CenteredContent } from '../index.js' import { ComponentCover } from './ComponentCover.js' +const description = ` +Covers sibling components. Useful for covering a component while it is loading. + +\`\`\`js +import { ComponentCover } from '@dhis2/ui' +\`\`\` +` + export default { - title: 'ComponentCover', + title: 'Helpers/Component Cover', component: ComponentCover, - decorators: [ - storyFn => ( - <div> - {storyFn()} - <style jsx>{` - div { - width: 400px; - height: 400px; - position: relative; - border: 1px dotted grey; - } - `}</style> - </div> - ), - ], + parameters: { docs: { description: { component: description } } }, } -export const Default = () => ( - <> - <ComponentCover /> - - <h1>Text behind the cover</h1> - <p>Lorem ipsum</p> - </> -) - -export const Translucent = () => ( - <> - <ComponentCover translucent /> +const Template = args => ( + <div + style={{ + width: '400px', + height: '200px', + position: 'relative', + border: '1px dotted grey', + }} + > + <ComponentCover {...args} /> <h1>Text behind the cover</h1> <p>Lorem ipsum</p> - </> + </div> ) -export const WithClickHandler = () => ( - <> - <ComponentCover onClick={() => alert('Cover was clicked')} /> +export const Default = Template.bind({}) - <h1>Text behind the cover</h1> - <p>Lorem ipsum</p> - </> -) +export const Translucent = Template.bind({}) +Translucent.args = { translucent: true } -export const WithCenteredContentCircularLoader = () => ( - <> - <ComponentCover translucent> - <CenteredContent> - <CircularLoader /> - </CenteredContent> - </ComponentCover> +export const WithClickHandler = Template.bind({}) +WithClickHandler.args = { onClick: () => alert('Cover was clicked') } - <h1>Text behind the cover</h1> - <p>Lorem ipsum</p> - </> -) +export const WithCenteredContentCircularLoader = Template.bind({}) +WithCenteredContentCircularLoader.args = { + translucent: true, + children: ( + <CenteredContent> + <CircularLoader /> + </CenteredContent> + ), +} diff --git a/packages/core/src/CssReset/CssReset.stories.js b/packages/core/src/CssReset/CssReset.stories.js index cfadc0111e..d8dcea6666 100644 --- a/packages/core/src/CssReset/CssReset.stories.js +++ b/packages/core/src/CssReset/CssReset.stories.js @@ -1,13 +1,29 @@ -import { storiesOf } from '@storybook/react' import React from 'react' import { CssReset } from './CssReset.js' +const description = ` +A tool for adding a global normalization stylesheet into the DOM that applies DHIS2 styles. + +- https://github.com/necolas/normalize.css +- https://www.paulirish.com/2012/box-sizing-border-box-ftw/ + +\`\`\`js +import { CssReset } from '@dhis2/ui' +\`\`\` +` + // eslint-disable-next-line react/prop-types const App = ({ children }) => <div>{children}</div> -storiesOf('CssReset', module).add('Default', () => ( +export default { + title: 'Helpers/CSS Reset', + component: CssReset, + parameters: { docs: { description: { component: description } } }, +} + +export const Default = args => ( <App> - <CssReset /> + <CssReset {...args} /> <p> The <code>CssReset</code> component injects a global normalization @@ -15,9 +31,9 @@ storiesOf('CssReset', module).add('Default', () => ( </p> <p> - This also sets the <code>font-family</code> on the - <code>body</code> element to the DHIS2 font, which allows it to - trickle down to the components as well. + This also sets the <code>font-family</code> on the <code>body</code>{' '} + element to the DHIS2 font, which allows it to trickle down to the + components as well. </p> <p> @@ -25,4 +41,4 @@ storiesOf('CssReset', module).add('Default', () => ( as possible to avoid FOUC.{' '} </p> </App> -)) +) diff --git a/packages/core/src/CssVariables/CssVariables.js b/packages/core/src/CssVariables/CssVariables.js index e5e4655aa2..afe561404c 100644 --- a/packages/core/src/CssVariables/CssVariables.js +++ b/packages/core/src/CssVariables/CssVariables.js @@ -1,5 +1,5 @@ -import propTypes from '@dhis2/prop-types' import * as theme from '@dhis2/ui-constants' +import PropTypes from 'prop-types' import React from 'react' const toPrefixedThemeSection = themeSectionKey => @@ -61,11 +61,11 @@ CssVariables.defaultProps = { * @prop {boolean} [elevations] */ CssVariables.propTypes = { - colors: propTypes.bool, - elevations: propTypes.bool, - layers: propTypes.bool, - spacers: propTypes.bool, - theme: propTypes.bool, + colors: PropTypes.bool, + elevations: PropTypes.bool, + layers: PropTypes.bool, + spacers: PropTypes.bool, + theme: PropTypes.bool, } export { CssVariables } diff --git a/packages/core/src/CssVariables/CssVariables.stories.js b/packages/core/src/CssVariables/CssVariables.stories.js index cb589599da..1c75f49bde 100644 --- a/packages/core/src/CssVariables/CssVariables.stories.js +++ b/packages/core/src/CssVariables/CssVariables.stories.js @@ -1,33 +1,52 @@ -import { storiesOf } from '@storybook/react' import React from 'react' import { CssVariables } from './CssVariables.js' +const description = ` +A utility for adding DHIS2 theme variables to global CSS variables. + +\`\`\`js +import { CssVariables } from '@dhis2/ui' +\`\`\` +` + // eslint-disable-next-line react/prop-types const App = ({ children }) => <div>{children}</div> -storiesOf('CssVariables', module) - .add('Default', () => ( - <App> - <CssVariables /> - - <p>By default no custom properties are inserted.</p> - </App> - )) - - .add('All', () => ( - <App> - <CssVariables colors theme layers spacers elevations /> - - <p> - The sections of the theme that should be inserted can be toggled - with flags, which allows the theme variables to be used as - regular CSS custom properties. So this{' '} - <span style={{ color: 'var(--colors-red500)' }}>text</span> uses - the vanilla CSS{' '} - <span style={{ color: 'var(--colors-blue500)' }}> - custom properties - </span>{' '} - set by the CssVariables component. - </p> - </App> - )) +export default { + title: 'Helpers/CSS Variables', + component: CssVariables, + parameters: { docs: { description: { component: description } } }, +} + +export const AllVariables = args => ( + <App> + <CssVariables {...args} /> + + <p> + The sections of the theme that should be inserted can be toggled + with flags, which allows the theme variables to be used as regular + CSS custom properties. So this{' '} + <span style={{ color: 'var(--colors-red500)' }}>text</span> uses the + vanilla CSS{' '} + <span style={{ color: 'var(--colors-blue500)' }}> + custom properties + </span>{' '} + set by the CssVariables component. + </p> + </App> +) +AllVariables.args = { + colors: true, + theme: true, + layers: true, + spacers: true, + elevations: true, +} + +export const NoVariables = args => ( + <App> + <CssVariables {...args} /> + + <p>By default no custom properties are inserted.</p> + </App> +) diff --git a/packages/core/src/Divider/Divider.js b/packages/core/src/Divider/Divider.js index 03648c7c54..aba2eb98fe 100644 --- a/packages/core/src/Divider/Divider.js +++ b/packages/core/src/Divider/Divider.js @@ -1,5 +1,5 @@ -import propTypes from '@dhis2/prop-types' import { colors, spacers } from '@dhis2/ui-constants' +import PropTypes from 'prop-types' import React from 'react' ;('') // TODO: https://github.com/jsdoc/jsdoc/issues/1718 @@ -41,10 +41,10 @@ Divider.defaultProps = { * @prop {string} [margin="${spacers.dp8} 0"] - DEPRECATED: A CSS shorthand declaration for margin. If margin and dense are used at the same time, dense has precedence. */ Divider.propTypes = { - className: propTypes.string, - dataTest: propTypes.string, - dense: propTypes.bool, - margin: propTypes.string, + className: PropTypes.string, + dataTest: PropTypes.string, + dense: PropTypes.bool, + margin: PropTypes.string, } export { Divider } diff --git a/packages/core/src/Divider/Divider.stories.js b/packages/core/src/Divider/Divider.stories.js index a1bd087b9a..63dd96cf15 100644 --- a/packages/core/src/Divider/Divider.stories.js +++ b/packages/core/src/Divider/Divider.stories.js @@ -1,13 +1,33 @@ import React from 'react' import { Divider } from './Divider.js' +const description = ` +A light divider to separate content. + +\`\`\`js +import { Divider } from '@dhis2/ui' +\`\`\` +` + export default { - title: 'Divider', + title: 'Layout/Divider', component: Divider, + argTypes: { margin: { defaultValue: '8px 0' } }, + parameters: { docs: { description: { component: description } } }, } -export const Default = () => <Divider /> +const Template = args => ( + <> + <div>Content above</div> + <Divider {...args} /> + <div>Content below</div> + </> +) + +export const Default = Template.bind({}) -export const Dense = () => <Divider dense /> +export const Dense = Template.bind({}) +Dense.args = { dense: true } -export const Margin = () => <Divider margin="20px 20px 20px 20px" /> +export const Margin = Template.bind({}) +Margin.args = { margin: '20px 20px 20px 20px' } diff --git a/packages/core/src/DropdownButton/DropdownButton.js b/packages/core/src/DropdownButton/DropdownButton.js index c4ea0fd98e..c51359b44d 100644 --- a/packages/core/src/DropdownButton/DropdownButton.js +++ b/packages/core/src/DropdownButton/DropdownButton.js @@ -1,6 +1,5 @@ -import propTypes from '@dhis2/prop-types' -import { spacers } from '@dhis2/ui-constants' -import { sharedPropTypes } from '@dhis2/ui-constants' +import { spacers, sharedPropTypes } from '@dhis2/ui-constants' +import PropTypes from 'prop-types' import React, { Component } from 'react' import { resolve } from 'styled-jsx/css' import { Button } from '../Button/Button.js' @@ -144,23 +143,37 @@ DropdownButton.defaultProps = { * @prop {string} [dataTest] */ DropdownButton.propTypes = { - children: propTypes.node, - className: propTypes.string, - component: propTypes.element, - dataTest: propTypes.string, + /** Children to render inside the buton */ + children: PropTypes.node, + className: PropTypes.string, + /** Component to show/hide when button is clicked */ + component: PropTypes.element, + dataTest: PropTypes.string, + /** Button variant. Mutually exclusive with `primary` and `secondary` props */ destructive: sharedPropTypes.buttonVariantPropType, - disabled: propTypes.bool, - icon: propTypes.element, - initialFocus: propTypes.bool, + /** Make the button non-interactive */ + disabled: PropTypes.bool, + icon: PropTypes.element, + /** Grants button initial focus on the page */ + initialFocus: PropTypes.bool, + /** Button size. Mutually exclusive with `small` prop */ large: sharedPropTypes.sizePropType, - name: propTypes.string, + name: PropTypes.string, + /** Button variant. Mutually exclusive with `destructive` and `secondary` props */ primary: sharedPropTypes.buttonVariantPropType, + /** Button variant. Mutually exclusive with `primary` and `destructive` props */ secondary: sharedPropTypes.buttonVariantPropType, + /** Button size. Mutually exclusive with `large` prop */ small: sharedPropTypes.sizePropType, - tabIndex: propTypes.string, - type: propTypes.oneOf(['submit', 'reset', 'button']), - value: propTypes.string, - onClick: propTypes.func, + tabIndex: PropTypes.string, + /** Type of button. Can take advantage of different default behavior */ + type: PropTypes.oneOf(['submit', 'reset', 'button']), + value: PropTypes.string, + /** + * Callback triggered on click. + * Called with signature `({ name: string, value: string, open: bool }, event)` + */ + onClick: PropTypes.func, } export { DropdownButton } diff --git a/packages/core/src/DropdownButton/DropdownButton.stories.js b/packages/core/src/DropdownButton/DropdownButton.stories.js index 1c614f9f1a..7755afb8c8 100644 --- a/packages/core/src/DropdownButton/DropdownButton.stories.js +++ b/packages/core/src/DropdownButton/DropdownButton.stories.js @@ -1,10 +1,16 @@ +import { sharedPropTypes } from '@dhis2/ui-constants' import React from 'react' +import { FlyoutMenu } from '../FlyoutMenu/FlyoutMenu.js' +import { MenuItem } from '../MenuItem/MenuItem.js' import { DropdownButton } from './DropdownButton.js' -export default { - title: 'DropdownButton', - component: DropdownButton, -} +const description = ` +Presents several actions to a user in a small space. Can replace single, individual buttons. Should only be used for actions that are related to one another. Ensure the button has a useful level that communicates that actions are contained within. Dropdown buttons do not have an explicit action, only expanding the list of contained actions. + +\`\`\`js +import { DropdownButton } from '@dhis2/ui' +\`\`\` +` window.onClick = (payload, event) => { console.log('onClick payload', payload) @@ -15,77 +21,72 @@ const onClick = (...args) => window.onClick(...args) const Simple = <span>Simplest thing</span> -export const Default = () => ( - <DropdownButton name="default" value="nothing" component={Simple}> - Label me! - </DropdownButton> -) - -export const WithClick = () => ( - <DropdownButton - name="default" - value="nothing" - onClick={onClick} - component={Simple} - > - Label me! - </DropdownButton> -) - -export const Primary = () => ( - <DropdownButton name="default" value="nothing" primary component={Simple}> - Label me! - </DropdownButton> -) - -export const Secondary = () => ( - <DropdownButton name="default" value="nothing" secondary component={Simple}> - Label me! - </DropdownButton> -) - -export const Destructive = () => ( - <DropdownButton - name="default" - value="nothing" - destructive - component={Simple} - > - Label me! - </DropdownButton> -) - -export const Disabled = () => ( - <DropdownButton name="default" value="nothing" disabled component={Simple}> - Label me! - </DropdownButton> -) - -export const Small = () => ( - <DropdownButton name="default" value="nothing" small component={Simple}> - Label me! - </DropdownButton> -) - -export const Large = () => ( - <DropdownButton name="default" value="nothing" large component={Simple}> - Label me! - </DropdownButton> -) - -export const WithMenu = () => ( - <DropdownButton name="default" value="nothing" component={Simple}> - Label me! - </DropdownButton> -) - -export const InitialFocus = () => ( - <DropdownButton - name="default" - value="nothing" - initialFocus - component={Simple} - > - Label me! - </DropdownButton> -) +const { sizeArgType, buttonVariantArgType } = sharedPropTypes + +export default { + title: 'Actions/Buttons/Dropdown Button', + component: DropdownButton, + parameters: { docs: { description: { component: description } } }, + argTypes: { + primary: { ...buttonVariantArgType }, + secondary: { ...buttonVariantArgType }, + destructive: { ...buttonVariantArgType }, + small: { ...sizeArgType }, + large: { ...sizeArgType }, + }, + // Default args for all stories (can be overridden) + args: { + name: 'buttonName', + value: 'buttonValue', + component: Simple, + children: 'Label me!', + }, +} + +const Template = args => <DropdownButton {...args} /> + +export const Default = Template.bind({}) + +export const WithClick = Template.bind({}) +WithClick.args = { onClick: onClick } + +export const Primary = Template.bind({}) +Primary.args = { primary: true } + +export const Secondary = Template.bind({}) +Secondary.args = { secondary: true } + +export const Destructive = Template.bind({}) +Destructive.args = { destructive: true } + +export const Disabled = Template.bind({}) +Disabled.args = { disabled: true } + +export const Small = Template.bind({}) +Small.args = { small: true } + +export const Large = Template.bind({}) +Large.args = { large: true } + +export const WithMenu = Template.bind({}) +WithMenu.args = { + component: ( + <FlyoutMenu> + <MenuItem label="Item 1" /> + <MenuItem label="Item 2" /> + <MenuItem label="Item 3" /> + </FlyoutMenu> + ), +} +// FlyoutMenu needs iframe +// But docs page down too much with iframe, so disabled +WithMenu.parameters = { docs: { disable: true } } + +export const InitialFocus = Template.bind({}) +InitialFocus.args = { initialFocus: true } +/** + * 'Initial focus' stories cause the docs page to scroll away each time + * a control is changed, therefore it is omitted from the docs page (but + * not the normal 'canvas' story viewer) + */ +InitialFocus.parameters = { docs: { disable: true } } diff --git a/packages/core/src/Field/Field.js b/packages/core/src/Field/Field.js index 988f77a9da..0332b6892b 100644 --- a/packages/core/src/Field/Field.js +++ b/packages/core/src/Field/Field.js @@ -1,5 +1,5 @@ -import propTypes from '@dhis2/prop-types' import { sharedPropTypes, spacers } from '@dhis2/ui-constants' +import PropTypes from 'prop-types' import React from 'react' import { Box } from '../Box/Box.js' import { Help } from '../Help/Help.js' @@ -85,17 +85,26 @@ Field.defaultProps = { * @prop {boolean} [error] */ Field.propTypes = { - children: propTypes.node, - className: propTypes.string, - dataTest: propTypes.string, - disabled: propTypes.bool, + children: PropTypes.node, + className: PropTypes.string, + dataTest: PropTypes.string, + /** Disabled status, shown when mouse is over label */ + disabled: PropTypes.bool, + /** Field status. Mutually exclusive with `valid` and `warning` props */ error: sharedPropTypes.statusPropType, - helpText: propTypes.string, - label: propTypes.string, - name: propTypes.string, - required: propTypes.bool, - valid: propTypes.bool, - validationText: propTypes.string, + /** Useful text within the field */ + helpText: PropTypes.string, + /** Label at the top of the field */ + label: PropTypes.string, + /** `name` will become the target of the `for`/`htmlFor` attribute on the `<label>` element */ + name: PropTypes.string, + /** Inidcates this field is required */ + required: PropTypes.bool, + /** Field status. Mutually exclusive with `error` and `warning` props */ + valid: sharedPropTypes.statusPropType, + /** Feedback given related to validation status of field */ + validationText: PropTypes.string, + /** Field status. Mutually exclusive with `valid` and `error` props */ warning: sharedPropTypes.statusPropType, } diff --git a/packages/core/src/Field/Field.stories.js b/packages/core/src/Field/Field.stories.js index 54407ce644..c4600dac57 100644 --- a/packages/core/src/Field/Field.stories.js +++ b/packages/core/src/Field/Field.stories.js @@ -1,11 +1,30 @@ -import { storiesOf } from '@storybook/react' +import { sharedPropTypes } from '@dhis2/ui-constants' import React from 'react' import { Input } from '../index.js' import { Field } from './Field.js' -storiesOf('Field', module).add('Default', () => ( +const description = ` +A useful container for form components, including a label, help text, and validation text. + +\`\`\`js +import { Field } from '@dhis2/ui' +\`\`\` +` + +export default { + title: 'Forms/Field', + component: Field, + argTypes: { + valid: { ...sharedPropTypes.statusArgType }, + warning: { ...sharedPropTypes.statusArgType }, + error: { ...sharedPropTypes.statusArgType }, + }, + parameters: { docs: { description: { component: description } } }, +} + +export const Default = args => ( <> - <Field helpText="Help me"> + <Field {...args}> <Input onChange={() => { console.log('Nothing happens') @@ -14,7 +33,7 @@ storiesOf('Field', module).add('Default', () => ( label="An input" /> </Field> - <Field helpText="Help!"> + <Field helpText="Help!" label="Second field"> <Input onChange={() => { console.log('Nothing happens') @@ -24,4 +43,9 @@ storiesOf('Field', module).add('Default', () => ( /> </Field> </> -)) +) +Default.args = { + label: 'First field (change me with controls)', + helpText: 'Help text!', + validationText: "I'm validation text", +} diff --git a/packages/core/src/FieldSet/FieldSet.js b/packages/core/src/FieldSet/FieldSet.js index cd9a91bbd8..98d9a1b44d 100644 --- a/packages/core/src/FieldSet/FieldSet.js +++ b/packages/core/src/FieldSet/FieldSet.js @@ -1,4 +1,4 @@ -import propTypes from '@dhis2/prop-types' +import PropTypes from 'prop-types' import React from 'react' /** @@ -33,9 +33,9 @@ FieldSet.defaultProps = { * @prop {string} [dataTest] */ FieldSet.propTypes = { - children: propTypes.node, - className: propTypes.string, - dataTest: propTypes.string, + children: PropTypes.node, + className: PropTypes.string, + dataTest: PropTypes.string, } export { FieldSet } diff --git a/packages/core/src/FieldSet/FieldSet.stories.js b/packages/core/src/FieldSet/FieldSet.stories.js index cf5d0b50bb..da11985f20 100644 --- a/packages/core/src/FieldSet/FieldSet.stories.js +++ b/packages/core/src/FieldSet/FieldSet.stories.js @@ -1,49 +1,63 @@ -import { storiesOf } from '@storybook/react' import React from 'react' import { Field, Radio, Help, Legend } from '../index.js' import { FieldSet } from './FieldSet.js' +const description = ` +A container for grouping several Field components together. Use a \`<Legend>\` component to add helpful guiding text. + +\`\`\`js +import { FieldSet } from '@dhis2/ui' +\`\`\` +` + const onChange = () => { console.log('Radio was clicked, nothing else will happen') } -storiesOf('FieldSet', module) - .add('Default', () => ( - <FieldSet> - It renders something in a fieldset element without the browser - styles - </FieldSet> - )) - .add('Usage example - a radio button group with error status', () => ( - <FieldSet> - <Legend required>Choose an option</Legend> - <Field> - <Radio - onChange={onChange} - name="radio" - value="first" - label="First" - error - /> - </Field> - <Field> - <Radio - onChange={onChange} - name="radio" - value="second" - label="Second" - error - /> - </Field> - <Field> - <Radio - onChange={onChange} - name="radio" - value="third" - label="Third" - error - /> - </Field> - <Help error>You really have to choose something!</Help> - </FieldSet> - )) +export default { + title: 'Forms/Field Set/Field Set', + component: FieldSet, + parameters: { docs: { description: { component: description } } }, +} + +export const Default = args => ( + <FieldSet {...args}> + It renders something in a fieldset element without the browser styles + </FieldSet> +) + +export const UsageExampleARadioButtonGroupWithErrorStatus = args => ( + <FieldSet {...args}> + <Legend required>Choose an option</Legend> + <Field> + <Radio + onChange={onChange} + name="radio" + value="first" + label="First" + error + /> + </Field> + <Field> + <Radio + onChange={onChange} + name="radio" + value="second" + label="Second" + error + /> + </Field> + <Field> + <Radio + onChange={onChange} + name="radio" + value="third" + label="Third" + error + /> + </Field> + <Help error>You really have to choose something!</Help> + </FieldSet> +) +UsageExampleARadioButtonGroupWithErrorStatus.storyName = + 'Usage example - a radio button group with error status' diff --git a/packages/core/src/FileInput/FileInput.js b/packages/core/src/FileInput/FileInput.js index 9c43717cb5..5602346176 100644 --- a/packages/core/src/FileInput/FileInput.js +++ b/packages/core/src/FileInput/FileInput.js @@ -1,6 +1,6 @@ -import propTypes from '@dhis2/prop-types' import { spacers, sharedPropTypes } from '@dhis2/ui-constants' import cx from 'classnames' +import PropTypes from 'prop-types' import React, { createRef, Component } from 'react' import { Button } from '../Button/Button.js' import { Upload, StatusIcon } from '../Icons/index.js' @@ -149,23 +149,37 @@ FileInput.defaultProps = { * @prop {string} [dataTest] */ FileInput.propTypes = { - accept: propTypes.string, - buttonLabel: propTypes.string, - className: propTypes.string, - dataTest: propTypes.string, - disabled: propTypes.bool, + /** + * The `accept` attribute of the [native file input](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/file#accept) + */ + accept: PropTypes.string, + buttonLabel: PropTypes.string, + className: PropTypes.string, + dataTest: PropTypes.string, + disabled: PropTypes.bool, + /** Input status. Mutually exclusive with `warning` and `valid` */ error: sharedPropTypes.statusPropType, - initialFocus: propTypes.bool, + initialFocus: PropTypes.bool, + /** Button size. Mutually exclusive with `small` */ large: sharedPropTypes.sizePropType, - multiple: propTypes.bool, - name: propTypes.string, + /** + * The `multiple` attribute of the [native file input](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/file#multiple) + */ + multiple: PropTypes.bool, + name: PropTypes.string, + /** Button size. Mutually exclusive with `large` */ small: sharedPropTypes.sizePropType, - tabIndex: propTypes.string, + tabIndex: PropTypes.string, + /** Input status. Mutually exclusive with `warning` and `error` */ valid: sharedPropTypes.statusPropType, + /** Input status. Mutually exclusive with `valid` and `error` */ warning: sharedPropTypes.statusPropType, - onBlur: propTypes.func, - onChange: propTypes.func, - onFocus: propTypes.func, + /** Called with signature `(object, event)` */ + onBlur: PropTypes.func, + /** Called with signature `(object, event)` */ + onChange: PropTypes.func, + /** Called with signature `(object, event)` */ + onFocus: PropTypes.func, } export { FileInput } diff --git a/packages/core/src/FileInput/FileInput.stories.js b/packages/core/src/FileInput/FileInput.stories.js index 895a506d49..29e5e4f6af 100644 --- a/packages/core/src/FileInput/FileInput.stories.js +++ b/packages/core/src/FileInput/FileInput.stories.js @@ -1,7 +1,17 @@ -import { storiesOf } from '@storybook/react' +import { sharedPropTypes } from '@dhis2/ui-constants' import React from 'react' import { FileInput } from './FileInput.js' +const subtitle = `The file input component allows users to select and upload files from their local machine.` + +const description = ` +Use a file input component in forms and interfaces wherever a user needs to be able to select and upload a file from their local machine. + +\`\`\`js +import { FileInput } from '@dhis2/ui' +\`\`\` +` + const onChange = (payload, event) => { console.log('onChange payload', payload) console.log('onChange event', event) @@ -18,75 +28,49 @@ const onChange = (payload, event) => { ) } -storiesOf('FileInput', module) - .add('Default', () => ( - <FileInput - onChange={onChange} - buttonLabel="Upload file" - name="upload" - /> - )) - .add('Multiple', () => ( - <FileInput - name="upload" - onChange={onChange} - buttonLabel="Upload files" - multiple - /> - )) - .add('Disabled', () => ( - <FileInput - name="upload" - onChange={onChange} - buttonLabel="Upload file" - disabled - /> - )) - .add('Sizes', () => ( - <> - <FileInput - onChange={onChange} - buttonLabel="Default size" - name="default" - /> - <FileInput - small - onChange={onChange} - buttonLabel="Small" - name="small" - /> - <FileInput - large - onChange={onChange} - buttonLabel="Large" - name="large" - /> - </> - )) - .add('Statuses', () => ( - <> - <FileInput - onChange={onChange} - buttonLabel="Default" - name="default" - /> - <FileInput - onChange={onChange} - buttonLabel="Valid" - name="valid" - valid - /> - <FileInput - onChange={onChange} - buttonLabel="Warning" - name="warning" - warning - /> - <FileInput - onChange={onChange} - buttonLabel="Error" - name="error" - error - /> - </> - )) +const { sizeArgType, statusArgType } = sharedPropTypes + +export default { + title: 'Forms/File Input/File Input', + component: FileInput, + // Default args for each story unless overridden + args: { buttonLabel: 'Upload file', name: 'upload', onChange }, + argTypes: { + valid: { ...statusArgType }, + warning: { ...statusArgType }, + error: { ...statusArgType }, + small: { ...sizeArgType }, + large: { ...sizeArgType }, + }, + parameters: { + componentSubtitle: subtitle, + docs: { description: { component: description } }, + }, +} + +const Template = args => <FileInput {...args} /> + +export const Default = Template.bind({}) + +export const Multiple = Template.bind({}) +Multiple.args = { multiple: true, buttonLabel: 'Upload files' } + +export const Disabled = Template.bind({}) +Disabled.args = { disabled: true } + +export const Sizes = args => ( + <> + <FileInput {...args} buttonLabel="Default size" name="default" /> + <FileInput {...args} small buttonLabel="Small" name="small" /> + <FileInput {...args} large buttonLabel="Large" name="large" /> + </> +) + +export const Statuses = args => ( + <> + <FileInput {...args} buttonLabel="Default" name="default" /> + <FileInput {...args} buttonLabel="Valid" name="valid" valid /> + <FileInput {...args} buttonLabel="Warning" name="warning" warning /> + <FileInput {...args} buttonLabel="Error" name="error" error /> + </> +) diff --git a/packages/core/src/FlyoutMenu/FlyoutMenu.js b/packages/core/src/FlyoutMenu/FlyoutMenu.js index d7beaba1c5..798757a2cc 100644 --- a/packages/core/src/FlyoutMenu/FlyoutMenu.js +++ b/packages/core/src/FlyoutMenu/FlyoutMenu.js @@ -1,5 +1,5 @@ -import propTypes from '@dhis2/prop-types' import { spacers } from '@dhis2/ui-constants' +import PropTypes from 'prop-types' import React, { Children, cloneElement, isValidElement, useState } from 'react' import { resolve } from 'styled-jsx/css' import { Card } from '../Card/Card.js' @@ -85,12 +85,14 @@ FlyoutMenu.defaultProps = { * @prop {string} [maxHeight='auto'] */ FlyoutMenu.propTypes = { - children: Menu.propTypes.children, - className: propTypes.string, - dataTest: propTypes.string, - dense: propTypes.bool, - maxHeight: propTypes.string, - maxWidth: propTypes.string, + /** Typically, but not limited to, `MenuItem` components */ + children: PropTypes.node, + className: PropTypes.string, + dataTest: PropTypes.string, + /** Menu uses smaller dimensions */ + dense: PropTypes.bool, + maxHeight: PropTypes.string, + maxWidth: PropTypes.string, } export { FlyoutMenu } diff --git a/packages/core/src/FlyoutMenu/FlyoutMenu.stories.js b/packages/core/src/FlyoutMenu/FlyoutMenu.stories.js index d00e96745c..c0819d8585 100644 --- a/packages/core/src/FlyoutMenu/FlyoutMenu.stories.js +++ b/packages/core/src/FlyoutMenu/FlyoutMenu.stories.js @@ -7,27 +7,43 @@ import { MenuSectionHeader } from '../MenuSectionHeader/MenuSectionHeader.js' import { Popper } from '../Popper/Popper.js' import { FlyoutMenu } from './FlyoutMenu.js' +const description = ` +Use menus to provide access to options and actions where space is limited and displaying all the options would be impractical. For example, providing access to a range of actions for every dashboard item displayed. Containing all those actions in menus keeps the page manageable. + +The menu component is flexible in where it can be used and its contents can be flexible too. However, the most common use case is a menu containing menu items. + +Make sure the menu item labels are short and easy to understand. One word is often enough to describe an action or option. Do not use sentences as labels. Some examples of good menu item labels: + +- "Save" +- "Open as map" +- "Export PDF" +- "Duplicate" + +See more about how to use menus at the [design system](https://github.com/dhis2/design-system/blob/master/molecules/menu.md). + +\`\`\`js +import { FlyoutMenu } from 'dhis2/ui' +\`\`\` +` + export default { - title: 'FlyoutMenu', + title: 'Actions/Menu/Flyout Menu', component: FlyoutMenu, + parameters: { docs: { description: { component: description } } }, } -export const Default = () => ( - <FlyoutMenu> +export const Default = args => ( + <FlyoutMenu {...args}> <MenuItem label="Item 1" /> <MenuItem label="Item 2" /> </FlyoutMenu> ) -export const Dense = () => ( - <FlyoutMenu dense> - <MenuItem label="Item 1" /> - <MenuItem label="Item 2" /> - </FlyoutMenu> -) +export const Dense = Default.bind({}) +Dense.args = { dense: true } -export const MaxHeight = () => ( - <FlyoutMenu maxHeight="250px"> +export const MaxHeight = args => ( + <FlyoutMenu {...args}> <MenuItem label="Item 1" /> <MenuItem label="Item 2" /> <MenuItem label="Item 3" /> @@ -40,8 +56,9 @@ export const MaxHeight = () => ( <MenuItem label="Item 10" /> </FlyoutMenu> ) +MaxHeight.args = { maxHeight: '250px' } -export const MaxWidth = () => ( +export const MaxWidth = args => ( <> <FlyoutMenu> <MenuItem label="Short item 1" /> @@ -59,21 +76,22 @@ export const MaxWidth = () => ( /> </FlyoutMenu> <br /> - <FlyoutMenu maxWidth="300px"> + <FlyoutMenu {...args}> <MenuItem - label="Item 1 - with a lot of text and using a custom maxWidth value of - 300px" + label={`Item 1 - with a lot of text and using a custom maxWidth value of + ${args.maxWidth}`} /> <MenuItem - label="Item 2 - with a lot of text and using a custom maxWidth value of - 300px" + label={`Item 2 - with a lot of text and using a custom maxWidth value of + ${args.maxWidth}`} /> </FlyoutMenu> </> ) +MaxWidth.args = { maxWidth: '300px' } -export const WithSubMenus = () => ( - <FlyoutMenu> +export const WithSubMenus = args => ( + <FlyoutMenu {...args}> <MenuItem label="Item 1" /> <MenuItem label="Item 2"> <MenuItem label="Item 2 a" /> @@ -95,9 +113,17 @@ export const WithSubMenus = () => ( <MenuItem label="Item 5" /> </FlyoutMenu> ) +WithSubMenus.parameters = { + docs: { + description: { + story: + 'See this demo in the Canvas tab for proper alignment of sub menus.', + }, + }, +} -export const WithVariousChildren = () => ( - <FlyoutMenu> +export const WithVariousChildren = args => ( + <FlyoutMenu {...args}> <MenuSectionHeader label="Section with sub-menus" /> <MenuItem label="Item 1" /> <MenuItem label="Item 2"> @@ -126,8 +152,16 @@ export const WithVariousChildren = () => ( <MenuItem label="Item 3" /> </FlyoutMenu> ) +WithVariousChildren.parameters = { + docs: { + description: { + story: + 'See this demo in the Canvas tab for proper alignment of sub menus.', + }, + }, +} -export const DropDownMenu = () => { +export const DropDownMenu = args => { const ref = useRef() const [open, setOpen] = useState(false) const toggle = () => setOpen(!open) @@ -140,15 +174,26 @@ export const DropDownMenu = () => { {open && ( <Layer onClick={toggle}> <Popper reference={ref} placement="bottom-start"> - <Default /> + <FlyoutMenu {...args}> + <MenuItem label="Item 1" /> + <MenuItem label="Item 2" /> + </FlyoutMenu> </Popper> </Layer> )} </> ) } +DropDownMenu.parameters = { + docs: { + description: { + story: 'View this demo in the canvas to see the dropdown menu.', + }, + source: { type: 'code' }, + }, +} -export const WithCustomMenuItem = () => { +export const WithCustomMenuItem = args => { // You should not create custom components in the render cycle // this is just for demo purposes // eslint-disable-next-line react/prop-types @@ -179,7 +224,7 @@ export const WithCustomMenuItem = () => { } return ( - <FlyoutMenu> + <FlyoutMenu {...args}> <MenuItem label="A normal menu item" /> <PopupWindowMenuItem to="http://dhis2.org"> A custom menu item (popup window) diff --git a/packages/core/src/Help/Help.js b/packages/core/src/Help/Help.js index e80e5ef116..de62b2f408 100644 --- a/packages/core/src/Help/Help.js +++ b/packages/core/src/Help/Help.js @@ -1,6 +1,6 @@ -import propTypes from '@dhis2/prop-types' import { spacers, theme, sharedPropTypes } from '@dhis2/ui-constants' import cx from 'classnames' +import PropTypes from 'prop-types' import React from 'react' ;('') // TODO: https://github.com/jsdoc/jsdoc/issues/1718 @@ -64,9 +64,9 @@ Help.defaultProps = { * @prop {string} [dataTest] */ Help.propTypes = { - children: propTypes.string, - className: propTypes.string, - dataTest: propTypes.string, + children: PropTypes.string, + className: PropTypes.string, + dataTest: PropTypes.string, error: sharedPropTypes.statusPropType, valid: sharedPropTypes.statusPropType, warning: sharedPropTypes.statusPropType, diff --git a/packages/core/src/Help/Help.stories.js b/packages/core/src/Help/Help.stories.js index 3f934d4c94..5f37be7d23 100644 --- a/packages/core/src/Help/Help.stories.js +++ b/packages/core/src/Help/Help.stories.js @@ -1,17 +1,45 @@ -import { storiesOf } from '@storybook/react' +import { sharedPropTypes } from '@dhis2/ui-constants' import React from 'react' import { Help } from './Help.js' -storiesOf('Help', module) - .add('Default', () => <Help>Allow me to be of assistance</Help>) - - .add('Status: Warning', () => ( - <Help warning>Allow me to be of assistance</Help> - )) - .add('Status: Valid', () => <Help valid>Allow me to be of assistance</Help>) - .add('Status: Error', () => <Help error>Allow me to be of assistance</Help>) - .add('Text overflow', () => ( - <div style={{ width: 200 }}> - <Help>I take up more space than my container</Help> - </div> - )) +const description = ` +Small text for giving guiding information or feedback, especially for data entry instructions or form validation feedback. + +\`\`\`js +import { Help } from '@dhis2/ui' +\`\`\` +` + +export default { + title: 'Forms/Help', + component: Help, + parameters: { docs: { description: { component: description } } }, + argTypes: { + valid: { ...sharedPropTypes.statusArgType }, + warning: { ...sharedPropTypes.statusArgType }, + error: { ...sharedPropTypes.statusArgType }, + }, + args: { children: 'Allow me to be of assistance' }, +} + +const Template = args => <Help {...args} /> + +export const Default = Template.bind({}) + +export const StatusWarning = Template.bind({}) +StatusWarning.args = { warning: true } +StatusWarning.storyName = 'Status: Warning' + +export const StatusValid = Template.bind({}) +StatusValid.args = { valid: true } +StatusValid.storyName = 'Status: Valid' + +export const StatusError = Template.bind({}) +StatusError.args = { error: true } +StatusError.storyName = 'Status: Error' + +export const TextOverflow = args => ( + <div style={{ width: 200 }}> + <Help {...args}>I take up more space than my container</Help> + </div> +) diff --git a/packages/core/src/IntersectionDetector/IntersectionDetector.stories.js b/packages/core/src/IntersectionDetector/IntersectionDetector.stories.js index e53d27a126..695987785e 100644 --- a/packages/core/src/IntersectionDetector/IntersectionDetector.stories.js +++ b/packages/core/src/IntersectionDetector/IntersectionDetector.stories.js @@ -67,7 +67,7 @@ const sizeLimitDecorator = tolerance => fn => { } export default { - title: 'IntersectionObserver', + title: 'Helpers/Intersection Observer', decorators: [sizeLimitDecorator(100)] } diff --git a/packages/core/src/Layer/Layer.js b/packages/core/src/Layer/Layer.js index e2d0f2b6d2..2c5a4c8fd7 100644 --- a/packages/core/src/Layer/Layer.js +++ b/packages/core/src/Layer/Layer.js @@ -1,6 +1,6 @@ -import propTypes from '@dhis2/prop-types' import { layers } from '@dhis2/ui-constants' import cx from 'classnames' +import PropTypes from 'prop-types' import React, { createContext, useState, useContext } from 'react' import { createPortal } from 'react-dom' @@ -43,48 +43,58 @@ const Layer = ({ const portalNode = level > parentLayer.level ? document.body : parentLayer.node - return createPortal( - <div - ref={setLayerEl} - className={cx('layer', className, position, `level-${level}`, { - translucent, - })} - data-test={dataTest} - onClick={createClickHandler(onClick)} - > - {layerEl && ( - <LayerContext.Provider value={nextLayer}> - {children} - </LayerContext.Provider> + return ( + <React.Fragment> + {createPortal( + <div + ref={setLayerEl} + className={cx( + 'layer', + className, + position, + `level-${level}`, + { + translucent, + } + )} + data-test={dataTest} + onClick={createClickHandler(onClick)} + > + {layerEl && ( + <LayerContext.Provider value={nextLayer}> + {children} + </LayerContext.Provider> + )} + <style jsx>{` + div { + z-index: ${level}; + } + `}</style> + <style jsx>{` + div { + top: 0; + left: 0; + min-height: 100vh; + min-width: 100vw; + } + div.fixed { + position: fixed; + height: 100vh; + width: 100vw; + } + div.absolute { + position: absolute; + height: 100%; + width: 100%; + } + div.translucent { + background-color: rgba(33, 43, 54, 0.4); + } + `}</style> + </div>, + portalNode )} - <style jsx>{` - div { - z-index: ${level}; - } - `}</style> - <style jsx>{` - div { - top: 0; - left: 0; - min-height: 100vh; - min-width: 100vw; - } - div.fixed { - position: fixed; - height: 100vh; - width: 100vw; - } - div.absolute { - position: absolute; - height: 100%; - width: 100%; - } - div.translucent { - background-color: rgba(33, 43, 54, 0.4); - } - `}</style> - </div>, - portalNode + </React.Fragment> ) } @@ -105,13 +115,16 @@ Layer.defaultProps = { * @prop {function} [onClick] */ Layer.propTypes = { - children: propTypes.node, - className: propTypes.string, - dataTest: propTypes.string, - level: propTypes.number, - position: propTypes.oneOf(['absolute', 'fixed']), - translucent: propTypes.bool, - onClick: propTypes.func, + children: PropTypes.node, + className: PropTypes.string, + dataTest: PropTypes.string, + /** Z-index level */ + level: PropTypes.number, + position: PropTypes.oneOf(['absolute', 'fixed']), + /** Adds a semi-transparent background */ + translucent: PropTypes.bool, + /** Click handler */ + onClick: PropTypes.func, } export { Layer, useLayerContext } diff --git a/packages/core/src/Layer/Layer.stories.js b/packages/core/src/Layer/Layer.stories.js index 15d0a6a803..3b60e00832 100644 --- a/packages/core/src/Layer/Layer.stories.js +++ b/packages/core/src/Layer/Layer.stories.js @@ -2,47 +2,61 @@ import React from 'react' import { CircularLoader, CenteredContent } from '../index.js' import { Layer } from './Layer.js' +const description = ` +Layers are used for creating different levels of stacking of interface elements. +See more about stacking guidelines at the [design system](https://github.com/dhis2/design-system/blob/master/principles/layout.md#stacking). + +Layers are used in Modals, Popovers, and Alerts. + +\`\`\`js +import { Layer } from '@dhis2/ui' +\`\`\` + +_**Note:** These demos may take some time to load._ +` + export default { - title: 'Layer', + title: 'Helpers/Layer', component: Layer, + /** + * `inlineStories: false` renders these layers in iframes instead of inline. + * This fixes an issue where all the layers on the docs page render on top + * of eachother, each covering the whole screen. + * There is a performance tradeof, and they are slow to load. + */ + parameters: { + docs: { + inlineStories: false, + iframeHeight: '180px', + description: { component: description }, + }, + }, + // Handle weird treatment of non-literal defaultProps (see Transfer.stories) + args: { ...Layer.defaultProps }, } -export const Default = () => ( - <> - <Layer /> - - <h1>Text behind the layer</h1> - <p>Lorem ipsum</p> - </> -) - -export const Translucent = () => ( +const Template = args => ( <> - <Layer translucent /> + <Layer {...args} /> <h1>Text behind the layer</h1> <p>Lorem ipsum</p> </> ) -export const WithClickHandler = () => ( - <> - <Layer onClick={() => alert('layer was clicked')} /> +export const Default = Template.bind({}) - <h1>Text behind the layer</h1> - <p>Lorem ipsum</p> - </> -) +export const Translucent = Template.bind({}) +Translucent.args = { translucent: true } -export const WithCenteredContentCircularLoader = () => ( - <> - <Layer translucent> - <CenteredContent> - <CircularLoader /> - </CenteredContent> - </Layer> +export const WithClickHandler = Template.bind({}) +WithClickHandler.args = { onClick: () => alert('layer was clicked') } - <h1>Text behind the layer</h1> - <p>Lorem ipsum</p> - </> -) +export const WithCenteredContentCircularLoader = Template.bind({}) +WithCenteredContentCircularLoader.args = { + children: ( + <CenteredContent> + <CircularLoader /> + </CenteredContent> + ), +} diff --git a/packages/core/src/Legend/Legend.js b/packages/core/src/Legend/Legend.js index 6cfdee4f09..1773afaeed 100644 --- a/packages/core/src/Legend/Legend.js +++ b/packages/core/src/Legend/Legend.js @@ -1,5 +1,5 @@ -import propTypes from '@dhis2/prop-types' import { colors } from '@dhis2/ui-constants' +import PropTypes from 'prop-types' import React from 'react' import { Required } from '../Required/Required.js' ;('') // TODO: https://github.com/jsdoc/jsdoc/issues/1718 @@ -42,10 +42,11 @@ Legend.defaultProps = { * @prop {string} [dataTest] */ Legend.propTypes = { - children: propTypes.node, - className: propTypes.string, - dataTest: propTypes.string, - required: propTypes.bool, + children: PropTypes.node, + className: PropTypes.string, + dataTest: PropTypes.string, + /** Indicates the associated field set is required */ + required: PropTypes.bool, } export { Legend } diff --git a/packages/core/src/Legend/Legend.stories.js b/packages/core/src/Legend/Legend.stories.js index 7a4407284e..1ec6f700d4 100644 --- a/packages/core/src/Legend/Legend.stories.js +++ b/packages/core/src/Legend/Legend.stories.js @@ -1,13 +1,27 @@ -import { storiesOf } from '@storybook/react' import React from 'react' import { Legend } from './Legend.js' -storiesOf('Legend', module) - .add('Default', () => ( - <Legend>I am wrapped in a legend which has some styling</Legend> - )) - .add('Required', () => ( - <Legend required> - I am wrapped in a legend which has some styling - </Legend> - )) +const description = ` +Legends are to be used in a Field Set to describe the form fields. They may indicate that the fields are required. + +See the [Field Set](../?path=/docs/forms-field-set-field-set--usage-example-a-radio-button-group-with-error-status) for a usage example. + +\`\`\`js +import { Legend } from '@dhis2/ui' +\`\`\` +` + +export default { + title: 'Forms/Field Set/Legend', + component: Legend, + parameters: { docs: { description: { component: description } } }, +} + +const Template = args => ( + <Legend {...args}>I am wrapped in a legend which has some styling</Legend> +) + +export const Default = Template.bind({}) + +export const Required = Template.bind({}) +Required.args = { required: true } diff --git a/packages/core/src/LinearLoader/LinearLoader.js b/packages/core/src/LinearLoader/LinearLoader.js index 7294d1781a..cc4bd62e86 100644 --- a/packages/core/src/LinearLoader/LinearLoader.js +++ b/packages/core/src/LinearLoader/LinearLoader.js @@ -1,5 +1,5 @@ -import propTypes from '@dhis2/prop-types' import { theme, spacers } from '@dhis2/ui-constants' +import PropTypes from 'prop-types' import React from 'react' const Progress = ({ amount }) => { @@ -22,7 +22,7 @@ const Progress = ({ amount }) => { } Progress.propTypes = { - amount: propTypes.number.isRequired, + amount: PropTypes.number.isRequired, } /** @@ -76,11 +76,14 @@ LinearLoader.defaultProps = { * @prop {string} [dataTest] */ LinearLoader.propTypes = { - amount: propTypes.number.isRequired, - className: propTypes.string, - dataTest: propTypes.string, - margin: propTypes.string, - width: propTypes.string, + /** The progression in percent without the '%' sign */ + amount: PropTypes.number.isRequired, + className: PropTypes.string, + dataTest: PropTypes.string, + /** The margin around the loader, can be a full shorthand */ + margin: PropTypes.string, + /** The width of the entire indicator */ + width: PropTypes.string, } export { LinearLoader } diff --git a/packages/core/src/LinearLoader/LinearLoader.stories.js b/packages/core/src/LinearLoader/LinearLoader.stories.js index 6cb504dd6b..89f678b3f6 100644 --- a/packages/core/src/LinearLoader/LinearLoader.stories.js +++ b/packages/core/src/LinearLoader/LinearLoader.stories.js @@ -3,27 +3,48 @@ import React from 'react' import { Layer, CenteredContent, ComponentCover } from '../index.js' import { LinearLoader } from './LinearLoader.js' +const subtitle = `Used to inform the user that an element or page is in a loading state` + +const description = ` +Use loading indicators whenever a component or application takes longer than 700ms to load. After this time a loader should be displayed so that the user can understand what is happening: loading is in progress. Consider that without a loading indicator a user would be unsure of their current status, so they are important UI elements. + +A circular loader is used when the loading time is uncertain and cannot be displayed as a percentage. A circular loader can spin many times, and each spin does not represent any amount of completion. + +\`\`\`js +import { CircularLoader } from '@dhis2/ui' +\`\`\` +` + export default { - title: 'LinearLoader', + title: 'Feedback/Loading Indicators/Linear Loader', component: LinearLoader, + parameters: { + componentSubtitle: subtitle, + docs: { description: { component: description } }, + }, + argTypes: { margin: { defaultValue: '12px' } }, } -export const Determinate = () => <LinearLoader amount={60} /> +export const Determinate = args => <LinearLoader {...args} /> +Determinate.args = { amount: 60 } -export const OverlayPage = () => ( +export const OverlayPage = args => ( <Layer level={layers.blocking} translucent> <CenteredContent> - <LinearLoader amount={30} /> + <LinearLoader {...args} /> </CenteredContent> </Layer> ) +OverlayPage.args = { amount: 30 } +OverlayPage.parameters = { docs: { inlineStories: false } } -export const OverlayComponent = () => ( +export const OverlayComponent = args => ( <div style={{ width: '400px', height: '400px' }}> <ComponentCover translucent> <CenteredContent> - <LinearLoader amount={80} /> + <LinearLoader {...args} /> </CenteredContent> </ComponentCover> </div> ) +OverlayComponent.args = { amount: 80 } diff --git a/packages/core/src/Logo/Logo.js b/packages/core/src/Logo/Logo.js index bdebfa9ed5..84699585d2 100644 --- a/packages/core/src/Logo/Logo.js +++ b/packages/core/src/Logo/Logo.js @@ -1,4 +1,4 @@ -import propTypes from '@dhis2/prop-types' +import PropTypes from 'prop-types' import React from 'react' import { LogoIconSvg } from './LogoIconSvg' import { LogoSvg } from './LogoSvg' @@ -43,8 +43,8 @@ LogoIcon.defaultProps = { } LogoIcon.propTypes = { - className: propTypes.string, - dataTest: propTypes.string, + className: PropTypes.string, + dataTest: PropTypes.string, } export const LogoIconWhite = ({ className, dataTest }) => ( @@ -56,8 +56,8 @@ LogoIconWhite.defaultProps = { } LogoIconWhite.propTypes = { - className: propTypes.string, - dataTest: propTypes.string, + className: PropTypes.string, + dataTest: PropTypes.string, } export const Logo = ({ className, dataTest }) => ( @@ -74,8 +74,8 @@ Logo.defaultProps = { } Logo.propTypes = { - className: propTypes.string, - dataTest: propTypes.string, + className: PropTypes.string, + dataTest: PropTypes.string, } export const LogoWhite = ({ className, dataTest }) => ( @@ -92,6 +92,6 @@ LogoWhite.defaultProps = { } LogoWhite.propTypes = { - className: propTypes.string, - dataTest: propTypes.string, + className: PropTypes.string, + dataTest: PropTypes.string, } diff --git a/packages/core/src/Logo/Logo.stories.js b/packages/core/src/Logo/Logo.stories.js index 6b1d1ab2ab..81a16fcb41 100644 --- a/packages/core/src/Logo/Logo.stories.js +++ b/packages/core/src/Logo/Logo.stories.js @@ -1,7 +1,14 @@ -import { storiesOf } from '@storybook/react' import React from 'react' import { Logo, LogoWhite, LogoIcon, LogoIconWhite } from './Logo.js' +const description = ` +The master DHIS2 logo should be used overall, whenever possible. The master logo is a blue colored icon with black colored dhis2 wordmark. + +\`\`\`js +import { Logo, LogoWhite, LogoIcon, LogoIconWhite } from '@dhis2/ui' +\`\`\` +` + const Wrapper = fn => <div style={{ width: '358px' }}>{fn()}</div> // eslint-disable-next-line react/prop-types @@ -9,21 +16,41 @@ const Background = ({ children }) => ( <div style={{ backgroundColor: '#276696' }}>{children}</div> ) -storiesOf('Logo', module) - .addDecorator(Wrapper) +export default { + title: 'Utils/Logo', + component: Logo, + decorators: [Wrapper], + parameters: { docs: { description: { component: description } } }, +} - .add('Logo', () => <Logo />) +export const _Logo = args => <Logo {...args} /> - .add('Logo White', () => ( - <Background> - <LogoWhite /> - </Background> - )) +export const _LogoWhite = args => ( + <Background> + <LogoWhite {...args} /> + </Background> +) +_LogoWhite.parameters = { + docs: { + description: { + story: + 'When placing the DHIS2 logo on a dark background, the reversed version can be used. The icon and wordmark are white in this version. The reversed logo can be placed on any colored background, but ideally blue would be used to remain consistent with the DHIS2 brand.', + }, + }, +} - .add('Logo Icon', () => <LogoIcon />) +export const _LogoIcon = args => <LogoIcon {...args} /> +_LogoIcon.parameters = { + docs: { + description: { + story: + 'There are times when it makes sense to use only the DHIS2 icon, such as in headers, toolbars and app icons. There are both blue colored and white colored icons, and these should be used as the main logo is used – blue colored where possible and white colored on darker backgrounds.', + }, + }, +} - .add('Logo Icon White', () => ( - <Background> - <LogoIconWhite /> - </Background> - )) +export const _LogoIconWhite = args => ( + <Background> + <LogoIconWhite {...args} /> + </Background> +) diff --git a/packages/core/src/Menu/Menu.js b/packages/core/src/Menu/Menu.js index 952d3fcbe1..27ee8e54fe 100644 --- a/packages/core/src/Menu/Menu.js +++ b/packages/core/src/Menu/Menu.js @@ -1,4 +1,4 @@ -import propTypes from '@dhis2/prop-types' +import PropTypes from 'prop-types' import React, { Children, cloneElement, isValidElement } from 'react' /** @@ -57,10 +57,12 @@ Menu.defaultProps = { * @prop {boolean} [dense] */ Menu.propTypes = { - children: propTypes.node, - className: propTypes.string, - dataTest: propTypes.string, - dense: propTypes.bool, + /** Typically `MenuItem`, `MenuDivider`, and `MenuSectionHeader` */ + children: PropTypes.node, + className: PropTypes.string, + dataTest: PropTypes.string, + /** Applies `dense` property to all child components unless already specified */ + dense: PropTypes.bool, } export { Menu } diff --git a/packages/core/src/Menu/Menu.stories.js b/packages/core/src/Menu/Menu.stories.js index e0adbe2b43..4e753f008d 100644 --- a/packages/core/src/Menu/Menu.stories.js +++ b/packages/core/src/Menu/Menu.stories.js @@ -3,33 +3,62 @@ import { MenuItem } from '../MenuItem/MenuItem.js' import { MenuSectionHeader } from '../MenuSectionHeader/MenuSectionHeader.js' import { Menu } from './Menu.js' +const description = ` +Use menus to provide access to options and actions where space is limited and displaying all the options would be impractical. For example, providing access to a range of actions for every dashboard item displayed. Containing all those actions in menus keeps the page manageable. + +The menu component is flexible in where it can be used and its contents can be flexible too. However, the most common use case is a menu containing menu items. + +Make sure the menu item labels are short and easy to understand. One word is often enough to describe an action or option. Do not use sentences as labels. Some examples of good menu item labels: + +- "Save" +- "Open as map" +- "Export PDF" +- "Duplicate" + +Typical children are Menu Items, Menu Dividers, and Menu Section Headers. + +\`\`\`js +import { Menu } from '@dhis2/ui' +\`\`\` +` + export default { - title: 'Menu', + title: 'Actions/Menu/Menu', component: Menu, + parameters: { docs: { description: { component: description } } }, } -export const Default = () => ( - <Menu> +export const Default = args => ( + <Menu {...args}> <MenuItem label="Menu item" /> <MenuItem label="Menu item" /> </Menu> ) -export const Dense = () => ( - <Menu dense> +export const Dense = args => ( + <Menu {...args}> <MenuItem label="Menu item" /> <MenuItem label="Menu item" /> </Menu> ) +Dense.args = { dense: true } +Dense.parameters = { + docs: { + description: { + story: + 'Menus are available in regular or dense sizes. Use dense menus in data-heavy applications used by users comfortable with technology. Use regular menus in apps that are less complex or have few controls.', + }, + }, +} -export const AutoHideFirstSectionHeaderDivider = () => ( - <Menu> +export const AutoHideFirstSectionHeaderDivider = args => ( + <Menu {...args}> <MenuSectionHeader label="First - no divider above" /> <MenuSectionHeader label="Second - with divider above" /> </Menu> ) -export const SideBarMenu = () => ( +export const SideBarMenu = args => ( <main style={{ display: 'flex', @@ -38,7 +67,7 @@ export const SideBarMenu = () => ( }} > <aside style={{ width: 200, height: '100%', flexGrow: 0 }}> - <Menu> + <Menu {...args}> <MenuItem label="Menu item" /> <MenuItem label="Menu item" /> </Menu> diff --git a/packages/core/src/MenuDivider/MenuDivider.js b/packages/core/src/MenuDivider/MenuDivider.js index b52a08d0ce..b67f23f4b2 100644 --- a/packages/core/src/MenuDivider/MenuDivider.js +++ b/packages/core/src/MenuDivider/MenuDivider.js @@ -1,5 +1,5 @@ -import propTypes from '@dhis2/prop-types' import { colors } from '@dhis2/ui-constants' +import PropTypes from 'prop-types' import React from 'react' import { Divider } from '../Divider/Divider.js' @@ -42,9 +42,9 @@ MenuDivider.defaultProps = { * @prop {boolean} [dense] */ MenuDivider.propTypes = { - className: propTypes.string, - dataTest: propTypes.string, - dense: propTypes.bool, + className: PropTypes.string, + dataTest: PropTypes.string, + dense: PropTypes.bool, } export { MenuDivider } diff --git a/packages/core/src/MenuDivider/MenuDivider.stories.js b/packages/core/src/MenuDivider/MenuDivider.stories.js index 7b98ac89e0..4f3427ea14 100644 --- a/packages/core/src/MenuDivider/MenuDivider.stories.js +++ b/packages/core/src/MenuDivider/MenuDivider.stories.js @@ -1,13 +1,31 @@ import React from 'react' import { Menu } from '../Menu/Menu.js' +import { MenuItem } from '../MenuItem/MenuItem.js' import { MenuDivider } from './MenuDivider.js' +const description = ` +Items in a menu can be split into separate sections by using dividers. Group relevant menu items together to help the user understand the options quickly. A divider can be used alone. If using a MenuSectionHeader, a divider will be automatically included. Try not to group single menu items together. An exception to this is a critical destructive menu item, like 'Delete', which can be separated from other menu items. + +\`\`\`js +import { MenuDivider } from '@dhis2/ui' +\`\`\` +` + export default { - title: 'MenuDivider', + title: 'Actions/Menu/Menu Divider', component: MenuDivider, - decorators: [storyFn => <Menu>{storyFn()}</Menu>], + parameters: { docs: { description: { component: description } } }, } -export const Default = () => <MenuDivider /> +const Template = args => ( + <Menu> + <MenuItem label="Item above divider" /> + <MenuDivider {...args} /> + <MenuItem label="Item below divider" /> + </Menu> +) + +export const Default = Template.bind({}) -export const Dense = () => <MenuDivider dense /> +export const Dense = Template.bind({}) +Dense.args = { dense: true } diff --git a/packages/core/src/MenuItem/MenuItem.js b/packages/core/src/MenuItem/MenuItem.js index f795d3e5d6..79cca4f690 100644 --- a/packages/core/src/MenuItem/MenuItem.js +++ b/packages/core/src/MenuItem/MenuItem.js @@ -1,5 +1,5 @@ -import propTypes from '@dhis2/prop-types' import cx from 'classnames' +import PropTypes from 'prop-types' import React, { useRef } from 'react' import { createPortal } from 'react-dom' import { FlyoutMenu } from '../FlyoutMenu/FlyoutMenu.js' @@ -125,22 +125,34 @@ MenuItem.defaultProps = { * @prop {function} [onClick] - Click handler called with `value` in the payload */ MenuItem.propTypes = { - active: propTypes.bool, - chevron: propTypes.bool, - children: propTypes.node, - className: propTypes.string, - dataTest: propTypes.string, - dense: propTypes.bool, - destructive: propTypes.bool, - disabled: propTypes.bool, - href: propTypes.string, - icon: propTypes.node, - label: propTypes.node, - showSubMenu: propTypes.bool, - target: propTypes.string, - toggleSubMenu: propTypes.func, - value: propTypes.string, - onClick: propTypes.func, + active: PropTypes.bool, + chevron: PropTypes.bool, + /** + * Nested menu items can become submenus. + * See `showSubMenu` and `toggleSubMenu` props, and 'Children' demo + */ + children: PropTypes.node, + className: PropTypes.string, + dataTest: PropTypes.string, + dense: PropTypes.bool, + destructive: PropTypes.bool, + disabled: PropTypes.bool, + /** For using menu item as a link */ + href: PropTypes.string, + /** An icon for the left side of the menu item */ + icon: PropTypes.node, + /** Text in the menu item */ + label: PropTypes.node, + /** When true, nested menu items are shown in a Popper */ + showSubMenu: PropTypes.bool, + /** For using menu item as a link */ + target: PropTypes.string, + /** On click, this function is called (without args) */ + toggleSubMenu: PropTypes.func, + /** Value associated with item. Passed as an argument to onClick handler. */ + value: PropTypes.string, + /** Click handler called with signature `({ value: string }, event)` */ + onClick: PropTypes.func, } export { MenuItem } diff --git a/packages/core/src/MenuItem/MenuItem.stories.e2e.js b/packages/core/src/MenuItem/MenuItem.stories.e2e.js index ea2f4dbc69..09fca3f436 100644 --- a/packages/core/src/MenuItem/MenuItem.stories.e2e.js +++ b/packages/core/src/MenuItem/MenuItem.stories.e2e.js @@ -7,7 +7,7 @@ window.onClick = window.Cypress && window.Cypress.cy.stub() export default { title: 'MenuItem', component: MenuItem, - decorators: [storyFn => <Menu>{storyFn()}</Menu>], + decorators: [story => <Menu>{story()}</Menu>], } export const WithLabel = () => <MenuItem label="label" /> diff --git a/packages/core/src/MenuItem/MenuItem.stories.js b/packages/core/src/MenuItem/MenuItem.stories.js index aaab6ddbcf..9add9b82b0 100644 --- a/packages/core/src/MenuItem/MenuItem.stories.js +++ b/packages/core/src/MenuItem/MenuItem.stories.js @@ -1,66 +1,152 @@ +import { IconApps24 } from '@dhis2/ui-icons' import React, { useState } from 'react' -import { resolve } from 'styled-jsx/css' -import { Apps } from '../Icons/index.js' import { Menu } from '../Menu/Menu.js' import { MenuItem } from './MenuItem.js' +const description = ` +Menu Items are intended to be children of Menu and Flyout Menu components. They can be nested to create submenus. + +Splitting menus into several levels with child menus makes sense when there are a lot of options that can be grouped together. An example may be an option in level 1 menu of 'Download' that has several different download formats as child menu items. Make sure that child menu items relate to their parent item, otherwise a user will struggle to discover them. A menu item with children is not selectable/actionable itself, it serves only as a container for the child elements. Try to keep menus to a maximum of 1, 2 or 3 levels, anything more than this can easily confuse the user. + +There is no enforced ordering of menu items, they should be presented in order of relevance. Put the most commonly used items at the top of the menu for easy discovery and access. + +See the [design system](https://github.com/dhis2/design-system/blob/master/molecules/menu.md) for more information about menus. + +\`\`\`js +import { MenuItem } from '@dhis2/ui' +\`\`\` +` + export default { - title: 'MenuItem', + title: 'Actions/Menu/Menu Item', component: MenuItem, - decorators: [storyFn => <Menu>{storyFn()}</Menu>], + args: { label: 'Menu item' }, + parameters: { docs: { description: { component: description } } }, } -export const Default = () => <MenuItem label="Menu item" /> +const Template = args => ( + <Menu> + <MenuItem {...args} /> + </Menu> +) -export const Active = () => <MenuItem active label="Menu item" /> +export const Default = Template.bind({}) -export const Chevron = () => <MenuItem chevron label="Menu item" /> +export const Active = Template.bind({}) +Active.args = { active: true } -export const Dense = () => <MenuItem dense label="Menu item" /> +export const Chevron = Template.bind({}) +Chevron.args = { chevron: true } -export const Destructive = () => <MenuItem destructive label="Menu item" /> +export const Dense = Template.bind({}) +Dense.args = { dense: true } -export const Disabled = () => <MenuItem disabled label="Menu item" /> +export const Destructive = Template.bind({}) +Destructive.args = { destructive: true } +Destructive.parameters = { + docs: { + description: { + story: + "Destructive menu items should be used for critical, destructive actions such as 'Delete', 'Remove' or 'End process'. Do not use destructive menu items for actions that are simply important, they must also be destructive in nature. A menu should, ideally, only have one destructive action. Using a divider to separate normal and destructive options helps the user to understand that the destructive options is different from the rest of the options.", + }, + }, +} -export const Link = () => ( - <MenuItem target="_blank" href="http://dhis2.org" label="Menu item" /> -) +export const Disabled = Template.bind({}) +Disabled.args = { disabled: true } +Disabled.parameters = { + docs: { + description: { + story: + 'Menu items should be disabled when they are not available, but could be available if something changes. Do not include menu items that will never be available, this will confuse a user. Instead, remove them from the menu.', + }, + }, +} -export const Icon = () => { - const { className, styles } = resolve` - fill: magenta; - ` +export const Link = Template.bind({}) +Link.args = { target: '_blank', href: 'http://dhis2.org' } +export const Icon = args => { + // import { IconApps24 } from '@dhis2/ui' return ( - <> - <MenuItem icon={<Apps />} label="Menu item" /> + <Menu> + <MenuItem {...args} icon={<IconApps24 />} label="Menu item" /> <MenuItem - icon={<Apps className={className} />} + icon={<IconApps24 color="magenta" />} label="Menu item - with custom icon fill" /> - - {styles} - </> + </Menu> ) } +Icon.parameters = { + docs: { + source: { type: 'code' }, + description: { + story: + 'A menu item can include an icon to help the user understand or recognize the option. An icon should support the menu item text and be simple enough to be understood in a dense UI. Icons add a lot of visual noise a menu, so only include them where they will help the user. Do not include icons only for visual reasons, the icon must functionally support the users understanding. Do not use complex icons. All menu items in a single menu do not need to have icons.', + }, + }, +} -export const OnClick = () => ( - <MenuItem - onClick={(payload, event) => { - console.log(payload.value, event.target) - }} - value="myValue" - label="Menu item" - /> +export const OnClick = args => ( + <Menu> + <MenuItem + onClick={(payload, event) => { + console.log(payload.value, event.target) + }} + value="myValue" + label="Menu item" + {...args} + /> + </Menu> ) +OnClick.parameters = { docs: { source: { type: 'code' } } } +OnClick.args = { onClick: console.log } -export const ToggleMenuItem = () => { +export const ToggleMenuItem = args => { const [on, setOn] = useState(false) const toggleOn = () => setOn(!on) const checkMarkStyle = { fontSize: '24px', lineHeight: '24px' } const icon = on ? <span style={checkMarkStyle}>✓</span> : <span /> return ( - <MenuItem onClick={toggleOn} icon={icon} label="A toggle menu item" /> + <Menu> + <MenuItem + {...args} + onClick={toggleOn} + icon={icon} + label="A toggle menu item" + /> + </Menu> + ) +} +ToggleMenuItem.parameters = { docs: { source: { type: 'code' } } } + +export const SubMenus = args => { + const [showSubMenu, setShowSubMenu] = React.useState(false) + const toggleSubMenu = () => setShowSubMenu(!showSubMenu) + + return ( + <Menu> + <MenuItem + showSubMenu={showSubMenu} + toggleSubMenu={toggleSubMenu} + {...args} + label="Parent of submenus" + > + <MenuItem label="Submenu child 1" /> + <MenuItem label="Submenu child 2" /> + </MenuItem> + <MenuItem {...args} label="Regular item" /> + </Menu> ) } +SubMenus.parameters = { + docs: { + source: { type: 'code' }, + description: { + story: + "_View this story in the 'Canvas' tab for proper submenu alignment._", + }, + }, +} diff --git a/packages/core/src/MenuSectionHeader/MenuSectionHeader.js b/packages/core/src/MenuSectionHeader/MenuSectionHeader.js index 412c9721c7..057b846527 100644 --- a/packages/core/src/MenuSectionHeader/MenuSectionHeader.js +++ b/packages/core/src/MenuSectionHeader/MenuSectionHeader.js @@ -1,6 +1,6 @@ -import propTypes from '@dhis2/prop-types' import { colors, spacers } from '@dhis2/ui-constants' import cx from 'classnames' +import PropTypes from 'prop-types' import React from 'react' import { Divider } from '../Divider/Divider.js' @@ -67,11 +67,11 @@ MenuSectionHeader.defaultProps = { * @prop {Node} [label] */ MenuSectionHeader.propTypes = { - className: propTypes.string, - dataTest: propTypes.string, - dense: propTypes.bool, - hideDivider: propTypes.bool, - label: propTypes.node, + className: PropTypes.string, + dataTest: PropTypes.string, + dense: PropTypes.bool, + hideDivider: PropTypes.bool, + label: PropTypes.node, } export { MenuSectionHeader } diff --git a/packages/core/src/MenuSectionHeader/MenuSectionHeader.stories.e2e.js b/packages/core/src/MenuSectionHeader/MenuSectionHeader.stories.e2e.js index c53db9cd32..4ded7d0d46 100644 --- a/packages/core/src/MenuSectionHeader/MenuSectionHeader.stories.e2e.js +++ b/packages/core/src/MenuSectionHeader/MenuSectionHeader.stories.e2e.js @@ -5,7 +5,7 @@ import { MenuSectionHeader } from './MenuSectionHeader.js' export default { title: 'MenuSectionHeader', component: MenuSectionHeader, - decorators: [storyFn => <Menu>{storyFn()}</Menu>], + decorators: [story => <Menu>{story()}</Menu>], } export const WithLabel = () => <MenuSectionHeader label="label" /> diff --git a/packages/core/src/MenuSectionHeader/MenuSectionHeader.stories.js b/packages/core/src/MenuSectionHeader/MenuSectionHeader.stories.js index bef957cf3b..eaf5df5491 100644 --- a/packages/core/src/MenuSectionHeader/MenuSectionHeader.stories.js +++ b/packages/core/src/MenuSectionHeader/MenuSectionHeader.stories.js @@ -1,17 +1,54 @@ +import PropTypes from 'prop-types' import React from 'react' import { Menu } from '../Menu/Menu.js' +import { MenuItem } from '../MenuItem/MenuItem.js' import { MenuSectionHeader } from './MenuSectionHeader.js' +const description = ` +Items in a menu can be split into separate sections by using Dividers. Group relevant menu items together to help the user understand the options quickly. A Divider can be used alone. If using a Menu Section Header, a divider will be automatically included. Try not to group single menu items together. An exception to this is a critical destructive menu item, like 'Delete', which can be separated from other menu items. + +\`\`\`js +import { MenuSectionHeader } from '@dhis2/ui' +\`\`\` +` + export default { - title: 'MenuSectionHeader', + title: 'Actions/Menu/Menu Section Header', component: MenuSectionHeader, - decorators: [storyFn => <Menu>{storyFn()}</Menu>], + args: { label: 'Section header' }, + parameters: { docs: { description: { component: description } } }, } -export const Default = () => <MenuSectionHeader label="Section header" /> +const Template = ({ menuArgs, ...args }) => ( + <Menu {...menuArgs}> + <MenuItem label="Menu item above" /> + <MenuSectionHeader {...args} /> + <MenuItem label="Menu item below" /> + </Menu> +) +Template.propTypes = { menuArgs: PropTypes.shape() } + +export const Default = Template.bind({}) + +export const Dense = Template.bind({}) +Dense.args = { menuArgs: { dense: true }, dense: true } -export const Dense = () => <MenuSectionHeader dense label="Section header" /> +export const WithoutDivider = Template.bind({}) +WithoutDivider.args = { hideDivider: true } -export const WithoutDivider = () => ( - <MenuSectionHeader hideDivider label="Section header" /> +export const TopOfList = args => ( + <Menu> + <MenuSectionHeader {...args} /> + <MenuItem label="Item 1" /> + <MenuItem label="Item 2" /> + </Menu> ) +TopOfList.args = { label: 'Top of list (so <Menu> hides divider)' } +TopOfList.parameters = { + docs: { + description: { + story: + 'When the Section Header is the first child of a `<Menu>`, the Menu parent automatically applies the `hideDivider` prop.', + }, + }, +} diff --git a/packages/core/src/Modal/Modal.js b/packages/core/src/Modal/Modal.js index c084bf936b..553ba6405b 100644 --- a/packages/core/src/Modal/Modal.js +++ b/packages/core/src/Modal/Modal.js @@ -1,7 +1,11 @@ -import propTypes from '@dhis2/prop-types' -import { layers, spacers, spacersNum } from '@dhis2/ui-constants' -import { sharedPropTypes } from '@dhis2/ui-constants' +import { + layers, + spacers, + spacersNum, + sharedPropTypes, +} from '@dhis2/ui-constants' import cx from 'classnames' +import PropTypes from 'prop-types' import React from 'react' import { resolve } from 'styled-jsx/css' import { Card } from '../Card/Card.js' @@ -35,7 +39,7 @@ const centeredContent = resolve` * @desc Modal provides a UI to prompt the user to respond to a question * or a note to the user. * - * Use Model with the following Components: + * Use Modal with the following Components: * ModelTitle (optional) * ModelContent (required) * ModelActions (optional) @@ -117,12 +121,12 @@ Modal.defaultProps = { * @prop {string} [dataTest] */ Modal.propTypes = { - children: propTypes.node, - className: propTypes.string, - dataTest: propTypes.string, + children: PropTypes.node, + className: PropTypes.string, + dataTest: PropTypes.string, large: sharedPropTypes.sizePropType, position: sharedPropTypes.insideAlignmentPropType, small: sharedPropTypes.sizePropType, - // Callback used when clicking on the screen cover - onClose: propTypes.func, + /** Callback used when screen cover is clicked */ + onClose: PropTypes.func, } diff --git a/packages/core/src/Modal/Modal.stories.js b/packages/core/src/Modal/Modal.stories.js index e447def3f6..ac7e9ad36a 100644 --- a/packages/core/src/Modal/Modal.stories.js +++ b/packages/core/src/Modal/Modal.stories.js @@ -1,4 +1,4 @@ -import { storiesOf } from '@storybook/react' +import { sharedPropTypes } from '@dhis2/ui-constants' import React from 'react' import { Button, @@ -11,6 +11,37 @@ import { } from '../index.js' import { Modal } from './Modal.js' +const description = ` +_**Note**: For performance reasons, only one representative example is shown here. For more (interactive) examples, see individual stories in the Canvas tab._ + +A modal focuses user attention on a single task or piece of information inside a container. A modal blocks the rest of the application until it is dismissed. + +#### Usage + +A modal should be used to focus user attention on a single task or piece of information. Modals take over the entire screen and should be used sparingly to avoid interrupting a user's flow too often. + +Use a modal in the following cases: + +- to collect short, focussed user input that is blocking progress +- to present critical information that a user needs to acknowledge before continuing +- to ask the user to confirm a destructive action that cannot be undone + +Do not use a modal in the following cases: + +- to display unrelated or non-critical information; use an alert bar or a notice box instead +- to display complex, workflows that span multiple screens; navigate to a new, full page in the app instead + +#### Children + +Modals should be used with children \`<ModalTitle>\` (optional), \`<ModalContent>\` (required), and \`<ModalActions>\` (recommended) + +See more about modal usage, including alignment and sizing, at [Design System: Modals](https://github.com/dhis2/design-system/blob/master/molecules/modal.md#usage). + +\`\`\`js +import { Modal } from '@dhis2/ui' +\`\`\` +` + const say = something => () => alert(something) window.onClose = (payload, event) => { @@ -20,663 +51,690 @@ window.onClose = (payload, event) => { const onClose = (...args) => window.onClose(...args) -storiesOf('Modal', module) - .add('Default: Content', () => ( - <Modal onClose={onClose}> - <ModalContent> - Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed - diam nonumy eirmod tempor invidunt ut labore et dolore magna - aliquyam erat, sed diam voluptua. At vero eos et accusam et - justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea - takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum - dolor sit amet, consetetur sadipscing elitr, sed diam nonumy - eirmod tempor invidunt ut labore et dolore magna aliquyam erat, - sed diam voluptua. At vero eos et accusam et justo duo dolores - et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus - est Lorem ipsum dolor sit amet. - </ModalContent> - </Modal> - )) - .add('Alignment: Middle', () => ( - <Modal onClose={onClose} position="middle"> - <ModalContent> - Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed - diam nonumy eirmod tempor invidunt ut labore et dolore magna - aliquyam erat, sed diam voluptua. At vero eos et accusam et - justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea - takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum - dolor sit amet, consetetur sadipscing elitr, sed diam nonumy - eirmod tempor invidunt ut labore et dolore magna aliquyam erat, - sed diam voluptua. At vero eos et accusam et justo duo dolores - et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus - est Lorem ipsum dolor sit amet. - </ModalContent> - </Modal> - )) - .add('Alignment: Bottom', () => ( - <Modal onClose={onClose} position="bottom"> - <ModalContent> - Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed - diam nonumy eirmod tempor invidunt ut labore et dolore magna - aliquyam erat, sed diam voluptua. At vero eos et accusam et - justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea - takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum - dolor sit amet, consetetur sadipscing elitr, sed diam nonumy - eirmod tempor invidunt ut labore et dolore magna aliquyam erat, - sed diam voluptua. At vero eos et accusam et justo duo dolores - et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus - est Lorem ipsum dolor sit amet. - </ModalContent> - </Modal> - )) - .add('Small: Title, Content, Action', () => ( - <Modal small onClose={onClose}> - <ModalTitle> - This is a small modal with title, content and primary action - </ModalTitle> - - <ModalContent> - Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed - diam nonumy eirmod tempor invidunt ut labore et dolore magna - aliquyam erat, sed diam voluptua. At vero eos et accusam et - justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea - takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum - dolor sit amet, consetetur sadipscing elitr, sed diam nonumy - eirmod tempor invidunt ut labore et dolore magna aliquyam erat, - sed diam voluptua. At vero eos et accusam et justo duo dolores - et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus - est Lorem ipsum dolor sit amet. - </ModalContent> - - <ModalActions> - <ButtonStrip end> - <Button onClick={say('Button secondary')} secondary> - Secondary action - </Button> - - <Button onClick={say('Button primary')} primary> - Primary action - </Button> - </ButtonStrip> - </ModalActions> - </Modal> - )) - .add('Medium: Title, Content, Action', () => ( - <Modal> - <ModalTitle> - This is a medium modal with title, content and primary action - </ModalTitle> - - <ModalContent> - Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed - diam nonumy eirmod tempor invidunt ut labore et dolore magna - aliquyam erat, sed diam voluptua. At vero eos et accusam et - justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea - takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum - dolor sit amet, consetetur sadipscing elitr, sed diam nonumy - eirmod tempor invidunt ut labore et dolore magna aliquyam erat, - sed diam voluptua. At vero eos et accusam et justo duo dolores - et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus - est Lorem ipsum dolor sit amet. - </ModalContent> - - <ModalActions> - <ButtonStrip end> - <Button onClick={say('Button secondary')} secondary> - Secondary action - </Button> - - <Button onClick={say('Button primary')} primary> - Primary action - </Button> - </ButtonStrip> - </ModalActions> - </Modal> - )) - .add('Large: Title, Content, Primary', () => ( - <Modal large> - <ModalTitle> - This is a large modal with title, content and primary action - </ModalTitle> - - <ModalContent> - Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed - diam nonumy eirmod tempor invidunt ut labore et dolore magna - aliquyam erat, sed diam voluptua. At vero eos et accusam et - justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea - takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum - dolor sit amet, consetetur sadipscing elitr, sed diam nonumy - eirmod tempor invidunt ut labore et dolore magna aliquyam erat, - sed diam voluptua. At vero eos et accusam et justo duo dolores - et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus - est Lorem ipsum dolor sit amet. - </ModalContent> - - <ModalActions> - <ButtonStrip end> - <Button onClick={say('Button secondary')} secondary> - Secondary action - </Button> - - <Button onClick={say('Button primary')} primary> - Primary action - </Button> - </ButtonStrip> - </ModalActions> - </Modal> - )) - .add('Small: Content & Primary', () => ( - <Modal small> - <ModalContent> - Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed - diam nonumy eirmod tempor invidunt ut labore et dolore magna - aliquyam erat, sed diam voluptua. At vero eos et accusam et - justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea - takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum - dolor sit amet, consetetur sadipscing elitr, sed diam nonumy - eirmod tempor invidunt ut labore et dolore magna aliquyam erat, - sed diam voluptua. At vero eos et accusam et justo duo dolores - et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus - est Lorem ipsum dolor sit amet. - </ModalContent> - - <ModalActions> - <ButtonStrip end> - <Button onClick={say('Button secondary')} secondary> - Secondary action - </Button> - - <Button onClick={say('Button primary')} primary> - Primary action - </Button> - </ButtonStrip> - </ModalActions> - </Modal> - )) - .add('Small: Destructive Primary', () => ( - <Modal small> - <ModalContent> - Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed - diam nonumy eirmod tempor invidunt ut labore et dolore magna - aliquyam erat, sed diam voluptua. At vero eos et accusam et - justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea - takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum - dolor sit amet, consetetur sadipscing elitr, sed diam nonumy - eirmod tempor invidunt ut labore et dolore magna aliquyam erat, - sed diam voluptua. At vero eos et accusam et justo duo dolores - et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus - est Lorem ipsum dolor sit amet. - </ModalContent> - - <ModalActions> - <ButtonStrip end> - <Button onClick={say('Button secondary')} secondary> - Secondary action - </Button> - - <Button destructive onClick={say('Button primary')}> - Primary action - </Button> - </ButtonStrip> - </ModalActions> - </Modal> - )) - .add('Small: Clickable screen cover', () => ( - <Modal small onClose={say('Clickable screen cover')}> - <ModalTitle>This is a modal with clickable screen cover</ModalTitle> - - <ModalContent> - Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed - diam nonumy eirmod tempor invidunt ut labore et dolore magna - aliquyam erat, sed diam voluptua. - </ModalContent> - - <ModalActions> - <ButtonStrip end> - <Button onClick={say('Button secondary')} secondary> - Secondary action - </Button> - - <Button primary onClick={say('Button primary')}> - Primary action - </Button> - </ButtonStrip> - </ModalActions> - </Modal> - )) - .add('Top: scrollable', () => ( - <Modal small onClose={say('Clickable screen cover')}> - <ModalTitle>This is a modal with scrollable content</ModalTitle> - - <ModalContent> - Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed - diam nonumy eirmod tempor invidunt ut labore et dolore magna - aliquyam erat, sed diam voluptua. Lorem ipsum dolor sit amet, - consetetur sadipscing elitr, sed diam nonumy eirmod tempor - invidunt ut labore et dolore magna aliquyam erat, sed diam - voluptua. At vero eos et accusam et justo duo dolores et ea - rebum. Stet clita kasd gubergren, no sea takimata sanctus est - Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, - consetetur sadipscing elitr, sed diam nonumy eirmod tempor - invidunt ut labore et dolore magna aliquyam erat, sed diam - voluptua. At vero eos et accusam et justo duo dolores et ea - rebum. Stet clita kasd gubergren, no sea takimata sanctus est - Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, - consetetur sadipscing elitr, sed diam nonumy eirmod tempor - invidunt ut labore et dolore magna aliquyam erat, sed diam - voluptua. Lorem ipsum dolor sit amet, consetetur sadipscing - elitr, sed diam nonumy eirmod tempor invidunt ut labore et - dolore magna aliquyam erat, sed diam voluptua. At vero eos et - accusam et justo duo dolores et ea rebum. Stet clita kasd - gubergren, no sea takimata sanctus est Lorem ipsum dolor sit - amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, - sed diam nonumy eirmod tempor invidunt ut labore et dolore magna - aliquyam erat, sed diam voluptua. At vero eos et accusam et - justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea - takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum - dolor sit amet, consetetur sadipscing elitr, sed diam nonumy - eirmod tempor invidunt ut labore et dolore magna aliquyam erat, - sed diam voluptua. Lorem ipsum dolor sit amet, consetetur - sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut - labore et dolore magna aliquyam erat, sed diam voluptua. At vero - eos et accusam et justo duo dolores et ea rebum. Stet clita kasd - gubergren, no sea takimata sanctus est Lorem ipsum dolor sit - amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, - sed diam nonumy eirmod tempor invidunt ut labore et dolore magna - aliquyam erat, sed diam voluptua. At vero eos et accusam et - justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea - takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum - dolor sit amet, consetetur sadipscing elitr, sed diam nonumy - eirmod tempor invidunt ut labore et dolore magna aliquyam erat, - sed diam voluptua. Lorem ipsum dolor sit amet, consetetur - sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut - labore et dolore magna aliquyam erat, sed diam voluptua. At vero - eos et accusam et justo duo dolores et ea rebum. Stet clita kasd - gubergren, no sea takimata sanctus est Lorem ipsum dolor sit - amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, - sed diam nonumy eirmod tempor invidunt ut labore et dolore magna - aliquyam erat, sed diam voluptua. At vero eos et accusam et - justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea - takimata sanctus est Lorem ipsum dolor sit amet. - </ModalContent> - - <ModalActions> - <ButtonStrip end> - <Button onClick={say('Button secondary')} secondary> - Secondary action - </Button> - - <Button destructive onClick={say('Button primary')}> - Primary action - </Button> - </ButtonStrip> - </ModalActions> - </Modal> - )) - .add('Middle: scrollable', () => ( - <Modal small onClose={say('Clickable screen cover')} position="middle"> - <ModalTitle>This is a modal with scrollable content</ModalTitle> - - <ModalContent> - Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed - diam nonumy eirmod tempor invidunt ut labore et dolore magna - aliquyam erat, sed diam voluptua. Lorem ipsum dolor sit amet, - consetetur sadipscing elitr, sed diam nonumy eirmod tempor - invidunt ut labore et dolore magna aliquyam erat, sed diam - voluptua. At vero eos et accusam et justo duo dolores et ea - rebum. Stet clita kasd gubergren, no sea takimata sanctus est - Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, - consetetur sadipscing elitr, sed diam nonumy eirmod tempor - invidunt ut labore et dolore magna aliquyam erat, sed diam - voluptua. At vero eos et accusam et justo duo dolores et ea - rebum. Stet clita kasd gubergren, no sea takimata sanctus est - Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, - consetetur sadipscing elitr, sed diam nonumy eirmod tempor - invidunt ut labore et dolore magna aliquyam erat, sed diam - voluptua. Lorem ipsum dolor sit amet, consetetur sadipscing - elitr, sed diam nonumy eirmod tempor invidunt ut labore et - dolore magna aliquyam erat, sed diam voluptua. At vero eos et - accusam et justo duo dolores et ea rebum. Stet clita kasd - gubergren, no sea takimata sanctus est Lorem ipsum dolor sit - amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, - sed diam nonumy eirmod tempor invidunt ut labore et dolore magna - aliquyam erat, sed diam voluptua. At vero eos et accusam et - justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea - takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum - dolor sit amet, consetetur sadipscing elitr, sed diam nonumy - eirmod tempor invidunt ut labore et dolore magna aliquyam erat, - sed diam voluptua. Lorem ipsum dolor sit amet, consetetur - sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut - labore et dolore magna aliquyam erat, sed diam voluptua. At vero - eos et accusam et justo duo dolores et ea rebum. Stet clita kasd - gubergren, no sea takimata sanctus est Lorem ipsum dolor sit - amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, - sed diam nonumy eirmod tempor invidunt ut labore et dolore magna - aliquyam erat, sed diam voluptua. At vero eos et accusam et - justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea - takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum - dolor sit amet, consetetur sadipscing elitr, sed diam nonumy - eirmod tempor invidunt ut labore et dolore magna aliquyam erat, - sed diam voluptua. Lorem ipsum dolor sit amet, consetetur - sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut - labore et dolore magna aliquyam erat, sed diam voluptua. At vero - eos et accusam et justo duo dolores et ea rebum. Stet clita kasd - gubergren, no sea takimata sanctus est Lorem ipsum dolor sit - amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, - sed diam nonumy eirmod tempor invidunt ut labore et dolore magna - aliquyam erat, sed diam voluptua. At vero eos et accusam et - justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea - takimata sanctus est Lorem ipsum dolor sit amet. - </ModalContent> - - <ModalActions> - <ButtonStrip end> - <Button onClick={say('Button secondary')} secondary> - Secondary action - </Button> - - <Button destructive onClick={say('Button primary')}> - Primary action - </Button> - </ButtonStrip> - </ModalActions> - </Modal> - )) - .add('Bottom: scrollable', () => ( - <Modal small onClose={say('Clickable screen cover')} position="bottom"> - <ModalTitle>This is a modal with scrollable content</ModalTitle> - - <ModalContent> - Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed - diam nonumy eirmod tempor invidunt ut labore et dolore magna - aliquyam erat, sed diam voluptua. Lorem ipsum dolor sit amet, - consetetur sadipscing elitr, sed diam nonumy eirmod tempor - invidunt ut labore et dolore magna aliquyam erat, sed diam - voluptua. At vero eos et accusam et justo duo dolores et ea - rebum. Stet clita kasd gubergren, no sea takimata sanctus est - Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, - consetetur sadipscing elitr, sed diam nonumy eirmod tempor - invidunt ut labore et dolore magna aliquyam erat, sed diam - voluptua. At vero eos et accusam et justo duo dolores et ea - rebum. Stet clita kasd gubergren, no sea takimata sanctus est - Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, - consetetur sadipscing elitr, sed diam nonumy eirmod tempor - invidunt ut labore et dolore magna aliquyam erat, sed diam - voluptua. Lorem ipsum dolor sit amet, consetetur sadipscing - elitr, sed diam nonumy eirmod tempor invidunt ut labore et - dolore magna aliquyam erat, sed diam voluptua. At vero eos et - accusam et justo duo dolores et ea rebum. Stet clita kasd - gubergren, no sea takimata sanctus est Lorem ipsum dolor sit - amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, - sed diam nonumy eirmod tempor invidunt ut labore et dolore magna - aliquyam erat, sed diam voluptua. At vero eos et accusam et - justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea - takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum - dolor sit amet, consetetur sadipscing elitr, sed diam nonumy - eirmod tempor invidunt ut labore et dolore magna aliquyam erat, - sed diam voluptua. Lorem ipsum dolor sit amet, consetetur - sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut - labore et dolore magna aliquyam erat, sed diam voluptua. At vero - eos et accusam et justo duo dolores et ea rebum. Stet clita kasd - gubergren, no sea takimata sanctus est Lorem ipsum dolor sit - amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, - sed diam nonumy eirmod tempor invidunt ut labore et dolore magna - aliquyam erat, sed diam voluptua. At vero eos et accusam et - justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea - takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum - dolor sit amet, consetetur sadipscing elitr, sed diam nonumy - eirmod tempor invidunt ut labore et dolore magna aliquyam erat, - sed diam voluptua. Lorem ipsum dolor sit amet, consetetur - sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut - labore et dolore magna aliquyam erat, sed diam voluptua. At vero - eos et accusam et justo duo dolores et ea rebum. Stet clita kasd - gubergren, no sea takimata sanctus est Lorem ipsum dolor sit - amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, - sed diam nonumy eirmod tempor invidunt ut labore et dolore magna - aliquyam erat, sed diam voluptua. At vero eos et accusam et - justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea - takimata sanctus est Lorem ipsum dolor sit amet. - </ModalContent> - - <ModalActions> - <ButtonStrip end> - <Button onClick={say('Button secondary')} secondary> - Secondary action - </Button> - - <Button destructive onClick={say('Button primary')}> - Primary action - </Button> - </ButtonStrip> - </ModalActions> - </Modal> - )) - .add('Small: Long title', () => ( - <Modal small> - <ModalTitle> - This headline should break into multiple lines because it's - way too long for one! - </ModalTitle> - - <ModalContent> - Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed - diam nonumy eirmod tempor invidunt ut labore et dolore magna - aliquyam erat, sed diam voluptua. At vero eos et accusam et - justo duo dolores et ea rebum. - </ModalContent> - - <ModalActions> - <ButtonStrip end> - <Button onClick={say('Button secondary')} secondary> - Secondary action - </Button> - - <Button onClick={say('Button primary')} primary> - Primary action - </Button> - </ButtonStrip> - </ModalActions> - </Modal> - )) - .add('Large: with Select component', () => ( - <Modal large> - <ModalTitle>Select opens on top of the Modal</ModalTitle> - - <ModalContent> - <SingleSelect> - <SingleSelectOption key="1" value="1" label="one" /> - <SingleSelectOption key="2" value="2" label="two" /> - <SingleSelectOption key="3" value="3" label="three" /> - <SingleSelectOption key="4" value="3" label="four" /> - <SingleSelectOption key="5" value="3" label="five" /> - <SingleSelectOption key="6" value="3" label="six" /> - <SingleSelectOption key="7" value="3" label="seven" /> - <SingleSelectOption key="8" value="3" label="eight" /> - <SingleSelectOption key="9" value="3" label="nine" /> - <SingleSelectOption key="10" value="3" label="ten" /> - </SingleSelect> - </ModalContent> - - <ModalActions> - <ButtonStrip end> - <Button onClick={say('Button secondary')} secondary> - Secondary action - </Button> - - <Button onClick={say('Button primary')} primary> - Primary action - </Button> - </ButtonStrip> - </ModalActions> - </Modal> - )) - .add('Large: modal with more nested modals', () => ( - <Modal large> - <ModalTitle>Select opens on top of the Modal - Level 1</ModalTitle> - - <ModalContent> - <SingleSelect> - <SingleSelectOption key="1" value="1" label="one" /> - <SingleSelectOption key="2" value="2" label="two" /> - <SingleSelectOption key="3" value="3" label="three" /> - <SingleSelectOption key="4" value="3" label="four" /> - <SingleSelectOption key="5" value="3" label="five" /> - <SingleSelectOption key="6" value="3" label="six" /> - <SingleSelectOption key="7" value="3" label="seven" /> - <SingleSelectOption key="8" value="3" label="eight" /> - <SingleSelectOption key="9" value="3" label="nine" /> - <SingleSelectOption key="10" value="3" label="ten" /> - </SingleSelect> - <Modal large> - <ModalTitle> - Select opens on top of the Modal - Level 2 - </ModalTitle> - - <ModalContent> - <SingleSelect> - <SingleSelectOption key="1" value="1" label="one" /> - <SingleSelectOption key="2" value="2" label="two" /> - <SingleSelectOption - key="3" - value="3" - label="three" - /> - <SingleSelectOption - key="4" - value="3" - label="four" - /> - <SingleSelectOption - key="5" - value="3" - label="five" - /> - <SingleSelectOption key="6" value="3" label="six" /> - <SingleSelectOption - key="7" - value="3" - label="seven" - /> - <SingleSelectOption - key="8" - value="3" - label="eight" - /> - <SingleSelectOption - key="9" - value="3" - label="nine" - /> - <SingleSelectOption - key="10" - value="3" - label="ten" - /> - </SingleSelect> - <Modal large> - <ModalTitle> - Select opens on top of the Modal - Level 3 - </ModalTitle> - - <ModalContent> - <SingleSelect> - <SingleSelectOption - key="1" - value="1" - label="one" - /> - <SingleSelectOption - key="2" - value="2" - label="two" - /> - <SingleSelectOption - key="3" - value="3" - label="three" - /> - <SingleSelectOption - key="4" - value="3" - label="four" - /> - <SingleSelectOption - key="5" - value="3" - label="five" - /> - <SingleSelectOption - key="6" - value="3" - label="six" - /> - <SingleSelectOption - key="7" - value="3" - label="seven" - /> - <SingleSelectOption - key="8" - value="3" - label="eight" - /> - <SingleSelectOption - key="9" - value="3" - label="nine" - /> - <SingleSelectOption - key="10" - value="3" - label="ten" - /> - </SingleSelect> - </ModalContent> - - <ModalActions> - <ButtonStrip end> - <Button - onClick={say('Button secondary')} - secondary - > - Secondary action - </Button> - - <Button - onClick={say('Button primary')} - primary - > - Primary action - </Button> - </ButtonStrip> - </ModalActions> - </Modal> - </ModalContent> - - <ModalActions> - <ButtonStrip end> - <Button onClick={say('Button secondary')} secondary> - Secondary action - </Button> - - <Button onClick={say('Button primary')} primary> - Primary action - </Button> - </ButtonStrip> - </ModalActions> - </Modal> - </ModalContent> - - <ModalActions> - <ButtonStrip end> - <Button onClick={say('Button secondary')} secondary> - Secondary action - </Button> - - <Button onClick={say('Button primary')} primary> - Primary action - </Button> - </ButtonStrip> - </ModalActions> - </Modal> - )) +export default { + title: 'Layout/Modal', + component: Modal, + parameters: { + docs: { + // Use iframes to contain modals in docs page (otherwise chaos ensues) + inlineStories: false, + iframeHeight: '500px', + /** + * Due to iframes being very slow, disable stories on the docs page by default and + * make one representative story as the primary ('SmallTitleContentAction') + */ + disable: true, + description: { component: description }, + }, + }, + argTypes: { + small: { ...sharedPropTypes.sizeArgType }, + large: { ...sharedPropTypes.sizeArgType }, + position: { ...sharedPropTypes.insideAlignmentArgType }, + }, +} + +export const DefaultContent = args => ( + <Modal {...args}> + <ModalContent> + Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam + nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam + erat, sed diam voluptua. At vero eos et accusam et justo duo dolores + et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est + Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur + sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore + et dolore magna aliquyam erat, sed diam voluptua. At vero eos et + accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, + no sea takimata sanctus est Lorem ipsum dolor sit amet. + </ModalContent> + </Modal> +) +DefaultContent.args = { + onClose: onClose, +} +DefaultContent.storyName = 'Default: Content' + +export const AlignmentMiddle = args => ( + <Modal {...args}> + <ModalContent> + Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam + nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam + erat, sed diam voluptua. At vero eos et accusam et justo duo dolores + et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est + Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur + sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore + et dolore magna aliquyam erat, sed diam voluptua. At vero eos et + accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, + no sea takimata sanctus est Lorem ipsum dolor sit amet. + </ModalContent> + </Modal> +) +AlignmentMiddle.args = { onClose, position: 'middle' } +AlignmentMiddle.storyName = 'Alignment: Middle' + +export const AlignmentBottom = args => ( + <Modal {...args}> + <ModalContent> + Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam + nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam + erat, sed diam voluptua. At vero eos et accusam et justo duo dolores + et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est + Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur + sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore + et dolore magna aliquyam erat, sed diam voluptua. At vero eos et + accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, + no sea takimata sanctus est Lorem ipsum dolor sit amet. + </ModalContent> + </Modal> +) +AlignmentBottom.args = { onClose, alignment: 'bottom' } +AlignmentBottom.storyName = 'Alignment: Bottom' + +export const SmallTitleContentAction = args => ( + <Modal {...args}> + <ModalTitle> + This is a small modal with title, content and primary action + </ModalTitle> + + <ModalContent> + Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam + nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam + erat, sed diam voluptua. At vero eos et accusam et justo duo dolores + et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est + Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur + sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore + et dolore magna aliquyam erat, sed diam voluptua. At vero eos et + accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, + no sea takimata sanctus est Lorem ipsum dolor sit amet. + </ModalContent> + + <ModalActions> + <ButtonStrip end> + <Button onClick={say('Button secondary')} secondary> + Secondary action + </Button> + + <Button onClick={say('Button primary')} primary> + Primary action + </Button> + </ButtonStrip> + </ModalActions> + </Modal> +) +// Have this be the primary story on the docs page +SmallTitleContentAction.parameters = { + docs: { disable: false, source: { type: 'dynamic' } }, +} +SmallTitleContentAction.args = { small: true, onClose } +SmallTitleContentAction.storyName = 'Small: Title, Content, Action' + +export const MediumTitleContentAction = args => ( + <Modal {...args}> + <ModalTitle> + This is a medium modal with title, content and primary action + </ModalTitle> + + <ModalContent> + Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam + nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam + erat, sed diam voluptua. At vero eos et accusam et justo duo dolores + et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est + Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur + sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore + et dolore magna aliquyam erat, sed diam voluptua. At vero eos et + accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, + no sea takimata sanctus est Lorem ipsum dolor sit amet. + </ModalContent> + + <ModalActions> + <ButtonStrip end> + <Button onClick={say('Button secondary')} secondary> + Secondary action + </Button> + + <Button onClick={say('Button primary')} primary> + Primary action + </Button> + </ButtonStrip> + </ModalActions> + </Modal> +) +MediumTitleContentAction.storyName = 'Medium: Title, Content, Action' + +export const LargeTitleContentPrimary = args => ( + <Modal {...args}> + <ModalTitle> + This is a large modal with title, content and primary action + </ModalTitle> + + <ModalContent> + Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam + nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam + erat, sed diam voluptua. At vero eos et accusam et justo duo dolores + et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est + Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur + sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore + et dolore magna aliquyam erat, sed diam voluptua. At vero eos et + accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, + no sea takimata sanctus est Lorem ipsum dolor sit amet. + </ModalContent> + + <ModalActions> + <ButtonStrip end> + <Button onClick={say('Button secondary')} secondary> + Secondary action + </Button> + + <Button onClick={say('Button primary')} primary> + Primary action + </Button> + </ButtonStrip> + </ModalActions> + </Modal> +) +LargeTitleContentPrimary.args = { large: true } +LargeTitleContentPrimary.storyName = 'Large: Title, Content, Primary' + +export const SmallContentPrimary = args => ( + <Modal {...args}> + <ModalContent> + Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam + nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam + erat, sed diam voluptua. At vero eos et accusam et justo duo dolores + et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est + Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur + sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore + et dolore magna aliquyam erat, sed diam voluptua. At vero eos et + accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, + no sea takimata sanctus est Lorem ipsum dolor sit amet. + </ModalContent> + + <ModalActions> + <ButtonStrip end> + <Button onClick={say('Button secondary')} secondary> + Secondary action + </Button> + + <Button onClick={say('Button primary')} primary> + Primary action + </Button> + </ButtonStrip> + </ModalActions> + </Modal> +) +SmallContentPrimary.args = { small: true } +SmallContentPrimary.storyName = 'Small: Content & Primary' + +export const SmallDestructivePrimary = args => ( + <Modal {...args}> + <ModalContent> + Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam + nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam + erat, sed diam voluptua. At vero eos et accusam et justo duo dolores + et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est + Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur + sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore + et dolore magna aliquyam erat, sed diam voluptua. At vero eos et + accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, + no sea takimata sanctus est Lorem ipsum dolor sit amet. + </ModalContent> + + <ModalActions> + <ButtonStrip end> + <Button onClick={say('Button secondary')} secondary> + Secondary action + </Button> + + <Button destructive onClick={say('Button primary')}> + Primary action + </Button> + </ButtonStrip> + </ModalActions> + </Modal> +) +SmallDestructivePrimary.args = { small: true } +SmallDestructivePrimary.storyName = 'Small: Destructive Primary' + +export const SmallClickableScreenCover = args => ( + <Modal {...args}> + <ModalTitle>This is a modal with clickable screen cover</ModalTitle> + + <ModalContent> + Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam + nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam + erat, sed diam voluptua. + </ModalContent> + + <ModalActions> + <ButtonStrip end> + <Button onClick={say('Button secondary')} secondary> + Secondary action + </Button> + + <Button primary onClick={say('Button primary')}> + Primary action + </Button> + </ButtonStrip> + </ModalActions> + </Modal> +) +SmallClickableScreenCover.args = { + small: true, + onClose: args => { + onClose(args) + say('Clickable screen cover')() + }, +} +SmallClickableScreenCover.storyName = 'Small: Clickable screen cover' + +export const TopScrollable = args => ( + <Modal {...args}> + <ModalTitle>This is a modal with scrollable content</ModalTitle> + + <ModalContent> + Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam + nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam + erat, sed diam voluptua. Lorem ipsum dolor sit amet, consetetur + sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore + et dolore magna aliquyam erat, sed diam voluptua. At vero eos et + accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, + no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum + dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod + tempor invidunt ut labore et dolore magna aliquyam erat, sed diam + voluptua. At vero eos et accusam et justo duo dolores et ea rebum. + Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum + dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing + elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore + magna aliquyam erat, sed diam voluptua. Lorem ipsum dolor sit amet, + consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt + ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero + eos et accusam et justo duo dolores et ea rebum. Stet clita kasd + gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. + Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam + nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam + erat, sed diam voluptua. At vero eos et accusam et justo duo dolores + et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est + Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur + sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore + et dolore magna aliquyam erat, sed diam voluptua. Lorem ipsum dolor + sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor + invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. + At vero eos et accusam et justo duo dolores et ea rebum. Stet clita + kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit + amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed + diam nonumy eirmod tempor invidunt ut labore et dolore magna + aliquyam erat, sed diam voluptua. At vero eos et accusam et justo + duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata + sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, + consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt + ut labore et dolore magna aliquyam erat, sed diam voluptua. Lorem + ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy + eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed + diam voluptua. At vero eos et accusam et justo duo dolores et ea + rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem + ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur + sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore + et dolore magna aliquyam erat, sed diam voluptua. At vero eos et + accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, + no sea takimata sanctus est Lorem ipsum dolor sit amet. + </ModalContent> + + <ModalActions> + <ButtonStrip end> + <Button onClick={say('Button secondary')} secondary> + Secondary action + </Button> + + <Button destructive onClick={say('Button primary')}> + Primary action + </Button> + </ButtonStrip> + </ModalActions> + </Modal> +) +TopScrollable.args = { ...SmallClickableScreenCover.args } +TopScrollable.storyName = 'Top: scrollable' + +export const MiddleScrollable = args => ( + <Modal {...args}> + <ModalTitle>This is a modal with scrollable content</ModalTitle> + + <ModalContent> + Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam + nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam + erat, sed diam voluptua. Lorem ipsum dolor sit amet, consetetur + sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore + et dolore magna aliquyam erat, sed diam voluptua. At vero eos et + accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, + no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum + dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod + tempor invidunt ut labore et dolore magna aliquyam erat, sed diam + voluptua. At vero eos et accusam et justo duo dolores et ea rebum. + Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum + dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing + elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore + magna aliquyam erat, sed diam voluptua. Lorem ipsum dolor sit amet, + consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt + ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero + eos et accusam et justo duo dolores et ea rebum. Stet clita kasd + gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. + Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam + nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam + erat, sed diam voluptua. At vero eos et accusam et justo duo dolores + et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est + Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur + sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore + et dolore magna aliquyam erat, sed diam voluptua. Lorem ipsum dolor + sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor + invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. + At vero eos et accusam et justo duo dolores et ea rebum. Stet clita + kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit + amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed + diam nonumy eirmod tempor invidunt ut labore et dolore magna + aliquyam erat, sed diam voluptua. At vero eos et accusam et justo + duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata + sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, + consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt + ut labore et dolore magna aliquyam erat, sed diam voluptua. Lorem + ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy + eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed + diam voluptua. At vero eos et accusam et justo duo dolores et ea + rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem + ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur + sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore + et dolore magna aliquyam erat, sed diam voluptua. At vero eos et + accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, + no sea takimata sanctus est Lorem ipsum dolor sit amet. + </ModalContent> + + <ModalActions> + <ButtonStrip end> + <Button onClick={say('Button secondary')} secondary> + Secondary action + </Button> + + <Button destructive onClick={say('Button primary')}> + Primary action + </Button> + </ButtonStrip> + </ModalActions> + </Modal> +) +MiddleScrollable.args = { ...TopScrollable.args, position: 'middle' } +MiddleScrollable.storyName = 'Middle: scrollable' + +export const BottomScrollable = args => ( + <Modal {...args}> + <ModalTitle>This is a modal with scrollable content</ModalTitle> + + <ModalContent> + Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam + nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam + erat, sed diam voluptua. Lorem ipsum dolor sit amet, consetetur + sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore + et dolore magna aliquyam erat, sed diam voluptua. At vero eos et + accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, + no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum + dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod + tempor invidunt ut labore et dolore magna aliquyam erat, sed diam + voluptua. At vero eos et accusam et justo duo dolores et ea rebum. + Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum + dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing + elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore + magna aliquyam erat, sed diam voluptua. Lorem ipsum dolor sit amet, + consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt + ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero + eos et accusam et justo duo dolores et ea rebum. Stet clita kasd + gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. + Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam + nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam + erat, sed diam voluptua. At vero eos et accusam et justo duo dolores + et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est + Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur + sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore + et dolore magna aliquyam erat, sed diam voluptua. Lorem ipsum dolor + sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor + invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. + At vero eos et accusam et justo duo dolores et ea rebum. Stet clita + kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit + amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed + diam nonumy eirmod tempor invidunt ut labore et dolore magna + aliquyam erat, sed diam voluptua. At vero eos et accusam et justo + duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata + sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, + consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt + ut labore et dolore magna aliquyam erat, sed diam voluptua. Lorem + ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy + eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed + diam voluptua. At vero eos et accusam et justo duo dolores et ea + rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem + ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur + sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore + et dolore magna aliquyam erat, sed diam voluptua. At vero eos et + accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, + no sea takimata sanctus est Lorem ipsum dolor sit amet. + </ModalContent> + + <ModalActions> + <ButtonStrip end> + <Button onClick={say('Button secondary')} secondary> + Secondary action + </Button> + + <Button destructive onClick={say('Button primary')}> + Primary action + </Button> + </ButtonStrip> + </ModalActions> + </Modal> +) +BottomScrollable.args = { ...TopScrollable.args, position: 'bottom' } +BottomScrollable.storyName = 'Bottom: scrollable' + +export const SmallLongTitle = args => ( + <Modal {...args}> + <ModalTitle> + This headline should break into multiple lines because it's way + too long for one! + </ModalTitle> + + <ModalContent> + Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam + nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam + erat, sed diam voluptua. At vero eos et accusam et justo duo dolores + et ea rebum. + </ModalContent> + + <ModalActions> + <ButtonStrip end> + <Button onClick={say('Button secondary')} secondary> + Secondary action + </Button> + + <Button onClick={say('Button primary')} primary> + Primary action + </Button> + </ButtonStrip> + </ModalActions> + </Modal> +) +SmallLongTitle.args = { small: true } +SmallLongTitle.storyName = 'Small: Long title' + +export const LargeWithSelectComponent = args => ( + <Modal {...args}> + <ModalTitle>Select opens on top of the Modal</ModalTitle> + + <ModalContent> + <SingleSelect> + <SingleSelectOption key="1" value="1" label="one" /> + <SingleSelectOption key="2" value="2" label="two" /> + <SingleSelectOption key="3" value="3" label="three" /> + <SingleSelectOption key="4" value="3" label="four" /> + <SingleSelectOption key="5" value="3" label="five" /> + <SingleSelectOption key="6" value="3" label="six" /> + <SingleSelectOption key="7" value="3" label="seven" /> + <SingleSelectOption key="8" value="3" label="eight" /> + <SingleSelectOption key="9" value="3" label="nine" /> + <SingleSelectOption key="10" value="3" label="ten" /> + </SingleSelect> + </ModalContent> + + <ModalActions> + <ButtonStrip end> + <Button onClick={say('Button secondary')} secondary> + Secondary action + </Button> + + <Button onClick={say('Button primary')} primary> + Primary action + </Button> + </ButtonStrip> + </ModalActions> + </Modal> +) +LargeWithSelectComponent.args = { large: true } +LargeWithSelectComponent.storyName = 'Large: with Select component' + +export const LargeModalWithMoreNestedModals = args => ( + <Modal {...args}> + <ModalTitle>Select opens on top of the Modal - Level 1</ModalTitle> + + <ModalContent> + <SingleSelect> + <SingleSelectOption key="1" value="1" label="one" /> + <SingleSelectOption key="2" value="2" label="two" /> + <SingleSelectOption key="3" value="3" label="three" /> + <SingleSelectOption key="4" value="3" label="four" /> + <SingleSelectOption key="5" value="3" label="five" /> + <SingleSelectOption key="6" value="3" label="six" /> + <SingleSelectOption key="7" value="3" label="seven" /> + <SingleSelectOption key="8" value="3" label="eight" /> + <SingleSelectOption key="9" value="3" label="nine" /> + <SingleSelectOption key="10" value="3" label="ten" /> + </SingleSelect> + <Modal large> + <ModalTitle> + Select opens on top of the Modal - Level 2 + </ModalTitle> + + <ModalContent> + <SingleSelect> + <SingleSelectOption key="1" value="1" label="one" /> + <SingleSelectOption key="2" value="2" label="two" /> + <SingleSelectOption key="3" value="3" label="three" /> + <SingleSelectOption key="4" value="3" label="four" /> + <SingleSelectOption key="5" value="3" label="five" /> + <SingleSelectOption key="6" value="3" label="six" /> + <SingleSelectOption key="7" value="3" label="seven" /> + <SingleSelectOption key="8" value="3" label="eight" /> + <SingleSelectOption key="9" value="3" label="nine" /> + <SingleSelectOption key="10" value="3" label="ten" /> + </SingleSelect> + <Modal large> + <ModalTitle> + Select opens on top of the Modal - Level 3 + </ModalTitle> + + <ModalContent> + <SingleSelect> + <SingleSelectOption + key="1" + value="1" + label="one" + /> + <SingleSelectOption + key="2" + value="2" + label="two" + /> + <SingleSelectOption + key="3" + value="3" + label="three" + /> + <SingleSelectOption + key="4" + value="3" + label="four" + /> + <SingleSelectOption + key="5" + value="3" + label="five" + /> + <SingleSelectOption + key="6" + value="3" + label="six" + /> + <SingleSelectOption + key="7" + value="3" + label="seven" + /> + <SingleSelectOption + key="8" + value="3" + label="eight" + /> + <SingleSelectOption + key="9" + value="3" + label="nine" + /> + <SingleSelectOption + key="10" + value="3" + label="ten" + /> + </SingleSelect> + </ModalContent> + + <ModalActions> + <ButtonStrip end> + <Button + onClick={say('Button secondary')} + secondary + > + Secondary action + </Button> + + <Button onClick={say('Button primary')} primary> + Primary action + </Button> + </ButtonStrip> + </ModalActions> + </Modal> + </ModalContent> + + <ModalActions> + <ButtonStrip end> + <Button onClick={say('Button secondary')} secondary> + Secondary action + </Button> + + <Button onClick={say('Button primary')} primary> + Primary action + </Button> + </ButtonStrip> + </ModalActions> + </Modal> + </ModalContent> + + <ModalActions> + <ButtonStrip end> + <Button onClick={say('Button secondary')} secondary> + Secondary action + </Button> + + <Button onClick={say('Button primary')} primary> + Primary action + </Button> + </ButtonStrip> + </ModalActions> + </Modal> +) +LargeModalWithMoreNestedModals.args = { large: true } +LargeModalWithMoreNestedModals.storyName = + 'Large: modal with more nested modals' diff --git a/packages/core/src/MultiSelect/MultiSelect.js b/packages/core/src/MultiSelect/MultiSelect.js index 300ddbe03e..a38fb2dcaa 100644 --- a/packages/core/src/MultiSelect/MultiSelect.js +++ b/packages/core/src/MultiSelect/MultiSelect.js @@ -1,5 +1,6 @@ import propTypes from '@dhis2/prop-types' import { spacers, sharedPropTypes } from '@dhis2/ui-constants' +import PropTypes from 'prop-types' import React from 'react' import { StatusIcon } from '../Icons/index.js' import { Loading } from '../Select/Loading.js' @@ -151,35 +152,39 @@ MultiSelect.defaultProps = { * @prop {string} [dataTest] */ MultiSelect.propTypes = { - children: propTypes.node, - className: propTypes.string, - clearText: propTypes.requiredIf(props => props.clearable, propTypes.string), - clearable: propTypes.bool, - dataTest: propTypes.string, - dense: propTypes.bool, - disabled: propTypes.bool, - empty: propTypes.node, + children: PropTypes.node, + className: PropTypes.string, + /** Required if `clearable` prop is `true` */ + clearText: propTypes.requiredIf(props => props.clearable, PropTypes.string), + /** Adds a 'clear' option to the menu */ + clearable: PropTypes.bool, + dataTest: PropTypes.string, + dense: PropTypes.bool, + disabled: PropTypes.bool, + empty: PropTypes.node, error: sharedPropTypes.statusPropType, - filterPlaceholder: propTypes.string, - filterable: propTypes.bool, - initialFocus: propTypes.bool, - inputMaxHeight: propTypes.string, - loading: propTypes.bool, - loadingText: propTypes.string, - maxHeight: propTypes.string, + filterPlaceholder: PropTypes.string, + /** Adds a 'filter' field to the menu */ + filterable: PropTypes.bool, + initialFocus: PropTypes.bool, + inputMaxHeight: PropTypes.string, + loading: PropTypes.bool, + loadingText: PropTypes.string, + maxHeight: PropTypes.string, + /** Required if `filterable` prop is `true` */ noMatchText: propTypes.requiredIf( props => props.filterable, - propTypes.string + PropTypes.string ), - placeholder: propTypes.string, - prefix: propTypes.string, - selected: propTypes.arrayOf(propTypes.string), - tabIndex: propTypes.string, + placeholder: PropTypes.string, + prefix: PropTypes.string, + selected: PropTypes.arrayOf(propTypes.string), + tabIndex: PropTypes.string, valid: sharedPropTypes.statusPropType, warning: sharedPropTypes.statusPropType, - onBlur: propTypes.func, - onChange: propTypes.func, - onFocus: propTypes.func, + onBlur: PropTypes.func, + onChange: PropTypes.func, + onFocus: PropTypes.func, } export { MultiSelect } diff --git a/packages/core/src/MultiSelect/MultiSelect.stories.js b/packages/core/src/MultiSelect/MultiSelect.stories.js index aad687c2d6..52fbde67d8 100644 --- a/packages/core/src/MultiSelect/MultiSelect.stories.js +++ b/packages/core/src/MultiSelect/MultiSelect.stories.js @@ -1,11 +1,30 @@ import propTypes from '@dhis2/prop-types' -import { storiesOf } from '@storybook/react' +import { sharedPropTypes } from '@dhis2/ui-constants' import React from 'react' import { MultiSelect, MultiSelectOption } from '../index.js' -window.onChange = window.Cypress && window.Cypress.cy.stub() -window.onFocus = window.Cypress && window.Cypress.cy.stub() -window.onBlur = window.Cypress && window.Cypress.cy.stub() +const description = ` +Multiple selection allows the user to select more than one option from the list. Checkboxes are used to highlight the possibility of making more than one selection. Selected options are displayed as chips inside the input. Make sure to communicate clearly to the user if there is a limit to the number of selectable elements. + +Use multiple selection wherever the user needs to make more than one selection. If the multiple selection is complex, requires a defined order or needs to made from different groups, consider using a transfer component instead. + +Read more about using \`Select\` components at [Design System: Select](https://github.com/dhis2/design-system/blob/master/molecules/select.md). + +\`\`\`js +import { MultiSelect, MultiSelectOption } from '@dhis2/ui' +\`\`\` + +_**Note:** Due to demo limitations on this page, only one representative example is rendered here. For more (props-interactive) examples, see individual stories in the 'Canvas' tab._ +` + +const eventHandler = handlerName => (payload, event) => { + console.log(`${handlerName} payload`, payload) + console.log(`${handlerName} event`, event) +} + +const onChange = eventHandler('onChange') +const onFocus = eventHandler('onFocus') +const onBlur = eventHandler('onBlur') const CustomMultiSelectOption = ({ label, onClick }) => ( <div onClick={e => onClick({}, e)}>{label}</div> @@ -16,98 +35,221 @@ CustomMultiSelectOption.propTypes = { onClick: propTypes.func, } -storiesOf('MultiSelect', module) - .add('With options', () => ( - <MultiSelect className="select"> - <MultiSelectOption value="1" label="option one" /> - <MultiSelectOption value="2" label="option two" /> - <MultiSelectOption value="3" label="option three" /> - </MultiSelect> - )) - .add('With options and onChange', () => ( - <MultiSelect - className="select" - onChange={val => console.log(val)} - selected={['1']} - > - <MultiSelectOption value="1" label="option one" /> - <MultiSelectOption value="2" label="option two" /> - <MultiSelectOption value="3" label="option three" /> - </MultiSelect> - )) - .add('With onFocus', () => ( - <MultiSelect className="select" onFocus={window.onFocus}> - <MultiSelectOption value="1" label="option one" /> - <MultiSelectOption value="2" label="option two" /> - <MultiSelectOption value="3" label="option three" /> - </MultiSelect> - )) - .add('With onBlur', () => ( - <MultiSelect className="select" onBlur={window.onBlur}> - <MultiSelectOption value="1" label="option one" /> - <MultiSelectOption value="2" label="option two" /> - <MultiSelectOption value="3" label="option three" /> - </MultiSelect> - )) - .add('With custom options and onChange', () => ( - <MultiSelect className="select" onChange={window.onChange}> - <CustomMultiSelectOption value="1" label="option one" /> - <CustomMultiSelectOption value="2" label="option two" /> - <CustomMultiSelectOption value="3" label="option three" /> - </MultiSelect> - )) - .add('With invalid options', () => ( - <MultiSelect className="select"> - <div>invalid one</div> - <MultiSelectOption value="1" label="option one" /> - <div>invalid two</div> - <MultiSelectOption value="2" label="option two" /> - <div>invalid three</div> - <MultiSelectOption value="3" label="option three" /> - {null} - {undefined} - {false} - </MultiSelect> - )) - .add('With invalid filterable options', () => ( - <MultiSelect filterable className="select" noMatchText="No match"> - <div>invalid one</div> - <MultiSelectOption value="1" label="option one" /> - <div>invalid two</div> - <MultiSelectOption value="2" label="option two" /> - <div>invalid three</div> - <MultiSelectOption value="3" label="option three" /> - </MultiSelect> - )) - .add('With initialFocus', () => ( - <MultiSelect className="select" initialFocus /> - )) - .add('Empty', () => <MultiSelect className="select" />) - .add('Empty with empty text', () => ( - <MultiSelect className="select" empty="Custom empty text" /> - )) - .add('Empty with empty component', () => ( - <MultiSelect - className="select" - empty={<div className="custom-empty">Custom empty component</div>} - /> - )) - .add('With options and loading', () => ( - <MultiSelect className="select" loading> - <MultiSelectOption value="1" label="option one" /> - <MultiSelectOption value="2" label="option two" /> - <MultiSelectOption value="3" label="option three" /> - </MultiSelect> - )) - .add('With options, loading and loading text', () => ( - <MultiSelect className="select" loadingText="Loading options" loading> - <MultiSelectOption value="1" label="option one" /> - <MultiSelectOption value="2" label="option two" /> - <MultiSelectOption value="3" label="option three" /> - </MultiSelect> - )) - .add('With more than ten options', () => ( - <MultiSelect className="select"> +const requiredIfArgType = { + table: { type: { summary: 'string' }, control: { type: 'string' } }, +} + +export default { + title: 'Forms/Multi Select/Multi Select', + component: MultiSelect, + parameters: { + docs: { + description: { component: description }, + // Disable stories in docs page by default, then use one representative example + disable: true, + // Use iframe to keep dropdown menu inside story for docs page + inlineStories: false, + iframeHeight: '300px', + }, + }, + // Use 'onChange' as a default arg, otherwise components throw an error when interacted with + // (Maybe this could be handled in the component - as a required prop) + args: { className: 'select', onChange }, + argTypes: { + valid: { ...sharedPropTypes.statusArgType }, + warning: { ...sharedPropTypes.statusArgType }, + error: { ...sharedPropTypes.statusArgType }, + clearText: { ...requiredIfArgType }, + noMatchText: { ...requiredIfArgType }, + }, +} + +const WithOptionsTemplate = args => ( + <MultiSelect {...args}> + <MultiSelectOption value="1" label="option one" /> + <MultiSelectOption value="2" label="option two" /> + <MultiSelectOption value="3" label="option three" /> + </MultiSelect> +) + +const EmptyTemplate = args => <MultiSelect {...args} /> + +export const WithOptionsAndOnChange = WithOptionsTemplate.bind({}) +WithOptionsAndOnChange.storyName = 'With options and onChange' + +export const WithOptionsAndASelection = WithOptionsTemplate.bind({}) +WithOptionsAndASelection.args = { selected: ['1'] } +// Enable this story as the primary for the docs page +WithOptionsAndASelection.parameters = { docs: { disable: false } } +WithOptionsAndASelection.storyName = 'With options and a selection' + +export const WithOnFocus = WithOptionsTemplate.bind({}) +WithOnFocus.args = { onFocus } +WithOnFocus.storyName = 'With onFocus' + +export const WithOnBlur = WithOptionsTemplate.bind({}) +WithOnBlur.args = { onBlur } +WithOnBlur.storyName = 'With onBlur' + +export const WithCustomOptionsAndOnChange = args => ( + <MultiSelect {...args}> + <CustomMultiSelectOption value="1" label="option one" /> + <CustomMultiSelectOption value="2" label="option two" /> + <CustomMultiSelectOption value="3" label="option three" /> + </MultiSelect> +) +WithCustomOptionsAndOnChange.storyName = 'With custom options and onChange' + +export const WithInvalidOptions = args => ( + <MultiSelect {...args}> + <div>invalid one</div> + <MultiSelectOption value="1" label="option one" /> + <div>invalid two</div> + <MultiSelectOption value="2" label="option two" /> + <div>invalid three</div> + <MultiSelectOption value="3" label="option three" /> + {null} + {undefined} + {false} + </MultiSelect> +) + +export const WithInvalidFilterableOptions = args => ( + <MultiSelect {...args}> + <div>invalid one</div> + <MultiSelectOption value="1" label="option one" /> + <div>invalid two</div> + <MultiSelectOption value="2" label="option two" /> + <div>invalid three</div> + <MultiSelectOption value="3" label="option three" /> + </MultiSelect> +) +WithInvalidFilterableOptions.args = { + filterable: true, + noMatchText: 'No match found', +} + +export const WithInitialFocus = EmptyTemplate.bind({}) +WithInitialFocus.args = { initialFocus: true } +WithInitialFocus.parameters = { docs: { disable: true } } +WithInitialFocus.storyName = 'With initialFocus' + +export const Empty = EmptyTemplate.bind({}) + +export const EmptyWithEmptyText = EmptyTemplate.bind({}) +EmptyWithEmptyText.args = { empty: 'Custom empty text' } + +export const EmptyWithEmptyComponent = EmptyTemplate.bind({}) +EmptyWithEmptyComponent.args = { + empty: <div className="custom-empty">Custom empty component</div>, +} + +export const WithOptionsAndLoading = WithOptionsTemplate.bind({}) +WithOptionsAndLoading.args = { loading: true } + +export const WithOptionsLoadingAndLoadingText = WithOptionsTemplate.bind({}) +WithOptionsLoadingAndLoadingText.args = { + ...WithOptionsAndLoading.args, + loadingText: 'Loading options', +} +WithOptionsLoadingAndLoadingText.storyName = + 'With options, loading and loading text' + +export const WithMoreThanTenOptions = args => ( + <MultiSelect {...args}> + <MultiSelectOption value="1" label="option one" /> + <MultiSelectOption value="2" label="option two" /> + <MultiSelectOption value="3" label="option three" /> + <MultiSelectOption value="4" label="option four" /> + <MultiSelectOption value="5" label="option five" /> + <MultiSelectOption value="6" label="option six" /> + <MultiSelectOption value="7" label="option seven" /> + <MultiSelectOption value="8" label="option eight" /> + <MultiSelectOption value="9" label="option nine" /> + <MultiSelectOption value="10" label="option ten" /> + <MultiSelectOption value="11" label="option eleven" /> + <MultiSelectOption value="12" label="option twelve" /> + </MultiSelect> +) + +export const WithMoreThanThreeOptionsAndA100PxMaxHeight = args => ( + <MultiSelect {...args}> + <MultiSelectOption value="1" label="option one" /> + <MultiSelectOption value="2" label="option two" /> + <MultiSelectOption value="3" label="option three" /> + <MultiSelectOption value="4" label="option four" /> + <MultiSelectOption value="5" label="option five" /> + <MultiSelectOption value="6" label="option six" /> + <MultiSelectOption value="7" label="option seven" /> + <MultiSelectOption value="8" label="option eight" /> + <MultiSelectOption value="9" label="option nine" /> + <MultiSelectOption value="10" label="option ten" /> + <MultiSelectOption value="11" label="option eleven" /> + <MultiSelectOption value="12" label="option twelve" /> + </MultiSelect> +) +WithMoreThanThreeOptionsAndA100PxMaxHeight.args = { maxHeight: '100px' } +WithMoreThanThreeOptionsAndA100PxMaxHeight.storyName = + 'With more than three options and a 100px max-height' + +export const WithOptionsAndDisabled = WithOptionsTemplate.bind({}) +WithOptionsAndDisabled.args = { disabled: true } +WithOptionsAndDisabled.storyName = 'With options and disabled' + +export const WithOptionsASelectionAndDisabled = WithOptionsTemplate.bind({}) +WithOptionsASelectionAndDisabled.args = { + ...WithOptionsAndDisabled.args, + ...WithOptionsAndASelection.args, +} +WithOptionsASelectionAndDisabled.storyName = + 'With options, a selection and disabled' + +export const WithPrefix = WithOptionsTemplate.bind({}) +WithPrefix.args = { prefix: 'Prefix text' } + +export const WithPrefixAndSelection = WithOptionsTemplate.bind({}) +WithPrefixAndSelection.args = { + ...WithPrefix.args, + ...WithOptionsAndASelection.args, +} + +export const WithPlaceholder = WithOptionsTemplate.bind({}) +WithPlaceholder.args = { placeholder: 'Placeholder text' } + +export const WithPlaceholderAndSelection = WithOptionsTemplate.bind({}) +WithPlaceholderAndSelection.args = { + ...WithPlaceholder.args, + ...WithOptionsAndASelection.args, +} + +export const WithDisabledOptionAndOnChange = args => ( + <MultiSelect {...args}> + <MultiSelectOption value="1" label="option one" /> + <MultiSelectOption value="2" label="option two" /> + <MultiSelectOption value="3" label="option three" /> + <MultiSelectOption value="4" label="disabled option" disabled /> + </MultiSelect> +) +WithDisabledOptionAndOnChange.storyName = 'With disabled option and onChange' + +export const WithOptionsAndMultipleSelections = WithOptionsTemplate.bind({}) +WithOptionsAndMultipleSelections.args = { selected: ['1', '2'] } + +export const WithClearButtonSelectionAndOnChange = WithOptionsTemplate.bind({}) +WithClearButtonSelectionAndOnChange.args = { + ...WithOptionsAndASelection.args, + clearable: true, + clearText: 'Clear', +} +WithClearButtonSelectionAndOnChange.storyName = + 'With clear button, selection and onChange' + +export const WithFilterField = WithOptionsTemplate.bind({}) +WithFilterField.args = { ...WithInvalidFilterableOptions.args } + +export const DefaultPosition = args => ( + <> + <MultiSelect {...args}> <MultiSelectOption value="1" label="option one" /> <MultiSelectOption value="2" label="option two" /> <MultiSelectOption value="3" label="option three" /> @@ -121,9 +263,17 @@ storiesOf('MultiSelect', module) <MultiSelectOption value="11" label="option eleven" /> <MultiSelectOption value="12" label="option twelve" /> </MultiSelect> - )) - .add('With more than three options and a 100px max-height', () => ( - <MultiSelect className="select" maxHeight="100px"> + <style jsx>{` + :global(#root) { + margin-bottom: 2000px; + } + `}</style> + </> +) + +export const FlippedPosition = args => ( + <> + <MultiSelect {...args}> <MultiSelectOption value="1" label="option one" /> <MultiSelectOption value="2" label="option two" /> <MultiSelectOption value="3" label="option three" /> @@ -137,186 +287,48 @@ storiesOf('MultiSelect', module) <MultiSelectOption value="11" label="option eleven" /> <MultiSelectOption value="12" label="option twelve" /> </MultiSelect> - )) - .add('With options, a selection and disabled', () => ( - <MultiSelect disabled className="select" selected={['1']}> - <MultiSelectOption value="1" label="option one" /> - <MultiSelectOption value="2" label="option two" /> - <MultiSelectOption value="3" label="option three" /> - </MultiSelect> - )) - .add('With options and disabled', () => ( - <MultiSelect disabled className="select"> - <MultiSelectOption value="1" label="option one" /> - <MultiSelectOption value="2" label="option two" /> - <MultiSelectOption value="3" label="option three" /> - </MultiSelect> - )) - .add('With prefix', () => ( - <MultiSelect className="select" prefix="Prefix text"> - <MultiSelectOption value="1" label="option one" /> - <MultiSelectOption value="2" label="option two" /> - <MultiSelectOption value="3" label="option three" /> - </MultiSelect> - )) - .add('With prefix and selection', () => ( - <MultiSelect className="select" prefix="Prefix text" selected={['1']}> - <MultiSelectOption value="1" label="option one" /> - <MultiSelectOption value="2" label="option two" /> - <MultiSelectOption value="3" label="option three" /> - </MultiSelect> - )) - .add('With placeholder', () => ( - <MultiSelect className="select" placeholder="Placeholder text"> - <MultiSelectOption value="1" label="option one" /> - <MultiSelectOption value="2" label="option two" /> - <MultiSelectOption value="3" label="option three" /> - </MultiSelect> - )) - .add('With placeholder and selection', () => ( - <MultiSelect - className="select" - selected={['1']} - placeholder="Placeholder text" - > - <MultiSelectOption value="1" label="option one" /> - <MultiSelectOption value="2" label="option two" /> - <MultiSelectOption value="3" label="option three" /> - </MultiSelect> - )) - .add('With disabled option and onChange', () => ( - <MultiSelect className="select" onChange={window.onChange}> - <MultiSelectOption value="1" label="option one" /> - <MultiSelectOption value="2" label="option two" /> - <MultiSelectOption value="3" label="option three" /> - <MultiSelectOption value="4" label="disabled option" disabled /> - </MultiSelect> - )) - .add('With options and a selection', () => ( - <MultiSelect className="select" selected={['1']}> - <MultiSelectOption value="1" label="option one" /> - <MultiSelectOption value="2" label="option two" /> - <MultiSelectOption value="3" label="option three" /> - </MultiSelect> - )) - .add('With options, a selection and onChange', () => ( - <MultiSelect - className="select" - selected={['1']} - onChange={window.onChange} - > - <MultiSelectOption value="1" label="option one" /> - <MultiSelectOption value="2" label="option two" /> - <MultiSelectOption value="3" label="option three" /> - </MultiSelect> - )) - .add('With options and multiple selections', () => ( - <MultiSelect className="select" selected={['1', '2']}> - <MultiSelectOption value="1" label="option one" /> - <MultiSelectOption value="2" label="option two" /> - <MultiSelectOption value="3" label="option three" /> - </MultiSelect> - )) - .add('With clear button, selection and onChange', () => ( - <MultiSelect - clearable - clearText="Clear" - className="select" - selected={['1']} - onChange={window.onChange} - > - <MultiSelectOption value="1" label="option one" /> - <MultiSelectOption value="2" label="option two" /> - <MultiSelectOption value="3" label="option three" /> - </MultiSelect> - )) - .add('With filter field', () => ( - <MultiSelect filterable noMatchText="No match found" className="select"> + <style jsx>{` + :global(html), + :global(body), + :global(#root) { + position: relative; + height: 500px; + max-height: 500px; + } + :global(#root) { + padding-top: 400px !important; + } + `}</style> + </> +) + +export const ShiftedIntoView = args => ( + <> + <MultiSelect {...args}> <MultiSelectOption value="1" label="option one" /> <MultiSelectOption value="2" label="option two" /> <MultiSelectOption value="3" label="option three" /> + <MultiSelectOption value="4" label="option four" /> + <MultiSelectOption value="5" label="option five" /> + <MultiSelectOption value="6" label="option six" /> + <MultiSelectOption value="7" label="option seven" /> + <MultiSelectOption value="8" label="option eight" /> + <MultiSelectOption value="9" label="option nine" /> + <MultiSelectOption value="10" label="option ten" /> + <MultiSelectOption value="11" label="option eleven" /> + <MultiSelectOption value="12" label="option twelve" /> </MultiSelect> - )) - .add('Default position', () => ( - <> - <MultiSelect className="select"> - <MultiSelectOption value="1" label="option one" /> - <MultiSelectOption value="2" label="option two" /> - <MultiSelectOption value="3" label="option three" /> - <MultiSelectOption value="4" label="option four" /> - <MultiSelectOption value="5" label="option five" /> - <MultiSelectOption value="6" label="option six" /> - <MultiSelectOption value="7" label="option seven" /> - <MultiSelectOption value="8" label="option eight" /> - <MultiSelectOption value="9" label="option nine" /> - <MultiSelectOption value="10" label="option ten" /> - <MultiSelectOption value="11" label="option eleven" /> - <MultiSelectOption value="12" label="option twelve" /> - </MultiSelect> - <style jsx>{` - :global(#root) { - margin-bottom: 2000px; - } - `}</style> - </> - )) - .add('Flipped position', () => ( - <> - <MultiSelect className="select"> - <MultiSelectOption value="1" label="option one" /> - <MultiSelectOption value="2" label="option two" /> - <MultiSelectOption value="3" label="option three" /> - <MultiSelectOption value="4" label="option four" /> - <MultiSelectOption value="5" label="option five" /> - <MultiSelectOption value="6" label="option six" /> - <MultiSelectOption value="7" label="option seven" /> - <MultiSelectOption value="8" label="option eight" /> - <MultiSelectOption value="9" label="option nine" /> - <MultiSelectOption value="10" label="option ten" /> - <MultiSelectOption value="11" label="option eleven" /> - <MultiSelectOption value="12" label="option twelve" /> - </MultiSelect> - <style jsx>{` - :global(html), - :global(body), - :global(#root) { - position: relative; - height: 500px; - max-height: 500px; - } - :global(#root) { - padding-top: 400px !important; - } - `}</style> - </> - )) - .add('Shifted into view', () => ( - <> - <MultiSelect className="select"> - <MultiSelectOption value="1" label="option one" /> - <MultiSelectOption value="2" label="option two" /> - <MultiSelectOption value="3" label="option three" /> - <MultiSelectOption value="4" label="option four" /> - <MultiSelectOption value="5" label="option five" /> - <MultiSelectOption value="6" label="option six" /> - <MultiSelectOption value="7" label="option seven" /> - <MultiSelectOption value="8" label="option eight" /> - <MultiSelectOption value="9" label="option nine" /> - <MultiSelectOption value="10" label="option ten" /> - <MultiSelectOption value="11" label="option eleven" /> - <MultiSelectOption value="12" label="option twelve" /> - </MultiSelect> - <style jsx>{` - :global(html), - :global(body), - :global(#root) { - position: relative; - height: 300px !important; - max-height: 300px; - } - :global(#root) { - padding-top: 130px !important; - } - `}</style> - </> - )) + <style jsx>{` + :global(html), + :global(body), + :global(#root) { + position: relative; + height: 300px !important; + max-height: 300px; + } + :global(#root) { + padding-top: 130px !important; + } + `}</style> + </> +) diff --git a/packages/core/src/Node/Node.js b/packages/core/src/Node/Node.js index f4a91d58e2..4e22f4e2d2 100644 --- a/packages/core/src/Node/Node.js +++ b/packages/core/src/Node/Node.js @@ -1,5 +1,5 @@ -import propTypes from '@dhis2/prop-types' import cx from 'classnames' +import PropTypes from 'prop-types' import React from 'react' import { Leaves } from './Leaves.js' import { Spacer } from './Spacer.js' @@ -78,12 +78,15 @@ Node.defaultProps = { * @prop {string} [dataTest] */ Node.propTypes = { - children: propTypes.node, - className: propTypes.string, - component: propTypes.element, - dataTest: propTypes.string, - icon: propTypes.node, - open: propTypes.bool, - onClose: propTypes.func, - onOpen: propTypes.func, + /** Content below this level of the hierarchy; children are revealed when this leaf is 'open' */ + children: PropTypes.node, + className: PropTypes.string, + /** Content/label for this leaf, for example a checkbox */ + component: PropTypes.element, + dataTest: PropTypes.string, + /** A custom icon to use instead of a toggle arrow */ + icon: PropTypes.node, + open: PropTypes.bool, + onClose: PropTypes.func, + onOpen: PropTypes.func, } diff --git a/packages/core/src/Node/Node.stories.js b/packages/core/src/Node/Node.stories.js index 851b43ff33..fe167485ab 100644 --- a/packages/core/src/Node/Node.stories.js +++ b/packages/core/src/Node/Node.stories.js @@ -1,9 +1,16 @@ -import { storiesOf } from '@storybook/react' import React from 'react' import { resolve } from 'styled-jsx/css' import { Checkbox, CircularLoader } from '../index.js' import { Node } from './Node.js' +const description = ` +A tool for creating hierarchical, multi-level selectors. Applied in the Organisation Unit Tree component, for example. + +\`\`\`js +import { Node } from '@dhis2/ui' +\`\`\` +` + const say = something => () => alert(something) window.onOpen = (payload, event) => { @@ -34,13 +41,34 @@ const LoadingSpinner = () => ( </React.Fragment> ) -storiesOf('Node', module) - .add('Custom icon', () => ( +export default { + title: 'Helpers/Node', + component: Node, + parameters: { docs: { description: { component: description } } }, +} + +export const CustomIcon = args => <Node {...args} /> +CustomIcon.args = { + icon: <LoadingSpinner />, + open: false, + onOpen: onOpen, + onClose: onClose, + component: ( + <Checkbox + label="Node label 1.1" + value="foobar" + name="l1.1" + onChange={say('checkbox 1.1 clicked')} + checked={false} + /> + ), +} +CustomIcon.storyName = 'Custom icon' + +export const MultipleRoots = args => ( + <div> <Node - icon={<LoadingSpinner />} - open={false} - onOpen={onOpen} - onClose={onClose} + {...args} component={ <Checkbox label="Node label 1.1" @@ -50,48 +78,44 @@ storiesOf('Node', module) checked={false} /> } - /> - )) - - .add('Multiple roots', () => ( - <div> - <Node - open={false} - onOpen={onOpen} - onClose={onClose} - component={ - <Checkbox - label="Node label 1.1" - value="foobar" - name="l1.1" - onChange={say('checkbox 1.1 clicked')} - checked={false} - /> - } - > - <span>Placeholder content</span> - </Node> + > + <span>Placeholder content</span> + </Node> - <Node - open={false} - onOpen={onOpen} - onClose={onClose} - component={ - <Checkbox - label="Node label 1.2" - value="foobar" - name="l1.2" - onChange={say('checkbox 1.2 clicked')} - checked={false} - /> - } - > - <span>Placeholder content</span> - </Node> - </div> - )) + <Node + {...args} + component={ + <Checkbox + label="Node label 1.2" + value="foobar" + name="l1.2" + onChange={say('checkbox 1.2 clicked')} + checked={false} + /> + } + > + <span>Placeholder content</span> + </Node> + </div> +) +MultipleRoots.args = { open: false, onOpen, onClose } +MultipleRoots.storyName = 'Multiple roots' - .add('2 Levels open', () => ( +export const _2LevelsOpen = () => ( + <Node + open={true} + onOpen={onOpen} + onClose={onClose} + component={ + <Checkbox + label="Node label" + value="foobar" + name="l1.1" + onChange={say('Check 1.1')} + checked={false} + /> + } + > <Node open={true} onOpen={onOpen} @@ -100,305 +124,290 @@ storiesOf('Node', module) <Checkbox label="Node label" value="foobar" - name="l1.1" - onChange={say('Check 1.1')} + name="l2.1" + onChange={say('Check 2.1')} checked={false} /> } > <Node - open={true} - onOpen={onOpen} - onClose={onClose} component={ <Checkbox label="Node label" + name="l3.1" value="foobar" - name="l2.1" - onChange={say('Check 2.1')} + onChange={say('Check 3.1')} checked={false} /> } - > - <Node - component={ - <Checkbox - label="Node label" - name="l3.1" - value="foobar" - onChange={say('Check 3.1')} - checked={false} - /> - } - /> - <Node - component={ - <Checkbox - label="Node label" - name="l3.2" - onChange={say('Check 3.2')} - value="foobar" - checked={false} - /> - } - /> - <Node - component={ - <Checkbox - label="Node label" - name="l3.3" - onChange={say('Check 3.3')} - checked={false} - value="foobar" - /> - } - /> - </Node> + /> <Node - open={false} - onOpen={onOpen} - onClose={say('close tree 2.2')} component={ <Checkbox label="Node label" - name="l3.1" - onChange={say('Check 2.2')} + name="l3.2" + onChange={say('Check 3.2')} value="foobar" checked={false} /> } - > - <Node - component={ - <Checkbox - label="Node label" - name="l3.4" - value="foobar" - onChange={say('Check 3.4')} - checked={false} - /> - } - /> - <Node - component={ - <Checkbox - label="Node label" - name="l3.5" - onChange={say('Check 3.5')} - value="foobar" - checked={false} - /> - } - /> - <Node - component={ - <Checkbox - label="Node label" - name="l3.6" - onChange={say('Check 3.6')} - value="foobar" - checked={false} - /> - } - /> - </Node> + /> <Node - open={false} - onOpen={onOpen} - onClose={onClose} component={ <Checkbox label="Node label" - name="l2.3" - value="foobar" - onChange={say('Check 2.3')} + name="l3.3" + onChange={say('Check 3.3')} checked={false} + value="foobar" /> } /> + </Node> + <Node + open={false} + onOpen={onOpen} + onClose={say('close tree 2.2')} + component={ + <Checkbox + label="Node label" + name="l3.1" + onChange={say('Check 2.2')} + value="foobar" + checked={false} + /> + } + > <Node - open={false} - onOpen={onOpen} - onClose={onClose} component={ <Checkbox label="Node label" - name="l2.4" - onChange={say('Check 2.4')} + name="l3.4" value="foobar" + onChange={say('Check 3.4')} checked={false} /> } /> <Node - open={false} - onOpen={onOpen} - onClose={onClose} component={ <Checkbox label="Node label" - name="l2.5" + name="l3.5" + onChange={say('Check 3.5')} value="foobar" - onChange={say('Check 2.5')} checked={false} /> } /> <Node - open={false} - onOpen={onOpen} - onClose={onClose} component={ <Checkbox label="Node label" - name="l2.5" + name="l3.6" + onChange={say('Check 3.6')} value="foobar" - onChange={say('Check 2.5')} checked={false} /> } - > - {false && 'Foo'} - </Node> + /> </Node> - )) + <Node + open={false} + onOpen={onOpen} + onClose={onClose} + component={ + <Checkbox + label="Node label" + name="l2.3" + value="foobar" + onChange={say('Check 2.3')} + checked={false} + /> + } + /> + <Node + open={false} + onOpen={onOpen} + onClose={onClose} + component={ + <Checkbox + label="Node label" + name="l2.4" + onChange={say('Check 2.4')} + value="foobar" + checked={false} + /> + } + /> + <Node + open={false} + onOpen={onOpen} + onClose={onClose} + component={ + <Checkbox + label="Node label" + name="l2.5" + value="foobar" + onChange={say('Check 2.5')} + checked={false} + /> + } + /> + <Node + open={false} + onOpen={onOpen} + onClose={onClose} + component={ + <Checkbox + label="Node label" + name="l2.5" + value="foobar" + onChange={say('Check 2.5')} + checked={false} + /> + } + > + {false && 'Foo'} + </Node> + </Node> +) +_2LevelsOpen.storyName = '2 Levels open' - .add('Text leaves', () => ( - <div> - <Node - open={true} - onOpen={onOpen} - onClose={onClose} - arrowTopOffset="10px" - component={ - <h2> - Lorem ipsum dolor sit amet, consetetur sadipscing elitr - </h2> - } - > - Sed diam nonumy eirmod tempor invidunt ut labore et dolore magna - aliquyam erat, sed diam voluptua. At vero eos et accusam et - justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea - takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum - dolor sit amet, consetetur sadipscing elitr, sed diam nonumy - eirmod tempor invidunt ut labore et dolore magna aliquyam erat, - sed diam voluptua. At vero eos et accusam et justo duo dolores - et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus - est Lorem ipsum dolor sit amet. - <div className="sub-tree sub-tree--open"> - <Node - open={true} - onOpen={onOpen} - onClose={onClose} - component={ - <h2> - Lorem ipsum dolor sit amet, consetetur - sadipscing elitr - </h2> - } - > - Sed diam nonumy eirmod tempor invidunt ut labore et - dolore magna aliquyam erat, sed diam voluptua. At vero - eos et accusam et justo duo dolores et ea rebum. Stet - clita kasd gubergren, no sea takimata sanctus est Lorem - ipsum dolor sit amet. Lorem ipsum dolor sit amet, - consetetur sadipscing elitr, sed diam nonumy eirmod - tempor invidunt ut labore et dolore magna aliquyam erat, - sed diam voluptua. At vero eos et accusam et justo duo - dolores et ea rebum. Stet clita kasd gubergren, no sea - takimata sanctus est Lorem ipsum dolor sit amet. - </Node> - </div> - <div className="sub-tree"> - <Node - open={false} - onOpen={onOpen} - onClose={onClose} - component={ - <h2> - Lorem ipsum dolor sit amet, consetetur - sadipscing elitr - </h2> - } - > - <span>Dummy content</span> - </Node> - </div> - <div className="sub-tree"> - <Node - open={false} - onOpen={onOpen} - onClose={onClose} - component={ - <h2> - Lorem ipsum dolor sit amet, consetetur - sadipscing elitr - </h2> - } - > - <span>Dummy content</span> - </Node> - </div> - <div className="sub-tree"> - <Node - open={false} - onOpen={onOpen} - onClose={onClose} - component={ - <h2> - Lorem ipsum dolor sit amet, consetetur - sadipscing elitr - </h2> - } - > - <span>Dummy content</span> - </Node> - </div> - <div className="sub-tree"> - <Node - open={false} - onOpen={onOpen} - onClose={onClose} - component={ - <h2> - Lorem ipsum dolor sit amet, consetetur - sadipscing elitr - </h2> - } - > - <span>Dummy content</span> - </Node> - </div> - </Node> +export const TextLeaves = () => ( + <div> + <Node + open={true} + onOpen={onOpen} + onClose={onClose} + arrowTopOffset="10px" + component={ + <h2>Lorem ipsum dolor sit amet, consetetur sadipscing elitr</h2> + } + > + Sed diam nonumy eirmod tempor invidunt ut labore et dolore magna + aliquyam erat, sed diam voluptua. At vero eos et accusam et justo + duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata + sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, + consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt + ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero + eos et accusam et justo duo dolores et ea rebum. Stet clita kasd + gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. + <div className="sub-tree sub-tree--open"> + <Node + open={true} + onOpen={onOpen} + onClose={onClose} + component={ + <h2> + Lorem ipsum dolor sit amet, consetetur sadipscing + elitr + </h2> + } + > + Sed diam nonumy eirmod tempor invidunt ut labore et dolore + magna aliquyam erat, sed diam voluptua. At vero eos et + accusam et justo duo dolores et ea rebum. Stet clita kasd + gubergren, no sea takimata sanctus est Lorem ipsum dolor sit + amet. Lorem ipsum dolor sit amet, consetetur sadipscing + elitr, sed diam nonumy eirmod tempor invidunt ut labore et + dolore magna aliquyam erat, sed diam voluptua. At vero eos + et accusam et justo duo dolores et ea rebum. Stet clita kasd + gubergren, no sea takimata sanctus est Lorem ipsum dolor sit + amet. + </Node> + </div> + <div className="sub-tree"> + <Node + open={false} + onOpen={onOpen} + onClose={onClose} + component={ + <h2> + Lorem ipsum dolor sit amet, consetetur sadipscing + elitr + </h2> + } + > + <span>Dummy content</span> + </Node> + </div> + <div className="sub-tree"> + <Node + open={false} + onOpen={onOpen} + onClose={onClose} + component={ + <h2> + Lorem ipsum dolor sit amet, consetetur sadipscing + elitr + </h2> + } + > + <span>Dummy content</span> + </Node> + </div> + <div className="sub-tree"> + <Node + open={false} + onOpen={onOpen} + onClose={onClose} + component={ + <h2> + Lorem ipsum dolor sit amet, consetetur sadipscing + elitr + </h2> + } + > + <span>Dummy content</span> + </Node> + </div> + <div className="sub-tree"> + <Node + open={false} + onOpen={onOpen} + onClose={onClose} + component={ + <h2> + Lorem ipsum dolor sit amet, consetetur sadipscing + elitr + </h2> + } + > + <span>Dummy content</span> + </Node> + </div> + </Node> - <style jsx>{` - div { - font-size: 16px; - line-height: 24px; - } + <style jsx>{` + div { + font-size: 16px; + line-height: 24px; + } - h2 { - font-size: 24px; - line-height: 32px; - margin: 0 0 10px; - } + h2 { + font-size: 24px; + line-height: 32px; + margin: 0 0 10px; + } - .sub-tree { - margin-top: 10px; - } + .sub-tree { + margin-top: 10px; + } - .sub-tree--open { - margin-bottom: 20px; - } + .sub-tree--open { + margin-bottom: 20px; + } - div :global(.tree__arrow > span) { - padding-top: 4px; - } + div :global(.tree__arrow > span) { + padding-top: 4px; + } - div :global(.tree__arrow.open:after) { - top: 17px; - height: calc(100% - 28px); - } - `}</style> - </div> - )) + div :global(.tree__arrow.open:after) { + top: 17px; + height: calc(100% - 28px); + } + `}</style> + </div> +) +TextLeaves.storyName = 'Text leaves' diff --git a/packages/core/src/NoticeBox/NoticeBox.js b/packages/core/src/NoticeBox/NoticeBox.js index a4ae21de11..fcd7acdd85 100644 --- a/packages/core/src/NoticeBox/NoticeBox.js +++ b/packages/core/src/NoticeBox/NoticeBox.js @@ -1,6 +1,7 @@ import propTypes from '@dhis2/prop-types' import { spacers, colors } from '@dhis2/ui-constants' import cx from 'classnames' +import PropTypes from 'prop-types' import React from 'react' import { NoticeBoxIcon } from './NoticeBoxIcon.js' import { NoticeBoxMessage } from './NoticeBoxMessage.js' @@ -78,10 +79,12 @@ NoticeBox.defaultProps = { * @prop {boolean} [error] */ NoticeBox.propTypes = { - children: propTypes.node, - className: propTypes.string, - dataTest: propTypes.string, - error: propTypes.mutuallyExclusive(['error', 'warning'], propTypes.bool), - title: propTypes.string, - warning: propTypes.mutuallyExclusive(['error', 'warning'], propTypes.bool), + children: PropTypes.node, + className: PropTypes.string, + dataTest: PropTypes.string, + /** Applies 'error' message styles. Mutually exclusive with the `warning` prop */ + error: propTypes.mutuallyExclusive(['error', 'warning'], PropTypes.bool), + title: PropTypes.string, + /** Applies 'warning' message styles. Mutually exclusive with the `error` prop */ + warning: propTypes.mutuallyExclusive(['error', 'warning'], PropTypes.bool), } diff --git a/packages/core/src/NoticeBox/NoticeBox.stories.js b/packages/core/src/NoticeBox/NoticeBox.stories.js index 508a389c32..403e40d15b 100644 --- a/packages/core/src/NoticeBox/NoticeBox.stories.js +++ b/packages/core/src/NoticeBox/NoticeBox.stories.js @@ -1,24 +1,61 @@ +import { sharedPropTypes } from '@dhis2/ui-constants' import React from 'react' import { NoticeBox } from './NoticeBox.js' +const subtitle = `Highlights useful information that is directly relevant to the page the user is viewing` + +const description = ` +Use a notice box wherever there is important, temporary information about a page or situation that the user needs to be aware of. + +Notice boxes are different from alert bars in several ways. Notice boxes cannot be dismissed, so they will always display until the situation is resolved. Notice boxes are for highlighting static information or information that is ongoing. Alert bars are suited to alerting a user about something that has just happened. + +Another way to decide which component to use: + +- a notice box will usually be displayed when a page loads, before a user takes action +- an alert bar will usually display in response to an action/event + +Notice boxes cannot be dismissed, so it is important to provide guidance on how to fix the problem/condition that is causing the notice box to display. + +Do not use a notice box to display permanent information. If there is information that always will be displayed on a page it should be designed as part of the page itself. Notice boxes are for temporary information. + +Learn more about Notice Boxes at [Design System: Notice Box](https://github.com/dhis2/design-system/blob/master/molecules/notice-box.md). + +\`\`\`js +import { NoticeBox } from '@dhis2/ui' +\`\`\` +` + export default { - title: 'NoticeBox', + title: 'Data Display/Notice Box', component: NoticeBox, + parameters: { + componentSubtitle: subtitle, + docs: { description: { component: description } }, + }, + argTypes: { + error: { ...sharedPropTypes.statusArgType }, + warning: { ...sharedPropTypes.statusArgType }, + }, } -export const Default = () => ( - <NoticeBox title="Your database was updated in the last 24 hours"> +export const Default = args => ( + <NoticeBox {...args}> Data shown in this dashboard may take a few hours to update. Scheduled dashboard updates can be managed in the scheduler app. </NoticeBox> ) +Default.args = { title: 'Your database was updated in the last 24 hours' } -export const Warning = () => ( - <NoticeBox title="This program has no assigned Organisation Units" warning> +export const Warning = args => ( + <NoticeBox {...args}> No one will be able to access this program. Add some Organisation Units to the access list. </NoticeBox> ) +Warning.args = { + warning: true, + title: 'This program has no assigned Organisation Units', +} export const Error = () => ( <NoticeBox title="Access rules for this instance are set to 'Public'" error> @@ -26,6 +63,10 @@ export const Error = () => ( immediately. </NoticeBox> ) +Error.args = { + error: true, + title: "Access rules for this instance are set to 'Public'", +} const text = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.' + @@ -33,8 +74,7 @@ const text = 'lacus pretium convallis vitae sit amet purus. Nam ut' + 'libero rhoncus, consectetur sem a, sollicitudin lectus.' -export const WithALongTitle = () => ( - <NoticeBox title={text} error> - The title text will wrap - </NoticeBox> +export const WithALongTitle = args => ( + <NoticeBox {...args}>The title text will wrap</NoticeBox> ) +WithALongTitle.args = { error: true, title: text } diff --git a/packages/core/src/Popover/Popover.js b/packages/core/src/Popover/Popover.js index f5635fcb87..557d6ad785 100644 --- a/packages/core/src/Popover/Popover.js +++ b/packages/core/src/Popover/Popover.js @@ -1,5 +1,5 @@ import { colors, elevations, sharedPropTypes } from '@dhis2/ui-constants' -import propTypes from 'prop-types' +import PropTypes from 'prop-types' import React, { useState, useMemo } from 'react' import { usePopper } from 'react-popper' import { Layer } from '../Layer/Layer.js' @@ -105,17 +105,20 @@ Popover.defaultProps = { * @prop {function} [onClickOutside] */ Popover.propTypes = { - children: propTypes.node.isRequired, - arrow: propTypes.bool, - className: propTypes.string, - dataTest: propTypes.string, - elevation: propTypes.string, - maxWidth: propTypes.number, - observePopperResize: propTypes.bool, - observeReferenceResize: propTypes.bool, + children: PropTypes.node.isRequired, + /** Show or hide the arrow */ + arrow: PropTypes.bool, + className: PropTypes.string, + dataTest: PropTypes.string, + /** Box-shadow to create appearance of elevation. Use `elevations` constants from the UI library. */ + elevation: PropTypes.string, + maxWidth: PropTypes.number, + observePopperResize: PropTypes.bool, + observeReferenceResize: PropTypes.bool, placement: sharedPropTypes.popperPlacementPropType, + /** A React ref that refers to the element the Popover should position against */ reference: sharedPropTypes.popperReferencePropType, - onClickOutside: propTypes.func, + onClickOutside: PropTypes.func, } export { Popover } diff --git a/packages/core/src/Popover/Popover.stories.js b/packages/core/src/Popover/Popover.stories.js index a7ece28255..3503124182 100644 --- a/packages/core/src/Popover/Popover.stories.js +++ b/packages/core/src/Popover/Popover.stories.js @@ -1,8 +1,48 @@ -import { elevations } from '@dhis2/ui-constants' +import { elevations, sharedPropTypes } from '@dhis2/ui-constants' import React, { useRef } from 'react' import { Popover } from './Popover.js' -export default { title: 'Popover', component: Popover } +const subtitle = `Useful to give a user more information or possible actions without disrupting a page or flow` + +const description = ` +Popovers are similar to tooltips: they are for displaying extra information, but popovers are intended for richer information and actions. + +Popovers are triggered by hovering or tapping on an element. Clicking on a element keeps the popover open until the user clicks or interacts elsewhere on the page. + +Popovers can contain information in the form of rich markup, as well as actions. Critical actions, or the only action on a page, should not be hidden inside a popover. + +Before using a popover, consider that some users may never see the information contained within. If that is a problem, display the information right on the page instead. Do not use a popover for content that is essential to the user experience or application. + +See more about Popovers at [Design System: Popover](https://github.com/dhis2/design-system/blob/master/molecules/popover.md). + +\`\`\`js +import { Popover } from '@dhis2/ui' +\`\`\` + +_**Note**: Due to the full-page interaction of this component, only one representative example in an iframe sandbox is shown here. See more (interactive) examples in the 'Canvas' tab._ +` + +export default { + title: 'Data Display/Popover', + component: Popover, + parameters: { + componentSubtitle: subtitle, + docs: { + description: { component: description }, + // Contain the popovers in iframes in the docs page + inlineStories: false, + iframeHeight: '500px', + // Disable stories in docs page by default to use one representative example + disable: true, + }, + }, + // Handles weird treatment of non-literal args (`elevation: elevations.e200`) + args: { ...Popover.defaultProps }, + argTypes: { + reference: { ...sharedPropTypes.popperReferenceArgType }, + placement: { ...sharedPropTypes.popperPlacementArgType }, + }, +} const boxStyle = { display: 'flex', @@ -20,7 +60,7 @@ const referenceElementStyle = { padding: 6, } -export const Default = () => { +const Template = args => { const ref = useRef(null) return ( @@ -28,7 +68,7 @@ export const Default = () => { <div style={referenceElementStyle} ref={ref}> Reference element </div> - <Popover reference={ref}> + <Popover {...args} reference={ref}> <div> Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna @@ -46,65 +86,26 @@ export const Default = () => { ) } -export const NoArrow = () => { - const ref = useRef(null) - - return ( - <div style={boxStyle}> - <div style={referenceElementStyle} ref={ref}> - Reference element - </div> - <Popover reference={ref} arrow={false}> - <div> - Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed - do eiusmod tempor incididunt ut labore et dolore magna - aliqua. Consectetur purus ut faucibus pulvinar elementum. - Dignissim diam quis enim lobortis scelerisque fermentum dui - faucibus. Rhoncus aenean vel elit scelerisque mauris - pellentesque. Non sodales neque sodales ut etiam sit amet. - Volutpat sed cras ornare arcu dui. Quis imperdiet massa - tincidunt nunc pulvinar sapien et ligula. Convallis posuere - morbi leo urna molestie at. Mauris cursus mattis molestie a - iaculis at. - </div> - </Popover> - </div> - ) +export const Default = Template.bind({}) +Default.parameters = { + docs: { + // Enable this story for the docs page + disable: false, + // Show source, including 'ref' hooks + source: { type: 'code' }, + }, } -export const Customization = () => { - const ref = useRef(null) +export const NoArrow = Template.bind({}) +NoArrow.args = { arrow: false } - return ( - <div style={boxStyle}> - <div style={referenceElementStyle} ref={ref}> - Reference element - </div> - <Popover - reference={ref} - arrow={true} - className="custom-classname" - dataTest="custom-data-test-id" - elevation={elevations.e200} - maxWidth={400} - placement="bottom-start" - onClickOutside={() => { - console.log('backdrop was clicked....') - }} - > - <div> - Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed - do eiusmod tempor incididunt ut labore et dolore magna - aliqua. Consectetur purus ut faucibus pulvinar elementum. - Dignissim diam quis enim lobortis scelerisque fermentum dui - faucibus. Rhoncus aenean vel elit scelerisque mauris - pellentesque. Non sodales neque sodales ut etiam sit amet. - Volutpat sed cras ornare arcu dui. Quis imperdiet massa - tincidunt nunc pulvinar sapien et ligula. Convallis posuere - morbi leo urna molestie at. Mauris cursus mattis molestie a - iaculis at. - </div> - </Popover> - </div> - ) +export const Customization = Template.bind({}) +Customization.args = { + arrow: true, + className: 'custom-classname', + dataTest: 'custom-data-test-id', + elevation: elevations.e200, + maxWidth: 400, + placement: 'bottom-start', + onClickOutside: () => console.log('backdrop was clicked...'), } diff --git a/packages/core/src/Popper/Popper.js b/packages/core/src/Popper/Popper.js index a61aea4aea..c4d8face59 100644 --- a/packages/core/src/Popper/Popper.js +++ b/packages/core/src/Popper/Popper.js @@ -1,5 +1,5 @@ -import propTypes from '@dhis2/prop-types' import { sharedPropTypes } from '@dhis2/ui-constants' +import PropTypes from 'prop-types' import React, { useState, useMemo } from 'react' import { usePopper } from 'react-popper' import { getReferenceElement } from './getReferenceElement.js' @@ -91,21 +91,29 @@ Popper.defaultProps = { */ // Prop names follow the names here: https://popper.js.org/docs/v2/constructors/ Popper.propTypes = { - children: propTypes.node.isRequired, - className: propTypes.string, - dataTest: propTypes.string, - modifiers: propTypes.arrayOf( - propTypes.shape({ - name: propTypes.string, - options: propTypes.object, + /** Content inside the Popper */ + children: PropTypes.node.isRequired, + className: PropTypes.string, + dataTest: PropTypes.string, + /** A property of the `createPopper` options. See [popper docs](https://popper.js.org/docs/v2/constructors/) */ + modifiers: PropTypes.arrayOf( + PropTypes.shape({ + name: PropTypes.string, + options: PropTypes.object, }) ), - observePopperResize: propTypes.bool, - observeReferenceResize: propTypes.bool, + /** Makes the Popper update position when the **Popper content** changes size */ + observePopperResize: PropTypes.bool, + /** Makes the Popper update position when the **reference element** changes size */ + observeReferenceResize: PropTypes.bool, + /** A property of the `createPopper` options. See [popper docs](https://popper.js.org/docs/v2/constructors/) */ placement: sharedPropTypes.popperPlacementPropType, + /** A React ref, DOM node, or [virtual element](https://popper.js.org/docs/v2/virtual-elements/) for the popper to position itself against */ reference: sharedPropTypes.popperReferencePropType, - strategy: propTypes.oneOf(['absolute', 'fixed']), // defaults to 'absolute' - onFirstUpdate: propTypes.func, + /** A property of the `createPopper` options. See [popper docs](https://popper.js.org/docs/v2/constructors/) */ + strategy: PropTypes.oneOf(['absolute', 'fixed']), // defaults to 'absolute' + /** A property of the `createPopper` options. See [popper docs](https://popper.js.org/docs/v2/constructors/) */ + onFirstUpdate: PropTypes.func, } export { Popper } diff --git a/packages/core/src/Popper/Popper.stories.js b/packages/core/src/Popper/Popper.stories.js index cebff0968c..d447bdea36 100644 --- a/packages/core/src/Popper/Popper.stories.js +++ b/packages/core/src/Popper/Popper.stories.js @@ -1,13 +1,27 @@ -import propTypes from '@dhis2/prop-types' -import React, { Component, createRef } from 'react' +import { sharedPropTypes } from '@dhis2/ui-constants' +import React, { useRef } from 'react' import { Popper } from './Popper.js' +const description = ` +A tool for adding additional information or content outside of the document flow, used for example in the Tooltip or Popover components. + +Since it's built using [Popper.js](https://popper.js.org/docs/v2/) and [react-popper](https://popper.js.org/react-popper/), some of that functionality can be accessed through the props of this component, like modifiers. + +\`\`\`js +import { Popper } from '@dhis2/ui' +\`\`\` + +_**Note**: Some of the stories may not look right on this page. View those examples in the 'Canvas' tab instead._ +` + export default { - title: 'Popper', + title: 'Helpers/Popper', component: Popper, - decorators: [ - storyFN => <BoxWithCenteredReferenceElement renderChildren={storyFN} />, - ], + parameters: { docs: { description: { component: description } } }, + argTypes: { + placement: { ...sharedPropTypes.popperPlacementArgType }, + reference: { ...sharedPropTypes.popperReferenceArgType }, + }, } const boxStyle = { @@ -16,7 +30,6 @@ const boxStyle = { justifyContent: 'center', width: 400, height: 400, - marginBottom: 1000, backgroundColor: 'aliceblue', } @@ -36,106 +49,77 @@ const popperStyle = { padding: 6, } -class BoxWithCenteredReferenceElement extends Component { - ref = createRef() - - render() { - const { renderChildren } = this.props - return ( - <div className="box" style={boxStyle}> - <div - className="reference-element" - style={referenceElementStyle} - ref={this.ref} - > - Reference element - </div> - {renderChildren({ referenceElement: this.ref })} +const Template = args => { + const ref = useRef(null) + + return ( + <div className="box" style={boxStyle}> + <div + className="reference-element" + style={referenceElementStyle} + ref={ref} + > + Reference Element </div> - ) - } -} -BoxWithCenteredReferenceElement.propTypes = { - renderChildren: propTypes.func, + <Popper {...args} reference={ref}> + <div style={popperStyle}>{args.placement}</div> + </Popper> + </div> + ) } -/* eslint-disable react/prop-types */ -export const Top = ({ referenceElement }) => ( - <Popper reference={referenceElement} placement="top"> - <div style={popperStyle}>Top</div> - </Popper> -) -export const TopStart = ({ referenceElement }) => ( - <Popper reference={referenceElement} placement="top-start"> - <div style={popperStyle}>Top start</div> - </Popper> -) -export const TopEnd = ({ referenceElement }) => ( - <Popper reference={referenceElement} placement="top-end"> - <div style={popperStyle}>Top end</div> - </Popper> -) -export const Bottom = ({ referenceElement }) => ( - <Popper reference={referenceElement} placement="bottom"> - <div style={popperStyle}>Bottom</div> - </Popper> -) -export const BottomStart = ({ referenceElement }) => ( - <Popper reference={referenceElement} placement="bottom-start"> - <div style={popperStyle}>Bottom start</div> - </Popper> -) -export const BottomEnd = ({ referenceElement }) => ( - <Popper reference={referenceElement} placement="bottom-end"> - <div style={popperStyle}>Bottom end</div> - </Popper> -) -export const Right = ({ referenceElement }) => ( - <Popper reference={referenceElement} placement="right"> - <div style={popperStyle}>Right</div> - </Popper> -) -export const RightStart = ({ referenceElement }) => ( - <Popper reference={referenceElement} placement="right-start"> - <div style={popperStyle}>Right start</div> - </Popper> -) -export const RightEnd = ({ referenceElement }) => ( - <Popper reference={referenceElement} placement="right-end"> - <div style={popperStyle}>Right end</div> - </Popper> -) -export const Left = ({ referenceElement }) => ( - <Popper reference={referenceElement} placement="left"> - <div style={popperStyle}>Left</div> - </Popper> -) -export const LeftStart = ({ referenceElement }) => ( - <Popper reference={referenceElement} placement="left-start"> - <div style={popperStyle}>Left start</div> - </Popper> -) -export const LeftEnd = ({ referenceElement }) => ( - <Popper reference={referenceElement} placement="left-end"> - <div style={popperStyle}>Left end</div> - </Popper> -) -export const ElementRef = () => { +export const Top = Template.bind({}) +Top.args = { placement: 'top' } + +export const TopStart = Template.bind({}) +TopStart.args = { placement: 'top-start' } + +export const TopEnd = Template.bind({}) +TopEnd.args = { placement: 'top-end' } + +export const Bottom = Template.bind({}) +Bottom.args = { placement: 'bottom' } + +export const BottomStart = Template.bind({}) +BottomStart.args = { placement: 'bottom-start' } + +export const BottomEnd = Template.bind({}) +BottomEnd.args = { placement: 'bottom-end' } + +export const Right = Template.bind({}) +Right.args = { placement: 'right' } + +export const RightStart = Template.bind({}) +RightStart.args = { placement: 'right-start' } + +export const RightEnd = Template.bind({}) +RightEnd.args = { placement: 'right-end' } + +export const Left = Template.bind({}) +Left.args = { placement: 'left' } + +export const LeftStart = Template.bind({}) +LeftStart.args = { placement: 'left-start' } + +export const LeftEnd = Template.bind({}) +LeftEnd.args = { placement: 'left-end' } + +export const ElementRef = args => { const anchor = document.createElement('div') document.body.appendChild(anchor) return ( - <Popper reference={anchor} placement="left-end"> - <div style={popperStyle}>Left end</div> - <style jsx>{` - :global(.reference-element) { - display: none !important; - } - `}</style> - </Popper> + <div className="box" style={{ ...boxStyle, marginBottom: '500px' }}> + <Popper {...args} reference={anchor}> + <div style={popperStyle}>{args.placement}</div> + </Popper> + </div> ) } -export const VirtualElementRef = () => { +ElementRef.args = { placement: 'left-end' } +ElementRef.parameters = { docs: { source: { type: 'code' } } } + +export const VirtualElementRef = args => { const virtualElement = { getBoundingClientRect: () => ({ width: 0, @@ -150,13 +134,12 @@ export const VirtualElementRef = () => { } return ( - <Popper reference={virtualElement} placement="left-end"> - <div style={popperStyle}>Left end</div> - <style jsx>{` - :global(.reference-element) { - display: none !important; - } - `}</style> - </Popper> + <div className="box" style={{ ...boxStyle, marginBottom: '500px' }}> + <Popper {...args} reference={virtualElement}> + <div style={popperStyle}>{args.placement}</div> + </Popper> + </div> ) } +VirtualElementRef.args = { placement: 'left-end' } +VirtualElementRef.parameters = { docs: { source: { type: 'code' } } } diff --git a/packages/core/src/Radio/Radio.js b/packages/core/src/Radio/Radio.js index 32096a2b5f..43a9807e05 100644 --- a/packages/core/src/Radio/Radio.js +++ b/packages/core/src/Radio/Radio.js @@ -1,6 +1,6 @@ -import propTypes from '@dhis2/prop-types' import { colors, theme, sharedPropTypes } from '@dhis2/ui-constants' import cx from 'classnames' +import PropTypes from 'prop-types' import React, { Component, createRef } from 'react' import { RadioRegular, RadioDense } from '../Icons/index.js' ;('') // TODO: https://github.com/jsdoc/jsdoc/issues/1718 @@ -189,22 +189,30 @@ Radio.defaultProps = { * @prop {string} [dataTest] */ Radio.propTypes = { - checked: propTypes.bool, - className: propTypes.string, - dataTest: propTypes.string, - dense: propTypes.bool, - disabled: propTypes.bool, + checked: PropTypes.bool, + className: PropTypes.string, + dataTest: PropTypes.string, + dense: PropTypes.bool, + disabled: PropTypes.bool, + /** Adds 'error' styling for feedback. Mutually exclusive with `valid` and `warning` props */ error: sharedPropTypes.statusPropType, - initialFocus: propTypes.bool, - label: propTypes.node, - name: propTypes.string, - tabIndex: propTypes.string, + initialFocus: PropTypes.bool, + label: PropTypes.node, + /** Name associated with this element. Passed in object to event handler functions */ + name: PropTypes.string, + tabIndex: PropTypes.string, + /** Adds 'valid' styling for feedback. Mutually exclusive with `error` and `warning` props */ valid: sharedPropTypes.statusPropType, - value: propTypes.string, + /** Value associated with this element. Passed in object to event handler functions */ + value: PropTypes.string, + /** Adds 'warning' styling for feedback. Mutually exclusive with `valid` and `error` props */ warning: sharedPropTypes.statusPropType, - onBlur: propTypes.func, - onChange: propTypes.func, - onFocus: propTypes.func, + /** Called with the signature `({ name: string, value: string, checked: bool }, event)` */ + onBlur: PropTypes.func, + /** Called with the signature `({ name: string, value: string, checked: bool }, event)` */ + onChange: PropTypes.func, + /** Called with the signature `({ name: string, value: string, checked: bool }, event)` */ + onFocus: PropTypes.func, } export { Radio } diff --git a/packages/core/src/Radio/Radio.stories.js b/packages/core/src/Radio/Radio.stories.js index 8ff5fd167b..bcf1abd811 100644 --- a/packages/core/src/Radio/Radio.stories.js +++ b/packages/core/src/Radio/Radio.stories.js @@ -1,7 +1,29 @@ -import { storiesOf } from '@storybook/react' +import { sharedPropTypes } from '@dhis2/ui-constants' import React from 'react' import { Radio } from './Radio.js' +const subtitle = `A control that allows a user to select a single option from a choice of several` + +const description = ` +Radio buttons are used where a user has the choice of several options but must select only one. Radio buttons should be used where the user has to make a choice, there is no 'off' or 'none' state unless explicitly defined. Radio buttons should be used when there are 5 or less options available. With more than five, a dropdown/Select menu should be used instead. + +Do not use a radio button if only a single option is available; use a Checkbox here instead. + +If there are many options that need to select from, consider using a Select instead. + +#### Size + +Radio buttons are available in Regular and Dense sizes. Regular size is usually used in forms and whenever radio buttons are used standalone. Dense size radio buttons are used inside other complex components, not as main elements of a UI. + +#### See more + +Learn more about Radio buttons at [Design System: Radio](https://github.com/dhis2/design-system/blob/master/atoms/radio.md). + +\`\`\`js +import { Radio } from '@dhis2/ui' +\`\`\` +` + window.onChange = (payload, event) => { console.log('onChange payload', payload) console.log('onChange event', event) @@ -21,362 +43,111 @@ const onChange = (...args) => window.onChange(...args) const onFocus = (...args) => window.onFocus(...args) const onBlur = (...args) => window.onBlur(...args) -storiesOf('Radio', module) - // Regular - .add('Default', () => ( - <Radio - name="Ex" - label="Radio" - value="default" - onChange={onChange} - onFocus={onFocus} - onBlur={onBlur} - /> - )) - - .add('Focused unchecked', () => ( - <> - <Radio - initialFocus - name="Ex" - label="Radio" - value="default" - className="initially-focused" - onChange={onChange} - onFocus={onFocus} - onBlur={onBlur} - /> - <Radio - name="Ex" - label="Radio" - value="default" - className="initially-unfocused" - onChange={onChange} - onFocus={onFocus} - onBlur={onBlur} - /> - </> - )) - - .add('Focused checked', () => ( - <> - <Radio - initialFocus - checked - name="Ex" - label="Radio" - value="default" - className="initially-focused" - onChange={onChange} - onFocus={onFocus} - onBlur={onBlur} - /> - <Radio - name="Ex" - label="Radio" - value="default" - className="initially-unfocused" - onChange={onChange} - onFocus={onFocus} - onBlur={onBlur} - /> - </> - )) - - .add('Checked', () => ( - <Radio - name="Ex" - label="Radio" - checked - value="checked" - onChange={onChange} - onFocus={onFocus} - onBlur={onBlur} - /> - )) - - .add('Disabled', () => ( - <> - <Radio - name="Ex" - label="Radio" - disabled - value="disabled" - onChange={onChange} - onFocus={onFocus} - onBlur={onBlur} - /> - <Radio - name="Ex" - label="Radio" - disabled - checked - value="disabled" - onChange={onChange} - onFocus={onFocus} - onBlur={onBlur} - /> - </> - )) - - .add('Valid', () => ( - <> - <Radio - name="Ex" - label="Radio" - valid - value="valid" - onChange={onChange} - onFocus={onFocus} - onBlur={onBlur} - /> - <Radio - name="Ex" - label="Radio" - valid - checked - value="valid" - onChange={onChange} - onFocus={onFocus} - onBlur={onBlur} - /> - </> - )) - - .add('Warning', () => ( - <> - <Radio - name="Ex" - label="Radio" - warning - value="warning" - onChange={onChange} - onFocus={onFocus} - onBlur={onBlur} - /> - <Radio - name="Ex" - label="Radio" - warning - checked - value="warning" - onChange={onChange} - onFocus={onFocus} - onBlur={onBlur} - /> - </> - )) - - .add('Error', () => ( - <> - <Radio - name="Ex" - label="Radio" - error - value="error" - onChange={onChange} - onFocus={onFocus} - onBlur={onBlur} - /> - <Radio - name="Ex" - label="Radio" - error - checked - value="error" - onChange={onChange} - onFocus={onFocus} - onBlur={onBlur} - /> - </> - )) - - .add('Image label', () => ( - <Radio - name="Ex" - label={<img src="https://picsum.photos/id/82/200/100" />} - value="with-help" - onChange={onChange} - onFocus={onFocus} - onBlur={onBlur} - /> - )) - - // Dense - .add('Default - Dense', () => ( - <Radio - dense - name="Ex" - label="Radio" - value="default" - onChange={onChange} - onFocus={onFocus} - onBlur={onBlur} - /> - )) - - .add('Focused unchecked - Dense', () => ( - <Radio - dense - initialFocus - name="Ex" - label="Radio" - value="default" - onChange={onChange} - onFocus={onFocus} - onBlur={onBlur} - /> - )) - - .add('Focused checked - Dense', () => ( - <Radio - dense - initialFocus - checked - name="Ex" - label="Radio" - value="default" - onChange={onChange} - onFocus={onFocus} - onBlur={onBlur} - /> - )) - - .add('Checked - Dense', () => ( - <Radio - dense - name="Ex" - label="Radio" - checked - value="checked" - onChange={onChange} - onFocus={onFocus} - onBlur={onBlur} - /> - )) - - .add('Disabled - Dense', () => ( - <> - <Radio - dense - name="Ex" - label="Radio" - disabled - value="disabled" - onChange={onChange} - onFocus={onFocus} - onBlur={onBlur} - /> - <Radio - dense - name="Ex" - label="Radio" - disabled - checked - value="disabled" - onChange={onChange} - onFocus={onFocus} - onBlur={onBlur} - /> - </> - )) - - .add('Valid - Dense', () => ( - <> - <Radio - dense - name="Ex" - label="Radio" - valid - value="valid" - onChange={onChange} - onFocus={onFocus} - onBlur={onBlur} - /> - <Radio - dense - name="Ex" - label="Radio" - valid - checked - value="valid" - onChange={onChange} - onFocus={onFocus} - onBlur={onBlur} - /> - </> - )) - - .add('Warning - Dense', () => ( - <> - <Radio - dense - name="Ex" - label="Radio" - warning - value="warning" - onChange={onChange} - onFocus={onFocus} - onBlur={onBlur} - /> - <Radio - dense - name="Ex" - label="Radio" - warning - checked - value="warning" - onChange={onChange} - onFocus={onFocus} - onBlur={onBlur} - /> - </> - )) - - .add('Error - Dense', () => ( - <> - <Radio - dense - name="Ex" - label="Radio" - error - value="error" - onChange={onChange} - onFocus={onFocus} - onBlur={onBlur} - /> - <Radio - dense - name="Ex" - label="Radio" - error - checked - value="error" - onChange={onChange} - onFocus={onFocus} - onBlur={onBlur} - /> - </> - )) - - .add('Image label - Dense', () => ( - <Radio - dense - name="Ex" - label={<img src="https://picsum.photos/id/82/200/100" />} - value="with-help" - onChange={onChange} - onFocus={onFocus} - onBlur={onBlur} - /> - )) - .add('No Label', () => ( - <Radio - name="Ex" - value="with-help" - onChange={onChange} - onFocus={onFocus} - onBlur={onBlur} - className="some-name" - /> - )) +export default { + title: 'Forms/Radio/Radio', + component: Radio, + parameters: { + componentSubtitle: subtitle, + docs: { description: { component: description } }, + }, + // Default args for all stories + args: { + name: 'Ex', + label: 'Radio', + value: 'default', + onChange: onChange, + onFocus: onFocus, + onBlur: onBlur, + }, + argTypes: { + valid: { ...sharedPropTypes.statusArgType }, + error: { ...sharedPropTypes.statusArgType }, + warning: { ...sharedPropTypes.statusArgType }, + }, +} + +const Template = args => <Radio {...args} /> + +const CheckedUncheckedTemplate = args => ( + <> + <Radio {...args} /> + <Radio {...args} checked /> + </> +) + +export const Default = Template.bind({}) + +export const FocusedUnchecked = args => ( + <> + <Radio {...args} initialFocus className="initially-focused" /> + <Radio {...args} className="initially-unfocused" /> + </> +) +// Stories with initial focus are distracting on docs page +FocusedUnchecked.parameters = { docs: { disable: true } } + +export const FocusedChecked = FocusedUnchecked.bind({}) +FocusedChecked.args = { checked: true } +FocusedChecked.parameters = { docs: { disable: true } } + +export const Checked = Template.bind({}) +Checked.args = { checked: true, value: 'checked' } + +export const Disabled = CheckedUncheckedTemplate.bind({}) +Disabled.args = { disabled: true, value: 'disabled' } + +export const Valid = CheckedUncheckedTemplate.bind({}) +Valid.args = { valid: true, value: 'valid' } + +export const Warning = CheckedUncheckedTemplate.bind({}) +Warning.args = { warning: true, value: 'warning' } + +export const Error = CheckedUncheckedTemplate.bind({}) +Error.args = { error: true, value: 'error' } + +export const ImageLabel = Template.bind({}) +ImageLabel.args = { + label: <img src="https://picsum.photos/id/82/200/100" />, + value: 'with-help', +} + +export const DefaultDense = Template.bind({}) +DefaultDense.args = { dense: true } +DefaultDense.storyName = 'Default - Dense' + +export const FocusedUncheckedDense = FocusedUnchecked.bind({}) +FocusedUncheckedDense.args = { ...DefaultDense.args } +FocusedUncheckedDense.storyName = 'Focused unchecked - Dense' +FocusedUncheckedDense.parameters = { docs: { disable: true } } + +export const FocusedCheckedDense = FocusedUnchecked.bind({}) +FocusedCheckedDense.args = { ...DefaultDense.args, checked: true } +FocusedCheckedDense.storyName = 'Focused checked - Dense' +FocusedCheckedDense.parameters = { docs: { disable: true } } + +export const CheckedDense = Template.bind({}) +CheckedDense.args = { ...Checked.args, ...DefaultDense.args } +CheckedDense.storyName = 'Checked - Dense' + +export const DisabledDense = CheckedUncheckedTemplate.bind({}) +DisabledDense.args = { ...Disabled.args, ...DefaultDense.args } +DisabledDense.storyName = 'Disabled - Dense' + +export const ValidDense = CheckedUncheckedTemplate.bind({}) +ValidDense.args = { ...Valid.args, ...DefaultDense.args } +ValidDense.storyName = 'Valid - Dense' + +export const WarningDense = CheckedUncheckedTemplate.bind({}) +WarningDense.args = { ...Warning.args, ...DefaultDense.args } +WarningDense.storyName = 'Warning - Dense' + +export const ErrorDense = CheckedUncheckedTemplate.bind({}) +ErrorDense.args = { ...Error.args, ...DefaultDense.args } +ErrorDense.storyName = 'Error - Dense' + +export const ImageLabelDense = Template.bind({}) +ImageLabelDense.args = { ...ImageLabel.args, ...DefaultDense.args } +ImageLabelDense.storyName = 'Image label - Dense' + +export const NoLabel = Template.bind({}) +NoLabel.args = { label: null, className: 'some-name' } diff --git a/packages/core/src/SingleSelect/SingleSelect.js b/packages/core/src/SingleSelect/SingleSelect.js index e5f97337c7..20c0e08b64 100644 --- a/packages/core/src/SingleSelect/SingleSelect.js +++ b/packages/core/src/SingleSelect/SingleSelect.js @@ -1,5 +1,6 @@ import propTypes from '@dhis2/prop-types' import { spacers, sharedPropTypes } from '@dhis2/ui-constants' +import PropTypes from 'prop-types' import React from 'react' import { StatusIcon } from '../Icons/index.js' import { Loading } from '../Select/Loading.js' @@ -151,35 +152,43 @@ SingleSelect.defaultProps = { * @prop {string} [dataTest] */ SingleSelect.propTypes = { - children: propTypes.node, - className: propTypes.string, - clearText: propTypes.requiredIf(props => props.clearable, propTypes.string), - clearable: propTypes.bool, - dataTest: propTypes.string, - dense: propTypes.bool, - disabled: propTypes.bool, - empty: propTypes.node, + children: PropTypes.node, + className: PropTypes.string, + /** Text on button that clears selection. Required if `clearable` prop is true */ + clearText: propTypes.requiredIf(props => props.clearable, PropTypes.string), + /** Adds a button to clear selection */ + clearable: PropTypes.bool, + dataTest: PropTypes.string, + dense: PropTypes.bool, + disabled: PropTypes.bool, + /** Text or component to display when there are no options */ + empty: PropTypes.node, + /** Applies 'error' appearance for validation feedback. Mutually exclusive with `warning` and `valid` props */ error: sharedPropTypes.statusPropType, - filterPlaceholder: propTypes.string, - filterable: propTypes.bool, - initialFocus: propTypes.bool, - inputMaxHeight: propTypes.string, - loading: propTypes.bool, - loadingText: propTypes.string, - maxHeight: propTypes.string, + filterPlaceholder: PropTypes.string, + /** Adds a filter field to add text to filter options */ + filterable: PropTypes.bool, + initialFocus: PropTypes.bool, + inputMaxHeight: PropTypes.string, + loading: PropTypes.bool, + loadingText: PropTypes.string, + maxHeight: PropTypes.string, + /** Text to show when filter returns no results. Required if `filterable` prop is true */ noMatchText: propTypes.requiredIf( props => props.filterable, - propTypes.string + PropTypes.string ), - placeholder: propTypes.string, - prefix: propTypes.string, - selected: propTypes.string, - tabIndex: propTypes.string, + placeholder: PropTypes.string, + prefix: PropTypes.string, + selected: PropTypes.string, + tabIndex: PropTypes.string, + /** Applies 'valid' appearance for validation feedback. Mutually exclusive with `warning` and `error` props */ valid: sharedPropTypes.statusPropType, + /** Applies 'warning' appearance for validation feedback. Mutually exclusive with `valid` and `error` props */ warning: sharedPropTypes.statusPropType, - onBlur: propTypes.func, - onChange: propTypes.func, - onFocus: propTypes.func, + onBlur: PropTypes.func, + onChange: PropTypes.func, + onFocus: PropTypes.func, } export { SingleSelect } diff --git a/packages/core/src/SingleSelect/SingleSelect.stories.js b/packages/core/src/SingleSelect/SingleSelect.stories.js index eda2561f89..20367fb861 100644 --- a/packages/core/src/SingleSelect/SingleSelect.stories.js +++ b/packages/core/src/SingleSelect/SingleSelect.stories.js @@ -1,11 +1,28 @@ import propTypes from '@dhis2/prop-types' -import { storiesOf } from '@storybook/react' +import { sharedPropTypes } from '@dhis2/ui-constants' import React from 'react' import { SingleSelect, SingleSelectOption } from '../index.js' -window.onChange = window.Cypress && window.Cypress.cy.stub() -window.onFocus = window.Cypress && window.Cypress.cy.stub() -window.onBlur = window.Cypress && window.Cypress.cy.stub() +const description = ` +Use a select component wherever the user needs to make a selection of one or more options from a list of 6 or more options. If there are less than 6 options to choose from and space permits, use checkboxes for multiple selection and radio buttons for single selection. If the user needs to make a complex selection with a specific ordering, use a transfer instead. + +Learn more about using Selects at [Design System: Select](https://github.com/dhis2/design-system/blob/master/molecules/select.md). + +\`\`\`js +import { SingleSelect, SingleSelectOption } from '@dhis2/ui' +\`\`\` + +_**Note:** Due to demo limitations on this page, only one representative example is rendered here. For more (interactive) examples, see individual stories in the 'Canvas' tab._ +` + +const eventHandler = handlerName => (payload, event) => { + console.log(`${handlerName} payload`, payload) + console.log(`${handlerName} event`, event) +} + +const onChange = eventHandler('onChange') +const onFocus = eventHandler('onFocus') +const onBlur = eventHandler('onBlur') const CustomSingleSelectOption = ({ label, onClick }) => ( <div onClick={e => onClick({}, e)}>{label}</div> @@ -16,94 +33,218 @@ CustomSingleSelectOption.propTypes = { onClick: propTypes.func, } -storiesOf('SingleSelect', module) - .add('With options', () => ( - <SingleSelect className="select"> - <SingleSelectOption value="1" label="option one" /> - <SingleSelectOption value="2" label="option two" /> - <SingleSelectOption value="3" label="option three" /> - </SingleSelect> - )) - .add('With options and onChange', () => ( - <SingleSelect className="select" onChange={window.onChange}> - <SingleSelectOption value="1" label="option one" /> - <SingleSelectOption value="2" label="option two" /> - <SingleSelectOption value="3" label="option three" /> - </SingleSelect> - )) - .add('With onFocus', () => ( - <SingleSelect className="select" onFocus={window.onFocus}> - <SingleSelectOption value="1" label="option one" /> - <SingleSelectOption value="2" label="option two" /> - <SingleSelectOption value="3" label="option three" /> - </SingleSelect> - )) - .add('With onBlur', () => ( - <SingleSelect className="select" onBlur={window.onBlur}> - <SingleSelectOption value="1" label="option one" /> - <SingleSelectOption value="2" label="option two" /> - <SingleSelectOption value="3" label="option three" /> - </SingleSelect> - )) - .add('With custom options and onChange', () => ( - <SingleSelect className="select" onChange={window.onChange}> - <CustomSingleSelectOption value="1" label="option one" /> - <CustomSingleSelectOption value="2" label="option two" /> - <CustomSingleSelectOption value="3" label="option three" /> - </SingleSelect> - )) - .add('With invalid options', () => ( - <SingleSelect className="select"> - <div>invalid one</div> - <SingleSelectOption value="1" label="option one" /> - <div>invalid two</div> - <SingleSelectOption value="2" label="option two" /> - <div>invalid three</div> - <SingleSelectOption value="3" label="option three" /> - {null} - {undefined} - {false} - </SingleSelect> - )) - .add('With invalid filterable options', () => ( - <SingleSelect filterable className="select" noMatchText="No match"> - <div>invalid one</div> - <SingleSelectOption value="1" label="option one" /> - <div>invalid two</div> - <SingleSelectOption value="2" label="option two" /> - <div>invalid three</div> - <SingleSelectOption value="3" label="option three" /> - </SingleSelect> - )) - .add('With initialFocus', () => ( - <SingleSelect className="select" initialFocus /> - )) - .add('Empty', () => <SingleSelect className="select" />) - .add('Empty with empty text', () => ( - <SingleSelect className="select" empty="Custom empty text" /> - )) - .add('Empty with empty component', () => ( - <SingleSelect - className="select" - empty={<div className="custom-empty">Custom empty component</div>} - /> - )) - .add('With options and loading', () => ( - <SingleSelect className="select" loading> - <SingleSelectOption value="1" label="option one" /> - <SingleSelectOption value="2" label="option two" /> - <SingleSelectOption value="3" label="option three" /> - </SingleSelect> - )) - .add('With options, loading and loading text', () => ( - <SingleSelect className="select" loadingText="Loading options" loading> - <SingleSelectOption value="1" label="option one" /> - <SingleSelectOption value="2" label="option two" /> - <SingleSelectOption value="3" label="option three" /> - </SingleSelect> - )) - .add('With more than ten options', () => ( - <SingleSelect className="select"> +const requiredIfArgType = { + table: { type: { summary: 'string' }, control: { type: 'string' } }, +} + +export default { + title: 'Forms/Single Select/Single Select', + component: SingleSelect, + parameters: { + docs: { + description: { component: description }, + // Disable stories in docs page by default, then use one representative examples + disable: true, + // Use iframe to keep dropdown menu inside story for docs page + inlineStories: false, + iframeHeight: '300px', + }, + }, + // Use 'onChange' as a default arg, otherwise components throw an error when interacted with + // (Maybe this could be handled in the component - as a required prop) + args: { className: 'select', onChange }, + argTypes: { + valid: { ...sharedPropTypes.statusArgType }, + warning: { ...sharedPropTypes.statusArgType }, + error: { ...sharedPropTypes.statusArgType }, + clearText: { ...requiredIfArgType }, + noMatchText: { ...requiredIfArgType }, + }, +} + +const WithOptionsTemplate = args => ( + <SingleSelect {...args}> + <SingleSelectOption value="1" label="option one" /> + <SingleSelectOption value="2" label="option two" /> + <SingleSelectOption value="3" label="option three" /> + </SingleSelect> +) + +const EmptyTemplate = args => <SingleSelect {...args} /> + +export const WithOptionsAndOnChange = WithOptionsTemplate.bind({}) +WithOptionsAndOnChange.storyName = 'With options and onChange' + +export const WithOptionsAndASelection = WithOptionsTemplate.bind({}) +WithOptionsAndASelection.args = { selected: '1' } +// Enable this story as the primary for docs as a representative example +WithOptionsAndASelection.parameters = { docs: { disable: false } } +WithOptionsAndASelection.storyName = 'With options and a selection' + +export const WithOnFocus = WithOptionsTemplate.bind({}) +WithOnFocus.args = { onFocus } +WithOnFocus.storyName = 'With onFocus' + +export const WithOnBlur = WithOptionsTemplate.bind({}) +WithOnBlur.args = { onBlur } +WithOnBlur.storyName = 'With onBlur' + +export const WithCustomOptionsAndOnChange = args => ( + <SingleSelect {...args}> + <CustomSingleSelectOption value="1" label="option one" /> + <CustomSingleSelectOption value="2" label="option two" /> + <CustomSingleSelectOption value="3" label="option three" /> + </SingleSelect> +) +WithCustomOptionsAndOnChange.storyName = 'With custom options and onChange' + +export const WithInvalidOptions = args => ( + <SingleSelect {...args}> + <div>invalid one</div> + <SingleSelectOption value="1" label="option one" /> + <div>invalid two</div> + <SingleSelectOption value="2" label="option two" /> + <div>invalid three</div> + <SingleSelectOption value="3" label="option three" /> + {null} + {undefined} + {false} + </SingleSelect> +) + +export const WithInvalidFilterableOptions = args => ( + <SingleSelect {...args}> + <div>invalid one</div> + <SingleSelectOption value="1" label="option one" /> + <div>invalid two</div> + <SingleSelectOption value="2" label="option two" /> + <div>invalid three</div> + <SingleSelectOption value="3" label="option three" /> + </SingleSelect> +) +WithInvalidFilterableOptions.args = { + filterable: true, + noMatchText: 'No match found', +} + +export const WithInitialFocus = EmptyTemplate.bind({}) +WithInitialFocus.args = { initialFocus: true } +WithInitialFocus.parameters = { docs: { disable: true } } +WithInitialFocus.storyName = 'With initialFocus' + +export const Empty = EmptyTemplate.bind({}) + +export const EmptyWithEmptyText = EmptyTemplate.bind({}) +EmptyWithEmptyText.args = { empty: 'Custom empty text' } + +export const EmptyWithEmptyComponent = EmptyTemplate.bind({}) +EmptyWithEmptyComponent.args = { + empty: <div className="custom-empty">Custom empty component</div>, +} + +export const WithOptionsAndLoading = WithOptionsTemplate.bind({}) +WithOptionsAndLoading.args = { loading: true } + +export const WithOptionsLoadingAndLoadingText = WithOptionsTemplate.bind({}) +WithOptionsLoadingAndLoadingText.args = { + ...WithOptionsAndLoading.args, + loadingText: 'Loading options', +} +WithOptionsLoadingAndLoadingText.storyName = + 'With options, loading and loading text' + +export const WithMoreThanTenOptions = args => ( + <SingleSelect {...args}> + <SingleSelectOption value="1" label="option one" /> + <SingleSelectOption value="2" label="option two" /> + <SingleSelectOption value="3" label="option three" /> + <SingleSelectOption value="4" label="option four" /> + <SingleSelectOption value="5" label="option five" /> + <SingleSelectOption value="6" label="option six" /> + <SingleSelectOption value="7" label="option seven" /> + <SingleSelectOption value="8" label="option eight" /> + <SingleSelectOption value="9" label="option nine" /> + <SingleSelectOption value="10" label="option ten" /> + <SingleSelectOption value="11" label="option eleven" /> + <SingleSelectOption value="12" label="option twelve" /> + </SingleSelect> +) + +export const WithMoreThanThreeOptionsAndA100PxMaxHeight = args => ( + <SingleSelect {...args}> + <SingleSelectOption value="1" label="option one" /> + <SingleSelectOption value="2" label="option two" /> + <SingleSelectOption value="3" label="option three" /> + <SingleSelectOption value="4" label="option four" /> + <SingleSelectOption value="5" label="option five" /> + <SingleSelectOption value="6" label="option six" /> + <SingleSelectOption value="7" label="option seven" /> + <SingleSelectOption value="8" label="option eight" /> + <SingleSelectOption value="9" label="option nine" /> + <SingleSelectOption value="10" label="option ten" /> + <SingleSelectOption value="11" label="option eleven" /> + <SingleSelectOption value="12" label="option twelve" /> + </SingleSelect> +) +WithMoreThanThreeOptionsAndA100PxMaxHeight.args = { maxHeight: '100px' } +WithMoreThanThreeOptionsAndA100PxMaxHeight.storyName = + 'With more than three options and a 100px max-height' + +export const WithOptionsAndDisabled = WithOptionsTemplate.bind({}) +WithOptionsAndDisabled.args = { disabled: true } +WithOptionsAndDisabled.storyName = 'With options and disabled' + +export const WithOptionsASelectionAndDisabled = WithOptionsTemplate.bind({}) +WithOptionsASelectionAndDisabled.args = { + ...WithOptionsAndDisabled.args, + ...WithOptionsAndASelection.args, +} +WithOptionsASelectionAndDisabled.storyName = + 'With options, a selection and disabled' + +export const WithPrefix = WithOptionsTemplate.bind({}) +WithPrefix.args = { prefix: 'Prefix text' } + +export const WithPrefixAndSelection = WithOptionsTemplate.bind({}) +WithPrefixAndSelection.args = { + ...WithPrefix.args, + ...WithOptionsAndASelection.args, +} + +export const WithPlaceholder = WithOptionsTemplate.bind({}) +WithPlaceholder.args = { placeholder: 'Placeholder text' } + +export const WithPlaceholderAndSelection = WithOptionsTemplate.bind({}) +WithPlaceholderAndSelection.args = { + ...WithPlaceholder.args, + ...WithOptionsAndASelection.args, +} + +export const WithDisabledOptionAndOnChange = args => ( + <SingleSelect {...args}> + <SingleSelectOption value="1" label="option one" /> + <SingleSelectOption value="2" label="option two" /> + <SingleSelectOption value="3" label="option three" /> + <SingleSelectOption value="4" label="disabled option" disabled /> + </SingleSelect> +) +WithDisabledOptionAndOnChange.storyName = 'With disabled option and onChange' + +export const WithClearButtonSelectionAndOnChange = WithOptionsTemplate.bind({}) +WithClearButtonSelectionAndOnChange.args = { + ...WithOptionsAndASelection.args, + clearable: true, + clearText: 'Clear', +} +WithClearButtonSelectionAndOnChange.storyName = + 'With clear button, selection and onChange' + +export const WithFilterField = WithOptionsTemplate.bind({}) +WithFilterField.args = { ...WithInvalidFilterableOptions.args } + +export const DefaultPosition = args => ( + <> + <SingleSelect {...args}> <SingleSelectOption value="1" label="option one" /> <SingleSelectOption value="2" label="option two" /> <SingleSelectOption value="3" label="option three" /> @@ -117,9 +258,17 @@ storiesOf('SingleSelect', module) <SingleSelectOption value="11" label="option eleven" /> <SingleSelectOption value="12" label="option twelve" /> </SingleSelect> - )) - .add('With more than three options and a 100px max-height', () => ( - <SingleSelect className="select" maxHeight="100px"> + <style jsx>{` + :global(#root) { + margin-bottom: 2000px; + } + `}</style> + </> +) + +export const FlippedPosition = args => ( + <> + <SingleSelect {...args}> <SingleSelectOption value="1" label="option one" /> <SingleSelectOption value="2" label="option two" /> <SingleSelectOption value="3" label="option three" /> @@ -133,183 +282,48 @@ storiesOf('SingleSelect', module) <SingleSelectOption value="11" label="option eleven" /> <SingleSelectOption value="12" label="option twelve" /> </SingleSelect> - )) - .add('With options, a selection and disabled', () => ( - <SingleSelect disabled className="select" selected="1"> - <SingleSelectOption value="1" label="option one" /> - <SingleSelectOption value="2" label="option two" /> - <SingleSelectOption value="3" label="option three" /> - </SingleSelect> - )) - .add('With options and disabled', () => ( - <SingleSelect disabled className="select"> - <SingleSelectOption value="1" label="option one" /> - <SingleSelectOption value="2" label="option two" /> - <SingleSelectOption value="3" label="option three" /> - </SingleSelect> - )) - .add('With prefix', () => ( - <SingleSelect className="select" prefix="Prefix text"> - <SingleSelectOption value="1" label="option one" /> - <SingleSelectOption value="2" label="option two" /> - <SingleSelectOption value="3" label="option three" /> - </SingleSelect> - )) - .add('With prefix and selection', () => ( - <SingleSelect className="select" prefix="Prefix text" selected="1"> - <SingleSelectOption value="1" label="option one" /> - <SingleSelectOption value="2" label="option two" /> - <SingleSelectOption value="3" label="option three" /> - </SingleSelect> - )) - .add('With placeholder', () => ( - <SingleSelect className="select" placeholder="Placeholder text"> - <SingleSelectOption value="1" label="option one" /> - <SingleSelectOption value="2" label="option two" /> - <SingleSelectOption value="3" label="option three" /> - </SingleSelect> - )) - .add('With placeholder and selection', () => ( - <SingleSelect - className="select" - selected="1" - placeholder="Placeholder text" - > - <SingleSelectOption value="1" label="option one" /> - <SingleSelectOption value="2" label="option two" /> - <SingleSelectOption value="3" label="option three" /> - </SingleSelect> - )) - .add('With disabled option and onChange', () => ( - <SingleSelect className="select" onChange={window.onChange}> - <SingleSelectOption value="1" label="option one" /> - <SingleSelectOption value="2" label="option two" /> - <SingleSelectOption value="3" label="option three" /> - <SingleSelectOption value="4" label="disabled option" disabled /> - </SingleSelect> - )) - .add('With options and a selection', () => ( - <SingleSelect className="select" selected="1"> - <SingleSelectOption value="1" label="option one" /> - <SingleSelectOption value="2" label="option two" /> - <SingleSelectOption value="3" label="option three" /> - </SingleSelect> - )) - .add('With options, a selection and onChange', () => ( - <SingleSelect - className="select" - selected="1" - onChange={window.onChange} - > - <SingleSelectOption value="1" label="option one" /> - <SingleSelectOption value="2" label="option two" /> - <SingleSelectOption value="3" label="option three" /> - </SingleSelect> - )) - .add('With clear button, selection and onChange', () => ( - <SingleSelect - clearable - clearText="Clear" - className="select" - selected="1" - onChange={window.onChange} - > - <SingleSelectOption value="1" label="option one" /> - <SingleSelectOption value="2" label="option two" /> - <SingleSelectOption value="3" label="option three" /> - </SingleSelect> - )) - .add('With filter field', () => ( - <SingleSelect - filterable - noMatchText="No match found" - className="select" - > + <style jsx>{` + :global(html), + :global(body), + :global(#root) { + position: relative; + height: 500px; + max-height: 500px; + } + :global(#root) { + padding-top: 400px !important; + } + `}</style> + </> +) + +export const ShiftedIntoView = args => ( + <> + <SingleSelect {...args}> <SingleSelectOption value="1" label="option one" /> <SingleSelectOption value="2" label="option two" /> <SingleSelectOption value="3" label="option three" /> + <SingleSelectOption value="4" label="option four" /> + <SingleSelectOption value="5" label="option five" /> + <SingleSelectOption value="6" label="option six" /> + <SingleSelectOption value="7" label="option seven" /> + <SingleSelectOption value="8" label="option eight" /> + <SingleSelectOption value="9" label="option nine" /> + <SingleSelectOption value="10" label="option ten" /> + <SingleSelectOption value="11" label="option eleven" /> + <SingleSelectOption value="12" label="option twelve" /> </SingleSelect> - )) - .add('Default position', () => ( - <> - <SingleSelect className="select"> - <SingleSelectOption value="1" label="option one" /> - <SingleSelectOption value="2" label="option two" /> - <SingleSelectOption value="3" label="option three" /> - <SingleSelectOption value="4" label="option four" /> - <SingleSelectOption value="5" label="option five" /> - <SingleSelectOption value="6" label="option six" /> - <SingleSelectOption value="7" label="option seven" /> - <SingleSelectOption value="8" label="option eight" /> - <SingleSelectOption value="9" label="option nine" /> - <SingleSelectOption value="10" label="option ten" /> - <SingleSelectOption value="11" label="option eleven" /> - <SingleSelectOption value="12" label="option twelve" /> - </SingleSelect> - <style jsx>{` - :global(#root) { - margin-bottom: 2000px; - } - `}</style> - </> - )) - .add('Flipped position', () => ( - <> - <SingleSelect className="select"> - <SingleSelectOption value="1" label="option one" /> - <SingleSelectOption value="2" label="option two" /> - <SingleSelectOption value="3" label="option three" /> - <SingleSelectOption value="4" label="option four" /> - <SingleSelectOption value="5" label="option five" /> - <SingleSelectOption value="6" label="option six" /> - <SingleSelectOption value="7" label="option seven" /> - <SingleSelectOption value="8" label="option eight" /> - <SingleSelectOption value="9" label="option nine" /> - <SingleSelectOption value="10" label="option ten" /> - <SingleSelectOption value="11" label="option eleven" /> - <SingleSelectOption value="12" label="option twelve" /> - </SingleSelect> - <style jsx>{` - :global(html), - :global(body), - :global(#root) { - position: relative; - height: 500px; - max-height: 500px; - } - :global(#root) { - padding-top: 400px !important; - } - `}</style> - </> - )) - .add('Shifted into view', () => ( - <> - <SingleSelect className="select"> - <SingleSelectOption value="1" label="option one" /> - <SingleSelectOption value="2" label="option two" /> - <SingleSelectOption value="3" label="option three" /> - <SingleSelectOption value="4" label="option four" /> - <SingleSelectOption value="5" label="option five" /> - <SingleSelectOption value="6" label="option six" /> - <SingleSelectOption value="7" label="option seven" /> - <SingleSelectOption value="8" label="option eight" /> - <SingleSelectOption value="9" label="option nine" /> - <SingleSelectOption value="10" label="option ten" /> - <SingleSelectOption value="11" label="option eleven" /> - <SingleSelectOption value="12" label="option twelve" /> - </SingleSelect> - <style jsx>{` - :global(html), - :global(body), - :global(#root) { - position: relative; - height: 300px !important; - max-height: 300px; - } - :global(#root) { - padding-top: 130px !important; - } - `}</style> - </> - )) + <style jsx>{` + :global(html), + :global(body), + :global(#root) { + position: relative; + height: 300px !important; + max-height: 300px; + } + :global(#root) { + padding-top: 130px !important; + } + `}</style> + </> +) diff --git a/packages/core/src/SplitButton/SplitButton.js b/packages/core/src/SplitButton/SplitButton.js index 0a85db0c03..8cf5624875 100644 --- a/packages/core/src/SplitButton/SplitButton.js +++ b/packages/core/src/SplitButton/SplitButton.js @@ -1,6 +1,6 @@ -import propTypes from '@dhis2/prop-types' import { spacers, sharedPropTypes } from '@dhis2/ui-constants' import cx from 'classnames' +import PropTypes from 'prop-types' import React, { Component } from 'react' import css from 'styled-jsx/css' import { Button } from '../Button/Button.js' @@ -174,23 +174,34 @@ SplitButton.defaultProps = { * @prop {string} [dataTest] */ SplitButton.propTypes = { - children: propTypes.string, - className: propTypes.string, - component: propTypes.element, - dataTest: propTypes.string, + children: PropTypes.string, + className: PropTypes.string, + /** Component to render when the dropdown is opened */ + component: PropTypes.element, + dataTest: PropTypes.string, + /** Applies 'destructive' appearance to indicate purpose. Mutually exclusive with `primary` and `secondary` props */ destructive: sharedPropTypes.buttonVariantPropType, - disabled: propTypes.bool, - icon: propTypes.element, - initialFocus: propTypes.bool, + /** Disables the button and makes it uninteractive */ + disabled: PropTypes.bool, + /** An icon to add inside the button */ + icon: PropTypes.element, + /** Grants the button the initial focus */ + initialFocus: PropTypes.bool, + /** Changes button size. Mutually exclusive with `small` prop */ large: sharedPropTypes.sizePropType, - name: propTypes.string, + name: PropTypes.string, + /** Applies 'primary' appearance to indicate purpose. Mutually exclusive with `destructive` and `secondary` props */ primary: sharedPropTypes.buttonVariantPropType, + /** Applies 'secondary' appearance to indicate purpose. Mutually exclusive with `primary` and `destructive` props */ secondary: sharedPropTypes.buttonVariantPropType, + /** Changes button size. Mutually exclusive with `large` prop */ small: sharedPropTypes.sizePropType, - tabIndex: propTypes.string, - type: propTypes.oneOf(['submit', 'reset', 'button']), - value: propTypes.string, - onClick: propTypes.func, + tabIndex: PropTypes.string, + /** Type of button. Applied to html `button` element */ + type: PropTypes.oneOf(['submit', 'reset', 'button']), + /** Value associated with the button. Passed in object to onClick handler */ + value: PropTypes.string, + onClick: PropTypes.func, } export { SplitButton } diff --git a/packages/core/src/SplitButton/SplitButton.stories.js b/packages/core/src/SplitButton/SplitButton.stories.js index e0e6ae7a2d..22d363477f 100644 --- a/packages/core/src/SplitButton/SplitButton.stories.js +++ b/packages/core/src/SplitButton/SplitButton.stories.js @@ -1,10 +1,14 @@ +import { sharedPropTypes } from '@dhis2/ui-constants' import React from 'react' import { SplitButton } from './SplitButton.js' -export default { - title: 'SplitButton', - component: SplitButton, -} +const description = ` +Similar to the dropdown button, but can be triggered independently of opening the contained action list. The main action may be 'Save' and the contained actions may be "Save and add another" and "Save and open". + +\`\`\`js +import { SplitButton } from '@dhis2/ui' +\`\`\` +` window.onClick = (payload, event) => { console.log('onClick payload', payload) @@ -13,80 +17,65 @@ window.onClick = (payload, event) => { const onClick = (...args) => window.onClick(...args) -const Simple = <span>Simplest thing</span> - -export const Default = () => ( - <SplitButton name="default" value="nothing" component={Simple}> - Label me! - </SplitButton> -) - -export const WithClick = () => ( - <SplitButton - name="default" - value="nothing" - onClick={onClick} - component={Simple} - > - Label me! - </SplitButton> -) - -export const Primary = () => ( - <SplitButton name="default" value="nothing" primary component={Simple}> - Label me! - </SplitButton> -) - -export const Secondary = () => ( - <SplitButton name="default" value="nothing" secondary component={Simple}> - Label me! - </SplitButton> -) - -export const Destructive = () => ( - <SplitButton name="default" value="nothing" destructive component={Simple}> - Label me! - </SplitButton> -) - -export const Disabled = () => ( - <SplitButton name="default" value="nothing" disabled component={Simple}> - Label me! - </SplitButton> -) - -export const Small = () => ( - <SplitButton name="default" value="nothing" small component={Simple}> - Label me! - </SplitButton> -) - -export const Large = () => ( - <SplitButton name="default" value="nothing" large component={Simple}> - Label me! - </SplitButton> -) - -export const WithMenu = () => ( - <SplitButton name="default" value="nothing" component={Simple}> - Label me! - </SplitButton> -) - -export const InitialFocus = () => ( - <SplitButton name="default" value="nothing" initialFocus component={Simple}> - Label me! - </SplitButton> -) - -export const WithIcon = () => ( - <SplitButton - name="Button" - value="default" - component={<div>Component</div>} - icon={<div>Icon</div>} - > - Children - </SplitButton> -) +const DropdownComponent = <span>Dropdown component</span> + +export default { + title: 'Actions/Buttons/Split Button', + component: SplitButton, + parameters: { docs: { description: { component: description } } }, + args: { + name: 'buttonName', + value: 'buttonValue', + component: DropdownComponent, + onClick: onClick, + children: 'Label me!', + }, + argTypes: { + small: { ...sharedPropTypes.sizeArgType }, + large: { ...sharedPropTypes.sizeArgType }, + primary: { ...sharedPropTypes.buttonVariantArgType }, + secondary: { ...sharedPropTypes.buttonVariantArgType }, + destructive: { ...sharedPropTypes.buttonVariantArgType }, + }, +} + +const Template = args => <SplitButton {...args} /> + +export const Default = Template.bind({}) + +export const Primary = Template.bind({}) +Primary.args = { primary: true } +Primary.parameters = { + docs: { + description: { + story: `_**Note**: The dropdown components in the following examples do not appear in the right place on this page. View the following examples in the 'Canvas' tab for the correct placement._`, + }, + }, +} + +export const Secondary = Template.bind({}) +Secondary.args = { secondary: true } + +export const Destructive = Template.bind({}) +Destructive.props = { destructive: true } + +export const Disabled = Template.bind({}) +Disabled.args = { disabled: true } + +export const Small = Template.bind({}) +Small.args = { small: true } + +export const Large = Template.bind({}) +Large.args = { large: true } + +export const InitialFocus = Template.bind({}) +InitialFocus.args = { initialFocus: true } +// Disable this on docs page because grabbing focus repeatedly is annoying +InitialFocus.parameters = { docs: { disable: true } } + +export const WithIcon = Template.bind({}) +WithIcon.args = { + children: 'Children', + component: <div>Component</div>, + icon: <div>Icon</div>, +} diff --git a/packages/core/src/StackedTable/StackedTable.js b/packages/core/src/StackedTable/StackedTable.js index 91e8d43357..0c59c585c9 100644 --- a/packages/core/src/StackedTable/StackedTable.js +++ b/packages/core/src/StackedTable/StackedTable.js @@ -1,4 +1,4 @@ -import propTypes from '@dhis2/prop-types' +import PropTypes from 'prop-types' import React from 'react' import { extractHeaderLabels } from './extractHeaderLabels.js' import { Table } from './Table.js' @@ -42,10 +42,11 @@ export const StackedTable = ({ * an empty string must be provided */ StackedTable.propTypes = { - children: propTypes.node, - className: propTypes.string, - dataTest: propTypes.string, - headerLabels: propTypes.arrayOf(propTypes.string), + children: PropTypes.node, + className: PropTypes.string, + dataTest: PropTypes.string, + /** Labels for columns. Use an empty string for a column without a header. */ + headerLabels: PropTypes.arrayOf(PropTypes.string), } StackedTable.defaultProps = { diff --git a/packages/core/src/StackedTable/StackedTable.stories.js b/packages/core/src/StackedTable/StackedTable.stories.js index 18894a8f12..69732b099e 100644 --- a/packages/core/src/StackedTable/StackedTable.stories.js +++ b/packages/core/src/StackedTable/StackedTable.stories.js @@ -1,5 +1,3 @@ -/* eslint-disable react/no-unescaped-entities */ -import { storiesOf } from '@storybook/react' import React from 'react' import { Button } from '../index.js' import { StackedTable } from './StackedTable.js' @@ -11,6 +9,25 @@ import { StackedTableHead } from './StackedTableHead.js' import { StackedTableRow } from './StackedTableRow.js' import { StackedTableRowHead } from './StackedTableRowHead.js' +const description = ` +Expresses tabular data such that each 'row' becomes a table section with the 'column' header in each cell. Multiple rows become multiple sections. + +Compose with StackedTable child components, as seen in the examples. + +\`\`\`js +import { + StackedTable, + StackedTableBody, + StackedTableCell, + StackedTableCellHead, + StackedTableFoot, + StackedTableHead, + StackedTableRow, + StackedTableRowHead, +} from 'dhis2/ui' +\`\`\` +` + const CustomCell = props => ( <td> Received props: @@ -20,429 +37,421 @@ const CustomCell = props => ( </td> ) -storiesOf('StackedTable', module) - .add('Default', () => ( - <StackedTable> - <StackedTableHead> - <StackedTableRowHead> - <StackedTableCellHead>First name</StackedTableCellHead> - <StackedTableCellHead>Last name</StackedTableCellHead> - <StackedTableCellHead>Incident date</StackedTableCellHead> - <StackedTableCellHead>Last updated</StackedTableCellHead> - <StackedTableCellHead>Age</StackedTableCellHead> - <StackedTableCellHead> - Registering unit - </StackedTableCellHead> - <StackedTableCellHead>Assigned user</StackedTableCellHead> - <StackedTableCellHead>Status</StackedTableCellHead> - </StackedTableRowHead> - </StackedTableHead> - <StackedTableBody> - <StackedTableRow> - <StackedTableCell>Onyekachukwu</StackedTableCell> - <StackedTableCell>Kariuki</StackedTableCell> - <StackedTableCell>02/06/2007</StackedTableCell> - <StackedTableCell>05/25/1972</StackedTableCell> - <StackedTableCell>66</StackedTableCell> - <StackedTableCell>Jawi</StackedTableCell> - <StackedTableCell>Sofie Hubert</StackedTableCell> - <StackedTableCell>Incomplete</StackedTableCell> - </StackedTableRow> - </StackedTableBody> - <StackedTableFoot> - <StackedTableRow> - <StackedTableCell colSpan="8" hideTitle> - <Button primary>StackedTable footer button</Button> - </StackedTableCell> - </StackedTableRow> - </StackedTableFoot> - </StackedTable> - )) - .add('Hidden label', () => ( - <StackedTable> - <StackedTableHead> - <StackedTableRowHead> - <StackedTableCellHead>First name</StackedTableCellHead> - <StackedTableCellHead>Last name</StackedTableCellHead> - <StackedTableCellHead>Incident date</StackedTableCellHead> - <StackedTableCellHead>Last updated</StackedTableCellHead> - <StackedTableCellHead>Age</StackedTableCellHead> - <StackedTableCellHead> - Registering unit - </StackedTableCellHead> - <StackedTableCellHead>Assigned user</StackedTableCellHead> - <StackedTableCellHead /> - </StackedTableRowHead> - </StackedTableHead> - <StackedTableBody> - <StackedTableRow> - <StackedTableCell>Onyekachukwu</StackedTableCell> - <StackedTableCell>Kariuki</StackedTableCell> - <StackedTableCell>02/06/2007</StackedTableCell> - <StackedTableCell>05/25/1972</StackedTableCell> - <StackedTableCell>66</StackedTableCell> - <StackedTableCell>Jawi</StackedTableCell> - <StackedTableCell>Sofie Hubert</StackedTableCell> - <StackedTableCell> - <Button - primary - onClick={() => alert('something should happen now')} - > - A row action - </Button> - </StackedTableCell> - </StackedTableRow> - </StackedTableBody> - </StackedTable> - )) - .add('Hidden label in cell', () => ( - <StackedTable> - <StackedTableHead> - <StackedTableRowHead> - <StackedTableCellHead>First name</StackedTableCellHead> - <StackedTableCellHead>Last name</StackedTableCellHead> - <StackedTableCellHead>Incident date</StackedTableCellHead> - <StackedTableCellHead>Last updated</StackedTableCellHead> - <StackedTableCellHead>Age</StackedTableCellHead> - <StackedTableCellHead> - Registering unit - </StackedTableCellHead> - <StackedTableCellHead>Assigned user</StackedTableCellHead> - <StackedTableCellHead>Status</StackedTableCellHead> - </StackedTableRowHead> - </StackedTableHead> - <StackedTableBody> - <StackedTableRow> - <StackedTableCell>Onyekachukwu</StackedTableCell> - <StackedTableCell>Kariuki</StackedTableCell> - <StackedTableCell>02/06/2007</StackedTableCell> - <StackedTableCell>05/25/1972</StackedTableCell> - <StackedTableCell>66</StackedTableCell> - <StackedTableCell>Jawi</StackedTableCell> - <StackedTableCell>Sofie Hubert</StackedTableCell> - <StackedTableCell hideTitle> - <Button - primary - onClick={() => alert('something should happen now')} - > - A row action - </Button> - </StackedTableCell> - </StackedTableRow> - <StackedTableRow> - <StackedTableCell>Onyekachukwu</StackedTableCell> - <StackedTableCell>Kariuki</StackedTableCell> - <StackedTableCell>02/06/2007</StackedTableCell> - <StackedTableCell>05/25/1972</StackedTableCell> - <StackedTableCell>66</StackedTableCell> - <StackedTableCell>Jawi</StackedTableCell> - <StackedTableCell>Sofie Hubert</StackedTableCell> - <StackedTableCell>Incomplete</StackedTableCell> - </StackedTableRow> - </StackedTableBody> - </StackedTable> - )) - .add('Colspan in header', () => ( - <StackedTable> - <StackedTableHead> - <StackedTableRowHead> - <StackedTableCellHead colSpan="2"> - Name - </StackedTableCellHead> - <StackedTableCellHead>Incident date</StackedTableCellHead> - <StackedTableCellHead>Last updated</StackedTableCellHead> - <StackedTableCellHead>Age</StackedTableCellHead> - <StackedTableCellHead> - Registering unit - </StackedTableCellHead> - <StackedTableCellHead>Assigned user</StackedTableCellHead> - <StackedTableCellHead>Status</StackedTableCellHead> - </StackedTableRowHead> - </StackedTableHead> - <StackedTableBody> - <StackedTableRow> - <StackedTableCell>Onyekachukwu</StackedTableCell> - <StackedTableCell>Kariuki</StackedTableCell> - <StackedTableCell>02/06/2007</StackedTableCell> - <StackedTableCell>05/25/1972</StackedTableCell> - <StackedTableCell>66</StackedTableCell> - <StackedTableCell>Jawi</StackedTableCell> - <StackedTableCell>Sofie Hubert</StackedTableCell> - <StackedTableCell>Incomplete</StackedTableCell> - </StackedTableRow> - </StackedTableBody> - </StackedTable> - )) - .add('Colspan in body', () => ( - <StackedTable> - <StackedTableHead> - <StackedTableRowHead> - <StackedTableCellHead>First name</StackedTableCellHead> - <StackedTableCellHead>Last name</StackedTableCellHead> - <StackedTableCellHead>Incident date</StackedTableCellHead> - <StackedTableCellHead>Last updated</StackedTableCellHead> - <StackedTableCellHead>Age</StackedTableCellHead> - <StackedTableCellHead> - Registering unit - </StackedTableCellHead> - <StackedTableCellHead>Assigned user</StackedTableCellHead> - <StackedTableCellHead>Status</StackedTableCellHead> - </StackedTableRowHead> - </StackedTableHead> - <StackedTableBody> - <StackedTableRow> - <StackedTableCell>Onyekachukwu</StackedTableCell> - <StackedTableCell>Kariuki</StackedTableCell> - <StackedTableCell colSpan="2"> - Colspan 2 here. Next cell doesn't get header "Last - updated". - </StackedTableCell> - <StackedTableCell>66</StackedTableCell> - <StackedTableCell>Jawi</StackedTableCell> - <StackedTableCell>Sofie Hubert</StackedTableCell> - <StackedTableCell>Incomplete</StackedTableCell> - </StackedTableRow> - <StackedTableRow> - <StackedTableCell>Onyekachukwu</StackedTableCell> - <StackedTableCell>Kariuki</StackedTableCell> - <StackedTableCell>02/06/2007</StackedTableCell> - <StackedTableCell>05/25/1972</StackedTableCell> - <StackedTableCell>66</StackedTableCell> - <StackedTableCell>Jawi</StackedTableCell> - <StackedTableCell>Sofie Hubert</StackedTableCell> - <StackedTableCell>Incomplete</StackedTableCell> - </StackedTableRow> - </StackedTableBody> - </StackedTable> - )) - .add('Multiple header rows', () => ( - <StackedTable> - <StackedTableHead> - <StackedTableRowHead> - <StackedTableCellHead colSpan="2"> - Name - </StackedTableCellHead> - <StackedTableCellHead>Incident date</StackedTableCellHead> - <StackedTableCellHead>Last updated</StackedTableCellHead> - <StackedTableCellHead>Age</StackedTableCellHead> - <StackedTableCellHead> - Registering unit - </StackedTableCellHead> - <StackedTableCellHead>Assigned user</StackedTableCellHead> - <StackedTableCellHead>Status</StackedTableCellHead> - </StackedTableRowHead> - <StackedTableRowHead> - <StackedTableCellHead>First name</StackedTableCellHead> - <StackedTableCellHead>Last name</StackedTableCellHead> - <StackedTableCellHead colSpan="6"></StackedTableCellHead> - </StackedTableRowHead> - </StackedTableHead> - <StackedTableBody> - <StackedTableRow> - <StackedTableCell>Onyekachukwu</StackedTableCell> - <StackedTableCell>Kariuki</StackedTableCell> - <StackedTableCell>02/06/2007</StackedTableCell> - <StackedTableCell>05/25/1972</StackedTableCell> - <StackedTableCell>66</StackedTableCell> - <StackedTableCell>Jawi</StackedTableCell> - <StackedTableCell>Sofie Hubert</StackedTableCell> - <StackedTableCell>Incomplete</StackedTableCell> - </StackedTableRow> - </StackedTableBody> - </StackedTable> - )) - .add('Long title', () => ( - <StackedTable> - <StackedTableHead> - <StackedTableRowHead> - <StackedTableCellHead> - This title is so long, it should be displayed in - multiple lines. Lorem ipsum dolor sit amet, consetetur - sadipscing elitr, sed diam nonumy eirmod tempor invidunt - ut labore et dolore magna aliquyam erat, sed diam - voluptua. At vero eos et accusam et justo duo dolores et - ea rebum. Stet clita kasd gubergren, no sea takimata - sanctus est Lorem ipsum dolor sit amet. Lorem ipsum - dolor sit amet, consetetur sadipscing elitr, sed diam - nonumy eirmod tempor invidunt ut labore et dolore magna - aliquyam erat, sed diam voluptua. At vero eos et accusam - et justo duo dolores et ea rebum. Stet clita kasd - gubergren, no sea takimata sanctus est Lorem ipsum dolor - sit amet. - </StackedTableCellHead> - </StackedTableRowHead> - </StackedTableHead> - <StackedTableBody> - <StackedTableRow> - <StackedTableCell>Onyekachukwu</StackedTableCell> - </StackedTableRow> - </StackedTableBody> - </StackedTable> - )) - .add('Custom cell', () => ( - <StackedTable> - <StackedTableHead> - <StackedTableRowHead> - <StackedTableCellHead colSpan="2"> - Name - </StackedTableCellHead> - <StackedTableCellHead>Incident date</StackedTableCellHead> - </StackedTableRowHead> - </StackedTableHead> - <StackedTableBody> - <StackedTableRow> - <CustomCell>Onyekachukwu</CustomCell> - <CustomCell>Kariuki</CustomCell> - <CustomCell>02/06/2007</CustomCell> - </StackedTableRow> - </StackedTableBody> - </StackedTable> - )) - .add('Custom cell title', () => ( - <StackedTable> - <StackedTableHead> - <StackedTableRowHead> - <StackedTableCellHead colSpan="2"> - Name - </StackedTableCellHead> - <StackedTableCellHead>Incident date</StackedTableCellHead> - </StackedTableRowHead> - </StackedTableHead> - <StackedTableBody> - <StackedTableRow> - <StackedTableCell>Onyekachukwu</StackedTableCell> - <StackedTableCell title="Custom title"> - Kariuki - </StackedTableCell> - <StackedTableCell>02/06/2007</StackedTableCell> - </StackedTableRow> - </StackedTableBody> - </StackedTable> - )) - .add('Larger table', () => ( - <StackedTable> - <StackedTableHead> - <StackedTableRowHead> - <StackedTableCellHead>First name</StackedTableCellHead> - <StackedTableCellHead>Last name</StackedTableCellHead> - <StackedTableCellHead>Incident date</StackedTableCellHead> - <StackedTableCellHead>Last updated</StackedTableCellHead> - <StackedTableCellHead>Age</StackedTableCellHead> - <StackedTableCellHead> - Registering unit - </StackedTableCellHead> - <StackedTableCellHead>Assigned user</StackedTableCellHead> - <StackedTableCellHead>Status</StackedTableCellHead> - </StackedTableRowHead> - </StackedTableHead> - <StackedTableBody> - <StackedTableRow> - <StackedTableCell>Onyekachukwu</StackedTableCell> - <StackedTableCell>Kariuki</StackedTableCell> - <StackedTableCell>02/06/2007</StackedTableCell> - <StackedTableCell>05/25/1972</StackedTableCell> - <StackedTableCell>66</StackedTableCell> - <StackedTableCell>Jawi</StackedTableCell> - <StackedTableCell>Sofie Hubert</StackedTableCell> - <StackedTableCell>Incomplete</StackedTableCell> - </StackedTableRow> - <StackedTableRow> - <StackedTableCell>Kwasi</StackedTableCell> - <StackedTableCell>Okafor</StackedTableCell> - <StackedTableCell>08/11/2010</StackedTableCell> - <StackedTableCell>02/26/1991</StackedTableCell> - <StackedTableCell>38</StackedTableCell> - <StackedTableCell>Mokassie MCHP</StackedTableCell> - <StackedTableCell>Dashonte Clarke</StackedTableCell> - <StackedTableCell>Complete</StackedTableCell> - </StackedTableRow> - <StackedTableRow> - <StackedTableCell>Siyabonga</StackedTableCell> - <StackedTableCell>Abiodun</StackedTableCell> - <StackedTableCell>07/21/1981</StackedTableCell> - <StackedTableCell>02/06/2007</StackedTableCell> - <StackedTableCell>98</StackedTableCell> - <StackedTableCell>Bathurst MCHP</StackedTableCell> - <StackedTableCell>Unassigned</StackedTableCell> - <StackedTableCell>Incomplete</StackedTableCell> - </StackedTableRow> - <StackedTableRow> - <StackedTableCell>Chiyembekezo</StackedTableCell> - <StackedTableCell>Okeke</StackedTableCell> - <StackedTableCell>01/23/1982</StackedTableCell> - <StackedTableCell>07/15/2003</StackedTableCell> - <StackedTableCell>2</StackedTableCell> - <StackedTableCell>Mayolla MCHP</StackedTableCell> - <StackedTableCell>Wan Gengxin</StackedTableCell> - <StackedTableCell>Incomplete</StackedTableCell> - </StackedTableRow> - <StackedTableRow> - <StackedTableCell>Mtendere</StackedTableCell> - <StackedTableCell>Afolayan</StackedTableCell> - <StackedTableCell>08/12/1994</StackedTableCell> - <StackedTableCell>05/12/1972</StackedTableCell> - <StackedTableCell>37</StackedTableCell> - <StackedTableCell>Gbangadu MCHP</StackedTableCell> - <StackedTableCell>Gvozden Boskovsky</StackedTableCell> - <StackedTableCell>Complete</StackedTableCell> - </StackedTableRow> - <StackedTableRow> - <StackedTableCell>Inyene</StackedTableCell> - <StackedTableCell>Okonkwo</StackedTableCell> - <StackedTableCell>04/01/1971</StackedTableCell> - <StackedTableCell>03/16/2000</StackedTableCell> - <StackedTableCell>70</StackedTableCell> - <StackedTableCell>Kunike Barina</StackedTableCell> - <StackedTableCell>Oscar de la Cavallería</StackedTableCell> - <StackedTableCell>Complete</StackedTableCell> - </StackedTableRow> - <StackedTableRow> - <StackedTableCell>Amaka</StackedTableCell> - <StackedTableCell>Pretorius</StackedTableCell> - <StackedTableCell>01/25/1996</StackedTableCell> - <StackedTableCell>09/15/1986</StackedTableCell> - <StackedTableCell>32</StackedTableCell> - <StackedTableCell>Bargbo</StackedTableCell> - <StackedTableCell>Alberto Raya</StackedTableCell> - <StackedTableCell>Incomplete</StackedTableCell> - </StackedTableRow> - <StackedTableRow> - <StackedTableCell>Meti</StackedTableCell> - <StackedTableCell>Abiodun</StackedTableCell> - <StackedTableCell>10/24/2010</StackedTableCell> - <StackedTableCell>07/26/1989</StackedTableCell> - <StackedTableCell>8</StackedTableCell> - <StackedTableCell>Majihun MCHP</StackedTableCell> - <StackedTableCell>Unassigned</StackedTableCell> - <StackedTableCell>Complete</StackedTableCell> - </StackedTableRow> - <StackedTableRow> - <StackedTableCell>Eshe</StackedTableCell> - <StackedTableCell>Okeke</StackedTableCell> - <StackedTableCell>01/31/1995</StackedTableCell> - <StackedTableCell>01/31/1995</StackedTableCell> - <StackedTableCell>63</StackedTableCell> - <StackedTableCell>Mambiama CHP</StackedTableCell> - <StackedTableCell>Shadrias Pearson</StackedTableCell> - <StackedTableCell>Incomplete</StackedTableCell> - </StackedTableRow> - <StackedTableRow> - <StackedTableCell>Obi</StackedTableCell> - <StackedTableCell>Okafor</StackedTableCell> - <StackedTableCell>06/07/1990</StackedTableCell> - <StackedTableCell>01/03/2006</StackedTableCell> - <StackedTableCell>28</StackedTableCell> - <StackedTableCell>Dalakuru CHP</StackedTableCell> - <StackedTableCell>Anatoliy Shcherbatykh</StackedTableCell> - <StackedTableCell>Incomplete</StackedTableCell> - </StackedTableRow> - </StackedTableBody> - <StackedTableFoot> - <StackedTableRow> - <StackedTableCell colSpan="8" hideTitle> - <Button primary>StackedTable footer button</Button> - </StackedTableCell> - </StackedTableRow> - </StackedTableFoot> - </StackedTable> - )) +export default { + title: 'Data Display/Stacked Table', + component: StackedTable, + parameters: { docs: { description: { component: description } } }, +} + +export const Default = args => ( + <StackedTable {...args}> + <StackedTableHead> + <StackedTableRowHead> + <StackedTableCellHead>First name</StackedTableCellHead> + <StackedTableCellHead>Last name</StackedTableCellHead> + <StackedTableCellHead>Incident date</StackedTableCellHead> + <StackedTableCellHead>Last updated</StackedTableCellHead> + <StackedTableCellHead>Age</StackedTableCellHead> + <StackedTableCellHead>Registering unit</StackedTableCellHead> + <StackedTableCellHead>Assigned user</StackedTableCellHead> + <StackedTableCellHead>Status</StackedTableCellHead> + </StackedTableRowHead> + </StackedTableHead> + <StackedTableBody> + <StackedTableRow> + <StackedTableCell>Onyekachukwu</StackedTableCell> + <StackedTableCell>Kariuki</StackedTableCell> + <StackedTableCell>02/06/2007</StackedTableCell> + <StackedTableCell>05/25/1972</StackedTableCell> + <StackedTableCell>66</StackedTableCell> + <StackedTableCell>Jawi</StackedTableCell> + <StackedTableCell>Sofie Hubert</StackedTableCell> + <StackedTableCell>Incomplete</StackedTableCell> + </StackedTableRow> + </StackedTableBody> + <StackedTableFoot> + <StackedTableRow> + <StackedTableCell colSpan="8" hideTitle> + <Button primary>StackedTable footer button</Button> + </StackedTableCell> + </StackedTableRow> + </StackedTableFoot> + </StackedTable> +) + +export const HiddenLabel = args => ( + <StackedTable {...args}> + <StackedTableHead> + <StackedTableRowHead> + <StackedTableCellHead>First name</StackedTableCellHead> + <StackedTableCellHead>Last name</StackedTableCellHead> + <StackedTableCellHead>Incident date</StackedTableCellHead> + <StackedTableCellHead>Last updated</StackedTableCellHead> + <StackedTableCellHead>Age</StackedTableCellHead> + <StackedTableCellHead>Registering unit</StackedTableCellHead> + <StackedTableCellHead>Assigned user</StackedTableCellHead> + <StackedTableCellHead /> + </StackedTableRowHead> + </StackedTableHead> + <StackedTableBody> + <StackedTableRow> + <StackedTableCell>Onyekachukwu</StackedTableCell> + <StackedTableCell>Kariuki</StackedTableCell> + <StackedTableCell>02/06/2007</StackedTableCell> + <StackedTableCell>05/25/1972</StackedTableCell> + <StackedTableCell>66</StackedTableCell> + <StackedTableCell>Jawi</StackedTableCell> + <StackedTableCell>Sofie Hubert</StackedTableCell> + <StackedTableCell> + <Button + primary + onClick={() => alert('something should happen now')} + > + A row action + </Button> + </StackedTableCell> + </StackedTableRow> + </StackedTableBody> + </StackedTable> +) + +export const HiddenLabelInCell = args => ( + <StackedTable {...args}> + <StackedTableHead> + <StackedTableRowHead> + <StackedTableCellHead>First name</StackedTableCellHead> + <StackedTableCellHead>Last name</StackedTableCellHead> + <StackedTableCellHead>Incident date</StackedTableCellHead> + <StackedTableCellHead>Last updated</StackedTableCellHead> + <StackedTableCellHead>Age</StackedTableCellHead> + <StackedTableCellHead>Registering unit</StackedTableCellHead> + <StackedTableCellHead>Assigned user</StackedTableCellHead> + <StackedTableCellHead>Status</StackedTableCellHead> + </StackedTableRowHead> + </StackedTableHead> + <StackedTableBody> + <StackedTableRow> + <StackedTableCell>Onyekachukwu</StackedTableCell> + <StackedTableCell>Kariuki</StackedTableCell> + <StackedTableCell>02/06/2007</StackedTableCell> + <StackedTableCell>05/25/1972</StackedTableCell> + <StackedTableCell>66</StackedTableCell> + <StackedTableCell>Jawi</StackedTableCell> + <StackedTableCell>Sofie Hubert</StackedTableCell> + <StackedTableCell hideTitle> + <Button + primary + onClick={() => alert('something should happen now')} + > + A row action + </Button> + </StackedTableCell> + </StackedTableRow> + <StackedTableRow> + <StackedTableCell>Onyekachukwu</StackedTableCell> + <StackedTableCell>Kariuki</StackedTableCell> + <StackedTableCell>02/06/2007</StackedTableCell> + <StackedTableCell>05/25/1972</StackedTableCell> + <StackedTableCell>66</StackedTableCell> + <StackedTableCell>Jawi</StackedTableCell> + <StackedTableCell>Sofie Hubert</StackedTableCell> + <StackedTableCell>Incomplete</StackedTableCell> + </StackedTableRow> + </StackedTableBody> + </StackedTable> +) + +export const ColspanInHeader = args => ( + <StackedTable {...args}> + <StackedTableHead> + <StackedTableRowHead> + <StackedTableCellHead colSpan="2">Name</StackedTableCellHead> + <StackedTableCellHead>Incident date</StackedTableCellHead> + <StackedTableCellHead>Last updated</StackedTableCellHead> + <StackedTableCellHead>Age</StackedTableCellHead> + <StackedTableCellHead>Registering unit</StackedTableCellHead> + <StackedTableCellHead>Assigned user</StackedTableCellHead> + <StackedTableCellHead>Status</StackedTableCellHead> + </StackedTableRowHead> + </StackedTableHead> + <StackedTableBody> + <StackedTableRow> + <StackedTableCell>Onyekachukwu</StackedTableCell> + <StackedTableCell>Kariuki</StackedTableCell> + <StackedTableCell>02/06/2007</StackedTableCell> + <StackedTableCell>05/25/1972</StackedTableCell> + <StackedTableCell>66</StackedTableCell> + <StackedTableCell>Jawi</StackedTableCell> + <StackedTableCell>Sofie Hubert</StackedTableCell> + <StackedTableCell>Incomplete</StackedTableCell> + </StackedTableRow> + </StackedTableBody> + </StackedTable> +) + +export const ColspanInBody = args => ( + <StackedTable {...args}> + <StackedTableHead> + <StackedTableRowHead> + <StackedTableCellHead>First name</StackedTableCellHead> + <StackedTableCellHead>Last name</StackedTableCellHead> + <StackedTableCellHead>Incident date</StackedTableCellHead> + <StackedTableCellHead>Last updated</StackedTableCellHead> + <StackedTableCellHead>Age</StackedTableCellHead> + <StackedTableCellHead>Registering unit</StackedTableCellHead> + <StackedTableCellHead>Assigned user</StackedTableCellHead> + <StackedTableCellHead>Status</StackedTableCellHead> + </StackedTableRowHead> + </StackedTableHead> + <StackedTableBody> + <StackedTableRow> + <StackedTableCell>Onyekachukwu</StackedTableCell> + <StackedTableCell>Kariuki</StackedTableCell> + <StackedTableCell colSpan="2"> + { + "Colspan 2 here. Next cell doesn't get header 'Last updated'." + } + </StackedTableCell> + <StackedTableCell>66</StackedTableCell> + <StackedTableCell>Jawi</StackedTableCell> + <StackedTableCell>Sofie Hubert</StackedTableCell> + <StackedTableCell>Incomplete</StackedTableCell> + </StackedTableRow> + <StackedTableRow> + <StackedTableCell>Onyekachukwu</StackedTableCell> + <StackedTableCell>Kariuki</StackedTableCell> + <StackedTableCell>02/06/2007</StackedTableCell> + <StackedTableCell>05/25/1972</StackedTableCell> + <StackedTableCell>66</StackedTableCell> + <StackedTableCell>Jawi</StackedTableCell> + <StackedTableCell>Sofie Hubert</StackedTableCell> + <StackedTableCell>Incomplete</StackedTableCell> + </StackedTableRow> + </StackedTableBody> + </StackedTable> +) + +export const MultipleHeaderRows = args => ( + <StackedTable {...args}> + <StackedTableHead> + <StackedTableRowHead> + <StackedTableCellHead colSpan="2">Name</StackedTableCellHead> + <StackedTableCellHead>Incident date</StackedTableCellHead> + <StackedTableCellHead>Last updated</StackedTableCellHead> + <StackedTableCellHead>Age</StackedTableCellHead> + <StackedTableCellHead>Registering unit</StackedTableCellHead> + <StackedTableCellHead>Assigned user</StackedTableCellHead> + <StackedTableCellHead>Status</StackedTableCellHead> + </StackedTableRowHead> + <StackedTableRowHead> + <StackedTableCellHead>First name</StackedTableCellHead> + <StackedTableCellHead>Last name</StackedTableCellHead> + <StackedTableCellHead colSpan="6"></StackedTableCellHead> + </StackedTableRowHead> + </StackedTableHead> + <StackedTableBody> + <StackedTableRow> + <StackedTableCell>Onyekachukwu</StackedTableCell> + <StackedTableCell>Kariuki</StackedTableCell> + <StackedTableCell>02/06/2007</StackedTableCell> + <StackedTableCell>05/25/1972</StackedTableCell> + <StackedTableCell>66</StackedTableCell> + <StackedTableCell>Jawi</StackedTableCell> + <StackedTableCell>Sofie Hubert</StackedTableCell> + <StackedTableCell>Incomplete</StackedTableCell> + </StackedTableRow> + </StackedTableBody> + </StackedTable> +) + +export const LongTitle = args => ( + <StackedTable {...args}> + <StackedTableHead> + <StackedTableRowHead> + <StackedTableCellHead> + This title is so long, it should be displayed in multiple + lines. Lorem ipsum dolor sit amet, consetetur sadipscing + elitr, sed diam nonumy eirmod tempor invidunt ut labore et + dolore magna aliquyam erat, sed diam voluptua. At vero eos + et accusam et justo duo dolores et ea rebum. Stet clita kasd + gubergren, no sea takimata sanctus est Lorem ipsum dolor sit + amet. Lorem ipsum dolor sit amet, consetetur sadipscing + elitr, sed diam nonumy eirmod tempor invidunt ut labore et + dolore magna aliquyam erat, sed diam voluptua. At vero eos + et accusam et justo duo dolores et ea rebum. Stet clita kasd + gubergren, no sea takimata sanctus est Lorem ipsum dolor sit + amet. + </StackedTableCellHead> + </StackedTableRowHead> + </StackedTableHead> + <StackedTableBody> + <StackedTableRow> + <StackedTableCell>Onyekachukwu</StackedTableCell> + </StackedTableRow> + </StackedTableBody> + </StackedTable> +) + +export const _CustomCell = args => ( + <StackedTable {...args}> + <StackedTableHead> + <StackedTableRowHead> + <StackedTableCellHead colSpan="2">Name</StackedTableCellHead> + <StackedTableCellHead>Incident date</StackedTableCellHead> + </StackedTableRowHead> + </StackedTableHead> + <StackedTableBody> + <StackedTableRow> + <CustomCell>Onyekachukwu</CustomCell> + <CustomCell>Kariuki</CustomCell> + <CustomCell>02/06/2007</CustomCell> + </StackedTableRow> + </StackedTableBody> + </StackedTable> +) + +export const CustomCellTitle = args => ( + <StackedTable {...args}> + <StackedTableHead> + <StackedTableRowHead> + <StackedTableCellHead colSpan="2">Name</StackedTableCellHead> + <StackedTableCellHead>Incident date</StackedTableCellHead> + </StackedTableRowHead> + </StackedTableHead> + <StackedTableBody> + <StackedTableRow> + <StackedTableCell>Onyekachukwu</StackedTableCell> + <StackedTableCell title="Custom title"> + Kariuki + </StackedTableCell> + <StackedTableCell>02/06/2007</StackedTableCell> + </StackedTableRow> + </StackedTableBody> + </StackedTable> +) + +export const LargerTable = args => ( + <StackedTable {...args}> + <StackedTableHead> + <StackedTableRowHead> + <StackedTableCellHead>First name</StackedTableCellHead> + <StackedTableCellHead>Last name</StackedTableCellHead> + <StackedTableCellHead>Incident date</StackedTableCellHead> + <StackedTableCellHead>Last updated</StackedTableCellHead> + <StackedTableCellHead>Age</StackedTableCellHead> + <StackedTableCellHead>Registering unit</StackedTableCellHead> + <StackedTableCellHead>Assigned user</StackedTableCellHead> + <StackedTableCellHead>Status</StackedTableCellHead> + </StackedTableRowHead> + </StackedTableHead> + <StackedTableBody> + <StackedTableRow> + <StackedTableCell>Onyekachukwu</StackedTableCell> + <StackedTableCell>Kariuki</StackedTableCell> + <StackedTableCell>02/06/2007</StackedTableCell> + <StackedTableCell>05/25/1972</StackedTableCell> + <StackedTableCell>66</StackedTableCell> + <StackedTableCell>Jawi</StackedTableCell> + <StackedTableCell>Sofie Hubert</StackedTableCell> + <StackedTableCell>Incomplete</StackedTableCell> + </StackedTableRow> + <StackedTableRow> + <StackedTableCell>Kwasi</StackedTableCell> + <StackedTableCell>Okafor</StackedTableCell> + <StackedTableCell>08/11/2010</StackedTableCell> + <StackedTableCell>02/26/1991</StackedTableCell> + <StackedTableCell>38</StackedTableCell> + <StackedTableCell>Mokassie MCHP</StackedTableCell> + <StackedTableCell>Dashonte Clarke</StackedTableCell> + <StackedTableCell>Complete</StackedTableCell> + </StackedTableRow> + <StackedTableRow> + <StackedTableCell>Siyabonga</StackedTableCell> + <StackedTableCell>Abiodun</StackedTableCell> + <StackedTableCell>07/21/1981</StackedTableCell> + <StackedTableCell>02/06/2007</StackedTableCell> + <StackedTableCell>98</StackedTableCell> + <StackedTableCell>Bathurst MCHP</StackedTableCell> + <StackedTableCell>Unassigned</StackedTableCell> + <StackedTableCell>Incomplete</StackedTableCell> + </StackedTableRow> + <StackedTableRow> + <StackedTableCell>Chiyembekezo</StackedTableCell> + <StackedTableCell>Okeke</StackedTableCell> + <StackedTableCell>01/23/1982</StackedTableCell> + <StackedTableCell>07/15/2003</StackedTableCell> + <StackedTableCell>2</StackedTableCell> + <StackedTableCell>Mayolla MCHP</StackedTableCell> + <StackedTableCell>Wan Gengxin</StackedTableCell> + <StackedTableCell>Incomplete</StackedTableCell> + </StackedTableRow> + <StackedTableRow> + <StackedTableCell>Mtendere</StackedTableCell> + <StackedTableCell>Afolayan</StackedTableCell> + <StackedTableCell>08/12/1994</StackedTableCell> + <StackedTableCell>05/12/1972</StackedTableCell> + <StackedTableCell>37</StackedTableCell> + <StackedTableCell>Gbangadu MCHP</StackedTableCell> + <StackedTableCell>Gvozden Boskovsky</StackedTableCell> + <StackedTableCell>Complete</StackedTableCell> + </StackedTableRow> + <StackedTableRow> + <StackedTableCell>Inyene</StackedTableCell> + <StackedTableCell>Okonkwo</StackedTableCell> + <StackedTableCell>04/01/1971</StackedTableCell> + <StackedTableCell>03/16/2000</StackedTableCell> + <StackedTableCell>70</StackedTableCell> + <StackedTableCell>Kunike Barina</StackedTableCell> + <StackedTableCell>Oscar de la Cavallería</StackedTableCell> + <StackedTableCell>Complete</StackedTableCell> + </StackedTableRow> + <StackedTableRow> + <StackedTableCell>Amaka</StackedTableCell> + <StackedTableCell>Pretorius</StackedTableCell> + <StackedTableCell>01/25/1996</StackedTableCell> + <StackedTableCell>09/15/1986</StackedTableCell> + <StackedTableCell>32</StackedTableCell> + <StackedTableCell>Bargbo</StackedTableCell> + <StackedTableCell>Alberto Raya</StackedTableCell> + <StackedTableCell>Incomplete</StackedTableCell> + </StackedTableRow> + <StackedTableRow> + <StackedTableCell>Meti</StackedTableCell> + <StackedTableCell>Abiodun</StackedTableCell> + <StackedTableCell>10/24/2010</StackedTableCell> + <StackedTableCell>07/26/1989</StackedTableCell> + <StackedTableCell>8</StackedTableCell> + <StackedTableCell>Majihun MCHP</StackedTableCell> + <StackedTableCell>Unassigned</StackedTableCell> + <StackedTableCell>Complete</StackedTableCell> + </StackedTableRow> + <StackedTableRow> + <StackedTableCell>Eshe</StackedTableCell> + <StackedTableCell>Okeke</StackedTableCell> + <StackedTableCell>01/31/1995</StackedTableCell> + <StackedTableCell>01/31/1995</StackedTableCell> + <StackedTableCell>63</StackedTableCell> + <StackedTableCell>Mambiama CHP</StackedTableCell> + <StackedTableCell>Shadrias Pearson</StackedTableCell> + <StackedTableCell>Incomplete</StackedTableCell> + </StackedTableRow> + <StackedTableRow> + <StackedTableCell>Obi</StackedTableCell> + <StackedTableCell>Okafor</StackedTableCell> + <StackedTableCell>06/07/1990</StackedTableCell> + <StackedTableCell>01/03/2006</StackedTableCell> + <StackedTableCell>28</StackedTableCell> + <StackedTableCell>Dalakuru CHP</StackedTableCell> + <StackedTableCell>Anatoliy Shcherbatykh</StackedTableCell> + <StackedTableCell>Incomplete</StackedTableCell> + </StackedTableRow> + </StackedTableBody> + <StackedTableFoot> + <StackedTableRow> + <StackedTableCell colSpan="8" hideTitle> + <Button primary>StackedTable footer button</Button> + </StackedTableCell> + </StackedTableRow> + </StackedTableFoot> + </StackedTable> +) diff --git a/packages/core/src/Switch/Switch.js b/packages/core/src/Switch/Switch.js index 31ccf687f2..87c8427590 100644 --- a/packages/core/src/Switch/Switch.js +++ b/packages/core/src/Switch/Switch.js @@ -1,6 +1,6 @@ -import propTypes from '@dhis2/prop-types' import { colors, theme, sharedPropTypes } from '@dhis2/ui-constants' import cx from 'classnames' +import PropTypes from 'prop-types' import React, { Component, createRef } from 'react' import { SwitchRegular } from '../Icons/index.js' ;('') // TODO: https://github.com/jsdoc/jsdoc/issues/1718 @@ -186,22 +186,34 @@ Switch.defaultProps = { * @prop {string} [dataTest] */ Switch.propTypes = { - checked: propTypes.bool, - className: propTypes.string, - dataTest: propTypes.string, - dense: propTypes.bool, - disabled: propTypes.bool, + checked: PropTypes.bool, + className: PropTypes.string, + dataTest: PropTypes.string, + /** Makes the switch smaller for information-dense layouts */ + dense: PropTypes.bool, + /** Disables the switch */ + disabled: PropTypes.bool, + /** Applies 'error' styles for validation feedback. Mutually exclusive with `valid` and `warning` prop types */ error: sharedPropTypes.statusPropType, - initialFocus: propTypes.bool, - label: propTypes.node, - name: propTypes.string, - tabIndex: propTypes.string, + /** Grab initial focus on the page */ + initialFocus: PropTypes.bool, + /** Label for the switch. Can be a string or an element, for example an image */ + label: PropTypes.node, + /** Name associated with the switch. Passed to event handlers in object */ + name: PropTypes.string, + tabIndex: PropTypes.string, + /** Applies 'valid' styles for validation feedback. Mutually exclusive with `error` and `warning` prop types */ valid: sharedPropTypes.statusPropType, - value: propTypes.string, + /** Value associated with the switch. Passed to event handlers in object */ + value: PropTypes.string, + /** Applies 'warning' styles for validation feedback. Mutually exclusive with `valid` and `error` prop types */ warning: sharedPropTypes.statusPropType, - onBlur: propTypes.func, - onChange: propTypes.func, - onFocus: propTypes.func, + /** Called with signature `({ name: string, value: string, checked: bool }, event)` */ + onBlur: PropTypes.func, + /** Called with signature `({ name: string, value: string, checked: bool }, event)` */ + onChange: PropTypes.func, + /** Called with signature `({ name: string, value: string, checked: bool }, event)` */ + onFocus: PropTypes.func, } export { Switch } diff --git a/packages/core/src/Switch/Switch.stories.js b/packages/core/src/Switch/Switch.stories.js index d377a8eda8..c0ac61c09a 100644 --- a/packages/core/src/Switch/Switch.stories.js +++ b/packages/core/src/Switch/Switch.stories.js @@ -1,7 +1,19 @@ -import { storiesOf } from '@storybook/react' +import { sharedPropTypes } from '@dhis2/ui-constants' import React from 'react' import { Switch } from './Switch.js' +const subtitle = 'An input control that allows an on and an off state' + +const description = ` +**Switches are used sparingly in DHIS2, as they are not yet an accepted input control on the web. Users are not always used to the concept of a switch, but understanding is growing with wide adoption on mobile platforms.** + +Use switches only when the user can toggle between on/off. Never use a switch for yes/no or any other states, use a checkbox instead. It is often safer to use a checkbox for things like turning options on/off, as users understand this pattern. Switches can be useful for ongoing or active processes, where turning them on/off makes more sense conceptually. An example of this may be toggling on/off 'Logging' or 'Update automatically', both processes that are ongoing. + +\`\`\`js +import { Switch } from '@dhis2/ui' +\`\`\` +` + window.onChange = (payload, event) => { console.log('onClick payload', payload) console.log('onClick event', event) @@ -21,352 +33,108 @@ const onChange = (...args) => window.onChange(...args) const onFocus = (...args) => window.onFocus(...args) const onBlur = (...args) => window.onBlur(...args) -storiesOf('Switch', module) - // Regular - .add('Default', () => ( - <Switch - name="Ex" - label="Switch" - value="default" - onChange={onChange} - onFocus={onFocus} - onBlur={onBlur} - /> - )) +export default { + title: 'Forms/Switch/Switch', + component: Switch, + parameters: { + componentSubtitle: subtitle, + docs: { description: { component: description } }, + }, + // Default args for all stories + args: { + name: 'exampleName', + label: 'Switch', + value: 'defaultValue', + onChange, + onFocus, + onBlur, + }, + argTypes: { + valid: { ...sharedPropTypes.statusArgType }, + error: { ...sharedPropTypes.statusArgType }, + warning: { ...sharedPropTypes.statusArgType }, + }, +} - .add('Focused unchecked', () => ( - <> - <Switch - initialFocus - name="Ex" - label="Switch" - value="default" - className="initially-focused" - onChange={onChange} - onFocus={onFocus} - onBlur={onBlur} - /> - <Switch - name="Ex2" - label="Switch" - value="default" - className="initially-unfocused" - onChange={onChange} - onFocus={onFocus} - onBlur={onBlur} - /> - </> - )) +const Template = args => <Switch {...args} /> - .add('Focused checked', () => ( - <> - <Switch - initialFocus - checked - name="Ex" - label="Switch" - value="default" - className="initially-focused" - onChange={onChange} - onFocus={onFocus} - onBlur={onBlur} - /> - <Switch - name="Ex2" - label="Switch" - value="default" - className="initially-unfocused" - onChange={onChange} - onFocus={onFocus} - onBlur={onBlur} - /> - </> - )) +const CheckedUncheckedTemplate = args => ( + <> + <Switch {...args} /> + <Switch {...args} checked /> + </> +) - .add('Checked', () => ( - <Switch - name="Ex" - label="Switch" - checked - value="checked" - onChange={onChange} - onFocus={onFocus} - onBlur={onBlur} - /> - )) +export const Default = Template.bind({}) - .add('Disabled', () => ( - <> - <Switch - name="Ex" - label="Switch" - disabled - value="disabled" - onChange={onChange} - onFocus={onFocus} - onBlur={onBlur} - /> - <Switch - name="Ex" - label="Switch" - disabled - checked - value="disabled" - onChange={onChange} - onFocus={onFocus} - onBlur={onBlur} - /> - </> - )) +export const FocusedUnchecked = args => ( + <> + <Switch {...args} initialFocus className="initially-focused" /> + <Switch {...args} className="initially-unfocused" /> + </> +) +// Stories with initial focus are distracting on docs page +FocusedUnchecked.parameters = { docs: { disable: true } } - .add('Valid', () => ( - <> - <Switch - name="Ex" - label="Switch" - valid - value="valid" - onChange={onChange} - onFocus={onFocus} - onBlur={onBlur} - /> - <Switch - name="Ex" - label="Switch" - valid - checked - value="valid" - onChange={onChange} - onFocus={onFocus} - onBlur={onBlur} - /> - </> - )) +export const FocusedChecked = FocusedUnchecked.bind({}) +FocusedChecked.args = { checked: true } +FocusedChecked.parameters = { docs: { disable: true } } - .add('Warning', () => ( - <> - <Switch - name="Ex" - label="Switch" - warning - value="warning" - onChange={onChange} - onFocus={onFocus} - onBlur={onBlur} - /> - <Switch - name="Ex" - label="Switch" - warning - checked - value="warning" - onChange={onChange} - onFocus={onFocus} - onBlur={onBlur} - /> - </> - )) +export const Checked = Template.bind({}) +Checked.args = { checked: true, value: 'checked' } - .add('Error', () => ( - <> - <Switch - name="Ex" - label="Switch" - error - value="error" - onChange={onChange} - onFocus={onFocus} - onBlur={onBlur} - /> - <Switch - name="Ex" - label="Switch" - error - checked - value="error" - onChange={onChange} - onFocus={onFocus} - onBlur={onBlur} - /> - </> - )) +export const Disabled = CheckedUncheckedTemplate.bind({}) +Disabled.args = { disabled: true, value: 'disabled' } - .add('Image label', () => ( - <Switch - name="Ex" - label={<img src="https://picsum.photos/id/82/200/100" />} - value="with-help" - onChange={onChange} - onFocus={onFocus} - onBlur={onBlur} - /> - )) +export const Valid = CheckedUncheckedTemplate.bind({}) +Valid.args = { valid: true, value: 'valid' } + +export const Warning = CheckedUncheckedTemplate.bind({}) +Warning.args = { warning: true, value: 'warning' } + +export const Error = CheckedUncheckedTemplate.bind({}) +Error.args = { error: true, value: 'error' } + +export const ImageLabel = Template.bind({}) +ImageLabel.args = { + label: <img src="https://picsum.photos/id/82/200/100" />, + value: 'with-help', +} - // Dense - .add('Default - Dense', () => ( - <Switch - dense - name="Ex" - label="Switch" - value="default" - onChange={onChange} - onFocus={onFocus} - onBlur={onBlur} - /> - )) +export const DefaultDense = Template.bind({}) +DefaultDense.args = { dense: true } +DefaultDense.storyName = 'Default - Dense' - .add('Focused unchecked - Dense', () => ( - <Switch - dense - initialFocus - name="Ex" - label="Switch" - value="default" - onChange={onChange} - onFocus={onFocus} - onBlur={onBlur} - /> - )) +export const FocusedUncheckedDense = FocusedUnchecked.bind({}) +FocusedUncheckedDense.args = { ...DefaultDense.args } +FocusedUncheckedDense.storyName = 'Focused unchecked - Dense' +FocusedUncheckedDense.parameters = { docs: { disable: true } } - .add('Focused checked - Dense', () => ( - <Switch - dense - initialFocus - checked - name="Ex" - label="Switch" - value="default" - onChange={onChange} - onFocus={onFocus} - onBlur={onBlur} - /> - )) +export const FocusedCheckedDense = FocusedUnchecked.bind({}) +FocusedCheckedDense.args = { ...DefaultDense.args, checked: true } +FocusedCheckedDense.storyName = 'Focused checked - Dense' +FocusedCheckedDense.parameters = { docs: { disable: true } } - .add('Checked - Dense', () => ( - <Switch - dense - name="Ex" - label="Switch" - checked - value="checked" - onChange={onChange} - onFocus={onFocus} - onBlur={onBlur} - /> - )) +export const CheckedDense = Template.bind({}) +CheckedDense.args = { ...Checked.args, ...DefaultDense.args } +CheckedDense.storyName = 'Checked - Dense' - .add('Disabled - Dense', () => ( - <> - <Switch - dense - name="Ex" - label="Switch" - disabled - value="disabled" - onChange={onChange} - onFocus={onFocus} - onBlur={onBlur} - /> - <Switch - dense - name="Ex" - label="Switch" - disabled - checked - value="disabled" - onChange={onChange} - onFocus={onFocus} - onBlur={onBlur} - /> - </> - )) +export const DisabledDense = CheckedUncheckedTemplate.bind({}) +DisabledDense.args = { ...Disabled.args, ...DefaultDense.args } +DisabledDense.storyName = 'Disabled - Dense' - .add('Valid - Dense', () => ( - <> - <Switch - dense - name="Ex" - label="Switch" - valid - value="valid" - onChange={onChange} - onFocus={onFocus} - onBlur={onBlur} - /> - <Switch - dense - name="Ex" - label="Switch" - valid - checked - value="valid" - onChange={onChange} - onFocus={onFocus} - onBlur={onBlur} - /> - </> - )) +export const ValidDense = CheckedUncheckedTemplate.bind({}) +ValidDense.args = { ...Valid.args, ...DefaultDense.args } +ValidDense.storyName = 'Valid - Dense' - .add('Warning - Dense', () => ( - <> - <Switch - dense - name="Ex" - label="Switch" - warning - value="warning" - onChange={onChange} - onFocus={onFocus} - onBlur={onBlur} - /> - <Switch - dense - name="Ex" - label="Switch" - warning - checked - value="warning" - onChange={onChange} - onFocus={onFocus} - onBlur={onBlur} - /> - </> - )) +export const WarningDense = CheckedUncheckedTemplate.bind({}) +WarningDense.args = { ...Warning.args, ...DefaultDense.args } +WarningDense.storyName = 'Warning - Dense' - .add('Error - Dense', () => ( - <> - <Switch - dense - name="Ex" - label="Switch" - error - value="error" - onChange={onChange} - onFocus={onFocus} - onBlur={onBlur} - /> - <Switch - dense - name="Ex" - label="Switch" - error - checked - value="error" - onChange={onChange} - onFocus={onFocus} - onBlur={onBlur} - /> - </> - )) +export const ErrorDense = CheckedUncheckedTemplate.bind({}) +ErrorDense.args = { ...Error.args, ...DefaultDense.args } +ErrorDense.storyName = 'Error - Dense' - .add('Image label - Dense', () => ( - <Switch - dense - name="Ex" - label={<img src="https://picsum.photos/id/82/200/100" />} - value="with-help" - onChange={onChange} - onFocus={onFocus} - onBlur={onBlur} - /> - )) +export const ImageLabelDense = Template.bind({}) +ImageLabelDense.args = { ...ImageLabel.args, ...DefaultDense.args } +ImageLabelDense.storyName = 'Image label - Dense' diff --git a/packages/core/src/Tab/Tab.js b/packages/core/src/Tab/Tab.js index 0c59aaa5fd..f28550040a 100644 --- a/packages/core/src/Tab/Tab.js +++ b/packages/core/src/Tab/Tab.js @@ -1,6 +1,6 @@ -import propTypes from '@dhis2/prop-types' import { colors, theme } from '@dhis2/ui-constants' import cx from 'classnames' +import PropTypes from 'prop-types' import React from 'react' ;('') // TODO: https://github.com/jsdoc/jsdoc/issues/1718 @@ -149,13 +149,15 @@ Tab.defaultProps = { * @prop {string} [dataTest] */ Tab.propTypes = { - children: propTypes.node, - className: propTypes.string, - dataTest: propTypes.string, - disabled: propTypes.bool, - icon: propTypes.element, - selected: propTypes.bool, - onClick: propTypes.func, + children: PropTypes.node, + className: PropTypes.string, + dataTest: PropTypes.string, + disabled: PropTypes.bool, + icon: PropTypes.element, + /** Indicates this tab is selected */ + selected: PropTypes.bool, + /** Called with the signature `({}, event)` */ + onClick: PropTypes.func, } export { Tab } diff --git a/packages/core/src/TabBar/TabBar.js b/packages/core/src/TabBar/TabBar.js index 6671c9be09..d97c700d0d 100644 --- a/packages/core/src/TabBar/TabBar.js +++ b/packages/core/src/TabBar/TabBar.js @@ -1,4 +1,4 @@ -import propTypes from '@dhis2/prop-types' +import PropTypes from 'prop-types' import React from 'react' import { ScrollBar } from './ScrollBar.js' import { Tabs } from './Tabs.js' @@ -51,11 +51,13 @@ TabBar.defaultProps = { * @prop {string} [dataTest] */ TabBar.propTypes = { - children: propTypes.node, - className: propTypes.string, - dataTest: propTypes.string, - fixed: propTypes.bool, - scrollable: propTypes.bool, + children: PropTypes.node, + className: PropTypes.string, + dataTest: PropTypes.string, + /** Fixed tabs fill the width of their container. If false (i.e. fluid), tabs take up an amount of space defined by the tab name. Fluid tabs should be used most of the time. */ + fixed: PropTypes.bool, + /** Enables horizontal scrolling for many tabs that don't fit the width of the container */ + scrollable: PropTypes.bool, } export { TabBar } diff --git a/packages/core/src/TabBar/TabBar.stories.js b/packages/core/src/TabBar/TabBar.stories.js index 76aba6af94..6cef9d75f4 100644 --- a/packages/core/src/TabBar/TabBar.stories.js +++ b/packages/core/src/TabBar/TabBar.stories.js @@ -1,9 +1,33 @@ -import { storiesOf } from '@storybook/react' import React from 'react' import { AttachFile } from '../Icons/index.js' import { Tab } from '../index.js' import { TabBar } from './TabBar.js' +const subtitle = 'Ssed to divide content into categories and/or sections' + +const description = ` +Use tabs to split related content into separate sections. + +- Each tab should contain content that relates to one another, but the content should not overlap. +- Tabs are especially useful for splitting up content that may be relevant to different user groups, instead of displaying overwhelming amounts of information on a single page. +- Do not use tabs to compare content. +- Do not use tabs for sequential content that needs to be done in order. +- Do not use tabs for content that needs to be viewed at the same time. +- The number of tabs is less important than splitting content into understandable, predictable groups. Do not group together unrelated content in order to reduce tab count. Users struggle more with unpredictable tabs than too many tabs. + +#### Naming + +Give tabs short, understandable names. Try to find a word or very short phrase that summarizes the content. If you cannot find a suitable word this may mean you are trying to fit too much content under a single tab. The content of a tab should be obvious from its name. + +For example: Do use "Legends" instead of "Set up legends", Do use "Data analysis" instead of "Options for analysis of data", + +Do not repeat a term across tabs. If tabs are used inside a 'Options' modal, it is enough to use tab names "Data", "Legend", "Style". Do not repeat 'options' for all, e.g. "Data options", "Legend options" etc. + +\`\`\`js +import { TabBar, Tab } from '@dhis2/ui' +\`\`\` +` + const Wrapper = fn => ( <div style={{ @@ -22,85 +46,105 @@ window.onClick = (payload, event) => { const onClick = (...args) => window.onClick(...args) -storiesOf('TabBar', module) - .addDecorator(Wrapper) - .add('Default (fluid)', () => ( - <TabBar> - <Tab onClick={onClick}>Tab A</Tab> - <Tab onClick={onClick}>Tab B</Tab> - <Tab onClick={onClick} selected> - Tab C - </Tab> - <Tab onClick={onClick}>Tab D</Tab> - <Tab onClick={onClick}>Tab E</Tab> - <Tab onClick={onClick}>Tab F</Tab> - <Tab onClick={onClick}>Tab G</Tab> - </TabBar> - )) - .add('Fixed - tabs fill content', () => ( - <TabBar fixed> - <Tab onClick={onClick}>Tab A</Tab> - <Tab onClick={onClick}>Tab B</Tab> - <Tab onClick={onClick} selected> - Tab C - </Tab> - <Tab onClick={onClick}>Tab D</Tab> - <Tab onClick={onClick}>Tab E</Tab> - <Tab onClick={onClick}>Tab F</Tab> - <Tab onClick={onClick}>Tab G</Tab> - </TabBar> - )) - .add('Tabs with scroller', () => ( - <TabBar scrollable> - <Tab onClick={onClick}>Tab A</Tab> - <Tab onClick={onClick}>Tab B</Tab> - <Tab onClick={onClick}>Tab C</Tab> - <Tab onClick={onClick}>Tab D</Tab> - <Tab onClick={onClick}>Tab E</Tab> - <Tab onClick={onClick}>Tab F</Tab> - <Tab onClick={onClick}>Tab G</Tab> - <Tab onClick={onClick}>Tab H</Tab> - <Tab onClick={onClick}>Tab I</Tab> - <Tab onClick={onClick}>Tab J</Tab> - <Tab onClick={onClick}>Tab K</Tab> - <Tab onClick={onClick}>Tab L</Tab> - <Tab onClick={onClick} selected> - Tab M - </Tab> - <Tab onClick={onClick}>Tab N</Tab> - <Tab onClick={onClick}>Tab O</Tab> - <Tab onClick={onClick}>Tab P</Tab> - <Tab onClick={onClick}>Tab Q</Tab> - <Tab onClick={onClick}>Tab R</Tab> - </TabBar> - )) - .add('Tab states', () => ( - <TabBar> - <Tab onClick={onClick}>Default</Tab> - <Tab onClick={onClick} selected> - Selected - </Tab> - <Tab disabled>Disabled</Tab> - <Tab onClick={onClick}> - Text overflow - This tab has a very long text and it exceeds the - maximum width of 320px - </Tab> - </TabBar> - )) - .add('Tab states - with icon', () => ( - <TabBar> - <Tab onClick={onClick} icon={<AttachFile />}> - Default - </Tab> - <Tab onClick={onClick} icon={<AttachFile />} selected> - Selected - </Tab> - <Tab icon={<AttachFile />} disabled> - Disabled - </Tab> - <Tab onClick={onClick} icon={<AttachFile />}> - Text overflow - This tab has a very long text and it exceeds the - maximum width of 320px - </Tab> - </TabBar> - )) +export default { + title: 'Navigation/Tab Bar', + component: TabBar, + subcomponents: { Tab }, + parameters: { + componentSubtitle: subtitle, + docs: { description: { component: description } }, + }, + decorators: [Wrapper], +} + +export const DefaultFluid = args => ( + <TabBar {...args}> + <Tab onClick={onClick}>Tab A</Tab> + <Tab onClick={onClick}>Tab B</Tab> + <Tab onClick={onClick} selected> + Tab C + </Tab> + <Tab onClick={onClick}>Tab D</Tab> + <Tab onClick={onClick}>Tab E</Tab> + <Tab onClick={onClick}>Tab F</Tab> + <Tab onClick={onClick}>Tab G</Tab> + </TabBar> +) +DefaultFluid.storyName = 'Default (fluid)' + +export const FixedTabsFillContent = args => ( + <TabBar {...args}> + <Tab onClick={onClick}>Tab A</Tab> + <Tab onClick={onClick}>Tab B</Tab> + <Tab onClick={onClick} selected> + Tab C + </Tab> + <Tab onClick={onClick}>Tab D</Tab> + <Tab onClick={onClick}>Tab E</Tab> + <Tab onClick={onClick}>Tab F</Tab> + <Tab onClick={onClick}>Tab G</Tab> + </TabBar> +) +FixedTabsFillContent.args = { fixed: true } +FixedTabsFillContent.storyName = 'Fixed - tabs fill content' + +export const TabsWithScroller = args => ( + <TabBar {...args}> + <Tab onClick={onClick}>Tab A</Tab> + <Tab onClick={onClick}>Tab B</Tab> + <Tab onClick={onClick}>Tab C</Tab> + <Tab onClick={onClick}>Tab D</Tab> + <Tab onClick={onClick}>Tab E</Tab> + <Tab onClick={onClick}>Tab F</Tab> + <Tab onClick={onClick}>Tab G</Tab> + <Tab onClick={onClick}>Tab H</Tab> + <Tab onClick={onClick}>Tab I</Tab> + <Tab onClick={onClick}>Tab J</Tab> + <Tab onClick={onClick}>Tab K</Tab> + <Tab onClick={onClick}>Tab L</Tab> + <Tab onClick={onClick} selected> + Tab M + </Tab> + <Tab onClick={onClick}>Tab N</Tab> + <Tab onClick={onClick}>Tab O</Tab> + <Tab onClick={onClick}>Tab P</Tab> + <Tab onClick={onClick}>Tab Q</Tab> + <Tab onClick={onClick}>Tab R</Tab> + </TabBar> +) +TabsWithScroller.args = { scrollable: true } +TabsWithScroller.storyName = 'Tabs with scroller' + +export const TabStates = args => ( + <TabBar {...args}> + <Tab onClick={onClick}>Default</Tab> + <Tab onClick={onClick} selected> + Selected + </Tab> + <Tab disabled>Disabled</Tab> + <Tab onClick={onClick}> + Text overflow - This tab has a very long text and it exceeds the + maximum width of 320px + </Tab> + </TabBar> +) +TabStates.storyName = 'Tab states' + +export const TabStatesWithIcon = args => ( + <TabBar {...args}> + <Tab onClick={onClick} icon={<AttachFile />}> + Default + </Tab> + <Tab onClick={onClick} icon={<AttachFile />} selected> + Selected + </Tab> + <Tab icon={<AttachFile />} disabled> + Disabled + </Tab> + <Tab onClick={onClick} icon={<AttachFile />}> + Text overflow - This tab has a very long text and it exceeds the + maximum width of 320px + </Tab> + </TabBar> +) +TabStatesWithIcon.storyName = 'Tab states - with icon' diff --git a/packages/core/src/Table/Table.js b/packages/core/src/Table/Table.js index ee74153903..5d4dc490ce 100644 --- a/packages/core/src/Table/Table.js +++ b/packages/core/src/Table/Table.js @@ -1,4 +1,4 @@ -import propTypes from '@dhis2/prop-types' +import PropTypes from 'prop-types' import React from 'react' import css from 'styled-jsx/css' import { Provider } from './TableContext.js' @@ -51,9 +51,11 @@ Table.defaultProps = { * @prop {bool} [suppressZebraStriping] */ Table.propTypes = { - children: propTypes.node, - className: propTypes.string, - dataTest: propTypes.string, - role: propTypes.string, - suppressZebraStriping: propTypes.bool, + /** Should be `<TableHead>`, `<TableBody>`, and `<TableFoot>` components */ + children: PropTypes.node, + className: PropTypes.string, + dataTest: PropTypes.string, + role: PropTypes.string, + /** Remove the default striping on alternating rows */ + suppressZebraStriping: PropTypes.bool, } diff --git a/packages/core/src/Table/Table.stories.js b/packages/core/src/Table/Table.stories.js index fde16a5c5e..f553bc4c35 100644 --- a/packages/core/src/Table/Table.stories.js +++ b/packages/core/src/Table/Table.stories.js @@ -9,6 +9,25 @@ import { TableHead } from './TableHead.js' import { TableRow } from './TableRow.js' import { TableRowHead } from './TableRowHead.js' +const subtitle = 'Used to display information in a standard, effective way.' + +const description = ` +Should be used with multiple Table-related child components - see the table and examples below. + +\`\`\`js +import { + Table, + TableBody, + TableCell, + TableCellHead, + TableFoot, + TableHead, + TableRow, + TableRowHead, +} from '@dhis2/ui' +\`\`\` +` + const TableFooterButton = () => ( <div> <Button primary>Table footer button</Button> @@ -24,10 +43,27 @@ const TableFooterButton = () => ( const TableButton = () => <Button primary>Table button</Button> -export default { title: 'Table' } +export default { + title: 'Data Display/Table', + component: Table, + // Add subcomponents to the args table + subcomponents: { + TableHead, + TableBody, + TableFoot, + TableRowHead, + TableCellHead, + TableRow, + TableCell, + }, + parameters: { + componentSubtitle: subtitle, + docs: { description: { component: description } }, + }, +} -export const StaticLayout = () => ( - <Table> +export const StaticLayout = args => ( + <Table {...args}> <TableHead> <TableRowHead> <TableCellHead>First name</TableCellHead> @@ -152,8 +188,8 @@ export const StaticLayout = () => ( </Table> ) -export const OneDenseCell = () => ( - <Table> +export const OneDenseCell = args => ( + <Table {...args}> <TableHead> <TableRowHead> <TableCellHead>First name</TableCellHead> @@ -298,8 +334,8 @@ export const OneDenseCell = () => ( </Table> ) -export const NoAlternatingBgColor = () => ( - <Table suppressZebraStriping> +export const NoAlternatingBgColor = args => ( + <Table {...args}> <TableHead> <TableRowHead> <TableCellHead>Name</TableCellHead> @@ -427,6 +463,7 @@ export const NoAlternatingBgColor = () => ( </TableFoot> </Table> ) +NoAlternatingBgColor.args = { suppressZebraStriping: true } export const CustomAlternatingBgColor = () => ( <Table> diff --git a/packages/core/src/Table/TableBody.js b/packages/core/src/Table/TableBody.js index b21a11d4aa..8c35445181 100644 --- a/packages/core/src/Table/TableBody.js +++ b/packages/core/src/Table/TableBody.js @@ -1,4 +1,4 @@ -import propTypes from '@dhis2/prop-types' +import PropTypes from 'prop-types' import React from 'react' /** @@ -27,8 +27,9 @@ TableBody.defaultProps = { * @prop {string} [dataTest] */ TableBody.propTypes = { - children: propTypes.node, - className: propTypes.string, - dataTest: propTypes.string, - role: propTypes.string, + /** Should be `<TableRow>` components */ + children: PropTypes.node, + className: PropTypes.string, + dataTest: PropTypes.string, + role: PropTypes.string, } diff --git a/packages/core/src/Table/TableCell.js b/packages/core/src/Table/TableCell.js index d775d86036..55f5e6f930 100644 --- a/packages/core/src/Table/TableCell.js +++ b/packages/core/src/Table/TableCell.js @@ -1,5 +1,5 @@ -import propTypes from '@dhis2/prop-types' import cx from 'classnames' +import PropTypes from 'prop-types' import React from 'react' import css from 'styled-jsx/css' @@ -63,11 +63,12 @@ TableCell.defaultProps = { * @prop {string} [dataTest] */ TableCell.propTypes = { - children: propTypes.node, - className: propTypes.string, - colSpan: propTypes.string, - dataTest: propTypes.string, - dense: propTypes.bool, - role: propTypes.string, - rowSpan: propTypes.string, + children: PropTypes.node, + className: PropTypes.string, + colSpan: PropTypes.string, + dataTest: PropTypes.string, + /** Usees less padding and height for information-dense layouts */ + dense: PropTypes.bool, + role: PropTypes.string, + rowSpan: PropTypes.string, } diff --git a/packages/core/src/Table/TableCellHead.js b/packages/core/src/Table/TableCellHead.js index 6aab683552..1f25143786 100644 --- a/packages/core/src/Table/TableCellHead.js +++ b/packages/core/src/Table/TableCellHead.js @@ -1,5 +1,5 @@ -import propTypes from '@dhis2/prop-types' import cx from 'classnames' +import PropTypes from 'prop-types' import React from 'react' import css from 'styled-jsx/css' @@ -63,11 +63,12 @@ TableCellHead.defaultProps = { * @prop {string} [dataTest] */ TableCellHead.propTypes = { - children: propTypes.node, - className: propTypes.string, - colSpan: propTypes.string, - dataTest: propTypes.string, - dense: propTypes.bool, - role: propTypes.string, - rowSpan: propTypes.string, + children: PropTypes.node, + className: PropTypes.string, + colSpan: PropTypes.string, + dataTest: PropTypes.string, + /** Uses less padding and height for information-dense layouts */ + dense: PropTypes.bool, + role: PropTypes.string, + rowSpan: PropTypes.string, } diff --git a/packages/core/src/Table/TableFoot.js b/packages/core/src/Table/TableFoot.js index 8a9f4ca7c4..0248113e1b 100644 --- a/packages/core/src/Table/TableFoot.js +++ b/packages/core/src/Table/TableFoot.js @@ -1,4 +1,4 @@ -import propTypes from '@dhis2/prop-types' +import PropTypes from 'prop-types' import React from 'react' /** @@ -27,8 +27,9 @@ TableFoot.defaultProps = { * @prop {string} [dataTest] */ TableFoot.propTypes = { - children: propTypes.node, - className: propTypes.string, - dataTest: propTypes.string, - role: propTypes.string, + /** Should be `<TableRow>` components */ + children: PropTypes.node, + className: PropTypes.string, + dataTest: PropTypes.string, + role: PropTypes.string, } diff --git a/packages/core/src/Table/TableHead.js b/packages/core/src/Table/TableHead.js index 11e12ed2cd..6f31617781 100644 --- a/packages/core/src/Table/TableHead.js +++ b/packages/core/src/Table/TableHead.js @@ -1,4 +1,4 @@ -import propTypes from '@dhis2/prop-types' +import PropTypes from 'prop-types' import React from 'react' /** @@ -27,8 +27,9 @@ TableHead.defaultProps = { * @prop {string} [dataTest] */ TableHead.propTypes = { - children: propTypes.node, - className: propTypes.string, - dataTest: propTypes.string, - role: propTypes.string, + /** Should be `<TableRowHead>` components */ + children: PropTypes.node, + className: PropTypes.string, + dataTest: PropTypes.string, + role: PropTypes.string, } diff --git a/packages/core/src/Table/TableRow.js b/packages/core/src/Table/TableRow.js index bc31c8445b..0d89e3f13d 100644 --- a/packages/core/src/Table/TableRow.js +++ b/packages/core/src/Table/TableRow.js @@ -1,5 +1,5 @@ -import propTypes from '@dhis2/prop-types' import cx from 'classnames' +import PropTypes from 'prop-types' import React, { useContext } from 'react' import css from 'styled-jsx/css' import { TableContext } from './TableContext' @@ -59,9 +59,11 @@ TableRow.defaultProps = { * @prop {string} [dataTest] */ TableRow.propTypes = { - children: propTypes.node, - className: propTypes.string, - dataTest: propTypes.string, - role: propTypes.string, - suppressZebraStriping: propTypes.bool, + /** Should be `<TableCell>` or `<TableCellHead>` components */ + children: PropTypes.node, + className: PropTypes.string, + dataTest: PropTypes.string, + role: PropTypes.string, + /** Disables the default row striping for this row */ + suppressZebraStriping: PropTypes.bool, } diff --git a/packages/core/src/Table/TableRowHead.js b/packages/core/src/Table/TableRowHead.js index ab3583f6a1..1437b7a563 100644 --- a/packages/core/src/Table/TableRowHead.js +++ b/packages/core/src/Table/TableRowHead.js @@ -1,4 +1,4 @@ -import propTypes from '@dhis2/prop-types' +import PropTypes from 'prop-types' import React from 'react' import { TableRow } from './TableRow.js' ;('') // TODO: https://github.com/jsdoc/jsdoc/issues/1718 @@ -40,9 +40,11 @@ TableRowHead.defaultProps = { * @prop {string} [dataTest] */ TableRowHead.propTypes = { - children: propTypes.node, - className: propTypes.string, - dataTest: propTypes.string, - role: propTypes.string, - suppressZebraStriping: propTypes.bool, + /** Should be `<TableCellHead>` components */ + children: PropTypes.node, + className: PropTypes.string, + dataTest: PropTypes.string, + role: PropTypes.string, + /** Disables the default row striping for this row */ + suppressZebraStriping: PropTypes.bool, } diff --git a/packages/core/src/Tag/Tag.js b/packages/core/src/Tag/Tag.js index d0e73f9fd2..70839fb5de 100644 --- a/packages/core/src/Tag/Tag.js +++ b/packages/core/src/Tag/Tag.js @@ -1,6 +1,7 @@ import propTypes from '@dhis2/prop-types' import { colors } from '@dhis2/ui-constants' import cx from 'classnames' +import PropTypes from 'prop-types' import React from 'react' import { TagIcon } from './TagIcon.js' import { TagText } from './TagText.js' @@ -98,7 +99,7 @@ export const Tag = ({ const tagVariantPropType = propTypes.mutuallyExclusive( ['neutral', 'positive', 'negative'], - propTypes.bool + PropTypes.bool ) Tag.defaultProps = { @@ -121,12 +122,17 @@ Tag.defaultProps = { */ Tag.propTypes = { - bold: propTypes.bool, - children: propTypes.string, - className: propTypes.string, - dataTest: propTypes.string, - icon: propTypes.node, + /** Use bold tags where it is important that the tag is seen by the user in an information dense interface. Bold tags should be reserved for edge cases and not overused. */ + bold: PropTypes.bool, + children: PropTypes.string, + className: PropTypes.string, + dataTest: PropTypes.string, + /** Tags can contain icons. Use icons where they will help users easily identify the content of the tag. Tags must have a text label and cannot display only an icon. */ + icon: PropTypes.node, + /** Red 'negative' tags imply an error or a problem. `neutral`, `positive`, and `negative` are mutually exclusive props */ negative: tagVariantPropType, + /** Blue 'neutral' tags are used when a tag _could_ have valid or error status but is currently neutral. `neutral`, `positive`, and `negative` are mutually exclusive props */ neutral: tagVariantPropType, + /** Green 'valid' tags should be used to indicate validity or success. `neutral`, `positive`, and `negative` are mutually exclusive props */ positive: tagVariantPropType, } diff --git a/packages/core/src/Tag/Tag.stories.js b/packages/core/src/Tag/Tag.stories.js index 4da6960965..c999083ade 100644 --- a/packages/core/src/Tag/Tag.stories.js +++ b/packages/core/src/Tag/Tag.stories.js @@ -1,29 +1,43 @@ import React from 'react' import { Tag } from './Tag.js' -export default { title: 'Tag', component: Tag } +const subtitle = + 'Used to display categorizing labels or information for other elements in a collection.' -export const Default = () => <Tag>Dog</Tag> +const description = ` +Tags are used whenever an element in a collection needs to display its category or status. Tags should not be used for one-off, unique information. Tags can be displayed in any kind of component. -export const WithIcon = () => <Tag icon={<ExampleIcon />}>Dog</Tag> +Tags are useful when displaying multiple elements in a collection that have the same basic attributes but belong to different categories or have different statuses. Do not use tags for elements that will always be the same, instead use a heading or other grouping method. -export const Neutral = () => <Tag neutral>Dog</Tag> +Tags are never used for primary interaction and should not be used as buttons. Clicking a tag could sort a collection by that tag, or open a page to display all elements that have that tag type. Tags should not be used as navigation elements. -export const Positive = () => <Tag positive>Dog</Tag> +\`\`\`js +import { Tag } from '@dhis2/ui' +\`\`\` -export const Negative = () => <Tag negative>Dog</Tag> +` -export const Bold = () => <Tag bold>Dog</Tag> +const tagArgType = { + table: { type: { summary: 'bool' } }, + control: { type: 'boolean' }, +} -export const WithClippedOversizedIcon = () => ( - <Tag icon={<ExampleLargeIcon />}>Dog</Tag> -) - -export const WithClippedLongText = () => ( - <Tag icon={<ExampleIcon />}> - I am long text, therefore I get clipped before I finish - </Tag> -) +export default { + title: 'Data Display/Tag', + component: Tag, + parameters: { + componentSubtitle: subtitle, + docs: { description: { component: description } }, + }, + argTypes: { + negative: { ...tagArgType }, + neutral: { ...tagArgType }, + positive: { ...tagArgType }, + }, + args: { + children: 'Dog', + }, +} const ExampleIcon = () => ( <svg @@ -55,3 +69,31 @@ const ExampleLargeIcon = () => ( /> </svg> ) + +const Template = args => <Tag {...args} /> + +export const Default = Template.bind({}) + +export const WithIcon = Template.bind({}) +WithIcon.args = { icon: <ExampleIcon /> } + +export const Neutral = Template.bind({}) +Neutral.args = { neutral: true } + +export const Positive = Template.bind({}) +Positive.args = { positive: true } + +export const Negative = Template.bind({}) +Negative.args = { negative: true } + +export const Bold = Template.bind({}) +Bold.args = { bold: true } + +export const WithClippedOversizedIcon = Template.bind({}) +WithClippedOversizedIcon.args = { icon: <ExampleLargeIcon /> } + +export const WithClippedLongText = Template.bind({}) +WithClippedLongText.args = { + icon: <ExampleIcon />, + children: 'I am long text, therefore I get clipped before I finish', +} diff --git a/packages/core/src/TextArea/TextArea.js b/packages/core/src/TextArea/TextArea.js index e1e248cc47..2d28b5792d 100644 --- a/packages/core/src/TextArea/TextArea.js +++ b/packages/core/src/TextArea/TextArea.js @@ -1,6 +1,6 @@ -import propTypes from '@dhis2/prop-types' import { sharedPropTypes } from '@dhis2/ui-constants' import cx from 'classnames' +import PropTypes from 'prop-types' import React, { Component } from 'react' import { StatusIcon } from '../Icons/index.js' import { styles } from './TextArea.styles.js' @@ -216,25 +216,43 @@ TextArea.defaultProps = { * @prop {string} [dataTest] */ TextArea.propTypes = { - autoGrow: propTypes.bool, - className: propTypes.string, - dataTest: propTypes.string, - dense: propTypes.bool, - disabled: propTypes.bool, + /** Grow the text area in response to overflow instead of adding a scroll bar */ + autoGrow: PropTypes.bool, + className: PropTypes.string, + dataTest: PropTypes.string, + /** Compact mode */ + dense: PropTypes.bool, + /** Disables the textarea and makes in non-interactive */ + disabled: PropTypes.bool, + /** Applies 'error' styles for validation feedback. Mutually exclusive with `valid` and `warning` props */ error: sharedPropTypes.statusPropType, - initialFocus: propTypes.bool, - loading: propTypes.bool, - name: propTypes.string, - placeholder: propTypes.string, - readOnly: propTypes.bool, - resize: propTypes.oneOf(['none', 'both', 'horizontal', 'vertical']), - rows: propTypes.number, - tabIndex: propTypes.string, + /** Grabs initial focus on the page */ + initialFocus: PropTypes.bool, + /** Adds a loading spinner */ + loading: PropTypes.bool, + /** Name associated with the text area. Passed in object argument to event handlers. */ + name: PropTypes.string, + /** Placeholder text for an empty textarea */ + placeholder: PropTypes.string, + /** Makes the textarea read-only */ + readOnly: PropTypes.bool, + /** [Resize property](https://developer.mozilla.org/en-US/docs/Web/CSS/resize) for the textarea element */ + resize: PropTypes.oneOf(['none', 'both', 'horizontal', 'vertical']), + /** Initial height of the textarea, in lines of text */ + rows: PropTypes.number, + tabIndex: PropTypes.string, + /** Applies 'valid' styles for validation feedback. Mutually exclusive with `warning` and `error` props */ valid: sharedPropTypes.statusPropType, - value: propTypes.string, + /** Value in the textarea. Can be used to control component (recommended). Passed in object argument to event handlers. */ + value: PropTypes.string, + /** Applies 'warning' styles for validation feedback. Mutually exclusive with `valid` and `error` props */ warning: sharedPropTypes.statusPropType, - width: propTypes.string, - onBlur: propTypes.func, - onChange: propTypes.func, - onFocus: propTypes.func, + /** Width of the text area. Can be any valid CSS measurement */ + width: PropTypes.string, + /** Called with signature ({ name: string, value: string }, event) */ + onBlur: PropTypes.func, + /** Called with signature ({ name: string, value: string }, event) */ + onChange: PropTypes.func, + /** Called with signature ({ name: string, value: string }, event) */ + onFocus: PropTypes.func, } diff --git a/packages/core/src/TextArea/TextArea.stories.js b/packages/core/src/TextArea/TextArea.stories.js index 54df4241d1..cec1be0792 100644 --- a/packages/core/src/TextArea/TextArea.stories.js +++ b/packages/core/src/TextArea/TextArea.stories.js @@ -1,7 +1,21 @@ -import { storiesOf } from '@storybook/react' +import { sharedPropTypes } from '@dhis2/ui-constants' import React from 'react' import { TextArea } from './TextArea.js' +const description = ` +A textarea allows multiple lines of text input. Use a textarea wherever a user needs to input a lot of information. Do not use a textarea if a short, single line of content is expected. + +Options for textarea inputs are: + +- Rows: the height of the input, defined by the number of rows of text +- Resizable: whether the textarea can be resized by the user or not. Can be set for both width and height. +- Autoheight: if enabled, the texarea will grow in height to adapt to the content. + +\`\`\`js +import { TextArea } from '@dhis2/ui' +\`\`\` +` + window.onChange = (payload, event) => { console.log('onChange payload', payload) console.log('onChange event', event) @@ -21,260 +35,178 @@ const onChange = (...args) => window.onChange(...args) const onFocus = (...args) => window.onFocus(...args) const onBlur = (...args) => window.onBlur(...args) -storiesOf('TextArea', module) - .add('Default', () => ( - <TextArea - onChange={onChange} - onFocus={onFocus} - onBlur={onBlur} - name="textarea" - /> - )) +export default { + title: 'Forms/Text Area/Text Area', + component: TextArea, + parameters: { docs: { description: { component: description } } }, + argTypes: { + valid: { ...sharedPropTypes.statusArgType }, + error: { ...sharedPropTypes.statusArgType }, + warning: { ...sharedPropTypes.statusArgType }, + }, + args: { + name: 'textAreaName', + onChange, + onFocus, + onBlur, + }, +} - .add('Placeholder, no value', () => ( - <TextArea - onChange={onChange} - onFocus={onFocus} - onBlur={onBlur} - name="textarea" - placeholder="Hold the place" - /> - )) +const Template = args => <TextArea {...args} /> - .add('With value', () => ( - <TextArea - onChange={onChange} - onFocus={onFocus} - onBlur={onBlur} - name="textarea" - value="This is set through the value prop, which means the component is controlled." - /> - )) +export const Default = Template.bind({}) - .add('Focus', () => ( - <> - <TextArea - onChange={onChange} - onFocus={onFocus} - onBlur={onBlur} - name="textarea" - initialFocus - className="initially-focused" - /> - <TextArea - onChange={onChange} - onFocus={onFocus} - onBlur={onBlur} - name="textarea" - className="initially-unfocused" - /> - </> - )) +export const PlaceholderNoValue = Template.bind({}) +PlaceholderNoValue.args = { placeholder: 'Hold the place' } +PlaceholderNoValue.storyName = 'Placeholder, no value' - .add('Status: Valid', () => ( - <TextArea - onChange={onChange} - onFocus={onFocus} - onBlur={onBlur} - name="textarea" - value="This value is valid" - valid - /> - )) +export const WithValue = Template.bind({}) +WithValue.args = { + value: + 'This is set through the value prop, which means the component is controlled.', +} +WithValue.storyName = 'With value' + +export const Focus = args => ( + <> + <TextArea {...args} initialFocus className="initially-focused" /> + <TextArea {...args} className="initially-unfocused" /> + </> +) +Focus.parameters = { docs: { disable: true } } + +export const StatusValid = Template.bind({}) +StatusValid.args = { valid: true, value: 'This value is valid' } +StatusValid.storyName = 'Status: Valid' + +export const StatusWarning = Template.bind({}) +StatusWarning.args = { warning: true, value: 'This value produces a warning' } +StatusWarning.storyName = 'Status: Warning' + +export const StatusError = Template.bind({}) +StatusError.args = { + error: true, + value: 'This value produces an error', + helpText: 'This is some help text to advise what this input actually is.', + validationText: 'This describes the error, if a message is supplied.', +} +StatusError.storyName = 'Status: Error' + +export const StatusLoading = Template.bind({}) +StatusLoading.args = { + loading: true, + value: 'This value produces a loadingn state', +} +StatusLoading.storyName = 'Status: Loading' + +export const Disabled = Template.bind({}) +Disabled.args = { disabled: true, value: 'This field is disabled' } + +export const ReadOnly = Template.bind({}) +ReadOnly.args = { readOnly: true, value: 'This field is readOnly' } + +export const Dense = Template.bind({}) +Dense.args = { dense: true, value: 'This field is dense' } + +export const TextareaTextOverflow = Template.bind({}) +TextareaTextOverflow.args = { + label: 'I have a scrollbar', + value: [ + 'A line of text', + 'A line of text', + 'A line of text', + 'A line of text', + 'A line of text', + 'A line of text', + 'A line of text', + 'A line of text', + 'A line of text', + 'A line of text', + 'A line of text', + 'A line of text', + 'A line of text', + 'A line of text', + 'A line of text', + ].join('\n'), +} + +export const Rows = Template.bind({}) +Rows.args = { + rows: 8, + label: 'You can set the height with the rows prop. I have 8', +} - .add('Status: Warning', () => ( +export const Resize = args => ( + <> <TextArea - onChange={onChange} - onFocus={onFocus} - onBlur={onBlur} - name="textarea" - value="This value produces a warning" - warning + {...args} + name="textarea1" + label="Resize: vertical (default)" /> - )) - - .add('Status: Error', () => ( <TextArea - onChange={onChange} - onFocus={onFocus} - onBlur={onBlur} - name="textarea" - error - value="This value produces an error" - helpText="This is some help text to advice what this input actually is." - validationText="This describes the error, if a message is supplied." + {...args} + name="textarea2" + label="Resize: none" + resize="none" /> - )) - - .add('Status: Loading', () => ( <TextArea - onChange={onChange} - onFocus={onFocus} - onBlur={onBlur} - name="textarea" - value="This value produces a loading state" - loading + {...args} + name="textarea3" + label="Resize: both" + resize="both" /> - )) - - .add('Disabled', () => ( <TextArea - onChange={onChange} - onFocus={onFocus} - onBlur={onBlur} - name="textarea" - value="This field is disabled" - disabled + {...args} + name="textarea4" + label="Resize: horizontal" + resize="horizontal" /> - )) + </> +) - .add('Read only', () => ( +export const Autogrow = args => ( + <> <TextArea - onChange={onChange} - onFocus={onFocus} - onBlur={onBlur} - name="textarea" - value="This field is readOnly" - readOnly + {...args} + name="textarea1" + label="Autogrow step 1" + autoGrow + rows={2} + value="This TextArea has a height of 2 rows" /> - )) - - .add('Dense', () => ( <TextArea - onChange={onChange} - onFocus={onFocus} - onBlur={onBlur} - name="textarea" - value="This field is dense" - dense + {...args} + name="textarea2" + label="Autogrow step 2" + autoGrow + rows={2} + value={[ + 'This TextArea has a height of two rows', + 'it also has autoGrow set to true so it will grow with the content', + ].join('\n')} /> - )) - - .add('Textarea text overflow', () => ( <TextArea - onChange={onChange} - onFocus={onFocus} - onBlur={onBlur} - name="textarea" - label="I have a scrollbar" + {...args} + name="textarea3" + label="Autogrow step 3" + autoGrow + rows={2} value={[ - 'A line of text', - 'A line of text', - 'A line of text', - 'A line of text', - 'A line of text', - 'A line of text', - 'A line of text', - 'A line of text', - 'A line of text', - 'A line of text', - 'A line of text', - 'A line of text', - 'A line of text', - 'A line of text', - 'A line of text', + 'This TextArea has a height of two rows', + 'it also has autoGrow set to true so it will grow with the content.', + 'See: rows is still 2, but I now have 3 lines.', ].join('\n')} /> - )) - - .add('Rows', () => ( <TextArea - onChange={onChange} - onFocus={onFocus} - onBlur={onBlur} - name="textarea" - label="You can set the height with the rows prop, I have 8" - rows={8} + {...args} + name="textarea4" + label="Autogrow step 4" + value={[ + 'This TextArea has a height of two rows', + 'it also has autoGrow set to true so it will grow with the content.', + 'See: rows is still 2...', + 'And now I have 4 lines and still no scroll bar in sight.', + ].join('\n')} /> - )) - - .add('Resize', () => ( - <> - <TextArea - onChange={onChange} - onFocus={onFocus} - onBlur={onBlur} - name="textarea1" - label="Resize: vertical (default)" - /> - <TextArea - onChange={onChange} - onFocus={onFocus} - onBlur={onBlur} - name="textarea2" - label="Resize: none" - resize="none" - /> - <TextArea - onChange={onChange} - onFocus={onFocus} - onBlur={onBlur} - name="textarea3" - label="Resize: both" - resize="both" - /> - <TextArea - onChange={onChange} - onFocus={onFocus} - onBlur={onBlur} - name="textarea4" - label="Resize: horizontal" - resize="horizontal" - /> - </> - )) - - .add('Autogrow', () => ( - <> - <TextArea - onChange={onChange} - onFocus={onFocus} - onBlur={onBlur} - name="textarea1" - label="Autogrow step 1" - autoGrow - rows={2} - value="This TextArea has a height of 2 rows" - /> - <TextArea - onChange={onChange} - onFocus={onFocus} - onBlur={onBlur} - name="textarea2" - label="Autogrow step 2" - autoGrow - rows={2} - value={[ - 'This TextArea has a height of two rows', - 'it also has autoGrow set to true so it will grow with the content', - ].join('\n')} - /> - <TextArea - onChange={onChange} - onFocus={onFocus} - onBlur={onBlur} - name="textarea3" - label="Autogrow step 3" - autoGrow - rows={2} - value={[ - 'This TextArea has a height of two rows', - 'it also has autoGrow set to true so it will grow with the content.', - 'See: rows is still 2, but I now have 3 lines.', - ].join('\n')} - /> - <TextArea - onChange={onChange} - onFocus={onFocus} - onBlur={onBlur} - name="textarea4" - label="Autogrow step 4" - value={[ - 'This TextArea has a height of two rows', - 'it also has autoGrow set to true so it will grow with the content.', - 'See: rows is still 2...', - 'And now I have 4 lines and still no scroll bar in sight.', - ].join('\n')} - /> - </> - )) + </> +) diff --git a/packages/core/src/Tooltip/Tooltip.js b/packages/core/src/Tooltip/Tooltip.js index 68aef32dc0..97122bea15 100644 --- a/packages/core/src/Tooltip/Tooltip.js +++ b/packages/core/src/Tooltip/Tooltip.js @@ -1,4 +1,3 @@ -import propTypes from '@dhis2/prop-types' import { colors, layers } from '@dhis2/ui-constants' import PropTypes from 'prop-types' import React, { useRef, useState } from 'react' @@ -155,16 +154,20 @@ Tooltip.defaultProps = { * @prop {('top'|'bottom'|'right'|'left')} [placement=top] */ Tooltip.propTypes = { - children: propTypes.oneOfType([propTypes.node, propTypes.func]), - className: propTypes.string, + /** If child is a function, it's called with `{ onMouseOver, onMouseOut, ref }` args to apply to a reference element. If child is a node, it is wrapped in a `span` with the appropriate attributes and handlers. */ + children: PropTypes.oneOfType([PropTypes.node, PropTypes.func]), + className: PropTypes.string, /** Time (in ms) until tooltip closes after mouse out */ closeDelay: PropTypes.number, - content: propTypes.node, - dataTest: propTypes.string, - maxWidth: propTypes.number, + /** Content to display when the tooltip is open */ + content: PropTypes.node, + dataTest: PropTypes.string, + /** Max width of the tooltip in px */ + maxWidth: PropTypes.number, /** Time (in ms) until tooltip open after mouse over */ openDelay: PropTypes.number, - placement: propTypes.oneOf(['top', 'right', 'bottom', 'left']), + /** Where to place the tooltip relative to its reference */ + placement: PropTypes.oneOf(['top', 'right', 'bottom', 'left']), } export { Tooltip, TOOLTIP_OFFSET } diff --git a/packages/core/src/Tooltip/Tooltip.stories.js b/packages/core/src/Tooltip/Tooltip.stories.js index 6a781c0a0d..35792eefb9 100644 --- a/packages/core/src/Tooltip/Tooltip.stories.js +++ b/packages/core/src/Tooltip/Tooltip.stories.js @@ -1,62 +1,77 @@ import React from 'react' import { Tooltip } from './Tooltip.js' +const subtitle = 'Displayed when a user hovers over the element' + +const description = ` +Tooltips only display when a user hovers over an element. Do not use tooltips for critical or important information, users may not find the information or it may completely unavailable to mobile users. Information provided in tooltips should be supplementary or provide helpful context. Icon buttons can use tooltips to inform the user of their action. Never put important information or actions inside a tooltip. + +- Common usage of a tooltip is to expand on the displayed information when the user hovers over the element. +- Do not place actions inside a tooltip, they would be hidden from the user and difficult to click. +- Only text can be displayed in a tooltip. A popover can be used for rich information. +- Limit the text inside a tooltip to a single, short sentence. +- Do not repeat information in a tooltip, provide extra relevant, useful information. + +By default the tooltip should display above the hovered element. Alternatively, a tooltip may be displayed underneath or to the side of an element if there is limited space. + +\`\`\`js +import { Tooltip } from '@dhis2/ui' +\`\`\` + +_**Note**: The tooltips may not be placed correctly on this page. View the demos in the 'Canvas' tab for correct placement._ +` + export default { - title: 'Tooltip', + title: 'Data Display/Tooltip', component: Tooltip, + parameters: { + componentSubtitle: subtitle, + docs: { + description: { component: description }, + source: { type: 'code' }, + }, + }, decorators: [ - storyFn => ( + story => ( <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', width: 500, - height: 300, + height: 200, }} > - <div style={{ width: 300, height: 100 }}>{storyFn()}</div> + <div style={{ width: 400, height: 100 }}>{story()}</div> </div> ), ], + args: { content: 'Some tooltip content here' }, } -export const DefaultPlacementTop = () => ( +const Template = args => ( <p> - I am a <Tooltip content="Some extra info">paragraph</Tooltip> that - contains a tooltip. + Mouse over <Tooltip {...args}>[these words]</Tooltip> to open a tooltip. </p> ) -export const PlacementRight = () => ( - <p> - I am a{' '} - <Tooltip placement="right" content="Some extra info"> - paragraph - </Tooltip>{' '} - that contains a tooltip. - </p> -) +export const DefaultPlacementTop = Template.bind({}) -export const PlacementBottom = () => ( - <p> - I am a{' '} - <Tooltip placement="bottom" content="Some extra info"> - paragraph - </Tooltip>{' '} - that contains a tooltip. - </p> -) +export const PlacementRight = Template.bind({}) +PlacementRight.args = { placement: 'right' } +PlacementRight.parameters = { + docs: { + description: { + story: `_**Note:** The tooltips may not be placed correctly on this page. View the demos in the 'Canvas' tab for correct placement._`, + }, + }, +} -export const PlacementLeft = () => ( - <p> - I am a{' '} - <Tooltip placement="left" content="Some extra info"> - paragraph - </Tooltip>{' '} - that contains a tooltip. - </p> -) +export const PlacementBottom = Template.bind({}) +PlacementBottom.args = { placement: 'bottom' } + +export const PlacementLeft = Template.bind({}) +PlacementLeft.args = { placement: 'left' } export const ConfigurableOpenAndCloseDelays = args => ( <p> @@ -84,18 +99,18 @@ export const CustomElementViaTagProp = () => { ) } -export const CustomBuiltInComponent = () => { +export const CustomBuiltInComponent = args => { return ( <p> - I am a{' '} - <Tooltip content="Some extra info"> + Mouse over{' '} + <Tooltip {...args}> {({ ref, onMouseOver, onMouseOut }) => ( <span ref={ref} onMouseOver={onMouseOver} onMouseOut={onMouseOut} > - paragraph + these words <style jsx>{` span { color: green; @@ -105,23 +120,23 @@ export const CustomBuiltInComponent = () => { </span> )} </Tooltip>{' '} - that contains a tooltip. + to open a tooltip. </p> ) } -export const CustomComponent = () => { +export const CustomComponent = args => { return ( <p> - I am a{' '} - <Tooltip content="Some extra info"> + Mouse over{' '} + <Tooltip {...args}> {({ ref, onMouseOver, onMouseOut }) => ( <span ref={ref} onMouseOver={onMouseOver} onMouseOut={onMouseOut} > - <button>paragraph</button> + <button>this button</button> <style jsx>{` span { display: inline-flex; @@ -130,7 +145,7 @@ export const CustomComponent = () => { </span> )} </Tooltip>{' '} - that contains a Button with a tooltip. + to open a tooltip. </p> ) } @@ -171,4 +186,3 @@ export const HidesWhenOutOfFrame = args => ( <p>{"I'm an extra paragraph"}</p> </div> ) -HidesWhenOutOfFrame.args = { content: 'Some extra info' } diff --git a/packages/forms/src/CheckboxFieldFF/CheckboxFieldFF.js b/packages/forms/src/CheckboxFieldFF/CheckboxFieldFF.js index a501f8be3a..1099df9c30 100644 --- a/packages/forms/src/CheckboxFieldFF/CheckboxFieldFF.js +++ b/packages/forms/src/CheckboxFieldFF/CheckboxFieldFF.js @@ -1,5 +1,5 @@ -import propTypes from '@dhis2/prop-types' import { CheckboxField } from '@dhis2/ui-widgets' +import PropTypes from 'prop-types' import React from 'react' import { createToggleChangeHandler, @@ -37,14 +37,16 @@ export const CheckboxFieldFF = ({ ) CheckboxFieldFF.propTypes = { + /** Provided by Final Form `Field` */ input: inputPropType.isRequired, + /** Provided by Final Form `Field` */ meta: metaPropType.isRequired, - error: propTypes.bool, - showValidStatus: propTypes.bool, - valid: propTypes.bool, - validationText: propTypes.string, + error: PropTypes.bool, + showValidStatus: PropTypes.bool, + valid: PropTypes.bool, + validationText: PropTypes.string, - onBlur: propTypes.func, - onFocus: propTypes.func, + onBlur: PropTypes.func, + onFocus: PropTypes.func, } diff --git a/packages/forms/src/CheckboxFieldFF/CheckboxFieldFF.stories.js b/packages/forms/src/CheckboxFieldFF/CheckboxFieldFF.stories.js index 51885a0944..c9a6a08182 100644 --- a/packages/forms/src/CheckboxFieldFF/CheckboxFieldFF.stories.js +++ b/packages/forms/src/CheckboxFieldFF/CheckboxFieldFF.stories.js @@ -1,101 +1,142 @@ -import { storiesOf } from '@storybook/react' import React from 'react' import { Field } from 'react-final-form' import { formDecorator } from '../formDecorator.js' +import { inputArgType, metaArgType } from '../shared/propTypes.js' import { hasValue } from '../validators/index.js' import { CheckboxFieldFF } from './CheckboxFieldFF.js' -storiesOf('CheckboxFieldFF', module) - .addDecorator(formDecorator) - .add('Default', () => ( +const description = ` +The \`CheckboxFieldFF\` is a wrapper around a \`CheckboxField\` that enables it to work with Final Form, the preferred library for form validation and utilities in DHIS 2 apps. + +#### Final Form + +See how to use Final Form at [Final Form - Getting Started](https://final-form.org/docs/react-final-form/getting-started). + +Inside a Final Form \`<Form>\` component, these 'FF' UI components are intended to be used in the \`component\` prop of the [Final Form \`<Field>\` components](https://final-form.org/docs/react-final-form/api/Field) where they will receive some props from the Field, e.g. \`<Field component={CheckboxFieldFF} />\`. See the code samples below for examples. + +#### Props + +The props shown in the table below are generally provided to the \`CheckboxFieldFF\` wrapper by the Final Form \`Field\`. + +Note that any props beyond the API of the \`Field\` component will be spread to the \`CheckboxFieldFF\`, which passes any extra props to the underlying \`CheckboxField\` using \`{...rest}\`. + +Therefore, to add any props to the \`CheckboxFieldFF\` or \`CheckboxField\`, add those props to the parent Final Form \`Field\` component. + +Also see \`Checkbox\` and \`CheckboxField\` for notes about props and implementation. + +\`\`\`js +import { CheckboxFieldFF } from '@dhis2/ui' +\`\`\` + +Press **Submit** to see the form values logged to the console. +` + +export default { + title: 'Forms/Checkbox/Checkbox Field (Final Form)', + component: CheckboxFieldFF, + decorators: [formDecorator], + parameters: { docs: { description: { component: description } } }, + argTypes: { + input: { ...inputArgType }, + meta: { ...metaArgType }, + }, +} + +export const Default = () => ( + <Field + type="checkbox" + component={CheckboxFieldFF} + name="agree" + label="Do you agree?" + /> +) + +export const RequiredWithValidate = () => ( + <Field + type="checkbox" + name="agree" + component={CheckboxFieldFF} + required + validate={hasValue} + label="Do you agree?" + value="yes" + /> +) + +export const Disabled = () => ( + <Field + type="checkbox" + name="agree" + component={CheckboxFieldFF} + disabled + label="Do you agree?" + /> +) + +export const HelpText = () => ( + <Field + type="checkbox" + name="agree" + component={CheckboxFieldFF} + label="Do you agree?" + helpText="Click to agree" + /> +) +HelpText.storyName = 'Help text' + +export const Statuses = () => ( + <> + <Field + type="checkbox" + name="valid" + component={CheckboxFieldFF} + label="Valid" + valid + validationText="Validation text" + /> + <Field + type="checkbox" + name="warning" + component={CheckboxFieldFF} + label="Warning" + warning + validationText="Validation text" + /> <Field type="checkbox" + name="error" component={CheckboxFieldFF} - name="agree" - label="Do you agree?" + label="Error" + error + validationText="Validation text" /> - )) - .add('Required', () => ( + </> +) + +export const ValueWhenChecked = () => ( + <> <Field type="checkbox" - name="agree" + name="bool" component={CheckboxFieldFF} - required - validate={hasValue} - label="Do you agree?" - value="yes" + label="I produce boolean form values" + helpText="Click submit and check the console" /> - )) - .add('Disabled', () => ( <Field type="checkbox" - name="agree" + name="string" component={CheckboxFieldFF} - disabled - label="Do you agree?" + label="I produce string form values" + value="checked-value" + helpText="Click submit and check the console" /> - )) - .add('Help text', () => ( <Field type="checkbox" - name="agree" + name="string" component={CheckboxFieldFF} - label="Do you agree?" - helpText="Click to agree" + label="I also produce string form values" + value="another-checked-value" + helpText="Click submit and check the console" /> - )) - .add('Statuses', () => ( - <> - <Field - type="checkbox" - name="valid" - component={CheckboxFieldFF} - label="Valid" - valid - validationText="Validation text" - /> - <Field - type="checkbox" - name="warning" - component={CheckboxFieldFF} - label="Warning" - warning - validationText="Validation text" - /> - <Field - type="checkbox" - name="error" - component={CheckboxFieldFF} - label="Error" - error - validationText="Validation text" - /> - </> - )) - .add('Value when checked', () => ( - <> - <Field - type="checkbox" - name="bool" - component={CheckboxFieldFF} - label="I produce boolean form values" - helpText="Click submit and check the console" - /> - <Field - type="checkbox" - name="string" - component={CheckboxFieldFF} - label="I produce string form values" - value="checked-value" - helpText="Click submit and check the console" - /> - <Field - type="checkbox" - name="string" - component={CheckboxFieldFF} - label="I also produce string form values" - value="another-checked-value" - helpText="Click submit and check the console" - /> - </> - )) + </> +) diff --git a/packages/forms/src/FieldGroupFF/FieldGroupFF.js b/packages/forms/src/FieldGroupFF/FieldGroupFF.js index d1ee92846e..30ea36307f 100644 --- a/packages/forms/src/FieldGroupFF/FieldGroupFF.js +++ b/packages/forms/src/FieldGroupFF/FieldGroupFF.js @@ -1,5 +1,5 @@ -import propTypes from '@dhis2/prop-types' import { FieldGroup } from '@dhis2/ui-widgets' +import PropTypes from 'prop-types' import React from 'react' import { useField } from 'react-final-form' @@ -35,8 +35,8 @@ export const FieldGroupFF = ({ name, label, children, required }) => { } FieldGroupFF.propTypes = { - children: propTypes.node, - label: propTypes.string, - name: propTypes.string, - required: propTypes.bool, + children: PropTypes.node, + label: PropTypes.string, + name: PropTypes.string, + required: PropTypes.bool, } diff --git a/packages/forms/src/FieldGroupFF/FieldGroupFF.stories.js b/packages/forms/src/FieldGroupFF/FieldGroupFF.stories.js index d4912ea346..b103682217 100644 --- a/packages/forms/src/FieldGroupFF/FieldGroupFF.stories.js +++ b/packages/forms/src/FieldGroupFF/FieldGroupFF.stories.js @@ -5,10 +5,23 @@ import { formDecorator } from '../formDecorator.js' import { hasValue } from '../validators/index.js' import { FieldGroupFF } from './FieldGroupFF.js' +const description = ` +This component is intended for use with [Final Form](https://final-form.org/docs/react-final-form/getting-started), the preferred library for form validation and utilities in DHIS 2 apps. + +\`FieldGroupFF\` groups related fields (using the Final Form \`<Field>\`), like checkboxes, and adds a label and name. + +\`\`\`js +import { FieldGroupFF } from '@dhis2/ui' +\`\`\` + +Press **Submit** to see the form values logged to the console. +` + export default { - title: 'FieldGroupFF', + title: 'Forms/Field Group/Field Group (Final Form)', component: FieldGroupFF, decorators: [formDecorator], + parameters: { docs: { description: { component: description } } }, } export const Default = () => ( diff --git a/packages/forms/src/FileInputFieldFF/FileInputFieldFF.js b/packages/forms/src/FileInputFieldFF/FileInputFieldFF.js index dffff0bc50..c93731b8f6 100644 --- a/packages/forms/src/FileInputFieldFF/FileInputFieldFF.js +++ b/packages/forms/src/FileInputFieldFF/FileInputFieldFF.js @@ -1,6 +1,6 @@ -import propTypes from '@dhis2/prop-types' import { FileListItem } from '@dhis2/ui-core' import { FileInputField } from '@dhis2/ui-widgets' +import PropTypes from 'prop-types' import React from 'react' import i18n from '../locales/index.js' import { hasError, isValid, getValidationText } from '../shared/helpers.js' @@ -82,18 +82,20 @@ FileInputFieldFF.defaultProps = { } FileInputFieldFF.propTypes = { + /** `input` props provided by Final Form `Field` */ input: inputPropType.isRequired, + /** `meta` props provided by Final Form `Field` */ meta: metaPropType.isRequired, - buttonLabel: propTypes.string, - disabled: propTypes.bool, - error: propTypes.bool, - multifile: propTypes.bool, - showValidStatus: propTypes.bool, - valid: propTypes.bool, - validationText: propTypes.string, - value: propTypes.oneOfType([ - propTypes.arrayOf(propTypes.instanceOf(File)), - propTypes.oneOf(['']), + buttonLabel: PropTypes.string, + disabled: PropTypes.bool, + error: PropTypes.bool, + multifile: PropTypes.bool, + showValidStatus: PropTypes.bool, + valid: PropTypes.bool, + validationText: PropTypes.string, + value: PropTypes.oneOfType([ + PropTypes.arrayOf(PropTypes.instanceOf(File)), + PropTypes.oneOf(['']), ]), } diff --git a/packages/forms/src/FileInputFieldFF/FileInputFieldFF.stories.js b/packages/forms/src/FileInputFieldFF/FileInputFieldFF.stories.js index f012cd919b..de9d01b74b 100644 --- a/packages/forms/src/FileInputFieldFF/FileInputFieldFF.stories.js +++ b/packages/forms/src/FileInputFieldFF/FileInputFieldFF.stories.js @@ -1,54 +1,95 @@ -import { storiesOf } from '@storybook/react' import React from 'react' import { Field } from 'react-final-form' import { formDecorator } from '../formDecorator.js' +import { inputArgType, metaArgType } from '../shared/propTypes.js' import { hasValue } from '../validators/index.js' import { FileInputFieldFF } from './FileInputFieldFF.js' +const description = ` +The \`FileInputFieldFF\` is a wrapper around a \`FileInputField\` that enables it to work with Final Form, the preferred library in DHIS 2 apps for form validation and utilities. + +#### Final Form + +See how to use Final Form at [Final Form - Getting Started](https://final-form.org/docs/react-final-form/getting-started). + +Inside a Final Form \`<Form>\` component, these 'FF' UI components are intended to be used in the \`component\` prop of the [Final Form \`<Field>\` components](https://final-form.org/docs/react-final-form/api/Field) where they will receive some props from the Field, e.g. \`<Field component={FileInputFieldFF} />\`. See the code samples below for examples. + +#### Props + +The props shown in the table below are generally provided to the \`FileInputFieldFF\` wrapper by the Final Form \`Field\`. + +Note that any props beyond the API of the \`Field\` component will be spread to the \`FileInputFieldFF\`, which passes any extra props to the underlying \`FileInputField\` using \`{...rest}\`. + +Therefore, to add any props to the \`FileInputFieldFF\` or \`FileInputField\`, add those props to the parent Final Form \`Field\` component. + +Also see \`FileInput\` and \`FileInputField\` for notes about props and implementation. + +\`\`\`js +import { FileInputFieldFF } from '@dhis2/ui' +\`\`\` + +Press **Submit** to see the form values logged to the console. +` + const files = [new File([], 'file1.txt'), new File([], 'file2.txt')] -storiesOf('FileInputFieldFF', module) - .addDecorator(formDecorator) - .add('Default', () => ( - <Field - component={FileInputFieldFF} - name="upload" - label="This is a file upload" - /> - )) - .add('Required', () => ( - <Field - component={FileInputFieldFF} - name="upload" - label="This is a file upload" - required - validate={hasValue} - /> - )) - .add('Multifile', () => ( - <Field - component={FileInputFieldFF} - name="upload" - label="This is a file upload" - multifile - /> - )) - .add('With values', () => ( - <Field - component={FileInputFieldFF} - name="upload" - label="This is a file upload" - required - multifile - initialValue={files} - validate={hasValue} - /> - )) - .add('Prevent placeholder', () => ( - <Field - component={FileInputFieldFF} - name="upload" - label="This is a file upload" - placeholder="" - /> - )) +export default { + title: 'Forms/File Input/File Input Field (Final Form)', + component: FileInputFieldFF, + decorators: [formDecorator], + parameters: { docs: { description: { component: description } } }, + argTypes: { + input: { ...inputArgType }, + meta: { ...metaArgType }, + }, +} + +export const Default = () => ( + <Field + component={FileInputFieldFF} + name="upload" + label="This is a file upload" + /> +) + +export const Required = () => ( + <Field + component={FileInputFieldFF} + name="upload" + label="This is a file upload" + required + validate={hasValue} + /> +) + +export const Multifile = () => ( + <Field + component={FileInputFieldFF} + name="upload" + label="This is a file upload" + multifile + /> +) + +export const WithValues = () => ( + <Field + component={FileInputFieldFF} + name="upload" + label="This is a file upload" + required + multifile + initialValue={files} + validate={hasValue} + /> +) +WithValues.storyName = 'With values' + +export const PreventPlaceholder = () => ( + <Field + component={FileInputFieldFF} + name="upload" + label="This is a file upload" + placeholder="" + /> +) +PreventPlaceholder.storyName = 'Prevent placeholder' diff --git a/packages/forms/src/InputFieldFF/InputFieldFF.js b/packages/forms/src/InputFieldFF/InputFieldFF.js index ea9782387a..abe9dd0b74 100644 --- a/packages/forms/src/InputFieldFF/InputFieldFF.js +++ b/packages/forms/src/InputFieldFF/InputFieldFF.js @@ -1,5 +1,5 @@ -import propTypes from '@dhis2/prop-types' import { InputField } from '@dhis2/ui-widgets' +import PropTypes from 'prop-types' import React from 'react' import { createChangeHandler, @@ -41,16 +41,18 @@ export const InputFieldFF = ({ ) InputFieldFF.propTypes = { + /** `input` props received from Final Form `Field` */ input: inputPropType.isRequired, + /** `meta` props received from Final Form `Field` */ meta: metaPropType.isRequired, - error: propTypes.bool, - loading: propTypes.bool, - showLoadingStatus: propTypes.bool, - showValidStatus: propTypes.bool, - valid: propTypes.bool, - validationText: propTypes.string, + error: PropTypes.bool, + loading: PropTypes.bool, + showLoadingStatus: PropTypes.bool, + showValidStatus: PropTypes.bool, + valid: PropTypes.bool, + validationText: PropTypes.string, - onBlur: propTypes.func, - onFocus: propTypes.func, + onBlur: PropTypes.func, + onFocus: PropTypes.func, } diff --git a/packages/forms/src/InputFieldFF/InputFieldFF.stories.js b/packages/forms/src/InputFieldFF/InputFieldFF.stories.js index da26747a58..fee3bffbda 100644 --- a/packages/forms/src/InputFieldFF/InputFieldFF.stories.js +++ b/packages/forms/src/InputFieldFF/InputFieldFF.stories.js @@ -1,62 +1,102 @@ -import { storiesOf } from '@storybook/react' import React from 'react' import { Field } from 'react-final-form' import { formDecorator } from '../formDecorator.js' +import { inputArgType, metaArgType } from '../shared/propTypes.js' import { hasValue } from '../validators/index.js' import { InputFieldFF } from './InputFieldFF.js' -storiesOf('InputFieldFF', module) - .addDecorator(formDecorator) - .add('Default', () => ( - <Field component={InputFieldFF} name="agree" label="Do you agree?" /> - )) - .add('Required', () => ( +const description = ` +The \`InputFieldFF\` is a wrapper around a \`InputField\` that enables it to work with Final Form, the preferred library for form validation and utilities in DHIS 2 apps. + +#### Final Form + +See how to use Final Form at [Final Form - Getting Started](https://final-form.org/docs/react-final-form/getting-started). + +Inside a Final Form \`<Form>\` component, these 'FF' UI components are intended to be used in the \`component\` prop of the [Final Form \`<Field>\` components](https://final-form.org/docs/react-final-form/api/Field) where they will receive some props from the Field, e.g. \`<Field component={InputFieldFF} />\`. See the code samples below for examples. + +#### Props + +The props shown in the table below are generally provided to the \`InputFieldFF\` wrapper by the Final Form \`Field\`. + +Note that any props beyond the API of the \`Field\` component will be spread to the \`InputFieldFF\`, which passes any extra props to the underlying \`InputField\` using \`{...rest}\`. + +Therefore, to add any props to the \`InputFieldFF\` or \`InputField\`, add those props to the parent Final Form \`Field\` component. + +Also see \`InputField\` for notes about props and implementation. + +\`\`\`js +import { InputFieldFF } from '@dhis2/ui' +\`\`\` + +Press **Submit** to see the form values logged to the console. +` + +export default { + title: 'Forms/Input/Input Field (Final Form)', + component: InputFieldFF, + decorators: [formDecorator], + parameters: { docs: { description: { component: description } } }, + argTypes: { + input: { ...inputArgType }, + meta: { ...metaArgType }, + }, +} + +export const Default = () => ( + <Field component={InputFieldFF} name="agree" label="Do you agree?" /> +) + +export const Required = () => ( + <Field + name="agree" + component={InputFieldFF} + required + validate={hasValue} + label="Do you agree?" + /> +) + +export const Disabled = () => ( + <Field + name="agree" + component={InputFieldFF} + disabled + label="Do you agree?" + /> +) + +export const HelpText = () => ( + <Field + name="agree" + component={InputFieldFF} + label="Do you agree?" + helpText="Click to agree" + /> +) +HelpText.storyName = 'Help text' + +export const Statuses = () => ( + <> <Field - name="agree" + name="valid" component={InputFieldFF} - required - validate={hasValue} - label="Do you agree?" + label="Valid" + valid + validationText="Validation text" /> - )) - .add('Disabled', () => ( <Field - name="agree" + name="warning" component={InputFieldFF} - disabled - label="Do you agree?" + label="Warning" + warning + validationText="Validation text" /> - )) - .add('Help text', () => ( <Field - name="agree" + name="error" component={InputFieldFF} - label="Do you agree?" - helpText="Click to agree" + label="Error" + error + validationText="Validation text" /> - )) - .add('Statuses', () => ( - <> - <Field - name="valid" - component={InputFieldFF} - label="Valid" - valid - validationText="Validation text" - /> - <Field - name="warning" - component={InputFieldFF} - label="Warning" - warning - validationText="Validation text" - /> - <Field - name="error" - component={InputFieldFF} - label="Error" - error - validationText="Validation text" - /> - </> - )) + </> +) diff --git a/packages/forms/src/MultiSelectFieldFF/MultiSelectFieldFF.js b/packages/forms/src/MultiSelectFieldFF/MultiSelectFieldFF.js index 149f6aeab8..11ac0cede7 100644 --- a/packages/forms/src/MultiSelectFieldFF/MultiSelectFieldFF.js +++ b/packages/forms/src/MultiSelectFieldFF/MultiSelectFieldFF.js @@ -1,6 +1,6 @@ -import propTypes from '@dhis2/prop-types' import { MultiSelectOption } from '@dhis2/ui-core' import { MultiSelectField } from '@dhis2/ui-widgets' +import PropTypes from 'prop-types' import React from 'react' import { createSelectChangeHandler, @@ -50,22 +50,24 @@ export const MultiSelectFieldFF = ({ } MultiSelectFieldFF.propTypes = { + /** `input` props provided by Final Form `Field` */ input: inputPropType.isRequired, + /** `meta` props provided by Final Form `Field` */ meta: metaPropType.isRequired, - error: propTypes.bool, - loading: propTypes.bool, - options: propTypes.arrayOf( - propTypes.shape({ - label: propTypes.string, - value: propTypes.string, + error: PropTypes.bool, + loading: PropTypes.bool, + options: PropTypes.arrayOf( + PropTypes.shape({ + label: PropTypes.string, + value: PropTypes.string, }) ), - showLoadingStatus: propTypes.bool, - showValidStatus: propTypes.bool, - valid: propTypes.bool, - validationText: propTypes.string, + showLoadingStatus: PropTypes.bool, + showValidStatus: PropTypes.bool, + valid: PropTypes.bool, + validationText: PropTypes.string, - onBlur: propTypes.func, - onFocus: propTypes.func, + onBlur: PropTypes.func, + onFocus: PropTypes.func, } diff --git a/packages/forms/src/MultiSelectFieldFF/MultiSelectFieldFF.stories.js b/packages/forms/src/MultiSelectFieldFF/MultiSelectFieldFF.stories.js index ba4389a5bd..e9879de607 100644 --- a/packages/forms/src/MultiSelectFieldFF/MultiSelectFieldFF.stories.js +++ b/packages/forms/src/MultiSelectFieldFF/MultiSelectFieldFF.stories.js @@ -1,9 +1,37 @@ -import { storiesOf } from '@storybook/react' import React from 'react' import { Field } from 'react-final-form' import { formDecorator } from '../formDecorator.js' +import { inputArgType, metaArgType } from '../shared/propTypes.js' import { MultiSelectFieldFF } from './MultiSelectFieldFF.js' +const description = ` +The \`MultiSelectFieldFF\` is a wrapper around a \`MultiSelectField\` that enables it to work with Final Form, the preferred library for form validation and utilities in DHIS 2 apps. + +#### Final Form + +See how to use Final Form at [Final Form - Getting Started](https://final-form.org/docs/react-final-form/getting-started). + +Inside a Final Form \`<Form>\` component, these 'FF' UI components are intended to be used in the \`component\` prop of the [Final Form \`<Field>\` components](https://final-form.org/docs/react-final-form/api/Field) where they will receive some props from the Field, e.g. \`<Field component={MultiSelectFieldFF} />\`. See the code samples below for examples. + +#### Props + +The props shown in the table below are generally provided to the \`MultiSelectFieldFF\` wrapper by the Final Form \`Field\`. + +Note that any props beyond the API of the \`Field\` component will be spread to the \`MultiSelectFieldFF\`, which passes any extra props to the underlying \`MultiSelectField\` using \`{...rest}\`. + +Therefore, to add any props to the \`MultiSelectFieldFF\` or \`MultiSelectField\`, add those props to the parent Final Form \`Field\` component. + +Also see \`MultiSelect\` and \`MultiSelectField\` for notes about props and implementation. + +\`\`\`js +import { MultiSelectFieldFF } from '@dhis2/ui' +\`\`\` + +Press **Submit** to see the form values logged to the console. + +_**Note:** Dropdowns may not appear correctly on this page. See the affected demos in the 'Canvas' tab for propper dropdown placement._ +` + const options = [ { value: '1', label: 'one' }, { value: '2', label: 'two' }, @@ -19,22 +47,33 @@ const options = [ const initialValue = ['3', '4', '9', '10'] -storiesOf('MultiSelectFieldFF', module) - .addDecorator(formDecorator) - .add('Default', () => ( - <Field - component={MultiSelectFieldFF} - name="agree" - label="Do you agree?" - options={options} - /> - )) - .add('InitialValue', () => ( - <Field - component={MultiSelectFieldFF} - name="agree" - label="Do you agree?" - options={options} - initialValue={initialValue} - /> - )) +export default { + title: 'Forms/Multi Select/Multi Select Field (Final Form)', + component: MultiSelectFieldFF, + decorators: [formDecorator], + parameters: { docs: { description: { component: description } } }, + argTypes: { + input: { ...inputArgType }, + meta: { ...metaArgType }, + }, +} + +export const Default = () => ( + <Field + component={MultiSelectFieldFF} + name="agree" + label="Do you agree?" + options={options} + /> +) + +export const InitialValue = () => ( + <Field + component={MultiSelectFieldFF} + name="agree" + label="Do you agree?" + options={options} + initialValue={initialValue} + /> +) +InitialValue.storyName = 'InitialValue' diff --git a/packages/forms/src/RadioFieldFF/RadioFieldFF.js b/packages/forms/src/RadioFieldFF/RadioFieldFF.js index 69fa05a751..0c74a044ab 100644 --- a/packages/forms/src/RadioFieldFF/RadioFieldFF.js +++ b/packages/forms/src/RadioFieldFF/RadioFieldFF.js @@ -1,5 +1,5 @@ -import propTypes from '@dhis2/prop-types' import { Radio } from '@dhis2/ui-core' +import PropTypes from 'prop-types' import React from 'react' import { createToggleChangeHandler, @@ -37,14 +37,16 @@ export const RadioFieldFF = ({ ) RadioFieldFF.propTypes = { + /** `input` props received from Final Form `Field` */ input: inputPropType.isRequired, + /** `meta` props received from Final Form `Field` */ meta: metaPropType.isRequired, - error: propTypes.bool, - showValidStatus: propTypes.bool, - valid: propTypes.bool, - validationText: propTypes.string, + error: PropTypes.bool, + showValidStatus: PropTypes.bool, + valid: PropTypes.bool, + validationText: PropTypes.string, - onBlur: propTypes.func, - onFocus: propTypes.func, + onBlur: PropTypes.func, + onFocus: PropTypes.func, } diff --git a/packages/forms/src/RadioFieldFF/RadioFieldFF.stories.js b/packages/forms/src/RadioFieldFF/RadioFieldFF.stories.js index 039af69c6f..3fdd51ffe8 100644 --- a/packages/forms/src/RadioFieldFF/RadioFieldFF.stories.js +++ b/packages/forms/src/RadioFieldFF/RadioFieldFF.stories.js @@ -1,69 +1,104 @@ import { FieldGroup } from '@dhis2/ui-widgets' -import { storiesOf } from '@storybook/react' import React from 'react' import { Field } from 'react-final-form' import { formDecorator } from '../formDecorator.js' +import { inputArgType, metaArgType } from '../shared/propTypes.js' import { RadioFieldFF } from './RadioFieldFF.js' -storiesOf('RadioFieldFF', module) - .addDecorator(formDecorator) +const description = ` +The \`RadioFieldFF\` is a wrapper around a \`Radio\` that enables it to work with Final Form, the preferred library for form validation and utilities in DHIS 2 apps. - .add('Default', () => ( - <FieldGroup> +#### Final Form + +See how to use Final Form at [Final Form - Getting Started](https://final-form.org/docs/react-final-form/getting-started). + +Inside a Final Form \`<Form>\` component, these 'FF' UI components are intended to be used in the \`component\` prop of the [Final Form \`<Field>\` components](https://final-form.org/docs/react-final-form/api/Field) where they will receive some props from the Field, e.g. \`<Field component={RadioFieldFF} />\`. See the code samples below for examples. + +#### Props + +The props shown in the table below are generally provided to the \`RadioFieldFF\` wrapper by the Final Form \`Field\`. + +Note that any props beyond the API of the \`Field\` component will be spread to the \`RadioFieldFF\`, which passes any extra props to the underlying \`Radio\` using \`{...rest}\`. + +Therefore, to add any props to the \`RadioFieldFF\` or \`Radio\`, add those props to the parent Final Form \`Field\` component. + +Also see \`Radio\` for notes about props and implementation. + +\`\`\`js +import { RadioFieldFF } from '@dhis2/ui' +\`\`\` + +Press **Submit** to see the form values logged to the console. +` + +export default { + title: 'Forms/Radio/Radio Field (Final Form)', + component: RadioFieldFF, + decorators: [formDecorator], + parameters: { docs: { description: { component: description } } }, + argTypes: { + input: { ...inputArgType }, + meta: { ...metaArgType }, + }, +} + +export const Default = () => ( + <FieldGroup> + <Field + type="radio" + component={RadioFieldFF} + name="choice" + label="One" + value="one" + /> + <Field + type="radio" + component={RadioFieldFF} + name="choice" + label="Two" + value="two" + /> + <Field + type="radio" + component={RadioFieldFF} + name="choice" + label="Three" + value="three" + /> + </FieldGroup> +) + +export const Statuses = () => ( + <> + <FieldGroup label="Valid"> <Field type="radio" + name="valid" component={RadioFieldFF} - name="choice" - label="One" - value="one" + label="Valid" + value="valid" + valid /> + </FieldGroup> + <FieldGroup label="Warning"> <Field type="radio" + name="warning" component={RadioFieldFF} - name="choice" - label="Two" - value="two" + label="Warning" + value="warning" + warning /> + </FieldGroup> + <FieldGroup label="Error"> <Field type="radio" + name="error" component={RadioFieldFF} - name="choice" - label="Three" - value="three" + label="Error" + value="error" + error /> </FieldGroup> - )) - .add('Statuses', () => ( - <> - <FieldGroup label="Valid"> - <Field - type="radio" - name="valid" - component={RadioFieldFF} - label="Valid" - value="valid" - valid - /> - </FieldGroup> - <FieldGroup label="Warning"> - <Field - type="radio" - name="warning" - component={RadioFieldFF} - label="Warning" - value="warning" - warning - /> - </FieldGroup> - <FieldGroup label="Error"> - <Field - type="radio" - name="error" - component={RadioFieldFF} - label="Error" - value="error" - error - /> - </FieldGroup> - </> - )) + </> +) diff --git a/packages/forms/src/SingleSelectFieldFF/SingleSelectFieldFF.js b/packages/forms/src/SingleSelectFieldFF/SingleSelectFieldFF.js index 05f902ae78..6eb093b9a0 100644 --- a/packages/forms/src/SingleSelectFieldFF/SingleSelectFieldFF.js +++ b/packages/forms/src/SingleSelectFieldFF/SingleSelectFieldFF.js @@ -1,6 +1,6 @@ -import propTypes from '@dhis2/prop-types' import { SingleSelectOption } from '@dhis2/ui-core' import { SingleSelectField } from '@dhis2/ui-widgets' +import PropTypes from 'prop-types' import React from 'react' import { createSelectChangeHandler, @@ -48,22 +48,24 @@ export const SingleSelectFieldFF = ({ } SingleSelectFieldFF.propTypes = { + /** `input` props received from Final Form `Field` */ input: inputPropType.isRequired, + /** `meta` props received from Final Form `Field` */ meta: metaPropType.isRequired, - options: propTypes.arrayOf( - propTypes.shape({ - label: propTypes.string, - value: propTypes.string, + options: PropTypes.arrayOf( + PropTypes.shape({ + label: PropTypes.string, + value: PropTypes.string, }) ).isRequired, - error: propTypes.bool, - loading: propTypes.bool, - showLoadingStatus: propTypes.bool, - showValidStatus: propTypes.bool, - valid: propTypes.bool, - validationText: propTypes.string, + error: PropTypes.bool, + loading: PropTypes.bool, + showLoadingStatus: PropTypes.bool, + showValidStatus: PropTypes.bool, + valid: PropTypes.bool, + validationText: PropTypes.string, - onBlur: propTypes.func, - onFocus: propTypes.func, + onBlur: PropTypes.func, + onFocus: PropTypes.func, } diff --git a/packages/forms/src/SingleSelectFieldFF/SingleSelectFieldFF.stories.js b/packages/forms/src/SingleSelectFieldFF/SingleSelectFieldFF.stories.js index d1b717bca9..75fd28d637 100644 --- a/packages/forms/src/SingleSelectFieldFF/SingleSelectFieldFF.stories.js +++ b/packages/forms/src/SingleSelectFieldFF/SingleSelectFieldFF.stories.js @@ -1,9 +1,37 @@ -import { storiesOf } from '@storybook/react' import React from 'react' import { Field } from 'react-final-form' import { formDecorator } from '../formDecorator.js' +import { inputArgType, metaArgType } from '../shared/propTypes.js' import { SingleSelectFieldFF } from './SingleSelectFieldFF.js' +const description = ` +The \`SingleSelectFieldFF\` is a wrapper around a \`SingleSelectField\` that enables it to work with Final Form, the preferred library for form validation and utilities in DHIS 2 apps. + +#### Final Form + +See how to use Final Form at [Final Form - Getting Started](https://final-form.org/docs/react-final-form/getting-started). + +Inside a Final Form \`<Form>\` component, these 'FF' UI components are intended to be used in the \`component\` prop of the [Final Form \`<Field>\` components](https://final-form.org/docs/react-final-form/api/Field) where they will receive some props from the Field, e.g. \`<Field component={SingleSelectFieldFF} />\`. See the code samples below for examples. + +#### Props + +The props shown in the table below are generally provided to the \`SingleSelectFieldFF\` wrapper by the Final Form \`Field\`. + +Note that any props beyond the API of the \`Field\` component will be spread to the \`SingleSelectFieldFF\`, which passes any extra props to the underlying \`SingleSelectField\` using \`{...rest}\`. + +Therefore, to add any props to the \`SingleSelectFieldFF\` or \`SingleSelectField\`, add those props to the parent Final Form \`Field\` component. + +Also see \`SingleSelect\` and \`SingleSelectField\` for notes about props and implementation. + +\`\`\`js +import { SingleSelectFieldFF } from '@dhis2/ui' +\`\`\` + +Press **Submit** to see the form values logged to the console. + +_**Note:** Dropdowns may not appear correctly on this page. See the affected demos in the 'Canvas' tab for propper dropdown placement._ +` + const options = [ { value: '1', label: 'one' }, { value: '2', label: 'two' }, @@ -17,22 +45,33 @@ const options = [ { value: '10', label: 'ten' }, ] -storiesOf('SingleSelectFieldFF', module) - .addDecorator(formDecorator) - .add('Default', () => ( - <Field - component={SingleSelectFieldFF} - name="agree" - label="Do you agree?" - options={options} - /> - )) - .add('InitialValue', () => ( - <Field - component={SingleSelectFieldFF} - name="agree" - label="Do you agree?" - options={options} - initialValue="4" - /> - )) +export default { + title: 'Forms/Single Select/Single Select Field (Final Form)', + component: SingleSelectFieldFF, + decorators: [formDecorator], + parameters: { docs: { description: { component: description } } }, + argTypes: { + input: { ...inputArgType }, + meta: { ...metaArgType }, + }, +} + +export const Default = () => ( + <Field + component={SingleSelectFieldFF} + name="agree" + label="Do you agree?" + options={options} + /> +) + +export const InitialValue = () => ( + <Field + component={SingleSelectFieldFF} + name="agree" + label="Do you agree?" + options={options} + initialValue="4" + /> +) +InitialValue.storyName = 'InitialValue' diff --git a/packages/forms/src/SwitchFieldFF/SwitchFieldFF.js b/packages/forms/src/SwitchFieldFF/SwitchFieldFF.js index 1091030ccb..3908afa14b 100644 --- a/packages/forms/src/SwitchFieldFF/SwitchFieldFF.js +++ b/packages/forms/src/SwitchFieldFF/SwitchFieldFF.js @@ -1,5 +1,5 @@ -import propTypes from '@dhis2/prop-types' import { SwitchField } from '@dhis2/ui-widgets' +import PropTypes from 'prop-types' import React from 'react' import { createToggleChangeHandler, @@ -37,14 +37,16 @@ export const SwitchFieldFF = ({ ) SwitchFieldFF.propTypes = { + /** `input` props received from Final Form `Field` */ input: inputPropType.isRequired, + /** `meta` props received from Final Form `Field` */ meta: metaPropType.isRequired, - error: propTypes.bool, - showValidStatus: propTypes.bool, - valid: propTypes.bool, - validationText: propTypes.string, + error: PropTypes.bool, + showValidStatus: PropTypes.bool, + valid: PropTypes.bool, + validationText: PropTypes.string, - onBlur: propTypes.func, - onFocus: propTypes.func, + onBlur: PropTypes.func, + onFocus: PropTypes.func, } diff --git a/packages/forms/src/SwitchFieldFF/SwitchFieldFF.stories.js b/packages/forms/src/SwitchFieldFF/SwitchFieldFF.stories.js index 39851d04e7..0f2087a052 100644 --- a/packages/forms/src/SwitchFieldFF/SwitchFieldFF.stories.js +++ b/packages/forms/src/SwitchFieldFF/SwitchFieldFF.stories.js @@ -1,88 +1,149 @@ -import { storiesOf } from '@storybook/react' import React from 'react' import { Field } from 'react-final-form' import { formDecorator } from '../formDecorator.js' +import { inputArgType, metaArgType } from '../shared/propTypes.js' import { hasValue } from '../validators/index.js' import { SwitchFieldFF } from './SwitchFieldFF.js' -storiesOf('SwitchFieldFF', module) - .addDecorator(formDecorator) - .add('Default', () => ( - <Field component={SwitchFieldFF} name="agree" label="Do you agree?" /> - )) - .add('Required', () => ( +const description = ` +The \`SwitchFieldFF\` is a wrapper around a \`SwitchField\` that enables it to work with Final Form, the preferred library for form validation and utilities in DHIS 2 apps. + +#### Final Form + +See how to use Final Form at [Final Form - Getting Started](https://final-form.org/docs/react-final-form/getting-started). + +Inside a Final Form \`<Form>\` component, these 'FF' UI components are intended to be used in the \`component\` prop of the [Final Form \`<Field>\` components](https://final-form.org/docs/react-final-form/api/Field) where they will receive some props from the Field, e.g. \`<Field component={SwitchFieldFF} />\`. See the code samples below for examples. + +#### Props + +The props shown in the table below are generally provided to the \`SwitchFieldFF\` wrapper by the Final Form \`Field\`. + +Note that any props beyond the API of the \`Field\` component will be spread to the \`SwitchFieldFF\`, which passes any extra props to the underlying \`SwitchField\` using \`{...rest}\`. + +Therefore, to add any props to the \`SwitchFieldFF\` or \`SwitchField\`, add those props to the parent Final Form \`Field\` component. + +Also see \`Switch\` and \`SwitchField\` for notes about props and implementation. + +\`\`\`js +import { SwitchFieldFF } from '@dhis2/ui' +\`\`\` + +Press **Submit** to see the form values logged to the console. +` + +export default { + title: 'Forms/Switch/Switch Field (Final Form)', + component: SwitchFieldFF, + decorators: [formDecorator], + parameters: { docs: { description: { component: description } } }, + argTypes: { + input: { ...inputArgType }, + meta: { ...metaArgType }, + }, +} + +export const Default = () => ( + <Field + type="checkbox" + component={SwitchFieldFF} + name="agree" + label="Do you agree?" + /> +) + +export const Required = () => ( + <Field + type="checkbox" + name="agree" + component={SwitchFieldFF} + required + validate={hasValue} + label="Do you agree?" + /> +) + +export const Disabled = () => ( + <Field + type="checkbox" + name="agree" + component={SwitchFieldFF} + disabled + label="Do you agree?" + /> +) + +export const HelpText = () => ( + <Field + type="checkbox" + name="agree" + component={SwitchFieldFF} + label="Do you agree?" + helpText="Click to agree" + /> +) +HelpText.storyName = 'Help text' + +export const Statuses = () => ( + <> + <Field + type="checkbox" + name="valid" + component={SwitchFieldFF} + label="Valid" + valid + validationText="Validation text" + /> + <Field + type="checkbox" + name="warning" + component={SwitchFieldFF} + label="Warning" + warning + validationText="Validation text" + /> + <Field + type="checkbox" + name="error" + component={SwitchFieldFF} + label="Error" + error + validationText="Validation text" + /> + </> +) + +export const ValueWhenChecked = () => ( + <> <Field - name="agree" + type="checkbox" + name="bool" component={SwitchFieldFF} - required - validate={hasValue} - label="Do you agree?" + label="I produce boolean form values" + helpText="Click submit and check the console" /> - )) - .add('Disabled', () => ( <Field - name="agree" + type="checkbox" + name="string" component={SwitchFieldFF} - disabled - label="Do you agree?" + label="I produce string form values because the 'value' prop is set" + value="value_when_checked" + helpText="Click submit and check the console" /> - )) - .add('Help text', () => ( <Field - name="agree" + type="checkbox" + name="string" component={SwitchFieldFF} - label="Do you agree?" - helpText="Click to agree" + label="I also produce string form values" + value="another_value_when_checked" + helpText="Click submit and check the console" /> - )) - .add('Statuses', () => ( - <> - <Field - name="valid" - component={SwitchFieldFF} - label="Valid" - valid - validationText="Validation text" - /> - <Field - name="warning" - component={SwitchFieldFF} - label="Warning" - warning - validationText="Validation text" - /> - <Field - name="error" - component={SwitchFieldFF} - label="Error" - error - validationText="Validation text" - /> - </> - )) - .add('Value when checked', () => ( - <> - <Field - name="bool" - component={SwitchFieldFF} - label="I produce boolean form values" - helpText="Click submit and check the console" - /> - <Field - type="checkbox" - name="string" - component={SwitchFieldFF} - label="I produce string form values" - value="value_when_checked" - helpText="Click submit and check the console" - /> - <Field - type="checkbox" - name="string" - component={SwitchFieldFF} - label="I also produce string form values" - value="another_value_when_checked" - helpText="Click submit and check the console" - /> - </> - )) + </> +) +ValueWhenChecked.parameters = { + docs: { + description: { + story: + 'See the details about using the `value` prop at the [Final Form docs](https://final-form.org/docs/react-final-form/types/FieldProps#value)', + }, + }, +} diff --git a/packages/forms/src/TextAreaFieldFF/TextAreaFieldFF.js b/packages/forms/src/TextAreaFieldFF/TextAreaFieldFF.js index ea1e7c21dd..51f2e5514e 100644 --- a/packages/forms/src/TextAreaFieldFF/TextAreaFieldFF.js +++ b/packages/forms/src/TextAreaFieldFF/TextAreaFieldFF.js @@ -1,5 +1,5 @@ -import propTypes from '@dhis2/prop-types' import { TextAreaField } from '@dhis2/ui-widgets' +import PropTypes from 'prop-types' import React from 'react' import { createChangeHandler, @@ -40,16 +40,18 @@ export const TextAreaFieldFF = ({ ) TextAreaFieldFF.propTypes = { + /** `input` props received from Final Form `Field` */ input: inputPropType.isRequired, + /** `meta` props received from Final Form `Field` */ meta: metaPropType.isRequired, - error: propTypes.bool, - loading: propTypes.bool, - showLoadingStatus: propTypes.bool, - showValidStatus: propTypes.bool, - valid: propTypes.bool, - validationText: propTypes.string, + error: PropTypes.bool, + loading: PropTypes.bool, + showLoadingStatus: PropTypes.bool, + showValidStatus: PropTypes.bool, + valid: PropTypes.bool, + validationText: PropTypes.string, - onBlur: propTypes.func, - onFocus: propTypes.func, + onBlur: PropTypes.func, + onFocus: PropTypes.func, } diff --git a/packages/forms/src/TextAreaFieldFF/TextAreaFieldFF.stories.js b/packages/forms/src/TextAreaFieldFF/TextAreaFieldFF.stories.js index 73daa56aa1..8daf683e3c 100644 --- a/packages/forms/src/TextAreaFieldFF/TextAreaFieldFF.stories.js +++ b/packages/forms/src/TextAreaFieldFF/TextAreaFieldFF.stories.js @@ -1,70 +1,111 @@ -import { storiesOf } from '@storybook/react' import React from 'react' import { Field } from 'react-final-form' import { formDecorator } from '../formDecorator.js' +import { inputArgType, metaArgType } from '../shared/propTypes.js' import { hasValue } from '../validators/index.js' import { TextAreaFieldFF } from './TextAreaFieldFF.js' -storiesOf('TextAreaFieldFF', module) - .addDecorator(formDecorator) - .add('Default', () => ( - <Field component={TextAreaFieldFF} name="agree" label="Do you agree?" /> - )) - .add('Autogrow', () => ( - <Field - component={TextAreaFieldFF} - name="agree" - label="Do you agree?" - autoGrow - /> - )) - .add('Required', () => ( +const description = ` +The \`TextAreaFieldFF\` is a wrapper around a \`TextAreaField\` that enables it to work with Final Form, the preferred library for form validation and utilities in DHIS 2 apps. + +#### Final Form + +See how to use Final Form at [Final Form - Getting Started](https://final-form.org/docs/react-final-form/getting-started). + +Inside a Final Form \`<Form>\` component, these 'FF' UI components are intended to be used in the \`component\` prop of the [Final Form \`<Field>\` components](https://final-form.org/docs/react-final-form/api/Field) where they will receive some props from the Field, e.g. \`<Field component={TextAreaFieldFF} />\`. See the code samples below for examples. + +#### Props + +The props shown in the table below are generally provided to the \`TextAreaFieldFF\` wrapper by the Final Form \`Field\`. + +Note that any props beyond the API of the \`Field\` component will be spread to the \`TextAreaFieldFF\`, which passes any extra props to the underlying \`TextAreaField\` using \`{...rest}\`. + +Therefore, to add any props to the \`TextAreaFieldFF\` or \`TextAreaField\`, add those props to the parent Final Form \`Field\` component. + +Also see \`TextArea\` and \`TextAreaField\` for notes about props and implementation. + +\`\`\`js +import { TextAreaFieldFF } from '@dhis2/ui' +\`\`\` + +Press **Submit** to see the form values logged to the console. +` + +export default { + title: 'Forms/Text Area/Text Area Field (Final Form)', + component: TextAreaFieldFF, + decorators: [formDecorator], + parameters: { docs: { description: { component: description } } }, + argTypes: { + input: { ...inputArgType }, + meta: { ...metaArgType }, + }, +} + +export const Default = () => ( + <Field component={TextAreaFieldFF} name="agree" label="Do you agree?" /> +) + +export const Autogrow = () => ( + <Field + component={TextAreaFieldFF} + name="agree" + label="Do you agree?" + autoGrow + /> +) + +export const Required = () => ( + <Field + name="agree" + component={TextAreaFieldFF} + required + validate={hasValue} + label="Do you agree?" + /> +) + +export const Disabled = () => ( + <Field + name="agree" + component={TextAreaFieldFF} + disabled + label="Do you agree?" + /> +) + +export const HelpText = () => ( + <Field + name="agree" + component={TextAreaFieldFF} + label="Do you agree?" + helpText="Click to agree" + /> +) +HelpText.storyName = 'Help text' + +export const Statuses = () => ( + <> <Field - name="agree" + name="valid" component={TextAreaFieldFF} - required - validate={hasValue} - label="Do you agree?" + label="Valid" + valid + validationText="Validation text" /> - )) - .add('Disabled', () => ( <Field - name="agree" + name="warning" component={TextAreaFieldFF} - disabled - label="Do you agree?" + label="Warning" + warning + validationText="Validation text" /> - )) - .add('Help text', () => ( <Field - name="agree" + name="error" component={TextAreaFieldFF} - label="Do you agree?" - helpText="Click to agree" + label="Error" + error + validationText="Validation text" /> - )) - .add('Statuses', () => ( - <> - <Field - name="valid" - component={TextAreaFieldFF} - label="Valid" - valid - validationText="Validation text" - /> - <Field - name="warning" - component={TextAreaFieldFF} - label="Warning" - warning - validationText="Validation text" - /> - <Field - name="error" - component={TextAreaFieldFF} - label="Error" - error - validationText="Validation text" - /> - </> - )) + </> +) diff --git a/packages/forms/src/shared/propTypes.js b/packages/forms/src/shared/propTypes.js index 4ffd331069..3c7c266b95 100644 --- a/packages/forms/src/shared/propTypes.js +++ b/packages/forms/src/shared/propTypes.js @@ -14,6 +14,14 @@ const inputPropType = propTypes.shape({ onBlur: propTypes.func, onFocus: propTypes.func, }) +const inputArgType = { + table: { + type: { + summary: + '{ name: string (required), onChange: func (required), value: any, onBlur: func, onFocus: func }', + }, + }, +} const metaPropType = propTypes.shape({ error: propTypes.string, @@ -22,5 +30,19 @@ const metaPropType = propTypes.shape({ valid: propTypes.bool, validating: propTypes.bool, }) +const metaArgType = { + table: { + type: { + summary: + '{ error: string, invalid: bool, touched: bool, valid: bool, validating: bool }', + }, + }, +} -export { toggleGroupOptionsProp, inputPropType, metaPropType } +export { + toggleGroupOptionsProp, + inputPropType, + inputArgType, + metaPropType, + metaArgType, +} diff --git a/packages/widgets/src/CheckboxField/CheckboxField.js b/packages/widgets/src/CheckboxField/CheckboxField.js index bd493d3ece..749c017965 100644 --- a/packages/widgets/src/CheckboxField/CheckboxField.js +++ b/packages/widgets/src/CheckboxField/CheckboxField.js @@ -1,6 +1,6 @@ -import propTypes from '@dhis2/prop-types' import { sharedPropTypes } from '@dhis2/ui-constants' import { Field, Checkbox, Required } from '@dhis2/ui-core' +import PropTypes from 'prop-types' import React from 'react' const AddRequired = ({ label, required, dataTest }) => ( @@ -10,9 +10,9 @@ const AddRequired = ({ label, required, dataTest }) => ( </React.Fragment> ) AddRequired.propTypes = { - dataTest: propTypes.string, - label: propTypes.node, - required: propTypes.bool, + dataTest: PropTypes.string, + label: PropTypes.node, + required: PropTypes.bool, } /** @@ -116,25 +116,39 @@ CheckboxField.defaultProps = { * @prop {string} [dataTest] */ CheckboxField.propTypes = { - checked: propTypes.bool, - className: propTypes.string, - dataTest: propTypes.string, - dense: propTypes.bool, - disabled: propTypes.bool, + checked: PropTypes.bool, + className: PropTypes.string, + dataTest: PropTypes.string, + /** Smaller dimensions for information-dense layouts */ + dense: PropTypes.bool, + /** Disables the checkbox */ + disabled: PropTypes.bool, + /** Applies 'error' styling to checkbox and validation text for feedback. Mutually exclusive with `warning` and `valid` props */ error: sharedPropTypes.statusPropType, - helpText: propTypes.string, - initialFocus: propTypes.bool, - label: propTypes.node, - name: propTypes.string, - required: propTypes.bool, - tabIndex: propTypes.string, + /** Useful instructions for the user */ + helpText: PropTypes.string, + initialFocus: PropTypes.bool, + /** Labels the checkbox */ + label: PropTypes.node, + /** Name associate with the checkbox. Passed in object as argument to event handlers */ + name: PropTypes.string, + /** Adds an asterisk to indicate this field is required */ + required: PropTypes.bool, + tabIndex: PropTypes.string, + /** Applies 'valid' styling to checkbox and validation text for feedback. Mutually exclusive with `warning` and `error` props */ valid: sharedPropTypes.statusPropType, - validationText: propTypes.string, - value: propTypes.string, + /** Adds text below the checkbox to provide validation feedback. Acquires styles from `valid`, `warning` and `error` statuses */ + validationText: PropTypes.string, + /** Value associated with the checkbox. Passed in object as argument to event handlers */ + value: PropTypes.string, + /** Applies 'warning' styling to checkbox and validation text for feedback. Mutually exclusive with `valid` and `error` props */ warning: sharedPropTypes.statusPropType, - onBlur: propTypes.func, - onChange: propTypes.func, - onFocus: propTypes.func, + /** Called with signature ({ name: string, value: string, checked: bool }, event) */ + onBlur: PropTypes.func, + /** Called with signature ({ name: string, value: string, checked: bool }, event) */ + onChange: PropTypes.func, + /** Called with signature ({ name: string, value: string, checked: bool }, event) */ + onFocus: PropTypes.func, } export { CheckboxField } diff --git a/packages/widgets/src/CheckboxField/CheckboxField.stories.js b/packages/widgets/src/CheckboxField/CheckboxField.stories.js index 75497877bb..b2c6549ded 100644 --- a/packages/widgets/src/CheckboxField/CheckboxField.stories.js +++ b/packages/widgets/src/CheckboxField/CheckboxField.stories.js @@ -1,334 +1,141 @@ -import { storiesOf } from '@storybook/react' +import { sharedPropTypes } from '@dhis2/ui-constants' import React from 'react' import { CheckboxField } from './CheckboxField.js' -const logger = ({ name, value, checked }) => - console.info(`name: ${name}, value: ${value}, checked: ${checked}`) - -storiesOf('CheckboxField', module) - // Regular - .add('Default', () => ( - <CheckboxField - name="Ex" - label="CheckboxField" - value="default" - onChange={logger} - /> - )) - - .add('Focused unchecked', () => ( - <CheckboxField - initialFocus - name="Ex" - label="CheckboxField" - value="default" - onChange={logger} - /> - )) - - .add('Focused checked', () => ( - <CheckboxField - initialFocus - checked - name="Ex" - label="CheckboxField" - value="default" - onChange={logger} - /> - )) - - .add('Checked', () => ( - <CheckboxField - name="Ex" - label="CheckboxField" - checked - value="checked" - onChange={logger} - /> - )) - - .add('Required', () => ( - <CheckboxField - name="Ex" - label="CheckboxField" - required - value="checked" - onChange={logger} - /> - )) - - .add('Disabled', () => ( - <> - <CheckboxField - name="Ex" - label="CheckboxField" - disabled - value="disabled" - onChange={logger} - /> - <CheckboxField - name="Ex" - label="CheckboxField" - disabled - checked - value="disabled" - onChange={logger} - /> - </> - )) - - .add('Help text', () => ( - <> - <CheckboxField - name="Ex" - label="CheckboxField" - value="disabled" - onChange={logger} - helpText="Help text" - /> - <CheckboxField - name="Ex" - label="CheckboxField" - error - validationText="Validation text (error state)" - helpText="Help text" - value="disabled" - onChange={logger} - /> - </> - )) - - .add('Valid', () => ( - <> - <CheckboxField - name="Ex" - label="CheckboxField" - valid - validationText="I am a validation text" - value="valid" - onChange={logger} - /> - <CheckboxField - name="Ex" - label="CheckboxField" - valid - validationText="I am a validation text" - checked - value="valid" - onChange={logger} - /> - </> - )) - - .add('Warning', () => ( - <> - <CheckboxField - name="Ex" - label="CheckboxField" - warning - validationText="I am a validation text" - value="warning" - onChange={logger} - /> - <CheckboxField - name="Ex" - label="CheckboxField" - warning - validationText="I am a validation text" - checked - value="warning" - onChange={logger} - /> - </> - )) - - .add('Error', () => ( - <> - <CheckboxField - name="Ex" - label="CheckboxField" - error - validationText="I am a validation text" - value="error" - onChange={logger} - /> - <CheckboxField - name="Ex" - label="CheckboxField" - error - validationText="I am a validation text" - checked - value="error" - onChange={logger} - /> - </> - )) +const description = ` +A \`CheckboxField\` is a Checkbox component wrapped with extra form utilities, including the ability to add a label, help text, and validation text. Validation styles like 'error' apply to all of these subcomponents. - .add('Image label', () => ( - <CheckboxField - name="Ex" - label={<img src="https://picsum.photos/id/82/200/100" />} - value="with-help" - onChange={logger} - /> - )) +See the basic Checkbox for usage and design system guidelines. - // Dense - .add('Default - Dense', () => ( - <CheckboxField - dense - name="Ex" - label="CheckboxField" - value="default" - onChange={logger} - /> - )) +\`\`\`js +import { CheckboxField } from '@dhis2/ui' +\`\`\` +` - .add('Focused unchecked - Dense', () => ( - <CheckboxField - dense - initialFocus - name="Ex" - label="CheckboxField" - value="default" - onChange={logger} - /> - )) - - .add('Focused checked - Dense', () => ( - <CheckboxField - dense - initialFocus - checked - name="Ex" - label="CheckboxField" - value="default" - onChange={logger} - /> - )) - - .add('Checked - Dense', () => ( - <CheckboxField - dense - name="Ex" - label="CheckboxField" - checked - value="checked" - onChange={logger} - /> - )) - - .add('Required - Dense', () => ( - <CheckboxField - dense - name="Ex" - label="CheckboxField" - required - value="checked" - onChange={logger} - /> - )) - - .add('Disabled - Dense', () => ( - <> - <CheckboxField - dense - name="Ex" - label="CheckboxField" - disabled - value="disabled" - onChange={logger} - /> - <CheckboxField - dense - name="Ex" - label="CheckboxField" - disabled - checked - value="disabled" - onChange={logger} - /> - </> - )) - - .add('Valid - Dense', () => ( - <> - <CheckboxField - dense - name="Ex" - label="CheckboxField" - valid - validationText="I am a validation text" - value="valid" - onChange={logger} - /> - <CheckboxField - dense - name="Ex" - label="CheckboxField" - valid - validationText="I am a validation text" - checked - value="valid" - onChange={logger} - /> - </> - )) - - .add('Warning - Dense', () => ( - <> - <CheckboxField - dense - name="Ex" - label="CheckboxField" - warning - validationText="I am a validation text" - value="warning" - onChange={logger} - /> - <CheckboxField - dense - name="Ex" - label="CheckboxField" - warning - validationText="I am a validation text" - checked - value="warning" - onChange={logger} - /> - </> - )) - - .add('Error - Dense', () => ( - <> - <CheckboxField - dense - name="Ex" - label="CheckboxField" - error - validationText="I am a validation text" - value="error" - onChange={logger} - /> - <CheckboxField - dense - name="Ex" - label="CheckboxField" - error - validationText="I am a validation text" - checked - value="error" - onChange={logger} - /> - </> - )) - - .add('Image label - Dense', () => ( +const logger = ({ name, value, checked }) => + console.log(`name: ${name}, value: ${value}, checked: ${checked}`) + +export default { + title: 'Forms/Checkbox/Checkbox Field', + component: CheckboxField, + parameters: { docs: { description: { component: description } } }, + // Default args for stories + args: { + name: 'checkboxName', + label: 'Checkbox Field', + value: 'defaultValue', + onChange: logger, + }, + argTypes: { + valid: { ...sharedPropTypes.statusArgType }, + warning: { ...sharedPropTypes.statusArgType }, + error: { ...sharedPropTypes.statusArgType }, + }, +} + +const Template = args => <CheckboxField {...args} /> + +const CheckedUncheckedTemplate = args => ( + <> + <CheckboxField {...args} /> + <CheckboxField {...args} checked /> + </> +) + +export const Default = Template.bind({}) + +export const FocusedUnchecked = Template.bind({}) +FocusedUnchecked.args = { initialFocus: true } +// Disable stories on docs page that grab focus +FocusedUnchecked.parameters = { docs: { disable: true } } + +export const FocusedChecked = Template.bind({}) +FocusedChecked.args = { ...FocusedUnchecked.args, checked: true } +FocusedChecked.parameters = { docs: { disable: true } } + +export const Checked = Template.bind({}) +Checked.args = { checked: true, value: 'checkedValue' } + +export const Required = Template.bind({}) +Required.args = { required: true } + +export const Disabled = CheckedUncheckedTemplate.bind({}) +Disabled.args = { disabled: true } + +export const HelpText = args => ( + <> + <CheckboxField {...args} /> <CheckboxField - dense - name="Ex" - label={<img src="https://picsum.photos/id/82/200/100" />} - value="with-help" - onChange={logger} + {...args} + error + validationText="Validation text (error state)" /> - )) + </> +) +HelpText.args = { helpText: 'Help text' } + +export const Valid = CheckedUncheckedTemplate.bind({}) +Valid.args = { + valid: true, + validationText: 'I am validation text', + value: 'validValue', +} + +export const Warning = CheckedUncheckedTemplate.bind({}) +Warning.args = { + warning: true, + value: 'warningValue', + validationText: 'I am validation text', +} + +export const Error = CheckedUncheckedTemplate.bind({}) +Error.args = { + error: true, + value: 'errorValue', + validationText: 'I am validation text', +} + +export const ImageLabel = Template.bind({}) +ImageLabel.args = { label: <img src="https://picsum.photos/id/82/200/100" /> } + +export const DefaultDense = Template.bind({}) +DefaultDense.storyName = 'Default - Dense' +DefaultDense.args = { dense: true } + +export const FocusedUncheckedDense = Template.bind({}) +FocusedUncheckedDense.args = { ...DefaultDense.args, ...FocusedUnchecked.args } +FocusedUncheckedDense.parameters = { docs: { disable: true } } +FocusedUncheckedDense.storyName = 'Focused unchecked - Dense' + +export const FocusedCheckedDense = Template.bind({}) +FocusedCheckedDense.args = { ...DefaultDense.args, ...FocusedChecked.args } +FocusedCheckedDense.parameters = { docs: { disable: true } } +FocusedCheckedDense.storyName = 'Focused checked - Dense' + +export const CheckedDense = Template.bind({}) +CheckedDense.args = { ...DefaultDense.args, ...Checked.args } +CheckedDense.storyName = 'Checked - Dense' + +export const RequiredDense = Template.bind({}) +RequiredDense.args = { ...DefaultDense.args, ...Required.args } +RequiredDense.storyName = 'Required - Dense' + +export const DisabledDense = CheckedUncheckedTemplate.bind({}) +DisabledDense.args = { ...DefaultDense.args, ...Disabled.args } +DisabledDense.storyName = 'Disabled - Dense' + +export const ValidDense = CheckedUncheckedTemplate.bind({}) +ValidDense.args = { ...DefaultDense.args, ...Valid.args } +ValidDense.storyName = 'Valid - Dense' + +export const WarningDense = CheckedUncheckedTemplate.bind({}) +WarningDense.args = { ...DefaultDense.args, ...Warning.args } +WarningDense.storyName = 'Warning - Dense' + +export const ErrorDense = CheckedUncheckedTemplate.bind({}) +ErrorDense.args = { ...DefaultDense.args, ...Error.args } +ErrorDense.storyName = 'Error - Dense' + +export const ImageLabelDense = Template.bind({}) +ImageLabelDense.args = { ...DefaultDense.args, ...ImageLabel.args } +ImageLabelDense.storyName = 'Image label - Dense' diff --git a/packages/widgets/src/FieldGroup/FieldGroup.js b/packages/widgets/src/FieldGroup/FieldGroup.js index a60fce9c83..e8fe272d48 100644 --- a/packages/widgets/src/FieldGroup/FieldGroup.js +++ b/packages/widgets/src/FieldGroup/FieldGroup.js @@ -1,6 +1,6 @@ -import propTypes from '@dhis2/prop-types' import { sharedPropTypes } from '@dhis2/ui-constants' import { Field, FieldSet } from '@dhis2/ui-core' +import PropTypes from 'prop-types' import React from 'react' /** @@ -66,17 +66,26 @@ FieldGroup.defaultProps = { * @prop {boolean} [error] */ FieldGroup.propTypes = { - children: propTypes.node, - className: propTypes.string, - dataTest: propTypes.string, - disabled: propTypes.bool, + children: PropTypes.node, + className: PropTypes.string, + dataTest: PropTypes.string, + /** Disables the form controls within */ + disabled: PropTypes.bool, + /** Applies 'error' styling to validation text for feedback. Mutually exclusive with `warning` and `valid` props */ error: sharedPropTypes.statusPropType, - helpText: propTypes.string, - label: propTypes.string, - name: propTypes.string, - required: propTypes.bool, + /** Useful instructions for the user */ + helpText: PropTypes.string, + /** Labels the Field Group */ + label: PropTypes.string, + /** Name associate with the Field Group. Passed in object as argument to event handlers */ + name: PropTypes.string, + /** Adds an asterisk to indicate this field is required */ + required: PropTypes.bool, + /** Applies 'valid' styling to validation text for feedback. Mutually exclusive with `warning` and `error` props */ valid: sharedPropTypes.statusPropType, - validationText: propTypes.string, + /** Adds text at the bottom of the field to provide validation feedback. Acquires styles from `valid`, `warning` and `error` statuses */ + validationText: PropTypes.string, + /** Applies 'warning' styling to validation text for feedback. Mutually exclusive with `valid` and `error` props */ warning: sharedPropTypes.statusPropType, } diff --git a/packages/widgets/src/FieldGroup/FieldGroup.stories.js b/packages/widgets/src/FieldGroup/FieldGroup.stories.js index e77f4e6753..870575155d 100644 --- a/packages/widgets/src/FieldGroup/FieldGroup.stories.js +++ b/packages/widgets/src/FieldGroup/FieldGroup.stories.js @@ -1,85 +1,108 @@ +import { sharedPropTypes } from '@dhis2/ui-constants' import { Checkbox, Radio, Switch } from '@dhis2/ui-core' -import { storiesOf } from '@storybook/react' import React from 'react' import { FieldGroup } from './FieldGroup.js' -storiesOf('FieldGroup', module) - .add('With Checkbox', () => ( - <FieldGroup> +const description = ` +Wraps a group of form components like Radios, Checkboxes, or Switches. The FieldGroup wraps the form controls in a FieldSet and a Field component to group them and add a label, help text, and/or validation text. + +\`\`\`js +import { FieldGroup } from '@dhis2/ui' +\`\`\` +` + +export default { + title: 'Forms/Field Group/Field Group', + component: FieldGroup, + parameters: { docs: { description: { component: description } } }, + argTypes: { + valid: { ...sharedPropTypes.statusArgType }, + warning: { ...sharedPropTypes.statusArgType }, + error: { ...sharedPropTypes.statusArgType }, + }, +} + +export const WithCheckbox = args => ( + <FieldGroup {...args}> + <Checkbox value="first" label="First" /> + <Checkbox value="second" label="Second" /> + <Checkbox value="third" label="Third" /> + </FieldGroup> +) + +export const WithRadio = args => ( + <FieldGroup {...args}> + <Radio value="first" label="First" /> + <Radio value="second" label="Second" checked /> + <Radio value="third" label="Third" /> + <Radio value="fourth" label="Fourth" /> + </FieldGroup> +) + +export const WithSwitch = args => ( + <FieldGroup {...args}> + <Switch value="first" label="First" /> + <Switch value="second" label="Second" /> + <Switch value="third" label="Third" /> + <Switch value="fourth" label="Fourth" /> + </FieldGroup> +) + +export const WithLabel = args => ( + <> + <FieldGroup {...args}> <Checkbox value="first" label="First" /> <Checkbox value="second" label="Second" /> <Checkbox value="third" label="Third" /> </FieldGroup> - )) - - .add('With Radio', () => ( - <FieldGroup> - <Radio value="first" label="First" /> - <Radio value="second" label="Second" checked /> - <Radio value="third" label="Third" /> - <Radio value="fourth" label="Fourth" /> + <FieldGroup label="I am a required field" required> + <Checkbox value="first" label="First" /> + <Checkbox value="second" label="Second" /> + <Checkbox value="third" label="Third" /> </FieldGroup> - )) + </> +) +WithLabel.args = { label: 'I am a label/legend' } - .add('With Switch', () => ( - <FieldGroup> - <Switch value="first" label="First" /> - <Switch value="second" label="Second" /> - <Switch value="third" label="Third" /> - <Switch value="fourth" label="Fourth" /> +export const HelpAndValidationTexts = args => ( + <> + <FieldGroup {...args}> + <Checkbox value="first" label="First" /> + <Checkbox value="second" label="Second" /> + <Checkbox value="third" label="Third" /> </FieldGroup> - )) - - .add('With label', () => ( - <> - <FieldGroup label="I am a legend"> - <Checkbox value="first" label="First" /> - <Checkbox value="second" label="Second" /> - <Checkbox value="third" label="Third" /> - </FieldGroup> - <FieldGroup label="I am a required field" required> - <Checkbox value="first" label="First" /> - <Checkbox value="second" label="Second" /> - <Checkbox value="third" label="Third" /> - </FieldGroup> - </> - )) - - .add('Help and validation texts', () => ( - <> - <FieldGroup label="I am a field" helpText="Please help me!"> - <Checkbox value="first" label="First" /> - <Checkbox value="second" label="Second" /> - <Checkbox value="third" label="Third" /> - </FieldGroup> - <FieldGroup label="I am a legend" helpText="I am disabled" disabled> - <Checkbox value="first" label="First" disabled /> - <Checkbox value="second" label="Second" disabled /> - <Checkbox value="third" label="Third" disabled /> - </FieldGroup> - <FieldGroup label="I am a legend" valid validationText="I am valid"> - <Checkbox value="first" label="First" /> - <Checkbox value="second" label="Second" checked /> - <Checkbox value="third" label="Third" checked /> - </FieldGroup> - <FieldGroup - label="I am a legend" - name="warning" - warning - validationText="I have a warning" - > - <Checkbox value="first" label="First" /> - <Checkbox value="second" label="Second" /> - <Checkbox value="third" label="Third" /> - </FieldGroup> - <FieldGroup - label="I am a legend" - error - validationText="I have an error" - > - <Checkbox value="first" label="First" /> - <Checkbox value="second" label="Second" /> - <Checkbox value="third" label="Third" /> - </FieldGroup> - </> - )) + <FieldGroup label="I am a legend" helpText="I am disabled" disabled> + <Checkbox value="first" label="First" disabled /> + <Checkbox value="second" label="Second" disabled /> + <Checkbox value="third" label="Third" disabled /> + </FieldGroup> + <FieldGroup label="I am a legend" valid validationText="I am valid"> + <Checkbox value="first" label="First" /> + <Checkbox value="second" label="Second" checked /> + <Checkbox value="third" label="Third" checked /> + </FieldGroup> + <FieldGroup + label="I am a legend" + name="warning" + warning + validationText="I have a warning" + > + <Checkbox value="first" label="First" /> + <Checkbox value="second" label="Second" /> + <Checkbox value="third" label="Third" /> + </FieldGroup> + <FieldGroup + label="I am a legend" + error + validationText="I have an error" + > + <Checkbox value="first" label="First" /> + <Checkbox value="second" label="Second" /> + <Checkbox value="third" label="Third" /> + </FieldGroup> + </> +) +HelpAndValidationTexts.args = { + label: 'I am a field', + helpText: 'I am help text!', +} diff --git a/packages/widgets/src/FileInputField/FileInputField.js b/packages/widgets/src/FileInputField/FileInputField.js index 96a1a8ce00..9219ebb2ee 100644 --- a/packages/widgets/src/FileInputField/FileInputField.js +++ b/packages/widgets/src/FileInputField/FileInputField.js @@ -1,4 +1,3 @@ -import propTypes from '@dhis2/prop-types' import { sharedPropTypes } from '@dhis2/ui-constants' import { FileInput, @@ -7,6 +6,7 @@ import { Field, Label, } from '@dhis2/ui-core' +import PropTypes from 'prop-types' import React from 'react' import i18n from '../locales/index.js' import translate from '../translate' @@ -134,29 +134,45 @@ FileInputField.defaultProps = { * @prop {string} [dataTest] */ FileInputField.propTypes = { - accept: propTypes.string, - buttonLabel: propTypes.oneOfType([propTypes.string, propTypes.func]), - children: propTypes.node, - className: propTypes.string, - dataTest: propTypes.string, - disabled: propTypes.bool, + /** The `accept` attribute of the [native file input](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/file#accept) */ + accept: PropTypes.string, + /** Text on the button */ + buttonLabel: PropTypes.oneOfType([PropTypes.string, PropTypes.func]), + children: PropTypes.node, + className: PropTypes.string, + dataTest: PropTypes.string, + /** Disables the button */ + disabled: PropTypes.bool, + /** Applies 'error' styling to the validation text. Mutually exclusive with `warning` and `valid` props */ error: sharedPropTypes.statusPropType, - helpText: propTypes.string, - initialFocus: propTypes.bool, - label: propTypes.string, + /** Useful guiding text for the user */ + helpText: PropTypes.string, + initialFocus: PropTypes.bool, + /** A descriptive label above the button */ + label: PropTypes.string, + /** Size of the button. Mutually exclusive with the `small` prop */ large: sharedPropTypes.sizePropType, - multiple: propTypes.bool, - name: propTypes.string, - placeholder: propTypes.oneOfType([propTypes.string, propTypes.func]), - required: propTypes.bool, + /** The `multiple` attribute of the [native file input](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/file#multiple) */ + multiple: PropTypes.bool, + /** Name associated with input. Passed to event handler callbacks */ + name: PropTypes.string, + /** Placeholder below the button */ + placeholder: PropTypes.oneOfType([PropTypes.string, PropTypes.func]), + /** Adds an asterisk to indicate this field is required */ + required: PropTypes.bool, + /** Size of the button. Mutually exclusive with the `large` prop */ small: sharedPropTypes.sizePropType, - tabIndex: propTypes.string, + tabIndex: PropTypes.string, + /** Applies 'valid' styling to the validation text. Mutually exclusive with `warning` and `error` props */ valid: sharedPropTypes.statusPropType, - validationText: propTypes.string, + /** Text below the button that provides validation feedback */ + validationText: PropTypes.string, + /** Applies 'warning' styling to the validation text. Mutually exclusive with `valid` and `error` props */ warning: sharedPropTypes.statusPropType, - onBlur: propTypes.func, - onChange: propTypes.func, - onFocus: propTypes.func, + onBlur: PropTypes.func, + /** Called with signature ({ name: string, files: [] }, event) */ + onChange: PropTypes.func, + onFocus: PropTypes.func, } export { FileInputField } diff --git a/packages/widgets/src/FileInputField/FileInputField.stories.js b/packages/widgets/src/FileInputField/FileInputField.stories.js index 33e0552789..01e836d258 100644 --- a/packages/widgets/src/FileInputField/FileInputField.stories.js +++ b/packages/widgets/src/FileInputField/FileInputField.stories.js @@ -1,210 +1,164 @@ +import { sharedPropTypes } from '@dhis2/ui-constants' import { FileListItem } from '@dhis2/ui-core' -import { storiesOf } from '@storybook/react' import React from 'react' import { FileInputField } from './FileInputField.js' -const onChange = obj => { - console.log('onChange', obj) -} -const onRemove = () => { - console.log('onRemove') +const description = ` +The \`FileInputField\` component wraps the \`FileInput\` component in a \`Field\` wrapper to add labels, help text, and validation text. + +\`\`\`js +import { FileInputField, FileListItem } from '@dhis2/ui' +\`\`\` +` + +const onChange = obj => console.log('onChange', obj) +const onRemove = () => console.log('onRemove') +const onCancel = () => console.log('onCancel') + +export default { + title: 'Forms/File Input/File Input Field', + component: FileInputField, + parameters: { docs: { description: { component: description } } }, + // Default args: + args: { + // Handle default values (see comment in Transfer.js) + ...FileInputField.defaultProps, + onChange: onChange, + name: 'uploadName', + label: 'Upload something', + }, + argTypes: { + small: { ...sharedPropTypes.sizeArgType }, + large: { ...sharedPropTypes.sizeArgType }, + valid: { ...sharedPropTypes.statusArgType }, + warning: { ...sharedPropTypes.statusArgType }, + error: { ...sharedPropTypes.statusArgType }, + }, } -const onCancel = () => { - console.log('onCancel') + +const Template = args => <FileInputField {...args} /> + +export const Default = Template.bind({}) +Default.args = { label: null } + +export const WithLabel = Template.bind({}) + +export const Required = Template.bind({}) +Required.args = { required: true } + +export const Multiple = Template.bind({}) +Multiple.args = { + multiple: true, + label: 'Upload multiple things', + buttonLabel: 'Upload files', } -storiesOf('FileInputField', module) - .add('Default', () => ( - <FileInputField - onChange={onChange} - buttonLabel="Upload file" - name="upload" - /> - )) - .add('With label', () => ( - <FileInputField - name="upload" - onChange={onChange} - label="Upload something" - buttonLabel="Upload file" - /> - )) - .add('Required', () => ( - <FileInputField - name="upload" - onChange={onChange} - label="upload something" - buttonLabel="Upload file" - required - /> - )) - .add('Multiple', () => ( - <FileInputField - name="upload" - onChange={onChange} - label="upload multiple things" - buttonLabel="Upload files" - multiple - /> - )) - .add('Disabled', () => ( - <FileInputField - name="upload" - onChange={onChange} - label="upload something" - buttonLabel="Upload file" - disabled - /> - )) - .add('Sizes', () => ( - <> - <FileInputField - onChange={onChange} - label="upload something" - buttonLabel="Default size" - name="default" - /> - <FileInputField - onChange={onChange} - label="upload something" - buttonLabel="Small" - small - name="small" - /> - <FileInputField - onChange={onChange} - label="upload something" - buttonLabel="Large" - large - name="large" - /> - </> - )) - .add('Statuses', () => ( - <> - <FileInputField - onChange={onChange} - label="upload something" - buttonLabel="Default" - name="default" - /> - <FileInputField - onChange={onChange} - label="upload something" - buttonLabel="Valid" - name="valid" - valid - /> - <FileInputField - onChange={onChange} - label="upload something" - buttonLabel="Warning" - name="warning" - warning - /> - <FileInputField - onChange={onChange} - label="upload something" - buttonLabel="Error" - name="error" - error - validationText="Something went wrong" - /> - </> - )) - .add('File list', () => ( - <div style={{ width: 250 }}> - <FileInputField - onChange={onChange} - label="Upload something" - buttonLabel="Upload file" - name="upload" - > - <FileListItem - label="picture1.jpg" - onRemove={onRemove} - onCancel={onCancel} - cancelText="Cancel" - removeText="Remove" - /> - <FileListItem - label="image_that_is_uploading.jpg" - onRemove={onRemove} - onCancel={onCancel} - cancelText="Cancel" - removeText="Remove" - loading - /> - <FileListItem - label="image_file_name_is_to_long_to_display_on_one_line.jpg" - onRemove={onRemove} - onCancel={onCancel} - cancelText="Cancel" - removeText="Remove" - /> - </FileInputField> - <br /> - <p style={{ color: 'grey' }}> - <em>Bounding box is 250px wide</em> - </p> - </div> - )) - .add('Placeholder text', () => ( +export const Disabled = Template.bind({}) +Disabled.args = { disabled: true } + +export const Sizes = args => ( + <> <FileInputField - onChange={onChange} - label="Upload something" - buttonLabel="Upload file" - name="upload" - placeholder="No file(s) selected yet" + {...args} + buttonLabel="Default size" + name="defaultName" /> - )) - .add('Help text', () => ( + <FileInputField {...args} buttonLabel="Small" small name="smallName" /> + <FileInputField {...args} buttonLabel="Large" large name="largeName" /> + </> +) + +export const Statuses = args => ( + <> + <FileInputField {...args} buttonLabel="Default" name="defaultName" /> + <FileInputField {...args} buttonLabel="Valid" name="validName" valid /> <FileInputField - onChange={onChange} - label="Upload something" - buttonLabel="Upload file" - name="upload" - helpText="Please select any file type" + {...args} + buttonLabel="Warning" + name="warningName" + warning /> - )) - .add('Design system stacking order', () => ( <FileInputField - onChange={onChange} - label="upload something" - buttonLabel="Upload file" + {...args} + buttonLabel="Error" + name="errorName" error - validationText="Oops" - placeholder="Select a file" - helpText="Please upload something" - name="upload" - > + validationText="Something went wrong" + /> + </> +) + +export const FileList = args => ( + <div style={{ width: 250 }}> + <FileInputField {...args}> <FileListItem - label="TestFile.txt" + label="picture1.jpg" onRemove={onRemove} - removeText="remove" + onCancel={onCancel} + cancelText="Cancel" + removeText="Remove" /> <FileListItem - label="BusyFile.txt" + label="image_that_is_uploading.jpg" onRemove={onRemove} onCancel={onCancel} - cancelText="cancel" - removeText="remove" + cancelText="Cancel" + removeText="Remove" loading /> + <FileListItem + label="image_file_name_is_to_long_to_display_on_one_line.jpg" + onRemove={onRemove} + onCancel={onCancel} + cancelText="Cancel" + removeText="Remove" + /> </FileInputField> - )) - .add('Design system stacking order - empty file list', () => ( - <FileInputField - onChange={onChange} - label="upload something" - buttonLabel="Upload file" - error - validationText="Oops" - placeholder="Select a file" - helpText="Please upload something" - name="upload" + <br /> + <p style={{ color: 'grey' }}> + <em>Bounding box is 250px wide</em> + </p> + </div> +) + +export const PlaceholderText = Template.bind({}) +PlaceholderText.args = { placeholder: 'No file(s) selected yet' } + +export const HelpText = Template.bind({}) +HelpText.args = { helpText: 'Please select any file type' } + +export const DesignSystemStackingOrder = args => ( + <FileInputField {...args}> + <FileListItem + label="TestFile.txt" + onRemove={onRemove} + removeText="remove" /> - )) - .add('Default: buttonLabel and placeholder', () => ( - <FileInputField onChange={onChange} name="upload" /> - )) + <FileListItem + label="BusyFile.txt" + onRemove={onRemove} + onCancel={onCancel} + cancelText="cancel" + removeText="remove" + loading + /> + </FileInputField> +) +DesignSystemStackingOrder.args = { + error: true, + validationText: 'Oops!', + placeholder: 'Select a file', + helpText: 'Please upload something', +} + +export const DesignSystemStackingOrderEmptyFileList = Template.bind({}) +DesignSystemStackingOrderEmptyFileList.args = { + ...DesignSystemStackingOrder.args, +} +DesignSystemStackingOrderEmptyFileList.storyName = + 'Design system stacking order - empty file list' + +export const DefaultButtonLabelAndPlaceholder = Template.bind({}) +DefaultButtonLabelAndPlaceholder.args = { label: null } +DefaultButtonLabelAndPlaceholder.storyName = + 'Default: buttonLabel and placeholder' diff --git a/packages/widgets/src/FileInputFieldWithList/FileInputFieldWithList.js b/packages/widgets/src/FileInputFieldWithList/FileInputFieldWithList.js index 8f674c5530..b3b75c7c27 100644 --- a/packages/widgets/src/FileInputFieldWithList/FileInputFieldWithList.js +++ b/packages/widgets/src/FileInputFieldWithList/FileInputFieldWithList.js @@ -1,5 +1,5 @@ -import propTypes from '@dhis2/prop-types' import { sharedPropTypes } from '@dhis2/ui-constants' +import PropTypes from 'prop-types' import React, { Component } from 'react' import { FileInputField } from '../FileInputField/FileInputField.js' import i18n from '../locales/index.js' @@ -174,30 +174,47 @@ FileInputFieldWithList.defaultProps = { * @prop {string} [dataTest=dhis2-uiwidgets-fileinputfieldwithlist] */ FileInputFieldWithList.propTypes = { - onChange: propTypes.func.isRequired, - accept: propTypes.string, - buttonLabel: propTypes.oneOfType([propTypes.string, propTypes.func]), - className: propTypes.string, - dataTest: propTypes.string, - disabled: propTypes.bool, + /** Called with signature ({ name: string, files: [File] }, event) */ + onChange: PropTypes.func.isRequired, + /** The `accept` attribute of the [native file input](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/file#accept) */ + accept: PropTypes.string, + /** Text on the button */ + buttonLabel: PropTypes.oneOfType([PropTypes.string, PropTypes.func]), + className: PropTypes.string, + dataTest: PropTypes.string, + /** Disables the button */ + disabled: PropTypes.bool, + /** Applies 'error' styling to the button and validation text. Mutually exclusive with `warning` and `valid` props */ error: sharedPropTypes.statusPropType, - files: propTypes.arrayOf(propTypes.instanceOf(File)), - helpText: propTypes.string, - initialFocus: propTypes.bool, - label: propTypes.string, + files: PropTypes.arrayOf(PropTypes.instanceOf(File)), + /** Useful guiding text for the user */ + helpText: PropTypes.string, + initialFocus: PropTypes.bool, + /** A descriptive label above the button */ + label: PropTypes.string, + /** Size of the button. Mutually exclusive with the `small` prop */ large: sharedPropTypes.sizePropType, - multiple: propTypes.bool, - name: propTypes.string, - placeholder: propTypes.oneOfType([propTypes.string, propTypes.func]), - removeText: propTypes.oneOfType([propTypes.string, propTypes.func]), - required: propTypes.bool, + /** The `multiple` attribute of the [native file input](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/file#multiple) */ + multiple: PropTypes.bool, + /** Name associated with input. Passed to event handler callbacks */ + name: PropTypes.string, + /** Placeholder below the button */ + placeholder: PropTypes.oneOfType([PropTypes.string, PropTypes.func]), + /** Text used for the button that removes a file from the list */ + removeText: PropTypes.oneOfType([PropTypes.string, PropTypes.func]), + /** Adds an asterisk to indicate this field is required */ + required: PropTypes.bool, + /** Size of the button. Mutually exclusive with the `large` prop */ small: sharedPropTypes.sizePropType, - tabIndex: propTypes.string, + tabIndex: PropTypes.string, + /** Applies 'valid' styling to the button and validation text. Mutually exclusive with `warning` and `error` props */ valid: sharedPropTypes.statusPropType, - validationText: propTypes.string, + /** Text below the button that provides validation feedback */ + validationText: PropTypes.string, + /** Applies 'warning' styling to the button and validation text. Mutually exclusive with `valid` and `error` props */ warning: sharedPropTypes.statusPropType, - onBlur: propTypes.func, - onFocus: propTypes.func, + onBlur: PropTypes.func, + onFocus: PropTypes.func, } export { FileInputFieldWithList } diff --git a/packages/widgets/src/FileInputFieldWithList/FileInputFieldWithList.stories.js b/packages/widgets/src/FileInputFieldWithList/FileInputFieldWithList.stories.js index 96916fe19f..2ec670a0f1 100644 --- a/packages/widgets/src/FileInputFieldWithList/FileInputFieldWithList.stories.js +++ b/packages/widgets/src/FileInputFieldWithList/FileInputFieldWithList.stories.js @@ -1,7 +1,15 @@ -import { storiesOf } from '@storybook/react' +import { sharedPropTypes } from '@dhis2/ui-constants' import React from 'react' import { FileInputFieldWithList } from './FileInputFieldWithList.js' +const description = ` +A FileInputField with logic for creating a dynamic list of removable files from an array of \`File\` objects. + +\`\`\`js +import { FileInputFieldWithList } from '@dhis2/ui' +\`\`\` +` + const files = new Array(10) .fill('dummy-file-name') .map((name, i) => new File([], `${name}-${i + 1}.txt`)) @@ -10,25 +18,39 @@ const onChange = ({ files }) => { console.log('files: ', files) } -storiesOf('FileInputFieldWithList', module) - .add('Default', () => ( - <FileInputFieldWithList - multiple - onChange={onChange} - buttonLabel="Upload file" - name="upload" - files={files} - removeText="remove" - /> - )) - .add('Default: buttonLabel and removeText', () => ( - <FileInputFieldWithList - multiple - onChange={onChange} - name="upload" - files={files} - /> - )) - .add('Default: placeholder', () => ( - <FileInputFieldWithList multiple onChange={onChange} name="upload" /> - )) +export default { + title: 'Forms/File Input/File Input Field With List', + component: FileInputFieldWithList, + parameters: { docs: { description: { component: description } } }, + args: { + // Handle default props bug (see Transfer stories) + ...FileInputFieldWithList.defaultProps, + multiple: true, + onChange: onChange, + name: 'uploadName', + }, + argTypes: { + small: { ...sharedPropTypes.sizeArgType }, + large: { ...sharedPropTypes.sizeArgType }, + valid: { ...sharedPropTypes.statusArgType }, + warning: { ...sharedPropTypes.statusArgType }, + error: { ...sharedPropTypes.statusArgType }, + }, +} + +const Template = args => <FileInputFieldWithList {...args} /> + +export const Default = Template.bind({}) +Default.args = { + buttonLabel: 'Upload file (custom label)', + files: files, + removeText: 'Custom remove text', +} + +export const DefaultButtonLabelAndRemoveText = Template.bind({}) +DefaultButtonLabelAndRemoveText.args = { files: files } +DefaultButtonLabelAndRemoveText.storyName = + 'Default: buttonLabel and removeText' + +export const DefaultPlaceholder = Template.bind({}) +DefaultPlaceholder.storyName = 'Default: placeholder' diff --git a/packages/widgets/src/HeaderBar/HeaderBar.js b/packages/widgets/src/HeaderBar/HeaderBar.js index 5de5bb313f..d079bd3a62 100755 --- a/packages/widgets/src/HeaderBar/HeaderBar.js +++ b/packages/widgets/src/HeaderBar/HeaderBar.js @@ -1,6 +1,6 @@ import { useDataQuery, useConfig } from '@dhis2/app-runtime' -import propTypes from '@dhis2/prop-types' import { colors } from '@dhis2/ui-constants' +import PropTypes from 'prop-types' import React, { useMemo } from 'react' import i18n from '../locales/index.js' import Apps from './Apps.js' @@ -105,6 +105,6 @@ export const HeaderBar = ({ appName, className }) => { } HeaderBar.propTypes = { - appName: propTypes.string, - className: propTypes.string, + appName: PropTypes.string, + className: PropTypes.string, } diff --git a/packages/widgets/src/HeaderBar/HeaderBar.stories.js b/packages/widgets/src/HeaderBar/HeaderBar.stories.js index ceadd0db62..3618180082 100644 --- a/packages/widgets/src/HeaderBar/HeaderBar.stories.js +++ b/packages/widgets/src/HeaderBar/HeaderBar.stories.js @@ -1,8 +1,33 @@ import { CustomDataProvider, Provider } from '@dhis2/app-runtime' -import { storiesOf } from '@storybook/react' import React from 'react' import { HeaderBar } from './HeaderBar.js' +const subtitle = 'The common navigation bar used in all DHIS2 apps' + +const description = ` +The header bar is mandatory for all apps. This creates a stable, understandable point of reference for the user across all kinds of different apps. It must always be displayed fixed to the top of the screen. Do not interfere or obstruct interaction with the header bar. + +The header bar is included automatically with the App Shell and should not need any configuration. + +#### Theme + +The header bar can be themeed to suit the brand/color of your DHIS2 instance. The color of the text/icons will be automatically adjusted based on the selected color. + +\`\`\`js +import { HeaderBar } from '@dhis2/ui' +\`\`\` +` + +export default { + title: 'Utils/Header Bar', + component: HeaderBar, + parameters: { + componentSubtitle: subtitle, + docs: { description: { component: description } }, + }, + args: { appName: 'Example!' }, +} + const mockConfig = { baseUrl: 'https://debug.dhis2.org/dev/', apiVersion: 33, @@ -153,61 +178,57 @@ const customAuthoritiesData = { }, } -storiesOf('HeaderBar', module) - .add('Default', () => ( - <Provider config={mockConfig}> - <CustomDataProvider data={customData}> - <HeaderBar appName="Example!" /> - </CustomDataProvider> - </Provider> - )) - .add('Custom Logo (wide dimension)', () => ( - <Provider config={mockConfig}> - <CustomDataProvider data={customLogoData}> - <HeaderBar appName="Example!" /> - </CustomDataProvider> - </Provider> - )) - .add('Non-english user locale', () => ( - <Provider config={mockConfig}> - <CustomDataProvider data={customLocaleData}> - <HeaderBar appName="Exemple!" /> - </CustomDataProvider> - </Provider> - )) - .add('No authority for interpretations app', () => ( - <Provider config={mockConfig}> - <CustomDataProvider data={customAuthoritiesData}> - <HeaderBar appName="Exemple!" /> - </CustomDataProvider> - </Provider> - )) - .add('Loading...', () => ( - <Provider config={mockConfig}> - <CustomDataProvider options={{ loadForever: true }}> - <HeaderBar appName="Example!" /> - </CustomDataProvider> - </Provider> - )) - .add('Error!', () => ( - <Provider config={mockConfig}> - <CustomDataProvider data={{}}> - <HeaderBar appName="Example!" /> - </CustomDataProvider> - </Provider> - )) - -/* - * Uncomment this story to test against a real API - */ - -/* - .add('Real API', () => ( - <Provider config={{ - apiVersion: '33', - baseUrl: 'https://dhis2.vardevs.se/dev', - }}> - <HeaderBar appName="Real API" /> - </Provider> - )) - */ +export const Default = args => ( + <Provider config={mockConfig}> + <CustomDataProvider data={customData}> + <HeaderBar {...args} /> + </CustomDataProvider> + </Provider> +) + +export const CustomLogoWideDimension = args => ( + <Provider config={mockConfig}> + <CustomDataProvider data={customLogoData}> + <HeaderBar {...args} /> + </CustomDataProvider> + </Provider> +) +CustomLogoWideDimension.storyName = 'Custom Logo (wide dimension)' + +export const NonEnglishUserLocale = args => ( + <Provider config={mockConfig}> + <CustomDataProvider data={customLocaleData}> + <HeaderBar {...args} /> + </CustomDataProvider> + </Provider> +) +NonEnglishUserLocale.args = { appName: 'Exemple!' } +NonEnglishUserLocale.storyName = 'Non-english user locale' + +export const NoAuthorityForInterpretationsApp = args => ( + <Provider config={mockConfig}> + <CustomDataProvider data={customAuthoritiesData}> + <HeaderBar {...args} /> + </CustomDataProvider> + </Provider> +) +NoAuthorityForInterpretationsApp.storyName = + 'No authority for interpretations app' + +export const Loading = args => ( + <Provider config={mockConfig}> + <CustomDataProvider options={{ loadForever: true }}> + <HeaderBar {...args} /> + </CustomDataProvider> + </Provider> +) +Loading.storyName = 'Loading...' + +export const Error = args => ( + <Provider config={mockConfig}> + <CustomDataProvider data={{}}> + <HeaderBar {...args} /> + </CustomDataProvider> + </Provider> +) +Error.storyName = 'Error!' diff --git a/packages/widgets/src/InputField/InputField.js b/packages/widgets/src/InputField/InputField.js index 949002075f..e1bedda35f 100644 --- a/packages/widgets/src/InputField/InputField.js +++ b/packages/widgets/src/InputField/InputField.js @@ -1,4 +1,3 @@ -import propTypes from '@dhis2/prop-types' import { sharedPropTypes } from '@dhis2/ui-constants' import { Field, Input, Box } from '@dhis2/ui-core' import PropTypes from 'prop-types' @@ -128,35 +127,55 @@ InputField.defaultProps = { * @prop {string} [dataTest] */ InputField.propTypes = { - className: propTypes.string, - dataTest: propTypes.string, - dense: propTypes.bool, - disabled: propTypes.bool, + className: PropTypes.string, + dataTest: PropTypes.string, + /** Makes the input smaller */ + dense: PropTypes.bool, + /** Disables the input */ + disabled: PropTypes.bool, + /** Applies 'error' appearance for validation feedback. Mutually exclusive with `valid` and `warning` props */ error: sharedPropTypes.statusPropType, - helpText: propTypes.string, - initialFocus: propTypes.bool, - inputWidth: propTypes.string, - label: propTypes.string, - loading: propTypes.bool, + /** Guiding text for how to use this input */ + helpText: PropTypes.string, + /** The input grabs initial focus on the page */ + initialFocus: PropTypes.bool, + /** Defines the width of the input. Can be any valid CSS measurement */ + inputWidth: PropTypes.string, + /** Label text for the input */ + label: PropTypes.string, + /** Adds a loading indicator beside the input */ + loading: PropTypes.bool, /** The [native `max` attribute](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#htmlattrdefmax), for use when `type` is `'number'` */ max: PropTypes.string, /** The [native `min` attribute](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#htmlattrdefmin), for use when `type` is `'number'` */ min: PropTypes.string, - name: propTypes.string, - placeholder: propTypes.string, - readOnly: propTypes.bool, - required: propTypes.bool, + /** Name associated with the input. Passed to event handler callbacks in object */ + name: PropTypes.string, + /** Placeholder text for the input */ + placeholder: PropTypes.string, + /** Makes the input read-only */ + readOnly: PropTypes.bool, + /** Indicates this input is required */ + required: PropTypes.bool, /** The [native `step` attribute](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#htmlattrdefstep), for use when `type` is `'number'` */ step: PropTypes.string, - tabIndex: propTypes.string, + tabIndex: PropTypes.string, + /** Type of input */ type: Input.propTypes.type, + /** Applies 'valid' appearance for validation feedback. Mutually exclusive with `error` and `warning` props */ valid: sharedPropTypes.statusPropType, - validationText: propTypes.string, - value: propTypes.string, + /** Text below input for validation feedback. Receives styles depending on validation status */ + validationText: PropTypes.string, + /** Value in the input. Can be used to control the component (recommended). Passed to event handler callbacks in object */ + value: PropTypes.string, + /** Applies 'warning' appearance for validation feedback. Mutually exclusive with `valid` and `error` props */ warning: sharedPropTypes.statusPropType, - onBlur: propTypes.func, - onChange: propTypes.func, - onFocus: propTypes.func, + /** Called with signature `({ name: string, value: string }, event)` */ + onBlur: PropTypes.func, + /** Called with signature `({ name: string, value: string }, event)` */ + onChange: PropTypes.func, + /** Called with signature `({ name: string, value: string }, event)` */ + onFocus: PropTypes.func, } export { InputField } diff --git a/packages/widgets/src/InputField/InputField.stories.js b/packages/widgets/src/InputField/InputField.stories.js index f8cb43dfc8..3a4f611aa0 100644 --- a/packages/widgets/src/InputField/InputField.stories.js +++ b/packages/widgets/src/InputField/InputField.stories.js @@ -1,118 +1,161 @@ -import { storiesOf } from '@storybook/react' +import { sharedPropTypes } from '@dhis2/ui-constants' import React from 'react' import { InputField } from './InputField.js' -const logger = ({ name, value }) => console.info(`${name}: ${value}`) - -createStory('InputField', { - label: 'Default label', - name: 'Default', - onChange: logger, -}) - -function createStory(name, props) { - return storiesOf(name, module) - .add('Default', () => <InputField name="nolabel" onChange={logger} />) - - .add('No placeholder, no value', () => <InputField {...props} />) - - .add('Placeholder, no value', () => ( - <InputField {...props} placeholder="Hold the place" /> - )) - - .add('With Help text', () => ( - <InputField - {...props} - placeholder="Hold the place" - helpText="With some helping text to guide the user along" - /> - )) - - .add('With value', () => ( - <InputField - {...props} - value="This is set through the value prop, which means the component is controlled." - /> - )) - - .add('Focus', () => <InputField {...props} initialFocus />) - - .add('Status: Valid', () => ( - <InputField {...props} value="This value is valid" valid /> - )) - - .add('Status: Warning', () => ( - <InputField - {...props} - value="This value produces a warning" - warning - /> - )) - - .add('Status: Error', () => ( - <InputField - {...props} - error - value="This value produces an error" - helpText="This is some help text to advice what this input actually is." - validationText="This describes the error, if a message is supplied." - /> - )) - - .add('Status: Loading', () => ( - <InputField - {...props} - value="This value produces a loading state" - loading - /> - )) - - .add('Disabled', () => ( - <InputField {...props} value="This field is disabled" disabled /> - )) - - .add('Read only', () => ( - <InputField {...props} value="This field is disabled" readOnly /> - )) - - .add('Dense', () => ( - <InputField {...props} value="This field is dense" dense /> - )) - - .add('Input width', () => ( - <> - <InputField - name="input1" - label="My textarea has a width of 100px" - inputWidth="100px" - onChange={logger} - /> - <InputField - name="input2" - label="My textarea has a width of 220px" - inputWidth="220px" - onChange={logger} - /> - </> - )) - - .add('Label text overflow', () => ( - <InputField - {...props} - label="This label is too long to show on a single line of the input field's label. We just let it flow to the next line so the user can still read it. However, we should always aim to keep it shorter than this!" - dense - warning - /> - )) - - .add('Value text overflow', () => ( - <InputField - {...props} - value="This value is too long in order to show on a single line of the input field. It should stay on one line, not in an extra line and which wouldn't look like a standard input" - dense - warning - /> - )) - - .add('Required', () => <InputField {...props} required />) +const subtitle = 'Allows a user to enter data, usually text' + +const description = ` +Inputs are used wherever a user needs to input standard text information. Inputs are often used as part of forms. An input can also be used to capture information outside of a form, perhaps as a 'Filter' or 'Search' field. + +InputField wraps an Input component with a label, help text, validation text, and some other features. + +Please see more about options and features of inputs at [Design System: Input Field](https://github.com/dhis2/design-system/blob/master/atoms/inputfield.md#input). + +\`\`\`js +import { InputField } from '@dhis2/ui' +\`\`\` +` + +const logger = ({ name, value }) => + console.log(`Name: ${name}, value: ${value}`) + +const inputTypeArgType = { + table: { type: { summary: 'string' } }, + control: { + type: 'select', + options: [ + 'text', + 'number', + 'password', + 'email', + 'url', + 'tel', + 'date', + 'datetime', + 'datetime-local', + 'month', + 'week', + 'time', + 'search', + ], + }, } + +export default { + title: 'Forms/Input/Input Field', + component: InputField, + parameters: { + componentSubtitle: subtitle, + docs: { description: { component: description } }, + }, + // Default args + args: { + label: 'Default label', + name: 'defaultName', + onChange: logger, + }, + argTypes: { + type: { ...inputTypeArgType }, + valid: { ...sharedPropTypes.statusArgType }, + warning: { ...sharedPropTypes.statusArgType }, + error: { ...sharedPropTypes.statusArgType }, + }, +} + +const Template = args => <InputField {...args} /> + +export const Default = Template.bind({}) + +export const NoPlaceholderNoValue = Template.bind({}) +NoPlaceholderNoValue.storyName = 'No placeholder, no value' + +export const PlaceholderNoValue = Template.bind({}) +PlaceholderNoValue.args = { placeholder: 'Hold the place' } +PlaceholderNoValue.storyName = 'Placeholder, no value' + +export const WithHelpText = Template.bind({}) +WithHelpText.args = { + ...PlaceholderNoValue.args, + helpText: 'With some helping text to guide the user along', +} + +export const WithValue = Template.bind({}) +WithValue.args = { + value: + 'This is set through the value prop, which means the component is controlled.', +} + +export const Focus = Template.bind({}) +Focus.args = { initialFocus: true } +// Disabled initial focus stories on docs page +Focus.parameters = { docs: { disable: true } } + +export const StatusValid = Template.bind({}) +StatusValid.args = { valid: true, value: 'This value is valid' } +StatusValid.storyName = 'Status: Valid' + +export const StatusWarning = Template.bind({}) +StatusWarning.args = { warning: true, value: 'This value produces a warning' } +StatusWarning.storyName = 'Status: Warning' + +export const StatusError = Template.bind({}) +StatusError.args = { + error: true, + value: 'This value produces an error', + helpText: 'This is some help text to advise what this input actually is.', + validationText: + 'This validation text describes the error, if a message is supplied.', +} +StatusError.storyName = 'Status: Error' + +export const StatusLoading = Template.bind({}) +StatusLoading.args = { + loading: true, + value: 'This value produces a loading state', +} +StatusLoading.storyName = 'Status: Loading' + +export const Disabled = Template.bind({}) +Disabled.args = { disabled: true, value: 'This field is disabled' } + +export const ReadOnly = Template.bind({}) +ReadOnly.args = { readOnly: true, value: 'This field is read-only' } + +export const Dense = Template.bind({}) +Dense.args = { dense: true, value: 'This field is dense' } + +export const InputWidth = args => ( + <> + <InputField + {...args} + name="input1" + label="My textarea has a width of 100px" + inputWidth="100px" + /> + <InputField + {...args} + name="input2" + label="My textarea has a width of 220px" + inputWidth="220px" + /> + </> +) + +export const LabelTextOverflow = Template.bind({}) +LabelTextOverflow.args = { + dense: true, + warning: true, + label: + "This label is too long to show on a single line of the input field's label. We just let it flow to the next line so the user can still read it. However, we should always aim to keep it shorter than this!", +} + +export const ValueTextOverflow = Template.bind({}) +ValueTextOverflow.args = { + value: + "This value is too long in order to show on a single line of the input field. It should stay on one line, not in an extra line and which wouldn't look like a standard input", + dense: true, + warning: true, +} + +export const Required = Template.bind({}) +Required.args = { required: true } diff --git a/packages/widgets/src/MultiSelectField/MultiSelectField.js b/packages/widgets/src/MultiSelectField/MultiSelectField.js index 76755266d5..d54e207bb4 100644 --- a/packages/widgets/src/MultiSelectField/MultiSelectField.js +++ b/packages/widgets/src/MultiSelectField/MultiSelectField.js @@ -1,6 +1,6 @@ -import propTypes from '@dhis2/prop-types' import { sharedPropTypes } from '@dhis2/ui-constants' import { Field, Box, MultiSelect } from '@dhis2/ui-core' +import PropTypes from 'prop-types' import React from 'react' import i18n from '../locales/index.js' import translate from '../translate' @@ -147,37 +147,65 @@ MultiSelectField.defaultProps = { * @prop {string} [dataTest] */ MultiSelectField.propTypes = { - children: propTypes.node, - className: propTypes.string, - clearText: propTypes.oneOfType([propTypes.string, propTypes.func]), - clearable: propTypes.bool, - dataTest: propTypes.string, - dense: propTypes.bool, - disabled: propTypes.bool, - empty: propTypes.oneOfType([propTypes.node, propTypes.func]), + /** Should be `MultiSelectOption` components */ + children: PropTypes.node, + className: PropTypes.string, + /** Label for the button that clears selections */ + clearText: PropTypes.oneOfType([PropTypes.string, PropTypes.func]), + /** Adds a button to the MultiSelect that clears selections when pressed */ + clearable: PropTypes.bool, + dataTest: PropTypes.string, + /** Makes the MultiSelect smaller */ + dense: PropTypes.bool, + /** Disables the MultiSelect */ + disabled: PropTypes.bool, + /** Text to display when there are no options */ + empty: PropTypes.oneOfType([PropTypes.node, PropTypes.func]), + /** Adds 'error' appearance for validation feedback. Mutually exclusive with 'valid' and 'warning' props */ error: sharedPropTypes.statusPropType, - filterPlaceholder: propTypes.oneOfType([propTypes.node, propTypes.func]), - filterable: propTypes.bool, - helpText: propTypes.string, - initialFocus: propTypes.bool, - inputMaxHeight: propTypes.string, - inputWidth: propTypes.string, - label: propTypes.string, - loading: propTypes.bool, - loadingText: propTypes.oneOfType([propTypes.string, propTypes.func]), - maxHeight: propTypes.string, - noMatchText: propTypes.oneOfType([propTypes.string, propTypes.func]), - placeholder: propTypes.string, - prefix: propTypes.string, - required: propTypes.bool, - selected: propTypes.arrayOf(propTypes.string), - tabIndex: propTypes.string, + /** Placeholder text to show in the filter field when it is empty */ + filterPlaceholder: PropTypes.oneOfType([PropTypes.node, PropTypes.func]), + /** Adds a field to filter options */ + filterable: PropTypes.bool, + /** Useful guiding text to display below the MultiSelect */ + helpText: PropTypes.string, + /** Grabs initial focus on the page */ + initialFocus: PropTypes.bool, + /** Constrains the height of the input */ + inputMaxHeight: PropTypes.string, + /** Sets the width of the input. Can be any valid CSS measurement */ + inputWidth: PropTypes.string, + /** Text for the label above the MultiSelect */ + label: PropTypes.string, + /** Applies a loading appearance to the dropdown options */ + loading: PropTypes.bool, + /** Text to display when `loading` is true */ + loadingText: PropTypes.oneOfType([PropTypes.string, PropTypes.func]), + /** Constrains height of the MultiSelect */ + maxHeight: PropTypes.string, + /** Text to display when there are no filter results */ + noMatchText: PropTypes.oneOfType([PropTypes.string, PropTypes.func]), + /** Placeholder text when the MultiSelect is empty */ + placeholder: PropTypes.string, + /** Leading text to prefix selections */ + prefix: PropTypes.string, + /** Indicates that a selection is required */ + required: PropTypes.bool, + /** Selected items in the MultiSelect (each string should refer to the item's `value` attribute) */ + selected: PropTypes.arrayOf(PropTypes.string), + tabIndex: PropTypes.string, + /** Adds 'valid' appearance for validation feedback. Mutually exclusive with 'error' and 'warning' props */ valid: sharedPropTypes.statusPropType, - validationText: propTypes.string, + /** Text to provide form validation feedback. Receives styles according to validation status */ + validationText: PropTypes.string, + /** Adds 'warning' appearance for validation feedback. Mutually exclusive with 'valid' and 'error' props */ warning: sharedPropTypes.statusPropType, - onBlur: propTypes.func, - onChange: propTypes.func, - onFocus: propTypes.func, + /** Called with signature `({ selected: [String] }, event) */ + onBlur: PropTypes.func, + /** Called with signature `({ selected: [String] }, event) */ + onChange: PropTypes.func, + /** Called with signature `({ selected: [String] }, event) */ + onFocus: PropTypes.func, } export { MultiSelectField } diff --git a/packages/widgets/src/MultiSelectField/MultiSelectField.stories.js b/packages/widgets/src/MultiSelectField/MultiSelectField.stories.js index 776ebb4d86..e3b2e57128 100644 --- a/packages/widgets/src/MultiSelectField/MultiSelectField.stories.js +++ b/packages/widgets/src/MultiSelectField/MultiSelectField.stories.js @@ -1,14 +1,22 @@ +import { sharedPropTypes } from '@dhis2/ui-constants' import { MultiSelectOption } from '@dhis2/ui-core' -import { storiesOf } from '@storybook/react' import React from 'react' import { MultiSelectField } from './MultiSelectField.js' -const defaultProps = { - label: 'Default label', - selected: ['1'], - onChange: selected => - alert(`Selected changed to: ${JSON.stringify(selected, null, 2)}`), -} +const description = ` +\`MultiSelectField\` is a wrapper around a \`MultiSelect\` component that adds a label, help text, validation text, and other features. + +See the MultiSelect for more information, and read more at [Design System: Select](https://github.com/dhis2/design-system/blob/master/molecules/select.md#multiple-selection). + +\`\`\`js +import { MultiSelectField, MultiSelectOption } from '@dhis2/ui' +\`\`\` + +_**Note**: The dropdowns in some of the following stories won't appear correctly on this page. View these demos in the 'Canvas' tab._ +` + +const onChange = selected => + alert(`Selected changed to: ${JSON.stringify(selected, null, 2)}`) const options = [ <MultiSelectOption key="1" value="1" label="one" />, @@ -23,71 +31,102 @@ const options = [ <MultiSelectOption key="10" value="10" label="ten" />, ] -storiesOf('MultiSelectField', module) - .add('Default', () => ( - <MultiSelectField {...defaultProps}>{options}</MultiSelectField> - )) - .add('With Help text', () => ( - <MultiSelectField {...defaultProps} helpText="A helpful text."> - {options} - </MultiSelectField> - )) - .add('Status: Valid', () => ( - <MultiSelectField - {...defaultProps} - helpText="A helpful text." - validationText="Totally valid" - valid - > - {options} - </MultiSelectField> - )) - .add('Status: Warning', () => ( - <MultiSelectField - {...defaultProps} - helpText="A helpful text." - validationText="Hm, not quite, I warn thee!" - warning - > - {options} - </MultiSelectField> - )) - .add('Status: Error', () => ( - <MultiSelectField - {...defaultProps} - helpText="A helpful text." - validationText="NO! TOTALLY WRONG!" - error - > - {options} - </MultiSelectField> - )) - .add('Required', () => ( - <MultiSelectField {...defaultProps} required> - {options} - </MultiSelectField> - )) - .add('Input width', () => ( - <MultiSelectField - inputWidth="200px" - {...defaultProps} - label="A very long label indeed, well at least longer than the input field to show how it looks and works and stuff" - required - > - {options} - </MultiSelectField> - )) - .add('Default: clearText', () => ( - <MultiSelectField selected={['1']} clearable> - <MultiSelectOption - key="1" - value="1" - label="Not translated, just for showing clear button" - /> - </MultiSelectField> - )) - .add('Default: filterPlaceholder and noMatchText', () => ( - <MultiSelectField filterable /> - )) - .add('Default: loadingText', () => <MultiSelectField loading />) - .add('Default: empty', () => <MultiSelectField />) +export default { + title: 'Forms/Multi Select/Multi Select Field', + component: MultiSelectField, + subcomponents: { MultiSelectOption }, + parameters: { docs: { description: { component: description } } }, + // default args for stories + args: { + // Fix default prop issues - causes 'i18n is not defined' error + ...MultiSelectField.defaultProps, + label: 'Default label', + selected: ['1'], + children: options, + onChange: onChange, + }, + argTypes: { + valid: { ...sharedPropTypes.statusArgType }, + warning: { ...sharedPropTypes.statusArgType }, + error: { ...sharedPropTypes.statusArgType }, + }, +} + +const Template = args => <MultiSelectField {...args} /> + +export const Default = Template.bind({}) + +export const WithHelpText = Template.bind({}) +WithHelpText.args = { helpText: 'Helpful text.' } +WithHelpText.parameters = { + docs: { + description: { + story: `_**Note**: The dropdowns in the following stories won't appear correctly on this page. View these demos in the 'Canvas' tab._`, + }, + }, +} + +export const StatusValid = Template.bind({}) +StatusValid.args = { + valid: true, + validationText: 'Totally valid!', + ...WithHelpText.args, +} +StatusValid.storyName = 'Status: Valid' + +export const StatusWarning = Template.bind({}) +StatusWarning.args = { + warning: true, + validationText: 'Hm, not quite, I warn thee!', + ...WithHelpText.args, +} +StatusWarning.storyName = 'Status: Warning' + +export const StatusError = Template.bind({}) +StatusError.args = { + error: true, + validationText: 'That value is wrong. Sorry!', + ...WithHelpText.args, +} +StatusError.storyName = 'Status: Error' + +export const Required = Template.bind({}) +Required.args = { required: true } + +export const InputWidth = Template.bind({}) +InputWidth.args = { + inputWidth: '200px', + label: + 'A very long label indeed, well at least longer than the input field to show how it looks and works and stuff', + required: true, +} + +export const DefaultClearText = Template.bind({}) +DefaultClearText.args = { + clearable: true, + children: ( + <MultiSelectOption + key="1" + value="1" + label="Not translated, just for showing clear button" + /> + ), + label: null, +} +DefaultClearText.storyName = 'Default: clearText' + +export const DefaultEmpty = Template.bind({}) +DefaultEmpty.args = { children: null, selected: [], label: null } +DefaultEmpty.storyName = 'Default: empty' + +export const DefaultFilterPlaceholderAndNoMatchText = Template.bind({}) +DefaultFilterPlaceholderAndNoMatchText.args = { + filterable: true, + ...DefaultEmpty.args, +} +DefaultFilterPlaceholderAndNoMatchText.storyName = + 'Default: filterPlaceholder and noMatchText' + +export const DefaultLoadingText = Template.bind({}) +DefaultLoadingText.args = { loading: true, ...DefaultEmpty.args } +DefaultLoadingText.storyName = 'Default: loadingText' diff --git a/packages/widgets/src/OrganisationUnitTree/OrganisationUnitTree.js b/packages/widgets/src/OrganisationUnitTree/OrganisationUnitTree.js index 2379f21c1e..325d296505 100644 --- a/packages/widgets/src/OrganisationUnitTree/OrganisationUnitTree.js +++ b/packages/widgets/src/OrganisationUnitTree/OrganisationUnitTree.js @@ -178,25 +178,57 @@ const OrganisationUnitTree = ({ * Called with the children's data that was loaded */ OrganisationUnitTree.propTypes = { + /** Root org unit ID(s) */ roots: propTypes.oneOfType([ propTypes.string, propTypes.arrayOf(propTypes.string), ]).isRequired, + /** Will be called with the following object: + * `{ id: string, displayName: string, path: string, checked: boolean, selected: string[] }` */ onChange: propTypes.func.isRequired, + /** When set, the error when loading children fails will be shown automatically */ autoExpandLoadingError: propTypes.bool, dataTest: propTypes.string, + /** When set to true, no unit can be selected */ disableSelection: propTypes.bool, + /** + * All organisation units with a path that includes the provided paths will be shown. + * All others will not be rendered. When not provided, all org units will be shown. + */ filter: propTypes.arrayOf(orgUnitPathPropType), + /** When true, everything will be reloaded. In order to load it again after reloading, `forceReload` has to be set to `false` and then to `true` again */ forceReload: propTypes.bool, + /** + * All units provided to "highlighted" as path will be visually + * highlighted. + * Note: + * The d2-ui component used two props for this: + * * searchResults + * * highlightSearchResults + */ highlighted: propTypes.arrayOf(orgUnitPathPropType), + /** + * An array of OU paths that will be expanded automatically + * as soon as they are encountered. + * The path of an OU is the UIDs of the OU + * and all its parent OUs separated by slashes (/) + * Note: This replaces "openFirstLevel" as that's redundant + */ initiallyExpanded: propTypes.arrayOf(orgUnitPathPropType), + /** When provided, the 'isUserDataViewFallback' option will be sent when requesting the org units */ isUserDataViewFallback: propTypes.bool, + /** An array of paths of selected OUs. The path of an OU is the UIDs of the OU and all its parent OUs separated by slashes (`/`) */ selected: propTypes.arrayOf(orgUnitPathPropType), + /** When set, no checkboxes will be displayed and only the first selected path in `selected` will be highlighted */ singleSelection: propTypes.bool, + /** Turns off alphabetical sorting of units */ suppressAlphabeticalSorting: propTypes.bool, + /** Called with the children's data that was loaded */ onChildrenLoaded: propTypes.func, + /** Called with `{ path: string }` with the path of the parent of the level closed */ onCollapse: propTypes.func, + /** Called with `{ path: string }` with the path of the parent of the level opened */ onExpand: propTypes.func, /** diff --git a/packages/widgets/src/OrganisationUnitTree/OrganisationUnitTree.stories.js b/packages/widgets/src/OrganisationUnitTree/OrganisationUnitTree.stories.js index 0b3d1a3fb9..9f4f2919b7 100644 --- a/packages/widgets/src/OrganisationUnitTree/OrganisationUnitTree.stories.js +++ b/packages/widgets/src/OrganisationUnitTree/OrganisationUnitTree.stories.js @@ -1,9 +1,38 @@ /* eslint-disable react/no-unescaped-entities,react/prop-types */ import { CustomDataProvider, DataProvider } from '@dhis2/app-runtime' -import { storiesOf } from '@storybook/react' import React, { useEffect, useState } from 'react' import { OrganisationUnitTree } from './OrganisationUnitTree.js' +const subtitle = + 'Display, manipulate and select organization units displayed in a hierarchical tree' + +const description = ` +This is a complex, controlled component. It needs access to an App Runtime data provider to fetch org unit data. + +Several props require arrays of org. unit paths (referred to as \`orgUnitPathPropType[]\` in the table below). Take a look at the \`initiallyExpanded\` and \`filter\` props in the example to see an example of the paths format. + +Example: + +\`\`\`js +import { OrganisationUnitTree } from '@dhis2/ui' + +const orgUnitTree = ( + <OrganisationUnitTree + name="Root org unit" + roots="A0000000000" + onChange={onChange} + onExpand={onExpand} + onCollapse={onCollapse} + onChildrenLoaded={onChildrenLoaded} + // Notice the format of the org unit paths + initiallyExpanded={['/A0000000000/A0000000001']} + filter={['/A0000000000/A0000000001/A0000000003']} + /> +) +\`\`\` + +` + const log = true const onChange = (...args) => log && console.log('onChange', ...args) const onExpand = (...args) => log && console.log('onExpand', ...args) @@ -177,106 +206,133 @@ const ReplaceRoots = ({ delay }) => { //) } -storiesOf('OrganisationUnitTree', module) - .addDecorator(fn => ( - <CustomDataProvider data={customData}>{fn()}</CustomDataProvider> - )) - .add('Collapsed', () => ( - <OrganisationUnitTree - onChange={onChange} - name="Root org unit" - roots={['A0000000000']} - /> - )) - .add('Expanded', () => ( - <OrganisationUnitTree - onChange={onChange} - name="Root org unit" - roots={['A0000000000']} - initiallyExpanded={['/A0000000000/A0000000001']} - /> - )) - .add('Multiple roots', () => ( - <OrganisationUnitTree - onChange={onChange} - name="Root org unit" - roots={['A0000000000', 'A0000000001']} - initiallyExpanded={['/A0000000000/A0000000001']} - /> - )) - .add('Filtered (root)', () => ( - <OrganisationUnitTree - onChange={onChange} - name="Root org unit" - roots={['A0000000000', 'A0000000001']} - initiallyExpanded={['/A0000000000/A0000000001']} - filter={['/A0000000000']} - /> - )) - .add('Filtered', () => ( - <OrganisationUnitTree - onChange={onChange} - name="Root org unit" - roots={['A0000000000']} - initiallyExpanded={['/A0000000000/A0000000001']} - filter={['/A0000000000/A0000000001']} - /> - )) - .add('Selected multiple', () => ( - <OrganisationUnitTree - onChange={onChange} - name="Root org unit" - roots={['A0000000000']} - selected={[ - '/A0000000000/A0000000002', - '/A0000000000/A0000000001/A0000000003', - ]} - initiallyExpanded={['/A0000000000', '/A0000000000/A0000000001']} - /> - )) - .add('Indeterminate', () => ( - <OrganisationUnitTree - onChange={onChange} - name="Root org unit" - roots={['A0000000000']} - selected={['/A0000000000/A0000000001']} - initiallyExpanded={['/A0000000000']} - /> - )) - .add('Single selection', () => ( - <OrganisationUnitTree - onChange={onChange} - singleSelection - name="Root org unit" - roots={['A0000000000']} - selected={['/A0000000000/A0000000001']} - initiallyExpanded={['/A0000000000']} - /> - )) - .add('No selection', () => ( - <OrganisationUnitTree - onChange={onChange} - disableSelection - name="Root org unit" - roots={['A0000000000']} - selected={['/A0000000000/A0000000001']} - initiallyExpanded={['/A0000000000']} - /> - )) - .add('Highlighted', () => ( - <OrganisationUnitTree - onChange={onChange} - highlighted={['/A0000000000/A0000000001']} - name="Root org unit" - roots={['A0000000000']} - initiallyExpanded={['/A0000000000']} - /> - )) - .add('Force reload all', () => <ForceReloadAll delay={2000} />) - .add('Force reload one unit', () => <ForceReloadIds delay={2000} />) - .add('Replace roots', () => <ReplaceRoots delay={1000} />) +export default { + title: 'Forms/Organisation Unit Tree', + component: OrganisationUnitTree, + decorators: [ + fn => <CustomDataProvider data={customData}>{fn()}</CustomDataProvider>, + ], + parameters: { + componentSubtitle: subtitle, + docs: { description: { component: description } }, + }, +} + +export const Collapsed = args => <OrganisationUnitTree {...args} /> +Collapsed.args = { + onChange: onChange, + name: 'Root org unit', + roots: ['A0000000000'], +} + +export const Expanded = () => ( + <OrganisationUnitTree + onChange={onChange} + name="Root org unit" + roots={['A0000000000']} + initiallyExpanded={['/A0000000000/A0000000001']} + /> +) + +export const MultipleRoots = () => ( + <OrganisationUnitTree + onChange={onChange} + name="Root org unit" + roots={['A0000000000', 'A0000000001']} + initiallyExpanded={['/A0000000000/A0000000001']} + /> +) +MultipleRoots.storyName = 'Multiple roots' + +export const FilteredRoot = () => ( + <OrganisationUnitTree + onChange={onChange} + name="Root org unit" + roots={['A0000000000', 'A0000000001']} + initiallyExpanded={['/A0000000000/A0000000001']} + filter={['/A0000000000']} + /> +) +FilteredRoot.storyName = 'Filtered (root)' + +export const Filtered = () => ( + <OrganisationUnitTree + onChange={onChange} + name="Root org unit" + roots={['A0000000000']} + initiallyExpanded={['/A0000000000/A0000000001']} + filter={['/A0000000000/A0000000001']} + /> +) -storiesOf('OrganisationUnitTree', module).add('Loading', () => ( +export const SelectedMultiple = () => ( + <OrganisationUnitTree + onChange={onChange} + name="Root org unit" + roots={['A0000000000']} + selected={[ + '/A0000000000/A0000000002', + '/A0000000000/A0000000001/A0000000003', + ]} + initiallyExpanded={['/A0000000000', '/A0000000000/A0000000001']} + /> +) +SelectedMultiple.storyName = 'Selected multiple' + +export const Indeterminate = () => ( + <OrganisationUnitTree + onChange={onChange} + name="Root org unit" + roots={['A0000000000']} + selected={['/A0000000000/A0000000001']} + initiallyExpanded={['/A0000000000']} + /> +) + +export const SingleSelection = () => ( + <OrganisationUnitTree + onChange={onChange} + singleSelection + name="Root org unit" + roots={['A0000000000']} + selected={['/A0000000000/A0000000001']} + initiallyExpanded={['/A0000000000']} + /> +) +SingleSelection.storyName = 'Single selection' + +export const NoSelection = () => ( + <OrganisationUnitTree + onChange={onChange} + disableSelection + name="Root org unit" + roots={['A0000000000']} + selected={['/A0000000000/A0000000001']} + initiallyExpanded={['/A0000000000']} + /> +) +NoSelection.storyName = 'No selection' + +export const Highlighted = () => ( + <OrganisationUnitTree + onChange={onChange} + highlighted={['/A0000000000/A0000000001']} + name="Root org unit" + roots={['A0000000000']} + initiallyExpanded={['/A0000000000']} + /> +) + +export const _ForceReloadAll = () => <ForceReloadAll delay={2000} /> +_ForceReloadAll.storyName = 'Force reload all' + +export const ForceReloadOneUnit = () => <ForceReloadIds delay={2000} /> +ForceReloadOneUnit.storyName = 'Force reload one unit' + +export const _ReplaceRoots = () => <ReplaceRoots delay={1000} /> +_ReplaceRoots.storyName = 'Replace roots' + +export const Loading = () => ( <CustomDataProvider data={{ ...customData, @@ -290,9 +346,9 @@ storiesOf('OrganisationUnitTree', module).add('Loading', () => ( initiallyExpanded={['/A0000000000/A0000000001']} /> </CustomDataProvider> -)) +) -storiesOf('OrganisationUnitTree', module).add('Root loading', () => ( +export const RootLoading = () => ( <CustomDataProvider data={{ ...customData, @@ -311,9 +367,10 @@ storiesOf('OrganisationUnitTree', module).add('Root loading', () => ( /> </fieldset> </CustomDataProvider> -)) +) +RootLoading.storyName = 'Root loading' -storiesOf('OrganisationUnitTree', module).add('Root error', () => ( +export const RootError = () => ( <CustomDataProvider data={{ ...customData, @@ -337,33 +394,30 @@ storiesOf('OrganisationUnitTree', module).add('Root error', () => ( /> </fieldset> </CustomDataProvider> -)) - -storiesOf('OrganisationUnitTree', module).add( - 'Loading error grandchild', - () => ( - <CustomDataProvider - data={{ - ...customData, - 'organisationUnits/A0000000003': () => - Promise.reject( - new Error('Loading org unit 4 and 5 failed') - ), - }} - > - <OrganisationUnitTree - autoExpandLoadingError - name="Root org unit" - roots={['A0000000000']} - onChange={onChange} - onExpand={onExpand} - onCollapse={onCollapse} - onChildrenLoaded={onChildrenLoaded} - initiallyExpanded={['/A0000000000/A0000000001']} - /> - </CustomDataProvider> - ) ) +RootError.storyName = 'Root error' + +export const LoadingErrorGrandchild = () => ( + <CustomDataProvider + data={{ + ...customData, + 'organisationUnits/A0000000003': () => + Promise.reject(new Error('Loading org unit 4 and 5 failed')), + }} + > + <OrganisationUnitTree + autoExpandLoadingError + name="Root org unit" + roots={['A0000000000']} + onChange={onChange} + onExpand={onExpand} + onCollapse={onCollapse} + onChildrenLoaded={onChildrenLoaded} + initiallyExpanded={['/A0000000000/A0000000001']} + /> + </CustomDataProvider> +) +LoadingErrorGrandchild.storyName = 'Loading error grandchild' const DX_onChange = (selected, setSelected, singleSelection) => ({ id, @@ -402,52 +456,50 @@ const Wrapper = props => { ) } -storiesOf('OrganisationUnitTree', module) - .addDecorator(fn => ( - <CustomDataProvider data={customData}>{fn()}</CustomDataProvider> - )) - .add('DX: Multi selection', () => <Wrapper />) - .add('DX: Single selection', () => <Wrapper singleSelection />) - .add('DX: No selection', () => <Wrapper disableSelection />) +export const DxMultiSelection = () => <Wrapper /> +DxMultiSelection.storyName = 'DX: Multi selection' -storiesOf('OrganisationUnitTree', module) - .addDecorator(fn => ( +export const DxSingleSelection = () => <Wrapper singleSelection /> +DxSingleSelection.storyName = 'DX: Single selection' + +export const DxNoSelection = () => <Wrapper disableSelection /> +DxNoSelection.storyName = 'DX: No selection' + +export const DxWithRealBackend = () => ( + <div> + <div style={{ marginBottom: 20, lineHeight: '28px' }}> + <b> + This story doesn't work on netlify for some reason, just run it + locally. + </b> + <br /> + You need to log in to{' '} + <a + href="https://debug.dhis2.org/dev" + target="_blank" + rel="noopener noreferrer" + > + https://debug.dhis2.org/dev + </a> + <br /> + Make sure the{' '} + <code style={{ background: '#ccc' }}>localhost:[PORT]</code> is part + of the accepted list:{' '} + <a + href="https://debug.dhis2.org/dev/dhis-web-settings/#/access" + target="_blank" + rel="noopener noreferrer" + > + Settings app / Access + </a> + </div> <DataProvider baseUrl="https://debug.dhis2.org/dev" apiVersion=""> - {fn()} - </DataProvider> - )) - .add('DX: With real backend', () => ( - <div> - <div style={{ marginBottom: 20, lineHeight: '28px' }}> - <b> - This story doesn't work on netlify for some reason, just run - it locally. - </b> - <br /> - You need to log in to{' '} - <a - href="https://debug.dhis2.org/dev" - target="_blank" - rel="noopener noreferrer" - > - https://debug.dhis2.org/dev - </a> - <br /> - Make sure the{' '} - <code style={{ background: '#ccc' }}>localhost:[PORT]</code> is - part of the accepted list:{' '} - <a - href="https://debug.dhis2.org/dev/dhis-web-settings/#/access" - target="_blank" - rel="noopener noreferrer" - > - Settings app / Access - </a> - </div> <Wrapper //initiallyExpanded={['/ImspTQPwCqd/eIQbndfxQMb']} suppressAlphabeticalSorting roots="ImspTQPwCqd" /> - </div> - )) + </DataProvider> + </div> +) +DxWithRealBackend.storyName = 'DX: With real backend' diff --git a/packages/widgets/src/Pagination/Pagination.js b/packages/widgets/src/Pagination/Pagination.js index f0ec07e8cf..28f81451c8 100644 --- a/packages/widgets/src/Pagination/Pagination.js +++ b/packages/widgets/src/Pagination/Pagination.js @@ -1,5 +1,5 @@ -import propTypes from '@dhis2/prop-types' import cx from 'classnames' +import PropTypes from 'prop-types' import React from 'react' import i18n from '../locales/index.js' import { PageControls } from './PageControls' @@ -129,22 +129,22 @@ Pagination.defaultProps = { * @prop {string|function} [previousPageText] */ Pagination.propTypes = { - page: propTypes.number.isRequired, - pageCount: propTypes.number.isRequired, - pageSize: propTypes.number.isRequired, - total: propTypes.number.isRequired, - onPageChange: propTypes.func.isRequired, - onPageSizeChange: propTypes.func.isRequired, - className: propTypes.string, - dataTest: propTypes.string, - hidePageSelect: propTypes.bool, - hidePageSizeSelect: propTypes.bool, - nextPageText: propTypes.oneOfType([propTypes.string, propTypes.func]), - pageSelectText: propTypes.oneOfType([propTypes.string, propTypes.func]), - pageSizeSelectText: propTypes.oneOfType([propTypes.string, propTypes.func]), - pageSizes: propTypes.arrayOf(propTypes.string), - pageSummaryText: propTypes.oneOfType([propTypes.string, propTypes.func]), - previousPageText: propTypes.oneOfType([propTypes.string, propTypes.func]), + page: PropTypes.number.isRequired, + pageCount: PropTypes.number.isRequired, + pageSize: PropTypes.number.isRequired, + total: PropTypes.number.isRequired, + onPageChange: PropTypes.func.isRequired, + onPageSizeChange: PropTypes.func.isRequired, + className: PropTypes.string, + dataTest: PropTypes.string, + hidePageSelect: PropTypes.bool, + hidePageSizeSelect: PropTypes.bool, + nextPageText: PropTypes.oneOfType([PropTypes.string, PropTypes.func]), + pageSelectText: PropTypes.oneOfType([PropTypes.string, PropTypes.func]), + pageSizeSelectText: PropTypes.oneOfType([PropTypes.string, PropTypes.func]), + pageSizes: PropTypes.arrayOf(PropTypes.string), + pageSummaryText: PropTypes.oneOfType([PropTypes.string, PropTypes.func]), + previousPageText: PropTypes.oneOfType([PropTypes.string, PropTypes.func]), } export { Pagination } diff --git a/packages/widgets/src/Pagination/Pagination.stories.js b/packages/widgets/src/Pagination/Pagination.stories.js index b5d2154240..fccdc3960c 100644 --- a/packages/widgets/src/Pagination/Pagination.stories.js +++ b/packages/widgets/src/Pagination/Pagination.stories.js @@ -2,7 +2,19 @@ import React from 'react' import * as pagers from './__fixtures__' import { Pagination } from './Pagination.js' -export default { title: 'Pagination', component: Pagination } +const subtitle = 'Allows navigation through data displayed over several pages' + +const description = ` +Pagination allows data to be split in pages. Paging large amounts of data avoids overwhelming users and should always be used wherever a lot of data is displayed. Pagination controls allow a user to browse through a set of data or navigate to a specific page depending on the type of pagination used. + +**Do not rely on pagination for navigating datasets. A user should be able to search within, sort and filter datasets too, rather than needing to click through many pages looking for the right data item.** + +\`\`\`js +import { Pagination } from '@dhis2/ui' +\`\`\` + +_**Note**: Dropdown menus may not display properly on this page. View these demos in the 'Canvas' tab._ +` const logOnPageChange = page => { console.log(`Now navigate to page ${page}...`) @@ -12,54 +24,41 @@ const logOnPageSizeChange = pageSize => { console.log(`Now change page size to ${pageSize}...`) } -export const Default = () => ( - <Pagination - {...pagers.atTenthPage} - onPageChange={logOnPageChange} - onPageSizeChange={logOnPageSizeChange} - /> -) +export default { + title: 'Navigation/Pagination', + component: Pagination, + parameters: { + componentSubtitle: subtitle, + docs: { description: { component: description } }, + }, + // Default args for stories + args: { + // Fixes 'defaultProps' errors for storybook + ...Pagination.defaultProps, + onPageChange: logOnPageChange, + onPageSizeChange: logOnPageSizeChange, + ...pagers.atTenthPage, + }, +} + +const Template = args => <Pagination {...args} /> -export const PagerAtFirstPage = () => ( - <Pagination - {...pagers.atFirstPage} - onPageChange={logOnPageChange} - onPageSizeChange={logOnPageSizeChange} - /> -) +export const Default = Template.bind({}) -export const PagerAtLastPage = () => ( - <Pagination - {...pagers.atLastPage} - onPageChange={logOnPageChange} - onPageSizeChange={logOnPageSizeChange} - /> -) +export const PagerAtFirstPage = Template.bind({}) +PagerAtFirstPage.args = { ...pagers.atFirstPage } -export const WithoutPageSizeSelect = () => ( - <Pagination - {...pagers.atTenthPage} - onPageChange={logOnPageChange} - onPageSizeChange={logOnPageSizeChange} - hidePageSizeSelect - /> -) +export const PagerAtLastPage = Template.bind({}) +PagerAtLastPage.args = { ...pagers.atLastPage } -export const WithoutGoToPageSelect = () => ( - <Pagination - {...pagers.atTenthPage} - onPageChange={logOnPageChange} - onPageSizeChange={logOnPageSizeChange} - hidePageSelect - /> -) +export const WithoutPageSizeSelect = Template.bind({}) +WithoutPageSizeSelect.args = { hidePageSizeSelect: true } -export const WithoutAnySelect = () => ( - <Pagination - {...pagers.atTenthPage} - onPageChange={logOnPageChange} - onPageSizeChange={logOnPageSizeChange} - hidePageSizeSelect - hidePageSelect - /> -) +export const WithoutGoToPageSelect = Template.bind({}) +WithoutGoToPageSelect.args = { hidePageSelect: true } + +export const WithoutAnySelect = Template.bind({}) +WithoutAnySelect.args = { + ...WithoutGoToPageSelect.args, + ...WithoutPageSizeSelect.args, +} diff --git a/packages/widgets/src/SingleSelectField/SingleSelectField.js b/packages/widgets/src/SingleSelectField/SingleSelectField.js index 960f84a4fb..a5f5bcf3fa 100644 --- a/packages/widgets/src/SingleSelectField/SingleSelectField.js +++ b/packages/widgets/src/SingleSelectField/SingleSelectField.js @@ -1,6 +1,6 @@ -import propTypes from '@dhis2/prop-types' import { sharedPropTypes } from '@dhis2/ui-constants' import { Field, SingleSelect, Box } from '@dhis2/ui-core' +import PropTypes from 'prop-types' import React from 'react' import i18n from '../locales/index.js' import translate from '../translate' @@ -147,37 +147,65 @@ SingleSelectField.defaultProps = { * @prop {string} [dataTest] */ SingleSelectField.propTypes = { - children: propTypes.node, - className: propTypes.string, - clearText: propTypes.oneOfType([propTypes.string, propTypes.func]), - clearable: propTypes.bool, - dataTest: propTypes.string, - dense: propTypes.bool, - disabled: propTypes.bool, - empty: propTypes.oneOfType([propTypes.node, propTypes.func]), + /** Should be `SingleSelectOption` components */ + children: PropTypes.node, + className: PropTypes.string, + /** Label for the button that clears selections */ + clearText: PropTypes.oneOfType([PropTypes.string, PropTypes.func]), + /** Adds a button to the SingleSelect that clears selections when pressed */ + clearable: PropTypes.bool, + dataTest: PropTypes.string, + /** Makes the SingleSelect smaller */ + dense: PropTypes.bool, + /** Disables the SingleSelect */ + disabled: PropTypes.bool, + /** Text to display when there are no options */ + empty: PropTypes.oneOfType([PropTypes.node, PropTypes.func]), + /** Adds 'error' appearance for validation feedback. Mutually exclusive with 'valid' and 'warning' props */ error: sharedPropTypes.statusPropType, - filterPlaceholder: propTypes.oneOfType([propTypes.node, propTypes.func]), - filterable: propTypes.bool, - helpText: propTypes.string, - initialFocus: propTypes.bool, - inputMaxHeight: propTypes.string, - inputWidth: propTypes.string, - label: propTypes.string, - loading: propTypes.bool, - loadingText: propTypes.oneOfType([propTypes.string, propTypes.func]), - maxHeight: propTypes.string, - noMatchText: propTypes.oneOfType([propTypes.string, propTypes.func]), - placeholder: propTypes.string, - prefix: propTypes.string, - required: propTypes.bool, - selected: propTypes.string, - tabIndex: propTypes.string, + /** Placeholder text to show in the filter field when it is empty */ + filterPlaceholder: PropTypes.oneOfType([PropTypes.node, PropTypes.func]), + /** Adds a field to filter options */ + filterable: PropTypes.bool, + /** Useful guiding text to display below the SingleSelect */ + helpText: PropTypes.string, + /** Grabs initial focus on the page */ + initialFocus: PropTypes.bool, + /** Constrains the height of the input */ + inputMaxHeight: PropTypes.string, + /** Sets the width of the input. Can be any valid CSS measurement */ + inputWidth: PropTypes.string, + /** Text for the label above the SingleSelect */ + label: PropTypes.string, + /** Applies a loading appearance to the dropdown options */ + loading: PropTypes.bool, + /** Text to display when `loading` is true */ + loadingText: PropTypes.oneOfType([PropTypes.string, PropTypes.func]), + /** Constrains height of the SingleSelect */ + maxHeight: PropTypes.string, + /** Text to display when there are no filter results */ + noMatchText: PropTypes.oneOfType([PropTypes.string, PropTypes.func]), + /** Placeholder text when the SingleSelect is empty */ + placeholder: PropTypes.string, + /** Leading text to prefix selections */ + prefix: PropTypes.string, + /** Indicates that a selection is required */ + required: PropTypes.bool, + /** Selected item in the SingleSelect (the string should refer to the item's `value` attribute) */ + selected: PropTypes.string, + tabIndex: PropTypes.string, + /** Adds 'valid' appearance for validation feedback. Mutually exclusive with 'error' and 'warning' props */ valid: sharedPropTypes.statusPropType, - validationText: propTypes.string, + /** Text to provide form validation feedback. Receives styles according to validation status */ + validationText: PropTypes.string, + /** Adds 'warning' appearance for validation feedback. Mutually exclusive with 'valid' and 'error' props */ warning: sharedPropTypes.statusPropType, - onBlur: propTypes.func, - onChange: propTypes.func, - onFocus: propTypes.func, + /** Called with signature `({ selected: string }, event) */ + onBlur: PropTypes.func, + /** Called with signature `({ selected: string }, event) */ + onChange: PropTypes.func, + /** Called with signature `({ selected: string }, event) */ + onFocus: PropTypes.func, } export { SingleSelectField } diff --git a/packages/widgets/src/SingleSelectField/SingleSelectField.stories.js b/packages/widgets/src/SingleSelectField/SingleSelectField.stories.js index bde1729686..361968e0cf 100644 --- a/packages/widgets/src/SingleSelectField/SingleSelectField.stories.js +++ b/packages/widgets/src/SingleSelectField/SingleSelectField.stories.js @@ -1,14 +1,22 @@ +import { sharedPropTypes } from '@dhis2/ui-constants' import { SingleSelectOption } from '@dhis2/ui-core' -import { storiesOf } from '@storybook/react' import React from 'react' import { SingleSelectField } from './SingleSelectField.js' -const defaultProps = { - label: 'Default label', - selected: '1', - onChange: selected => - alert(`Selected changed to: ${JSON.stringify(selected, null, 2)}`), -} +const description = ` +\`SingleSelectField\` is a wrapper around a \`SingleSelect\` component that adds a label, help text, validation text, and other features. + +See the SingleSelect for more information, and read more at [Design System: Select](https://github.com/dhis2/design-system/blob/master/molecules/select.md#multiple-selection). + +\`\`\`js +import { SingleSelectField, SingleSelectOption } from '@dhis2/ui' +\`\`\` + +_**Note**: The dropdowns in some of the following stories won't appear correctly on this page. View these demos in the 'Canvas' tab._ +` + +const onChange = selected => + alert(`Selected changed to: ${JSON.stringify(selected, null, 2)}`) const options = [ <SingleSelectOption key="1" value="1" label="one" />, @@ -23,71 +31,102 @@ const options = [ <SingleSelectOption key="10" value="10" label="ten" />, ] -storiesOf('SingleSelectField', module) - .add('Default', () => ( - <SingleSelectField {...defaultProps}>{options}</SingleSelectField> - )) - .add('With Help text', () => ( - <SingleSelectField {...defaultProps} helpText="A helpful text."> - {options} - </SingleSelectField> - )) - .add('Status: Valid', () => ( - <SingleSelectField - {...defaultProps} - helpText="A helpful text." - validationText="Totally valid" - valid - > - {options} - </SingleSelectField> - )) - .add('Status: Warning', () => ( - <SingleSelectField - {...defaultProps} - helpText="A helpful text." - validationText="Hm, not quite, I warn thee!" - warning - > - {options} - </SingleSelectField> - )) - .add('Status: Error', () => ( - <SingleSelectField - {...defaultProps} - helpText="A helpful text." - validationText="NO! TOTALLY WRONG!" - error - > - {options} - </SingleSelectField> - )) - .add('Required', () => ( - <SingleSelectField {...defaultProps} required> - {options} - </SingleSelectField> - )) - .add('Input width', () => ( - <SingleSelectField - inputWidth="200px" - {...defaultProps} - label="A very long label indeed, well at least longer than the input field to show how it looks and works and stuff" - required - > - {options} - </SingleSelectField> - )) - .add('Default: clearText', () => ( - <SingleSelectField selected="1" clearable> - <SingleSelectOption - key="1" - value="1" - label="Not translated, just for showing clear button" - /> - </SingleSelectField> - )) - .add('Default: filterPlaceholder and noMatchText', () => ( - <SingleSelectField filterable /> - )) - .add('Default: loadingText', () => <SingleSelectField loading />) - .add('Default: empty', () => <SingleSelectField />) +export default { + title: 'Forms/Single Select/Single Select Field', + component: SingleSelectField, + subcomponents: { SingleSelectOption }, + parameters: { docs: { description: { component: description } } }, + // default args for stories + args: { + // Fix default prop issues - causes 'i18n is not defined' error + ...SingleSelectField.defaultProps, + label: 'Default label', + children: options, + onChange: onChange, + }, + argTypes: { + valid: { ...sharedPropTypes.statusArgType }, + warning: { ...sharedPropTypes.statusArgType }, + error: { ...sharedPropTypes.statusArgType }, + }, +} + +const Template = args => <SingleSelectField {...args} /> + +export const Default = Template.bind({}) + +export const WithHelpText = Template.bind({}) +WithHelpText.args = { helpText: 'Helpful text.' } +WithHelpText.parameters = { + docs: { + description: { + story: `_**Note**: The dropdowns in the following stories won't appear correctly on this page. View these demos in the 'Canvas' tab._`, + }, + }, +} + +export const StatusValid = Template.bind({}) +StatusValid.args = { + valid: true, + validationText: 'Totally valid!', + ...WithHelpText.args, +} +StatusValid.storyName = 'Status: Valid' + +export const StatusWarning = Template.bind({}) +StatusWarning.args = { + warning: true, + validationText: 'Hm, not quite, I warn thee!', + ...WithHelpText.args, +} +StatusWarning.storyName = 'Status: Warning' + +export const StatusError = Template.bind({}) +StatusError.args = { + error: true, + validationText: 'That value is wrong. Sorry!', + ...WithHelpText.args, +} +StatusError.storyName = 'Status: Error' + +export const Required = Template.bind({}) +Required.args = { required: true } + +export const InputWidth = Template.bind({}) +InputWidth.args = { + inputWidth: '200px', + label: + 'A very long label indeed, well at least longer than the input field to show how it looks and works and stuff', + required: true, +} + +export const DefaultClearText = Template.bind({}) +DefaultClearText.args = { + selected: '1', + clearable: true, + label: null, + children: ( + <SingleSelectOption + key="1" + value="1" + label="Not translated, just for showing clear button" + /> + ), +} +DefaultClearText.storyName = 'Default: clearText' + +export const DefaultEmpty = Template.bind({}) +DefaultEmpty.args = { children: null, label: null } +DefaultEmpty.storyName = 'Default: empty' + +export const DefaultFilterPlaceholderAndNoMatchText = Template.bind({}) +DefaultFilterPlaceholderAndNoMatchText.args = { + filterable: true, + ...DefaultEmpty.args, +} +DefaultFilterPlaceholderAndNoMatchText.storyName = + 'Default: filterPlaceholder and noMatchText' + +export const DefaultLoadingText = Template.bind({}) +DefaultLoadingText.args = { loading: true, ...DefaultEmpty.args } +DefaultLoadingText.storyName = 'Default: loadingText' diff --git a/packages/widgets/src/SwitchField/SwitchField.js b/packages/widgets/src/SwitchField/SwitchField.js index c7fd348574..9c97e7f195 100644 --- a/packages/widgets/src/SwitchField/SwitchField.js +++ b/packages/widgets/src/SwitchField/SwitchField.js @@ -1,6 +1,6 @@ -import propTypes from '@dhis2/prop-types' import { sharedPropTypes } from '@dhis2/ui-constants' import { Field, Switch, Required } from '@dhis2/ui-core' +import PropTypes from 'prop-types' import React from 'react' const AddRequired = ({ label, required, dataTest }) => ( @@ -10,9 +10,9 @@ const AddRequired = ({ label, required, dataTest }) => ( </React.Fragment> ) AddRequired.propTypes = { - dataTest: propTypes.string, - label: propTypes.node, - required: propTypes.bool, + dataTest: PropTypes.string, + label: PropTypes.node, + required: PropTypes.bool, } /** @@ -118,25 +118,39 @@ SwitchField.defaultProps = { * @prop {string} [dataTest] */ SwitchField.propTypes = { - checked: propTypes.bool, - className: propTypes.string, - dataTest: propTypes.string, - dense: propTypes.bool, - disabled: propTypes.bool, + checked: PropTypes.bool, + className: PropTypes.string, + dataTest: PropTypes.string, + /** Smaller dimensions for information-dense layouts */ + dense: PropTypes.bool, + /** Disables the switch */ + disabled: PropTypes.bool, + /** Applies 'error' styling to switch and validation text for feedback. Mutually exclusive with `warning` and `valid` props */ error: sharedPropTypes.statusPropType, - helpText: propTypes.string, - initialFocus: propTypes.bool, - label: propTypes.node, - name: propTypes.string, - required: propTypes.bool, - tabIndex: propTypes.string, + /** Useful instructions for the user */ + helpText: PropTypes.string, + initialFocus: PropTypes.bool, + /** Labels the switch */ + label: PropTypes.node, + /** Name associate with the switch. Passed in object as argument to event handlers */ + name: PropTypes.string, + /** Adds an asterisk to indicate this field is required */ + required: PropTypes.bool, + tabIndex: PropTypes.string, + /** Applies 'valid' styling to switch and validation text for feedback. Mutually exclusive with `warning` and `error` props */ valid: sharedPropTypes.statusPropType, - validationText: propTypes.string, - value: propTypes.string, + /** Adds text below the switch to provide validation feedback. Acquires styles from `valid`, `warning` and `error` statuses */ + validationText: PropTypes.string, + /** Value associated with the switch. Passed in object as argument to event handlers */ + value: PropTypes.string, + /** Applies 'warning' styling to switch and validation text for feedback. Mutually exclusive with `valid` and `error` props */ warning: sharedPropTypes.statusPropType, - onBlur: propTypes.func, - onChange: propTypes.func, - onFocus: propTypes.func, + /** Called with signature ({ name: string, value: string, checked: bool }, event) */ + onBlur: PropTypes.func, + /** Called with signature ({ name: string, value: string, checked: bool }, event) */ + onChange: PropTypes.func, + /** Called with signature ({ name: string, value: string, checked: bool }, event) */ + onFocus: PropTypes.func, } export { SwitchField } diff --git a/packages/widgets/src/SwitchField/SwitchField.stories.js b/packages/widgets/src/SwitchField/SwitchField.stories.js index dfd107cb56..87a26d190e 100644 --- a/packages/widgets/src/SwitchField/SwitchField.stories.js +++ b/packages/widgets/src/SwitchField/SwitchField.stories.js @@ -1,334 +1,141 @@ -import { storiesOf } from '@storybook/react' +import { sharedPropTypes } from '@dhis2/ui-constants' import React from 'react' import { SwitchField } from './SwitchField.js' -const logger = ({ name, value, checked }) => - console.info(`name: ${name}, value: ${value}, checked: ${checked}`) - -storiesOf('SwitchField', module) - // Regular - .add('Default', () => ( - <SwitchField - name="Ex" - label="SwitchField" - value="default" - onChange={logger} - /> - )) - - .add('Focused unchecked', () => ( - <SwitchField - initialFocus - name="Ex" - label="SwitchField" - value="default" - onChange={logger} - /> - )) - - .add('Focused checked', () => ( - <SwitchField - initialFocus - checked - name="Ex" - label="SwitchField" - value="default" - onChange={logger} - /> - )) - - .add('Checked', () => ( - <SwitchField - name="Ex" - label="SwitchField" - checked - value="checked" - onChange={logger} - /> - )) - - .add('Required', () => ( - <SwitchField - name="Ex" - label="SwitchField" - required - value="checked" - onChange={logger} - /> - )) - - .add('Disabled', () => ( - <> - <SwitchField - name="Ex" - label="SwitchField" - disabled - value="disabled" - onChange={logger} - /> - <SwitchField - name="Ex" - label="SwitchField" - disabled - checked - value="disabled" - onChange={logger} - /> - </> - )) - - .add('Help text', () => ( - <> - <SwitchField - name="Ex" - label="SwitchField" - value="disabled" - onChange={logger} - helpText="Help text" - /> - <SwitchField - name="Ex" - label="SwitchField" - error - validationText="Validation text (error state)" - helpText="Help text" - value="disabled" - onChange={logger} - /> - </> - )) - - .add('Valid', () => ( - <> - <SwitchField - name="Ex" - label="SwitchField" - valid - validationText="I am a validation text" - value="valid" - onChange={logger} - /> - <SwitchField - name="Ex" - label="SwitchField" - valid - validationText="I am a validation text" - checked - value="valid" - onChange={logger} - /> - </> - )) - - .add('Warning', () => ( - <> - <SwitchField - name="Ex" - label="SwitchField" - warning - validationText="I am a validation text" - value="warning" - onChange={logger} - /> - <SwitchField - name="Ex" - label="SwitchField" - warning - validationText="I am a validation text" - checked - value="warning" - onChange={logger} - /> - </> - )) - - .add('Error', () => ( - <> - <SwitchField - name="Ex" - label="SwitchField" - error - validationText="I am a validation text" - value="error" - onChange={logger} - /> - <SwitchField - name="Ex" - label="SwitchField" - error - validationText="I am a validation text" - checked - value="error" - onChange={logger} - /> - </> - )) +const description = ` +A \`SwitchField\` is a Switch component wrapped with extra form utilities, including the ability to add a label, help text, and validation text. Validation styles like 'error' apply to all of these subcomponents. - .add('Image label', () => ( - <SwitchField - name="Ex" - label={<img src="https://picsum.photos/id/82/200/100" />} - value="with-help" - onChange={logger} - /> - )) +See the basic Switch for usage and design system guidelines. - // Dense - .add('Default - Dense', () => ( - <SwitchField - dense - name="Ex" - label="SwitchField" - value="default" - onChange={logger} - /> - )) +\`\`\`js +import { SwitchField } from '@dhis2/ui' +\`\`\` +` - .add('Focused unchecked - Dense', () => ( - <SwitchField - dense - initialFocus - name="Ex" - label="SwitchField" - value="default" - onChange={logger} - /> - )) - - .add('Focused checked - Dense', () => ( - <SwitchField - dense - initialFocus - checked - name="Ex" - label="SwitchField" - value="default" - onChange={logger} - /> - )) - - .add('Checked - Dense', () => ( - <SwitchField - dense - name="Ex" - label="SwitchField" - checked - value="checked" - onChange={logger} - /> - )) - - .add('Required - Dense', () => ( - <SwitchField - dense - name="Ex" - label="SwitchField" - required - value="checked" - onChange={logger} - /> - )) - - .add('Disabled - Dense', () => ( - <> - <SwitchField - dense - name="Ex" - label="SwitchField" - disabled - value="disabled" - onChange={logger} - /> - <SwitchField - dense - name="Ex" - label="SwitchField" - disabled - checked - value="disabled" - onChange={logger} - /> - </> - )) - - .add('Valid - Dense', () => ( - <> - <SwitchField - dense - name="Ex" - label="SwitchField" - valid - validationText="I am a validation text" - value="valid" - onChange={logger} - /> - <SwitchField - dense - name="Ex" - label="SwitchField" - valid - validationText="I am a validation text" - checked - value="valid" - onChange={logger} - /> - </> - )) - - .add('Warning - Dense', () => ( - <> - <SwitchField - dense - name="Ex" - label="SwitchField" - warning - validationText="I am a validation text" - value="warning" - onChange={logger} - /> - <SwitchField - dense - name="Ex" - label="SwitchField" - warning - validationText="I am a validation text" - checked - value="warning" - onChange={logger} - /> - </> - )) - - .add('Error - Dense', () => ( - <> - <SwitchField - dense - name="Ex" - label="SwitchField" - error - validationText="I am a validation text" - value="error" - onChange={logger} - /> - <SwitchField - dense - name="Ex" - label="SwitchField" - error - validationText="I am a validation text" - checked - value="error" - onChange={logger} - /> - </> - )) - - .add('Image label - Dense', () => ( +const logger = ({ name, value, checked }) => + console.log(`name: ${name}, value: ${value}, checked: ${checked}`) + +export default { + title: 'Forms/Switch/Switch Field', + component: SwitchField, + parameters: { docs: { description: { component: description } } }, + // Default args for stories + args: { + name: 'switchName', + label: 'Switch Field', + value: 'defaultValue', + onChange: logger, + }, + argTypes: { + valid: { ...sharedPropTypes.statusArgType }, + warning: { ...sharedPropTypes.statusArgType }, + error: { ...sharedPropTypes.statusArgType }, + }, +} + +const Template = args => <SwitchField {...args} /> + +const CheckedUncheckedTemplate = args => ( + <> + <SwitchField {...args} /> + <SwitchField {...args} checked /> + </> +) + +export const Default = Template.bind({}) + +export const FocusedUnchecked = Template.bind({}) +FocusedUnchecked.args = { initialFocus: true } +// Disable stories on docs page that grab focus +FocusedUnchecked.parameters = { docs: { disable: true } } + +export const FocusedChecked = Template.bind({}) +FocusedChecked.args = { ...FocusedUnchecked.args, checked: true } +FocusedChecked.parameters = { docs: { disable: true } } + +export const Checked = Template.bind({}) +Checked.args = { checked: true, value: 'checkedValue' } + +export const Required = Template.bind({}) +Required.args = { required: true } + +export const Disabled = CheckedUncheckedTemplate.bind({}) +Disabled.args = { disabled: true } + +export const HelpText = args => ( + <> + <SwitchField {...args} /> <SwitchField - dense - name="Ex" - label={<img src="https://picsum.photos/id/82/200/100" />} - value="with-help" - onChange={logger} + {...args} + error + validationText="Validation text (error state)" /> - )) + </> +) +HelpText.args = { helpText: 'Help text' } + +export const Valid = CheckedUncheckedTemplate.bind({}) +Valid.args = { + valid: true, + validationText: 'I am validation text', + value: 'validValue', +} + +export const Warning = CheckedUncheckedTemplate.bind({}) +Warning.args = { + warning: true, + value: 'warningValue', + validationText: 'I am validation text', +} + +export const Error = CheckedUncheckedTemplate.bind({}) +Error.args = { + error: true, + value: 'errorValue', + validationText: 'I am validation text', +} + +export const ImageLabel = Template.bind({}) +ImageLabel.args = { label: <img src="https://picsum.photos/id/82/200/100" /> } + +export const DefaultDense = Template.bind({}) +DefaultDense.storyName = 'Default - Dense' +DefaultDense.args = { dense: true } + +export const FocusedUncheckedDense = Template.bind({}) +FocusedUncheckedDense.args = { ...DefaultDense.args, ...FocusedUnchecked.args } +FocusedUncheckedDense.parameters = { docs: { disable: true } } +FocusedUncheckedDense.storyName = 'Focused unchecked - Dense' + +export const FocusedCheckedDense = Template.bind({}) +FocusedCheckedDense.args = { ...DefaultDense.args, ...FocusedChecked.args } +FocusedCheckedDense.parameters = { docs: { disable: true } } +FocusedCheckedDense.storyName = 'Focused checked - Dense' + +export const CheckedDense = Template.bind({}) +CheckedDense.args = { ...DefaultDense.args, ...Checked.args } +CheckedDense.storyName = 'Checked - Dense' + +export const RequiredDense = Template.bind({}) +RequiredDense.args = { ...DefaultDense.args, ...Required.args } +RequiredDense.storyName = 'Required - Dense' + +export const DisabledDense = CheckedUncheckedTemplate.bind({}) +DisabledDense.args = { ...DefaultDense.args, ...Disabled.args } +DisabledDense.storyName = 'Disabled - Dense' + +export const ValidDense = CheckedUncheckedTemplate.bind({}) +ValidDense.args = { ...DefaultDense.args, ...Valid.args } +ValidDense.storyName = 'Valid - Dense' + +export const WarningDense = CheckedUncheckedTemplate.bind({}) +WarningDense.args = { ...DefaultDense.args, ...Warning.args } +WarningDense.storyName = 'Warning - Dense' + +export const ErrorDense = CheckedUncheckedTemplate.bind({}) +ErrorDense.args = { ...DefaultDense.args, ...Error.args } +ErrorDense.storyName = 'Error - Dense' + +export const ImageLabelDense = Template.bind({}) +ImageLabelDense.args = { ...DefaultDense.args, ...ImageLabel.args } +ImageLabelDense.storyName = 'Image label - Dense' diff --git a/packages/widgets/src/TextAreaField/TextAreaField.js b/packages/widgets/src/TextAreaField/TextAreaField.js index da59d6ed1e..92ef4b7974 100644 --- a/packages/widgets/src/TextAreaField/TextAreaField.js +++ b/packages/widgets/src/TextAreaField/TextAreaField.js @@ -1,6 +1,6 @@ -import propTypes from '@dhis2/prop-types' import { sharedPropTypes } from '@dhis2/ui-constants' import { Field, TextArea, Box } from '@dhis2/ui-core' +import PropTypes from 'prop-types' import React from 'react' /** @@ -122,31 +122,53 @@ TextAreaField.defaultProps = { * @prop {string} [dataTest] */ TextAreaField.propTypes = { - autoGrow: propTypes.bool, - className: propTypes.string, - dataTest: propTypes.string, - dense: propTypes.bool, - disabled: propTypes.bool, + /** Grow the text area in response to overflow instead of adding a scroll bar */ + autoGrow: PropTypes.bool, + className: PropTypes.string, + dataTest: PropTypes.string, + /** Compact mode */ + dense: PropTypes.bool, + /** Disables the textarea and makes in non-interactive */ + disabled: PropTypes.bool, + /** Applies 'error' styles for validation feedback. Mutually exclusive with `valid` and `warning` props */ error: sharedPropTypes.statusPropType, - helpText: propTypes.string, - initialFocus: propTypes.bool, - inputWidth: propTypes.string, - label: propTypes.string, - loading: propTypes.bool, - name: propTypes.string, - placeholder: propTypes.string, - readOnly: propTypes.bool, - required: propTypes.bool, - resize: propTypes.oneOf(['none', 'both', 'horizontal', 'vertical']), - rows: propTypes.number, - tabIndex: propTypes.string, + /** Adds useful help text below the textarea */ + helpText: PropTypes.string, + /** Grabs initial focus on the page */ + initialFocus: PropTypes.bool, + /** Sets the width of the textarea. Minimum 220px. Any valid CSS measurement can be used */ + inputWidth: PropTypes.string, + /** Labels the textarea */ + label: PropTypes.string, + /** Adds a loading spinner */ + loading: PropTypes.bool, + /** Name associated with the text area. Passed in object argument to event handlers. */ + name: PropTypes.string, + /** Placeholder text for an empty textarea */ + placeholder: PropTypes.string, + /** Makes the textarea read-only */ + readOnly: PropTypes.bool, + /** Adds an asterisk to the label to indicate this field is required */ + required: PropTypes.bool, + /** [Resize property](https://developer.mozilla.org/en-US/docs/Web/CSS/resize) for the textarea element */ + resize: PropTypes.oneOf(['none', 'both', 'horizontal', 'vertical']), + /** Initial height of the textarea, in lines of text */ + rows: PropTypes.number, + tabIndex: PropTypes.string, + /** Applies 'valid' styles for validation feedback. Mutually exclusive with `warning` and `error` props */ valid: sharedPropTypes.statusPropType, - validationText: propTypes.string, - value: propTypes.string, + /** Validation text below the textarea to provide validation feedback. Changes appearance depending on validation status */ + validationText: PropTypes.string, + /** Value in the textarea. Can be used to control component (recommended). Passed in object argument to event handlers. */ + value: PropTypes.string, + /** Applies 'warning' styles for validation feedback. Mutually exclusive with `valid` and `error` props */ warning: sharedPropTypes.statusPropType, - onBlur: propTypes.func, - onChange: propTypes.func, - onFocus: propTypes.func, + /** Called with signature ({ name: string, value: string }, event) */ + onBlur: PropTypes.func, + /** Called with signature ({ name: string, value: string }, event) */ + onChange: PropTypes.func, + /** Called with signature ({ name: string, value: string }, event) */ + onFocus: PropTypes.func, } export { TextAreaField } diff --git a/packages/widgets/src/TextAreaField/TextAreaField.stories.js b/packages/widgets/src/TextAreaField/TextAreaField.stories.js index dbf850101e..d20839aa93 100644 --- a/packages/widgets/src/TextAreaField/TextAreaField.stories.js +++ b/packages/widgets/src/TextAreaField/TextAreaField.stories.js @@ -1,245 +1,223 @@ -import { storiesOf } from '@storybook/react' +import { sharedPropTypes } from '@dhis2/ui-constants' import React from 'react' import { TextAreaField } from './TextAreaField.js' -storiesOf('TextAreaField', module) - .add('No placeholder, no value', () => ( - <TextAreaField onChange={() => {}} name="textarea" /> - )) - - .add('Placeholder, no value', () => ( +const description = ` +\`TextAreaField\` wraps a \`TextArea\` component with a label, help text, validation text, and other functions. + +See the regular TextArea for usage information and options. + +\`\`\`js +import { TextAreaField } from '@dhis2/ui' +\`\`\` +` + +export default { + title: 'Forms/Text Area/Text Area Field', + component: TextAreaField, + parameters: { docs: { description: { component: description } } }, + // Default args: + args: { + onChange: console.log, + name: 'textareaName', + }, + argTypes: { + valid: { ...sharedPropTypes.statusArgType }, + warning: { ...sharedPropTypes.statusArgType }, + error: { ...sharedPropTypes.statusArgType }, + }, +} + +const Template = args => <TextAreaField {...args} /> + +export const NoPlaceholderNoValue = Template.bind({}) +NoPlaceholderNoValue.storyName = 'No placeholder, no value' + +export const PlaceholderNoValue = Template.bind({}) +PlaceholderNoValue.args = { placeholder: 'Hold the place' } +PlaceholderNoValue.storyName = 'Placeholder, no value' + +export const WithHelpText = Template.bind({}) +WithHelpText.args = { + helpText: 'With some helping text to guide the user along', + ...PlaceholderNoValue.args, +} + +export const WithValue = Template.bind({}) +WithValue.args = { + value: + 'This is set through the value prop, which means the component is controlled.', +} + +export const Focus = Template.bind({}) +Focus.args = { initialFocus: true } +// Disable stories that manipulate focus on docs page +Focus.parameters = { docs: { disable: true } } + +export const StatusValid = Template.bind({}) +StatusValid.args = { valid: true, value: 'This value is valid' } +StatusValid.storyName = 'Status: Valid' + +export const StatusWarning = Template.bind({}) +StatusWarning.args = { warning: true, value: 'This value produces a warning' } +StatusWarning.storyName = 'Status: Warning' + +export const StatusError = Template.bind({}) +StatusError.args = { + error: true, + value: 'This value produces an error', + helpText: 'This is some help text to advise what this input actually is.', + validationText: 'This describes the error, if a message is supplied.', +} +StatusError.storyName = 'Status: Error' + +export const StatusLoading = Template.bind({}) +StatusLoading.args = { + loading: true, + value: 'This value produces a loadingn state', +} +StatusLoading.storyName = 'Status: Loading' + +export const Disabled = Template.bind({}) +Disabled.args = { disabled: true, value: 'This field is disabled' } + +export const ReadOnly = Template.bind({}) +ReadOnly.args = { readOnly: true, value: 'This field is readOnly' } + +export const Dense = Template.bind({}) +Dense.args = { dense: true, value: 'This field is dense' } + +export const LabelTextOverflow = Template.bind({}) +LabelTextOverflow.args = { + label: + "This label is too long to show on a single line of the input field's label. We just let it flow to the next line so the user can still read it. However, we should always aim to keep it shorter than this!", +} +LabelTextOverflow.storyName = 'Label text overflow' + +export const TextareaTextOverflow = Template.bind({}) +TextareaTextOverflow.args = { + label: 'I have a scrollbar', + value: [ + 'A line of text', + 'A line of text', + 'A line of text', + 'A line of text', + 'A line of text', + 'A line of text', + 'A line of text', + 'A line of text', + 'A line of text', + 'A line of text', + 'A line of text', + 'A line of text', + 'A line of text', + 'A line of text', + 'A line of text', + ].join('\n'), +} + +export const Required = () => ( + <TextAreaField + onChange={() => {}} + name="textarea" + label="I am required and have an asterisk" + required + /> +) +Required.args = { required: true, label: 'I am required and have an asterisk' } + +export const Rows = Template.bind({}) +Rows.args = { + rows: 8, + label: 'You can set the height with the rows prop. I have 8', +} + +export const InputWidth = args => ( + <> <TextAreaField - onChange={() => {}} - name="textarea" - placeholder="Hold the place" + {...args} + label="My textarea has a width of 220px (the minimum)" + inputWidth="220px" /> - )) - - .add('With Help text', () => ( <TextAreaField - onChange={() => {}} - name="textarea" - placeholder="Hold the place" - helpText="With some helping text to guide the user along" + {...args} + label="My textarea has a width of 400px" + inputWidth="400px" /> - )) + </> +) - .add('With value', () => ( +export const Resize = args => ( + <> <TextAreaField - onChange={() => {}} - name="textarea" - value="This is set through the value prop, which means the component is controlled." + {...args} + name="textarea1" + label="Resize: vertical (default)" /> - )) - - .add('Focus', () => ( - <TextAreaField onChange={() => {}} name="textarea" initialFocus /> - )) - - .add('Status: Valid', () => ( <TextAreaField - onChange={() => {}} - name="textarea" - value="This value is valid" - valid + {...args} + name="textarea2" + label="Resize: none" + resize="none" /> - )) - - .add('Status: Warning', () => ( <TextAreaField - onChange={() => {}} - name="textarea" - value="This value produces a warning" - warning + {...args} + name="textarea3" + label="Resize: both" + resize="both" /> - )) - - .add('Status: Error', () => ( <TextAreaField - onChange={() => {}} - name="textarea" - error - value="This value produces an error" - helpText="This is some help text to advice what this input actually is." - validationText="This describes the error, if a message is supplied." + {...args} + name="textarea4" + label="Resize: horizontal" + resize="horizontal" /> - )) + </> +) - .add('Status: Loading', () => ( +export const Autogrow = args => ( + <> <TextAreaField - onChange={() => {}} - name="textarea" - value="This value produces a loading state" - loading + {...args} + name="textarea1" + label="Autogrow step 1" + autoGrow + rows={2} + value="This TextArea has a height of 2 rows" /> - )) - - .add('Disabled', () => ( - <TextAreaField - onChange={() => {}} - name="textarea" - value="This field is disabled" - disabled - /> - )) - - .add('Read only', () => ( - <TextAreaField - onChange={() => {}} - name="textarea" - value="This field is readOnly" - readOnly - /> - )) - - .add('Dense', () => ( <TextAreaField - onChange={() => {}} - name="textarea" - value="This field is dense" - dense - /> - )) - - .add('Label text overflow', () => ( - <TextAreaField - onChange={() => {}} - name="textarea" - label="This label is too long to show on a single line of the input field's label. We just let it flow to the next line so the user can still read it. However, we should always aim to keep it shorter than this!" - /> - )) - - .add('Textarea text overflow', () => ( - <TextAreaField - onChange={() => {}} - name="textarea" - label="I have a scrollbar" + {...args} + name="textarea2" + label="Autogrow step 2" + autoGrow + rows={2} value={[ - 'A line of text', - 'A line of text', - 'A line of text', - 'A line of text', - 'A line of text', - 'A line of text', - 'A line of text', - 'A line of text', - 'A line of text', - 'A line of text', - 'A line of text', - 'A line of text', - 'A line of text', - 'A line of text', - 'A line of text', + 'This TextArea has a height of two rows', + 'it also has autoGrow set to true so it will grow with the content', ].join('\n')} /> - )) - - .add('Required', () => ( <TextAreaField - onChange={() => {}} - name="textarea" - label="I am required and have an asterisk" - required + {...args} + name="textarea3" + label="Autogrow step 3" + autoGrow + rows={2} + value={[ + 'This TextArea has a height of two rows', + 'it also has autoGrow set to true so it will grow with the content.', + 'See: rows is still 2, but I now have 3 lines.', + ].join('\n')} /> - )) - - .add('Rows', () => ( <TextAreaField - onChange={() => {}} - name="textarea" - label="You can set the height with the rows prop, I have 8" - rows={8} + {...args} + name="textarea4" + label="Autogrow step 4" + value={[ + 'This TextArea has a height of two rows', + 'it also has autoGrow set to true so it will grow with the content.', + 'See: rows is still 2...', + 'And now I have 4 lines and still no scroll bar in sight.', + ].join('\n')} /> - )) - - .add('Input width', () => ( - <> - <TextAreaField - onChange={() => {}} - name="textarea" - label="My textarea has a width of 100px" - inputWidth="100px" - /> - <TextAreaField - onChange={() => {}} - name="textarea" - label="My textarea has a width of 220px" - inputWidth="220px" - /> - </> - )) - - .add('Resize', () => ( - <> - <TextAreaField - onChange={() => {}} - name="textarea1" - label="Resize: vertical (default)" - /> - <TextAreaField - onChange={() => {}} - name="textarea2" - label="Resize: none" - resize="none" - /> - <TextAreaField - onChange={() => {}} - name="textarea3" - label="Resize: both" - resize="both" - /> - <TextAreaField - onChange={() => {}} - name="textarea4" - label="Resize: horizontal" - resize="horizontal" - /> - </> - )) - - .add('Autogrow', () => ( - <> - <TextAreaField - onChange={() => {}} - name="textarea1" - label="Autogrow step 1" - autoGrow - rows={2} - value="This TextAreaField has a height of 2 rows" - /> - <TextAreaField - onChange={() => {}} - name="textarea2" - label="Autogrow step 2" - autoGrow - rows={2} - value={[ - 'This TextAreaField has a height of two rows', - 'it also has autoGrow set to true so it will grow with the content', - ].join('\n')} - /> - <TextAreaField - onChange={() => {}} - name="textarea3" - label="Autogrow step 3" - autoGrow - rows={2} - value={[ - 'This TextAreaField has a height of two rows', - 'it also has autoGrow set to true so it will grow with the content.', - 'See: rows is still 2, but I now have 3 lines.', - ].join('\n')} - /> - <TextAreaField - onChange={() => {}} - name="textarea4" - label="Autogrow step 4" - value={[ - 'This TextAreaField has a height of two rows', - 'it also has autoGrow set to true so it will grow with the content.', - 'See: rows is still 2...', - 'And now I have 4 lines and still no scroll bar in sight.', - ].join('\n')} - /> - </> - )) + </> +) diff --git a/packages/widgets/src/Transfer/Transfer.js b/packages/widgets/src/Transfer/Transfer.js index f1eedf3f43..2539aefb06 100644 --- a/packages/widgets/src/Transfer/Transfer.js +++ b/packages/widgets/src/Transfer/Transfer.js @@ -1,4 +1,4 @@ -import propTypes from '@dhis2/prop-types' +import PropTypes from 'prop-types' import React from 'react' import { Actions } from './Actions.js' import { AddAll } from './AddAll.js' @@ -449,53 +449,53 @@ Transfer.defaultProps = { * @prop {Function} [onFilterChange] */ Transfer.propTypes = { - options: propTypes.arrayOf( - propTypes.shape({ - label: propTypes.string.isRequired, - value: propTypes.string.isRequired, - disabled: propTypes.bool, + options: PropTypes.arrayOf( + PropTypes.shape({ + label: PropTypes.string.isRequired, + value: PropTypes.string.isRequired, + disabled: PropTypes.bool, }) ).isRequired, - onChange: propTypes.func.isRequired, + onChange: PropTypes.func.isRequired, - addAllText: propTypes.string, - addIndividualText: propTypes.string, - className: propTypes.string, - dataTest: propTypes.string, - disabled: propTypes.bool, - enableOrderChange: propTypes.bool, - filterCallback: propTypes.func, - filterCallbackPicked: propTypes.func, - filterLabel: propTypes.string, - filterLabelPicked: propTypes.string, - filterPlaceholder: propTypes.string, - filterPlaceholderPicked: propTypes.string, - filterable: propTypes.bool, - filterablePicked: propTypes.bool, - height: propTypes.string, - hideFilterInput: propTypes.bool, - hideFilterInputPicked: propTypes.bool, - initialSearchTerm: propTypes.string, - initialSearchTermPicked: propTypes.string, - leftFooter: propTypes.node, - leftHeader: propTypes.node, - loading: propTypes.bool, - loadingPicked: propTypes.bool, - maxSelections: propTypes.oneOf([1, Infinity]), - optionsWidth: propTypes.string, - removeAllText: propTypes.string, - removeIndividualText: propTypes.string, - renderOption: propTypes.func, - rightFooter: propTypes.node, - rightHeader: propTypes.node, - searchTerm: propTypes.string, - searchTermPicked: propTypes.string, - selected: propTypes.arrayOf(propTypes.string), - selectedEmptyComponent: propTypes.node, - selectedWidth: propTypes.string, - sourceEmptyPlaceholder: propTypes.node, - onEndReached: propTypes.func, - onEndReachedPicked: propTypes.func, - onFilterChange: propTypes.func, - onFilterChangePicked: propTypes.func, + addAllText: PropTypes.string, + addIndividualText: PropTypes.string, + className: PropTypes.string, + dataTest: PropTypes.string, + disabled: PropTypes.bool, + enableOrderChange: PropTypes.bool, + filterCallback: PropTypes.func, + filterCallbackPicked: PropTypes.func, + filterLabel: PropTypes.string, + filterLabelPicked: PropTypes.string, + filterPlaceholder: PropTypes.string, + filterPlaceholderPicked: PropTypes.string, + filterable: PropTypes.bool, + filterablePicked: PropTypes.bool, + height: PropTypes.string, + hideFilterInput: PropTypes.bool, + hideFilterInputPicked: PropTypes.bool, + initialSearchTerm: PropTypes.string, + initialSearchTermPicked: PropTypes.string, + leftFooter: PropTypes.node, + leftHeader: PropTypes.node, + loading: PropTypes.bool, + loadingPicked: PropTypes.bool, + maxSelections: PropTypes.oneOf([1, Infinity]), + optionsWidth: PropTypes.string, + removeAllText: PropTypes.string, + removeIndividualText: PropTypes.string, + renderOption: PropTypes.func, + rightFooter: PropTypes.node, + rightHeader: PropTypes.node, + searchTerm: PropTypes.string, + searchTermPicked: PropTypes.string, + selected: PropTypes.arrayOf(PropTypes.string), + selectedEmptyComponent: PropTypes.node, + selectedWidth: PropTypes.string, + sourceEmptyPlaceholder: PropTypes.node, + onEndReached: PropTypes.func, + onEndReachedPicked: PropTypes.func, + onFilterChange: PropTypes.func, + onFilterChangePicked: PropTypes.func, } diff --git a/packages/widgets/src/Transfer/Transfer.stories.js b/packages/widgets/src/Transfer/Transfer.stories.js index 5c6b7d15d3..4cf6a761f0 100644 --- a/packages/widgets/src/Transfer/Transfer.stories.js +++ b/packages/widgets/src/Transfer/Transfer.stories.js @@ -3,6 +3,40 @@ import { SingleSelectOption, Tab, TabBar } from '@dhis2/ui-core' import React, { useEffect, useState } from 'react' import { SingleSelectField, Transfer, TransferOption } from '../index.js' +const subtitle = 'Allows users to select options from a list' + +const description = ` +#### Usage + +Use a transfer component wherever a user needs to make a complex selection. Simple selections can be achieved with checkboxes, radio buttons or a select. + +There are use-cases that are particularly suitable for a transfer component: + +- when a user needs to select some options from several different groups at the same time +- if the selection needs to have a defined order +- when the user will be interacting with and editing the selection often +- if a user needs to easily compare non-selected and selected options +- if a user is making selections as part of a complex flow, especially where there are many options to choose from + +#### Terminology + +This component has to differentiate between different types of options, +here's an explanation of their meaning: + +- source options: These are options listed on the left and are available for selection +- picked options: These options have been selected by the user and are on the right side +- highlighted option: These are visually highlighted options than can be on either side and are ready for transferral with the action buttons to the other side +- filtered options: These are the displayed source options filtered by a search term or a custom search algorithm. The api surface uses "selected" for "picked" to be consistent with the rest of the library + +#### More details + +See more about the options available for Transfers at [Design System: Transfer](https://github.com/dhis2/design-system/blob/master/organisms/transfer.md#transfer). + +\`\`\`js +import { Transfer, TransferOption } from '@dhis2/ui' +\`\`\` +` + const options = [ { label: 'ANC 1st visit', @@ -96,7 +130,30 @@ const options = [ }, ] -export default { title: 'Transfer', component: Transfer } +/** + * Default args are needed because storybook currently struggles with + * functions as default props: they are sent to the component as strings when + * `{...args}` is spread into the component in the Template, which causes an + * error that looks like 'renderOption is not a function' + * + * https://github.com/storybookjs/storybook/issues/12455#issuecomment-702763930 + */ +export default { + title: 'Forms/Transfer', + component: Transfer, + parameters: { + componentSubtitle: subtitle, + docs: { + description: { component: description }, + source: { type: 'code' }, + }, + }, + // Default args: + args: { + ...Transfer.defaultProps, + options: options, + }, +} const StatefulTemplate = ({ initiallySelected, ...args }) => { const [selected, setSelected] = useState(initiallySelected) @@ -104,31 +161,19 @@ const StatefulTemplate = ({ initiallySelected, ...args }) => { return <Transfer {...args} selected={selected} onChange={onChange} /> } - -StatefulTemplate.defaultProps = { - initiallySelected: [], -} - -StatefulTemplate.propTypes = { - initiallySelected: propTypes.array, -} +StatefulTemplate.defaultProps = { initiallySelected: [] } +StatefulTemplate.propTypes = { initiallySelected: propTypes.array } export const SingleSelection = StatefulTemplate.bind({}) -SingleSelection.args = { - maxSelections: 1, - options, -} +SingleSelection.args = { maxSelections: 1 } export const Multiple = StatefulTemplate.bind({}) -Multiple.args = { - options: options.slice(0, 3), -} +Multiple.args = { options: options.slice(0, 3) } export const Header = StatefulTemplate.bind({}) Header.args = { leftHeader: <h3>Header on the left side</h3>, rightHeader: <h4>Header on the right side</h4>, - options, } export const OptionsFooter = StatefulTemplate.bind({}) @@ -145,14 +190,12 @@ OptionsFooter.args = { Reload list </a> ), - options, } export const Filtered = StatefulTemplate.bind({}) Filtered.args = { filterable: true, initialSearchTerm: 'ANC', - options, } export const FilteredPicked = StatefulTemplate.bind({}) @@ -160,7 +203,6 @@ FilteredPicked.args = { filterablePicked: true, initialSearchTermPicked: 'ANC', initiallySelected: options.map(({ value }) => value), - options, } export const FilteredPlaceholder = StatefulTemplate.bind({}) @@ -168,7 +210,6 @@ FilteredPlaceholder.args = { filterable: true, filterLabel: 'Filter with placeholder', filterPlaceholder: 'Search', - options, } const renderOption = ({ label, value, onClick, highlighted, selected }) => ( @@ -188,24 +229,37 @@ const RenderOptionCode = () => ( <strong>Custom option code:</strong> <code> <pre>{`const renderOption = ({ label, value, onClick, highlighted, selected }) => ( - <p - onClick={event => onClick({ label, value }, event)} - style={{ - background: highlighted ? 'green' : 'blue', - color: selected ? 'orange' : 'white', - }} - > - Custom: {label} (label), {value} (value) - </p> -)`}</pre> + <p + onClick={event => onClick({ label, value }, event)} + style={{ + background: highlighted ? 'green' : 'blue', + color: selected ? 'orange' : 'white', + }} + > + Custom: {label} (label), {value} (value) + </p> + )`}</pre> </code> </> ) +const StatefulTemplateCustomRenderOption = ({ initiallySelected, ...args }) => { + const [selected, setSelected] = useState(initiallySelected) + const onChange = payload => setSelected(payload.selected) + + return <Transfer {...args} selected={selected} onChange={onChange} /> +} +StatefulTemplateCustomRenderOption.defaultProps = { + initiallySelected: [], +} +StatefulTemplateCustomRenderOption.propTypes = { + initiallySelected: propTypes.array, +} + export const CustomListOptions = args => ( <> <RenderOptionCode /> - <StatefulTemplate {...args} /> + <StatefulTemplateCustomRenderOption {...args} /> </> ) CustomListOptions.args = { @@ -214,7 +268,9 @@ CustomListOptions.args = { initiallySelected: options.slice(0, 2).map(({ value }) => value), } -export const IndividualCustomOption = StatefulTemplate.bind({}) +export const IndividualCustomOption = StatefulTemplateCustomRenderOption.bind( + {} +) IndividualCustomOption.args = { addAllText: 'Add all', addIndividualText: 'Add individual', @@ -228,7 +284,6 @@ IndividualCustomOption.args = { return <TransferOption {...option} /> }, - options, } export const CustomButtonText = StatefulTemplate.bind({}) @@ -237,7 +292,6 @@ CustomButtonText.args = { addIndividualText: 'Add individual', removeAllText: 'Remove all', removeIndividualText: 'Remove individual', - options, } export const SourceEmptyPlaceholder = StatefulTemplate.bind({}) @@ -262,7 +316,6 @@ PickedEmptyComponent.args = { <br /> </p> ), - options, } export const Reordering = StatefulTemplate.bind({}) @@ -277,14 +330,12 @@ IncreasedOptionsHeight.args = { maxSelections: Infinity, filterable: true, height: '400px', - options, } export const DifferentWidths = StatefulTemplate.bind({}) DifferentWidths.args = { optionsWidth: '500px', selectedWidth: '240px', - options, } const createCustomFilteringInHeader = hideFilterInput => { @@ -420,7 +471,7 @@ const pageSize = 5 * To keep the code as small as possible, handling selecting items is not included */ -export const InfiniteLoading = () => { +export const InfiniteLoading = args => { useEffect(() => { console.clear() }, []) @@ -484,6 +535,7 @@ export const InfiniteLoading = () => { return ( <Transfer + {...args} loading={loading} options={options} selected={selected} diff --git a/yarn.lock b/yarn.lock index d931555abc..9f9edd755e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -23,6 +23,13 @@ dependencies: "@babel/highlight" "^7.10.4" +"@babel/code-frame@^7.12.13": + version "7.12.13" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.12.13.tgz#dcfc826beef65e75c50e21d3837d7d95798dd658" + integrity sha512-HV1Cm0Q3ZrpCR93tkWOYiuYIgLxZXZFVG2VgK+MBWjUqZTundupbfx2aXarXuw5Ko5aMcjtJgbSs4vUGBS5v6g== + dependencies: + "@babel/highlight" "^7.12.13" + "@babel/compat-data@^7.12.1", "@babel/compat-data@^7.12.5", "@babel/compat-data@^7.12.7": version "7.12.7" resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.12.7.tgz#9329b4782a7d6bbd7eef57e11addf91ee3ef1e41" @@ -50,6 +57,28 @@ semver "^5.4.1" source-map "^0.5.0" +"@babel/core@7.12.9", "@babel/core@^7.1.0", "@babel/core@^7.12.3", "@babel/core@^7.6.2", "@babel/core@^7.7.5", "@babel/core@^7.8.4", "@babel/core@^7.9.0": + version "7.12.9" + resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.12.9.tgz#fd450c4ec10cdbb980e2928b7aa7a28484593fc8" + integrity sha512-gTXYh3M5wb7FRXQy+FErKFAv90BnlOuNn1QkCK2lREoPAjrQCO49+HVSrFoe5uakFAF5eenS75KbO2vQiLrTMQ== + dependencies: + "@babel/code-frame" "^7.10.4" + "@babel/generator" "^7.12.5" + "@babel/helper-module-transforms" "^7.12.1" + "@babel/helpers" "^7.12.5" + "@babel/parser" "^7.12.7" + "@babel/template" "^7.12.7" + "@babel/traverse" "^7.12.9" + "@babel/types" "^7.12.7" + convert-source-map "^1.7.0" + debug "^4.1.0" + gensync "^1.0.0-beta.1" + json5 "^2.1.2" + lodash "^4.17.19" + resolve "^1.3.2" + semver "^5.4.1" + source-map "^0.5.0" + "@babel/core@7.4.5": version "7.4.5" resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.4.5.tgz#081f97e8ffca65a9b4b0fdc7e274e703f000c06a" @@ -70,7 +99,7 @@ semver "^5.4.1" source-map "^0.5.0" -"@babel/core@^7.1.0", "@babel/core@^7.1.6", "@babel/core@^7.12.3", "@babel/core@^7.6.2", "@babel/core@^7.7.5", "@babel/core@^7.8.4", "@babel/core@^7.9.0": +"@babel/core@^7.1.6", "@babel/core@^7.12.1": version "7.12.10" resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.12.10.tgz#b79a2e1b9f70ed3d84bbfb6d8c4ef825f606bccd" integrity sha512-eTAlQKq65zHfkHZV0sIVODCPGVgoo1HdBlbSLi9CqOzuZanMv2ihzY+4paiKr1mH+XmYESMAmJ/dpZ68eN6d8w== @@ -109,6 +138,15 @@ jsesc "^2.5.1" source-map "^0.5.0" +"@babel/generator@^7.12.13": + version "7.12.15" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.12.15.tgz#4617b5d0b25cc572474cc1aafee1edeaf9b5368f" + integrity sha512-6F2xHxBiFXWNSGb7vyCUTBF8RCLY66rS0zEPcP8t/nQyXjha5EuK4z7H5o7fWG8B4M7y6mqVWq1J+1PuwRhecQ== + dependencies: + "@babel/types" "^7.12.13" + jsesc "^2.5.1" + source-map "^0.5.0" + "@babel/helper-annotate-as-pure@^7.10.4": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.10.4.tgz#5bf0d495a3f757ac3bda48b5bf3b3ba309c72ba3" @@ -204,6 +242,15 @@ "@babel/template" "^7.12.7" "@babel/types" "^7.12.11" +"@babel/helper-function-name@^7.12.13": + version "7.12.13" + resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.12.13.tgz#93ad656db3c3c2232559fd7b2c3dbdcbe0eb377a" + integrity sha512-TZvmPn0UOqmvi5G4vvw0qZTpVptGkB1GL61R6lKvrSdIxGm5Pky7Q3fpKiIkQCAtRCBUwB0PaThlx9vebCDSwA== + dependencies: + "@babel/helper-get-function-arity" "^7.12.13" + "@babel/template" "^7.12.13" + "@babel/types" "^7.12.13" + "@babel/helper-get-function-arity@^7.10.4": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/helper-get-function-arity/-/helper-get-function-arity-7.10.4.tgz#98c1cbea0e2332f33f9a4661b8ce1505b2c19ba2" @@ -218,6 +265,13 @@ dependencies: "@babel/types" "^7.12.10" +"@babel/helper-get-function-arity@^7.12.13": + version "7.12.13" + resolved "https://registry.yarnpkg.com/@babel/helper-get-function-arity/-/helper-get-function-arity-7.12.13.tgz#bc63451d403a3b3082b97e1d8b3fe5bd4091e583" + integrity sha512-DjEVzQNz5LICkzN0REdpD5prGoidvbdYk1BVgRUOINaWJP2t6avB27X1guXK1kXNrX0WMfsrm1A/ZBthYuIMQg== + dependencies: + "@babel/types" "^7.12.13" + "@babel/helper-hoist-variables@^7.10.4": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/helper-hoist-variables/-/helper-hoist-variables-7.10.4.tgz#d49b001d1d5a68ca5e6604dda01a6297f7c9381e" @@ -261,7 +315,7 @@ dependencies: "@babel/types" "^7.12.7" -"@babel/helper-plugin-utils@^7.0.0", "@babel/helper-plugin-utils@^7.10.4", "@babel/helper-plugin-utils@^7.8.0", "@babel/helper-plugin-utils@^7.8.3": +"@babel/helper-plugin-utils@7.10.4", "@babel/helper-plugin-utils@^7.0.0", "@babel/helper-plugin-utils@^7.10.4", "@babel/helper-plugin-utils@^7.8.0", "@babel/helper-plugin-utils@^7.8.3": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.10.4.tgz#2f75a831269d4f677de49986dff59927533cf375" integrity sha512-O4KCvQA6lLiMU9l2eawBPMf1xPP8xPfB3iEQw150hOVTqj/rfXz0ThTb4HEzqQfs2Bmo5Ay8BzxfzVtBrr9dVg== @@ -313,6 +367,13 @@ dependencies: "@babel/types" "^7.12.11" +"@babel/helper-split-export-declaration@^7.12.13": + version "7.12.13" + resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.12.13.tgz#e9430be00baf3e88b0e13e6f9d4eaf2136372b05" + integrity sha512-tCJDltF83htUtXx5NLcaDqRmknv652ZWCHyoTETf1CXYJdPC7nohZohjUgieXhv0hTJdRf2FjDueFehdNucpzg== + dependencies: + "@babel/types" "^7.12.13" + "@babel/helper-validator-identifier@^7.10.4": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.4.tgz#a78c7a7251e01f616512d31b10adcf52ada5e0d2" @@ -356,6 +417,15 @@ chalk "^2.0.0" js-tokens "^4.0.0" +"@babel/highlight@^7.12.13": + version "7.12.13" + resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.12.13.tgz#8ab538393e00370b26271b01fa08f7f27f2e795c" + integrity sha512-kocDQvIbgMKlWxXe9fof3TQ+gkIPOUSEYhJjqUjvKMez3krV7vbzYCDq39Oj11UAVK7JqPVGQPlgE85dPNlQww== + dependencies: + "@babel/helper-validator-identifier" "^7.12.11" + chalk "^2.0.0" + js-tokens "^4.0.0" + "@babel/parser@^7.1.0", "@babel/parser@^7.12.3", "@babel/parser@^7.12.7", "@babel/parser@^7.4.3", "@babel/parser@^7.4.5", "@babel/parser@^7.7.0", "@babel/parser@^7.9.4": version "7.12.7" resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.12.7.tgz#fee7b39fe809d0e73e5b25eecaf5780ef3d73056" @@ -366,6 +436,11 @@ resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.12.11.tgz#9ce3595bcd74bc5c466905e86c535b8b25011e79" integrity sha512-N3UxG+uuF4CMYoNj8AhnbAcJF0PiuJ9KHuy1lQmkYsxTer/MAH9UBNHsBoAX/4s6NvlDD047No8mYVGGzLL4hg== +"@babel/parser@^7.12.13": + version "7.12.15" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.12.15.tgz#2b20de7f0b4b332d9b119dd9c33409c538b8aacf" + integrity sha512-AQBOU2Z9kWwSZMd6lNjCX0GUgFonL1wAM1db8L8PMk9UDaGsRCArBkU4Sc+UCM3AE4hjbXx+h58Lb3QT4oRmrA== + "@babel/plugin-proposal-async-generator-functions@^7.12.1", "@babel/plugin-proposal-async-generator-functions@^7.2.0": version "7.12.1" resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.12.1.tgz#dc6c1170e27d8aca99ff65f4925bd06b1c90550e" @@ -464,15 +539,7 @@ "@babel/helper-plugin-utils" "^7.10.4" "@babel/plugin-syntax-numeric-separator" "^7.10.4" -"@babel/plugin-proposal-object-rest-spread@7.3.2": - version "7.3.2" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.3.2.tgz#6d1859882d4d778578e41f82cc5d7bf3d5daf6c1" - integrity sha512-DjeMS+J2+lpANkYLLO+m6GjoTMygYglKmRe6cDTbFv3L9i6mmiE8fe6B8MtCSLZpVXscD5kn7s6SgtHrDoBWoA== - dependencies: - "@babel/helper-plugin-utils" "^7.0.0" - "@babel/plugin-syntax-object-rest-spread" "^7.2.0" - -"@babel/plugin-proposal-object-rest-spread@^7.12.1", "@babel/plugin-proposal-object-rest-spread@^7.4.4": +"@babel/plugin-proposal-object-rest-spread@7.12.1", "@babel/plugin-proposal-object-rest-spread@^7.12.1", "@babel/plugin-proposal-object-rest-spread@^7.4.4": version "7.12.1" resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.12.1.tgz#def9bd03cea0f9b72283dac0ec22d289c7691069" integrity sha512-s6SowJIjzlhx8o7lsFx5zmY4At6CTtDvgNQDdPzkBQucle58A6b/TTeEBYtyDgmcXjUTM+vE8YOGHZzzbc/ioA== @@ -481,6 +548,14 @@ "@babel/plugin-syntax-object-rest-spread" "^7.8.0" "@babel/plugin-transform-parameters" "^7.12.1" +"@babel/plugin-proposal-object-rest-spread@7.3.2": + version "7.3.2" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.3.2.tgz#6d1859882d4d778578e41f82cc5d7bf3d5daf6c1" + integrity sha512-DjeMS+J2+lpANkYLLO+m6GjoTMygYglKmRe6cDTbFv3L9i6mmiE8fe6B8MtCSLZpVXscD5kn7s6SgtHrDoBWoA== + dependencies: + "@babel/helper-plugin-utils" "^7.0.0" + "@babel/plugin-syntax-object-rest-spread" "^7.2.0" + "@babel/plugin-proposal-optional-catch-binding@^7.12.1", "@babel/plugin-proposal-optional-catch-binding@^7.2.0": version "7.12.1" resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-optional-catch-binding/-/plugin-proposal-optional-catch-binding-7.12.1.tgz#ccc2421af64d3aae50b558a71cede929a5ab2942" @@ -593,7 +668,7 @@ dependencies: "@babel/helper-plugin-utils" "^7.8.0" -"@babel/plugin-syntax-jsx@^7.12.1": +"@babel/plugin-syntax-jsx@7.12.1", "@babel/plugin-syntax-jsx@^7.12.1": version "7.12.1" resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.12.1.tgz#9d9d357cc818aa7ae7935917c1257f67677a0926" integrity sha512-1yRi7yAtB0ETgxdY9ti/p2TivUxJkTdhu/ZbF9MshVGqOx1TdB3b7xCXs49Fupgg50N45KcAsRP/ZqWjs9SRjg== @@ -621,7 +696,7 @@ dependencies: "@babel/helper-plugin-utils" "^7.10.4" -"@babel/plugin-syntax-object-rest-spread@^7.0.0", "@babel/plugin-syntax-object-rest-spread@^7.2.0", "@babel/plugin-syntax-object-rest-spread@^7.8.0", "@babel/plugin-syntax-object-rest-spread@^7.8.3": +"@babel/plugin-syntax-object-rest-spread@7.8.3", "@babel/plugin-syntax-object-rest-spread@^7.0.0", "@babel/plugin-syntax-object-rest-spread@^7.2.0", "@babel/plugin-syntax-object-rest-spread@^7.8.0", "@babel/plugin-syntax-object-rest-spread@^7.8.3": version "7.8.3" resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz#60e225edcbd98a640332a2e72dd3e66f1af55871" integrity sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA== @@ -1312,7 +1387,7 @@ dependencies: regenerator-runtime "^0.12.0" -"@babel/runtime@^7.10.0", "@babel/runtime@^7.10.2", "@babel/runtime@^7.11.2", "@babel/runtime@^7.12.0", "@babel/runtime@^7.12.1", "@babel/runtime@^7.12.5", "@babel/runtime@^7.3.1", "@babel/runtime@^7.5.0", "@babel/runtime@^7.5.5", "@babel/runtime@^7.7.2", "@babel/runtime@^7.7.6", "@babel/runtime@^7.8.4", "@babel/runtime@^7.9.2": +"@babel/runtime@^7.0.0", "@babel/runtime@^7.10.0", "@babel/runtime@^7.10.2", "@babel/runtime@^7.11.2", "@babel/runtime@^7.12.0", "@babel/runtime@^7.12.1", "@babel/runtime@^7.12.5", "@babel/runtime@^7.3.1", "@babel/runtime@^7.5.0", "@babel/runtime@^7.5.5", "@babel/runtime@^7.7.2", "@babel/runtime@^7.7.6", "@babel/runtime@^7.8.4", "@babel/runtime@^7.9.2": version "7.12.5" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.12.5.tgz#410e7e487441e1b360c29be715d870d9b985882e" integrity sha512-plcc+hbExy3McchJCEQG3knOsuh3HH+Prx1P6cLIkET/0dLuQDEnrT+s27Axgc9bqfsmNUNHfscgMUdBpC9xfg== @@ -1328,6 +1403,15 @@ "@babel/parser" "^7.12.7" "@babel/types" "^7.12.7" +"@babel/template@^7.12.13": + version "7.12.13" + resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.12.13.tgz#530265be8a2589dbb37523844c5bcb55947fb327" + integrity sha512-/7xxiGA57xMo/P2GVvdEumr8ONhFOhfgq2ihK3h1e6THqzTAkHbkXgB0xI9yeTfIUoH3+oAeHhqm/I43OTbbjA== + dependencies: + "@babel/code-frame" "^7.12.13" + "@babel/parser" "^7.12.13" + "@babel/types" "^7.12.13" + "@babel/traverse@^7.1.0", "@babel/traverse@^7.10.4", "@babel/traverse@^7.12.1", "@babel/traverse@^7.12.5", "@babel/traverse@^7.4.3", "@babel/traverse@^7.4.5", "@babel/traverse@^7.7.0": version "7.12.9" resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.12.9.tgz#fad26c972eabbc11350e0b695978de6cc8e8596f" @@ -1358,6 +1442,21 @@ globals "^11.1.0" lodash "^4.17.19" +"@babel/traverse@^7.12.9": + version "7.12.13" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.12.13.tgz#689f0e4b4c08587ad26622832632735fb8c4e0c0" + integrity sha512-3Zb4w7eE/OslI0fTp8c7b286/cQps3+vdLW3UcwC8VSJC6GbKn55aeVVu2QJNuCDoeKyptLOFrPq8WqZZBodyA== + dependencies: + "@babel/code-frame" "^7.12.13" + "@babel/generator" "^7.12.13" + "@babel/helper-function-name" "^7.12.13" + "@babel/helper-split-export-declaration" "^7.12.13" + "@babel/parser" "^7.12.13" + "@babel/types" "^7.12.13" + debug "^4.1.0" + globals "^11.1.0" + lodash "^4.17.19" + "@babel/types@7.8.3": version "7.8.3" resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.8.3.tgz#5a383dffa5416db1b73dedffd311ffd0788fb31c" @@ -1385,6 +1484,15 @@ lodash "^4.17.19" to-fast-properties "^2.0.0" +"@babel/types@^7.12.13": + version "7.12.13" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.12.13.tgz#8be1aa8f2c876da11a9cf650c0ecf656913ad611" + integrity sha512-oKrdZTld2im1z8bDwTOQvUbxKwE+854zc16qWZQlcTqMN00pWxHQ4ZeOq0yDMnisOpRykH2/5Qqcrk/OlbAjiQ== + dependencies: + "@babel/helper-validator-identifier" "^7.12.11" + lodash "^4.17.19" + to-fast-properties "^2.0.0" + "@base2/pretty-print-object@1.0.0": version "1.0.0" resolved "https://registry.yarnpkg.com/@base2/pretty-print-object/-/pretty-print-object-1.0.0.tgz#860ce718b0b73f4009e153541faff2cb6b85d047" @@ -2378,7 +2486,7 @@ source-map "^0.6.1" write-file-atomic "2.4.1" -"@jest/transform@^26.6.2": +"@jest/transform@^26.0.0", "@jest/transform@^26.6.2": version "26.6.2" resolved "https://registry.yarnpkg.com/@jest/transform/-/transform-26.6.2.tgz#5ac57c5fa1ad17b2aae83e73e45813894dcf2e4b" integrity sha512-E9JjhUgNzvuQ+vVAL21vlyfy12gP0GhazGgJC4h6qUt1jSdUXGWJ1wfu/X7Sd8etSgxV4ovT1pb9v5D6QW4XgA== @@ -2419,6 +2527,50 @@ "@types/yargs" "^15.0.0" chalk "^4.0.0" +"@mdx-js/loader@^1.6.19": + version "1.6.22" + resolved "https://registry.yarnpkg.com/@mdx-js/loader/-/loader-1.6.22.tgz#d9e8fe7f8185ff13c9c8639c048b123e30d322c4" + integrity sha512-9CjGwy595NaxAYp0hF9B/A0lH6C8Rms97e2JS9d3jVUtILn6pT5i5IV965ra3lIWc7Rs1GG1tBdVF7dCowYe6Q== + dependencies: + "@mdx-js/mdx" "1.6.22" + "@mdx-js/react" "1.6.22" + loader-utils "2.0.0" + +"@mdx-js/mdx@1.6.22", "@mdx-js/mdx@^1.6.19": + version "1.6.22" + resolved "https://registry.yarnpkg.com/@mdx-js/mdx/-/mdx-1.6.22.tgz#8a723157bf90e78f17dc0f27995398e6c731f1ba" + integrity sha512-AMxuLxPz2j5/6TpF/XSdKpQP1NlG0z11dFOlq+2IP/lSgl11GY8ji6S/rgsViN/L0BDvHvUMruRb7ub+24LUYA== + dependencies: + "@babel/core" "7.12.9" + "@babel/plugin-syntax-jsx" "7.12.1" + "@babel/plugin-syntax-object-rest-spread" "7.8.3" + "@mdx-js/util" "1.6.22" + babel-plugin-apply-mdx-type-prop "1.6.22" + babel-plugin-extract-import-names "1.6.22" + camelcase-css "2.0.1" + detab "2.0.4" + hast-util-raw "6.0.1" + lodash.uniq "4.5.0" + mdast-util-to-hast "10.0.1" + remark-footnotes "2.0.0" + remark-mdx "1.6.22" + remark-parse "8.0.3" + remark-squeeze-paragraphs "4.0.0" + style-to-object "0.3.0" + unified "9.2.0" + unist-builder "2.0.3" + unist-util-visit "2.0.3" + +"@mdx-js/react@1.6.22", "@mdx-js/react@^1.6.19": + version "1.6.22" + resolved "https://registry.yarnpkg.com/@mdx-js/react/-/react-1.6.22.tgz#ae09b4744fddc74714ee9f9d6f17a66e77c43573" + integrity sha512-TDoPum4SHdfPiGSAaRBw7ECyI8VaHpK8GJugbJIJuqyh6kzw9ZLJZW3HGL3NNrJGxcAixUvqROm+YuQOo5eXtg== + +"@mdx-js/util@1.6.22": + version "1.6.22" + resolved "https://registry.yarnpkg.com/@mdx-js/util/-/util-1.6.22.tgz#219dfd89ae5b97a8801f015323ffa4b62f45718b" + integrity sha512-H1rQc1ZOHANWBvPcW+JpGwr+juXSxM8Q8YCkm3GhZd8REu1fHR3z99CErO1p9pkcfcxZnMdIZdIsXkOHY0NilA== + "@mrmlnc/readdir-enhanced@^2.2.1": version "2.2.1" resolved "https://registry.yarnpkg.com/@mrmlnc/readdir-enhanced/-/readdir-enhanced-2.2.1.tgz#524af240d1a360527b730475ecfa1344aa540dde" @@ -2553,34 +2705,230 @@ dependencies: "@sinonjs/commons" "^1.7.0" -"@storybook/addons@6.1.16", "@storybook/addons@^6.1.16": - version "6.1.16" - resolved "https://registry.yarnpkg.com/@storybook/addons/-/addons-6.1.16.tgz#d5a5b940efeda4031fd406d8064b6774124543f4" - integrity sha512-UefxKL4JHd7wTlCGXq+YZzmUgv6GvSSUHMPS39OTtrZUGTL1Vr88h6wITnQPUi/MFtj0VAZ0Fnj3N+PK3fxYzQ== - dependencies: - "@storybook/api" "6.1.16" - "@storybook/channels" "6.1.16" - "@storybook/client-logger" "6.1.16" - "@storybook/core-events" "6.1.16" - "@storybook/router" "6.1.16" - "@storybook/theming" "6.1.16" +"@storybook/addon-a11y@^6.1.20": + version "6.1.20" + resolved "https://registry.yarnpkg.com/@storybook/addon-a11y/-/addon-a11y-6.1.20.tgz#71102a84ebaf37948e19a6d28fb58de5adadd1b6" + integrity sha512-ytktgT4XyXtbqpYt76R2PWlRTku7NjoZHLCF6Gv+OSEYfys8e0twZ+BJ+Cojch6WLwHYXVcSfcmhgY+DedI0BA== + dependencies: + "@storybook/addons" "6.1.20" + "@storybook/api" "6.1.20" + "@storybook/channels" "6.1.20" + "@storybook/client-api" "6.1.20" + "@storybook/client-logger" "6.1.20" + "@storybook/components" "6.1.20" + "@storybook/core-events" "6.1.20" + "@storybook/theming" "6.1.20" + axe-core "^4.0.1" + core-js "^3.0.1" + global "^4.3.2" + lodash "^4.17.15" + react-sizeme "^2.5.2" + regenerator-runtime "^0.13.7" + ts-dedent "^2.0.0" + util-deprecate "^1.0.2" + +"@storybook/addon-actions@6.1.20": + version "6.1.20" + resolved "https://registry.yarnpkg.com/@storybook/addon-actions/-/addon-actions-6.1.20.tgz#a7c5f8d079d309f89b38ac8fb89b0838e63afc43" + integrity sha512-94KH/+Y+Do9k9XgVGup2XgRnzaz/6fSR41nKW4x8oUbnmke8FeZEAurBzjsK+0EGZhVilEpVvabZXc7t9tRZyg== + dependencies: + "@storybook/addons" "6.1.20" + "@storybook/api" "6.1.20" + "@storybook/client-api" "6.1.20" + "@storybook/components" "6.1.20" + "@storybook/core-events" "6.1.20" + "@storybook/theming" "6.1.20" + core-js "^3.0.1" + fast-deep-equal "^3.1.1" + global "^4.3.2" + lodash "^4.17.15" + polished "^3.4.4" + prop-types "^15.7.2" + react-inspector "^5.0.1" + regenerator-runtime "^0.13.7" + ts-dedent "^2.0.0" + util-deprecate "^1.0.2" + uuid "^8.0.0" + +"@storybook/addon-backgrounds@6.1.20": + version "6.1.20" + resolved "https://registry.yarnpkg.com/@storybook/addon-backgrounds/-/addon-backgrounds-6.1.20.tgz#317cf6e123b8bc42f401cff1c0531aaa33d9d1ac" + integrity sha512-7YF+18DaekpiN1zyyLYOT6iqCPr8kGt6PFdAtMa/HtIalGryDwlRNHaUfylWAsaRWrOAz2tBzrX16olMuE+i3g== + dependencies: + "@storybook/addons" "6.1.20" + "@storybook/api" "6.1.20" + "@storybook/client-logger" "6.1.20" + "@storybook/components" "6.1.20" + "@storybook/core-events" "6.1.20" + "@storybook/theming" "6.1.20" + core-js "^3.0.1" + global "^4.3.2" + memoizerific "^1.11.3" + regenerator-runtime "^0.13.7" + ts-dedent "^2.0.0" + util-deprecate "^1.0.2" + +"@storybook/addon-console@^1.2.3": + version "1.2.3" + resolved "https://registry.yarnpkg.com/@storybook/addon-console/-/addon-console-1.2.3.tgz#f6c88a8f54fe00c8de9b77720eaef2bc1daa3af1" + integrity sha512-w5uCUwECA28fdZWoa+A4e/RS9XzBStdd3TwwmpSM5m4fjURJI7Qr+uVq30UeRdgZRH1K7CdWzYUE6RxWXMdVyw== + dependencies: + global "^4.3.2" + +"@storybook/addon-controls@6.1.20": + version "6.1.20" + resolved "https://registry.yarnpkg.com/@storybook/addon-controls/-/addon-controls-6.1.20.tgz#28fdbde325d55f910caad5f4694feee242c146ef" + integrity sha512-UZMZipa0B5IjKuZfOAa2xLYckzKuOtXbMTcTiT97ygyDSxMTkaCyfmuBdoUyoCv/+0PwQl2dN6EUqI+7I0ZZYA== + dependencies: + "@storybook/addons" "6.1.20" + "@storybook/api" "6.1.20" + "@storybook/client-api" "6.1.20" + "@storybook/components" "6.1.20" + "@storybook/node-logger" "6.1.20" + "@storybook/theming" "6.1.20" + core-js "^3.0.1" + ts-dedent "^2.0.0" + +"@storybook/addon-docs@6.1.20": + version "6.1.20" + resolved "https://registry.yarnpkg.com/@storybook/addon-docs/-/addon-docs-6.1.20.tgz#390ea245686cfc464cfb94d441e494f1dbaf1b89" + integrity sha512-dc51UHcgMe/sa68+GFaJALJnkxoU8HNmNJmjwJoxZ1boTMC9D6CjVZl3tGqoLwoStlGB98lM7s+esONz+RAXtA== + dependencies: + "@babel/core" "^7.12.1" + "@babel/generator" "^7.12.1" + "@babel/parser" "^7.12.3" + "@babel/plugin-transform-react-jsx" "^7.12.1" + "@babel/preset-env" "^7.12.1" + "@jest/transform" "^26.0.0" + "@mdx-js/loader" "^1.6.19" + "@mdx-js/mdx" "^1.6.19" + "@mdx-js/react" "^1.6.19" + "@storybook/addons" "6.1.20" + "@storybook/api" "6.1.20" + "@storybook/client-api" "6.1.20" + "@storybook/client-logger" "6.1.20" + "@storybook/components" "6.1.20" + "@storybook/core" "6.1.20" + "@storybook/core-events" "6.1.20" + "@storybook/csf" "0.0.1" + "@storybook/node-logger" "6.1.20" + "@storybook/postinstall" "6.1.20" + "@storybook/source-loader" "6.1.20" + "@storybook/theming" "6.1.20" + acorn "^7.1.0" + acorn-jsx "^5.1.0" + acorn-walk "^7.0.0" core-js "^3.0.1" + doctrine "^3.0.0" + escodegen "^1.12.0" + fast-deep-equal "^3.1.1" global "^4.3.2" + html-tags "^3.1.0" + js-string-escape "^1.0.1" + lodash "^4.17.15" + prettier "~2.0.5" + prop-types "^15.7.2" + react-element-to-jsx-string "^14.3.1" regenerator-runtime "^0.13.7" + remark-external-links "^6.0.0" + remark-slug "^6.0.0" + ts-dedent "^2.0.0" + util-deprecate "^1.0.2" -"@storybook/api@6.1.16": - version "6.1.16" - resolved "https://registry.yarnpkg.com/@storybook/api/-/api-6.1.16.tgz#52e86ea4ea85210b16c3df30fba639160d426ea7" - integrity sha512-WhSgXNktSFbrLchMyHQNW08GuNROsMSHTlGHHAPZN94Q95tEY/AkMO5l6AuhgGOwM51UWstfY3u8WDudLbjYEw== +"@storybook/addon-essentials@^6.1.20": + version "6.1.20" + resolved "https://registry.yarnpkg.com/@storybook/addon-essentials/-/addon-essentials-6.1.20.tgz#4cd38e61e6a763c3ff291d0d8da04f2968395265" + integrity sha512-8ne21UO3mE8nxUq8Nk8rF3zSJiLVjQdBv9aimwXUgOBeQTBRyY/H0nswjbIas8WrEk4D0pK+ylel4CdmMXJxxw== + dependencies: + "@storybook/addon-actions" "6.1.20" + "@storybook/addon-backgrounds" "6.1.20" + "@storybook/addon-controls" "6.1.20" + "@storybook/addon-docs" "6.1.20" + "@storybook/addon-toolbars" "6.1.20" + "@storybook/addon-viewport" "6.1.20" + "@storybook/addons" "6.1.20" + "@storybook/api" "6.1.20" + "@storybook/node-logger" "6.1.20" + core-js "^3.0.1" + regenerator-runtime "^0.13.7" + ts-dedent "^2.0.0" + +"@storybook/addon-storysource@^6.1.20": + version "6.1.20" + resolved "https://registry.yarnpkg.com/@storybook/addon-storysource/-/addon-storysource-6.1.20.tgz#72898b863ec7e11cbe82eb5f740a61530f8439b1" + integrity sha512-MT9ROmjM+WNmzJJrDP52WzJy6DdJHqTCFUaJ5cmUdodmWrwMFTx/MaD53blTU5xjsPQS2pnfEHDDAYESjgJ4bg== + dependencies: + "@storybook/addons" "6.1.20" + "@storybook/api" "6.1.20" + "@storybook/client-logger" "6.1.20" + "@storybook/components" "6.1.20" + "@storybook/router" "6.1.20" + "@storybook/source-loader" "6.1.20" + "@storybook/theming" "6.1.20" + core-js "^3.0.1" + estraverse "^4.2.0" + loader-utils "^2.0.0" + prettier "~2.0.5" + prop-types "^15.7.2" + react-syntax-highlighter "^13.5.0" + regenerator-runtime "^0.13.7" + +"@storybook/addon-toolbars@6.1.20": + version "6.1.20" + resolved "https://registry.yarnpkg.com/@storybook/addon-toolbars/-/addon-toolbars-6.1.20.tgz#a748cdf255607eba229809ab1e4ce51f811204e3" + integrity sha512-r+MGlY9MyGnlJQ6149GZOFnJ6rUZgrnX9RcpcuwOBmfZNUM0andnOlaV3L1s2LY+oEETDi/rqQkQcrr7jbO/wA== + dependencies: + "@storybook/addons" "6.1.20" + "@storybook/api" "6.1.20" + "@storybook/client-api" "6.1.20" + "@storybook/components" "6.1.20" + core-js "^3.0.1" + +"@storybook/addon-viewport@6.1.20": + version "6.1.20" + resolved "https://registry.yarnpkg.com/@storybook/addon-viewport/-/addon-viewport-6.1.20.tgz#42173010cecd5f762534b00c614deaa3a4d42381" + integrity sha512-Xh75q3eh29QYkgYUF7ZEc8/R4HcQjTPazsxxknYZKu+S5TZ1OhoToH74YOL7bDuMAMAco95zv4zHpW02/oYT0g== + dependencies: + "@storybook/addons" "6.1.20" + "@storybook/api" "6.1.20" + "@storybook/client-logger" "6.1.20" + "@storybook/components" "6.1.20" + "@storybook/core-events" "6.1.20" + "@storybook/theming" "6.1.20" + core-js "^3.0.1" + global "^4.3.2" + memoizerific "^1.11.3" + prop-types "^15.7.2" + regenerator-runtime "^0.13.7" + +"@storybook/addons@6.1.20", "@storybook/addons@^6.1.20": + version "6.1.20" + resolved "https://registry.yarnpkg.com/@storybook/addons/-/addons-6.1.20.tgz#da01dabd6692919b719fcb30519d53ea80887097" + integrity sha512-kIhXYgF+ARNpYxO3qhz8yThDvKpaq+HDst8odPU9sCNEI66PSH6hrILhTmnffNnqdtY3LnKkU9rGVfZn+3TOTA== + dependencies: + "@storybook/api" "6.1.20" + "@storybook/channels" "6.1.20" + "@storybook/client-logger" "6.1.20" + "@storybook/core-events" "6.1.20" + "@storybook/router" "6.1.20" + "@storybook/theming" "6.1.20" + core-js "^3.0.1" + global "^4.3.2" + regenerator-runtime "^0.13.7" + +"@storybook/api@6.1.20": + version "6.1.20" + resolved "https://registry.yarnpkg.com/@storybook/api/-/api-6.1.20.tgz#3738b0c859ead820b378ee94e936abcf0e2f7436" + integrity sha512-+Uvvj7B+0oGb83mOzNjFuxju3ColjJpgyDjNzD5jI2xCtGyau+c8Lr4rhI9xNc2Dw9b8gpfPmhkvEnBPmd/ecQ== dependencies: "@reach/router" "^1.3.3" - "@storybook/channels" "6.1.16" - "@storybook/client-logger" "6.1.16" - "@storybook/core-events" "6.1.16" + "@storybook/channels" "6.1.20" + "@storybook/client-logger" "6.1.20" + "@storybook/core-events" "6.1.20" "@storybook/csf" "0.0.1" - "@storybook/router" "6.1.16" + "@storybook/router" "6.1.20" "@storybook/semver" "^7.3.2" - "@storybook/theming" "6.1.16" + "@storybook/theming" "6.1.20" "@types/reach__router" "^1.3.7" core-js "^3.0.1" fast-deep-equal "^3.1.1" @@ -2593,38 +2941,38 @@ ts-dedent "^2.0.0" util-deprecate "^1.0.2" -"@storybook/channel-postmessage@6.1.16": - version "6.1.16" - resolved "https://registry.yarnpkg.com/@storybook/channel-postmessage/-/channel-postmessage-6.1.16.tgz#938daf4537ecbae4d3131562179bae706d22c1b6" - integrity sha512-Fxz938NVWeNh5mHjghF4PkoMkzO2sk5f/cBpKBveNdIPDImUHrbdfYsfwhDVaY4/JFKyxXQUJBFyt0iYR+Z4jA== +"@storybook/channel-postmessage@6.1.20": + version "6.1.20" + resolved "https://registry.yarnpkg.com/@storybook/channel-postmessage/-/channel-postmessage-6.1.20.tgz#d23956e553ff7e5f022bd8496ee7a26defa57440" + integrity sha512-4/zUd48qBnhoD96M4yBK+RlMQmZid0FSUzc6w7mXXjDE7vmRrXgP5ppIwYlzo4mcNSA5wCJsEp4YKRgAfZAUxw== dependencies: - "@storybook/channels" "6.1.16" - "@storybook/client-logger" "6.1.16" - "@storybook/core-events" "6.1.16" + "@storybook/channels" "6.1.20" + "@storybook/client-logger" "6.1.20" + "@storybook/core-events" "6.1.20" core-js "^3.0.1" global "^4.3.2" qs "^6.6.0" telejson "^5.0.2" -"@storybook/channels@6.1.16", "@storybook/channels@^6.1.16": - version "6.1.16" - resolved "https://registry.yarnpkg.com/@storybook/channels/-/channels-6.1.16.tgz#987ba0fbd1a2b538f847ae505e253cdf06133b1e" - integrity sha512-FxBt0xYxBHgD9mrI/NzGjoHqCvfoNQ++uiO0g06HWKKb9WQ8sd6cyUxAVjN9T2PvrivSKjPM5FNHhxrkCAbbbA== +"@storybook/channels@6.1.20", "@storybook/channels@^6.1.20": + version "6.1.20" + resolved "https://registry.yarnpkg.com/@storybook/channels/-/channels-6.1.20.tgz#8dc2763ffda301f3bda811cdcb19f8e88ff4ec80" + integrity sha512-UBvVf07LAUD6JTrk77f4qydS4v5hzjAHJWOfWO6b82oO5bu4hTXt/Rjj/TSz85Rl/NmM4GYAAPIfxJHg53TRTg== dependencies: core-js "^3.0.1" ts-dedent "^2.0.0" util-deprecate "^1.0.2" -"@storybook/client-api@6.1.16": - version "6.1.16" - resolved "https://registry.yarnpkg.com/@storybook/client-api/-/client-api-6.1.16.tgz#d6362d9c6d95a30e47906b8ff9c1daa3a0facd4d" - integrity sha512-cMMn3AdwXJm0KtT8RaIVAD5M2eNvxwOc/8AKMK83e79Mux7PujrG/k2TXuXo7aFXXWEzENnkxGZ/EJGPzcZelg== +"@storybook/client-api@6.1.20": + version "6.1.20" + resolved "https://registry.yarnpkg.com/@storybook/client-api/-/client-api-6.1.20.tgz#bf6fb3e247a599d2d6c7502898f336ff79e4df4c" + integrity sha512-QLM8h0h8HWkHRh3GYoO6PdwYX4No4/J7oYg6cNVhNbhA9l4a3HDLEyfBGojU4ZUDd2feJ8Sgml92UoP4Vrj0kg== dependencies: - "@storybook/addons" "6.1.16" - "@storybook/channel-postmessage" "6.1.16" - "@storybook/channels" "6.1.16" - "@storybook/client-logger" "6.1.16" - "@storybook/core-events" "6.1.16" + "@storybook/addons" "6.1.20" + "@storybook/channel-postmessage" "6.1.20" + "@storybook/channels" "6.1.20" + "@storybook/client-logger" "6.1.20" + "@storybook/core-events" "6.1.20" "@storybook/csf" "0.0.1" "@types/qs" "^6.9.0" "@types/webpack-env" "^1.15.3" @@ -2639,23 +2987,23 @@ ts-dedent "^2.0.0" util-deprecate "^1.0.2" -"@storybook/client-logger@6.1.16": - version "6.1.16" - resolved "https://registry.yarnpkg.com/@storybook/client-logger/-/client-logger-6.1.16.tgz#5523e9ebea22eda526e5d86d126d3139455e0481" - integrity sha512-k8vjm/WICYIY5avbg7g3vWBvp9eb8iHrvgcsM3LWt6mafHW/GxJK7IYTleAhDI9+sTj1oTMU+7zTCc0OTmQ51A== +"@storybook/client-logger@6.1.20": + version "6.1.20" + resolved "https://registry.yarnpkg.com/@storybook/client-logger/-/client-logger-6.1.20.tgz#f78102bbf4d169c45c5202c1b01cb1e58140be30" + integrity sha512-UKq+5vRXZXcwLgjXEK/NoL61JXar51aSDwnPa4jEFXRpXvIbHZzr6U3TO6r2J2LkTEJO54V2k8F2wnZgUvm3QA== dependencies: core-js "^3.0.1" global "^4.3.2" -"@storybook/components@6.1.16", "@storybook/components@^6.1.16": - version "6.1.16" - resolved "https://registry.yarnpkg.com/@storybook/components/-/components-6.1.16.tgz#5f58a50b6a4953f5f035713e66da618fdaf39c65" - integrity sha512-OhtsNKrfSXdVREqO9f89Yr4esP99kVNfzXOIL8WYdYoqW8AsGeznWcdUCkmDOJax0GuinJfRthZNoyTXDEm2Nw== +"@storybook/components@6.1.20", "@storybook/components@^6.1.20": + version "6.1.20" + resolved "https://registry.yarnpkg.com/@storybook/components/-/components-6.1.20.tgz#90834d76d50a17172f8c480e0f366ebade31e108" + integrity sha512-X4k2PF3Q60p3rgRkGtjWh0DWP9tqdwMRwDjA6TGj8WyRM2FdROlmH/hwRy9Op/cs2Yj8ApkUJk8AMUm3hBhYvQ== dependencies: "@popperjs/core" "^2.5.4" - "@storybook/client-logger" "6.1.16" + "@storybook/client-logger" "6.1.20" "@storybook/csf" "0.0.1" - "@storybook/theming" "6.1.16" + "@storybook/theming" "6.1.20" "@types/overlayscrollbars" "^1.9.0" "@types/react-color" "^3.0.1" "@types/react-syntax-highlighter" "11.0.4" @@ -2671,19 +3019,20 @@ react-popper-tooltip "^3.1.1" react-syntax-highlighter "^13.5.0" react-textarea-autosize "^8.1.1" + regenerator-runtime "^0.13.7" ts-dedent "^2.0.0" -"@storybook/core-events@6.1.16", "@storybook/core-events@^6.1.16": - version "6.1.16" - resolved "https://registry.yarnpkg.com/@storybook/core-events/-/core-events-6.1.16.tgz#333c4f17eb34db9c5513faaf320d96aa46e6f446" - integrity sha512-J07HWJhOaIKyOl8xMDQ6HwDKnBaR0HIvx5ea/3j2s3MPMZ+mDQD6d4voTNvkR2H7MDrZU20ZDcIIKeJ6gdzipg== +"@storybook/core-events@6.1.20", "@storybook/core-events@^6.1.20": + version "6.1.20" + resolved "https://registry.yarnpkg.com/@storybook/core-events/-/core-events-6.1.20.tgz#a23fe6ff858c0a4c48f89beaca1e50be5ba0b598" + integrity sha512-OPKNCbETTrGGypxFzDtsE2cGdHDNolVSJv1mZ17fr9lquc5eyJJCAJ4HbPk+OocRuHBKEnc1/pcA+wWKBM+vnA== dependencies: core-js "^3.0.1" -"@storybook/core@6.1.16": - version "6.1.16" - resolved "https://registry.yarnpkg.com/@storybook/core/-/core-6.1.16.tgz#868e1b9eeab07f93f58ea3b8fa6687138a44a774" - integrity sha512-qpIrgkuUcmiaWC+Oq91odiS0SxWmSReoeoLhrRsOFEdIL7ccIHf7ftCdOZ03nHuYMB/UV2Y7iEvdgpaIHygwuQ== +"@storybook/core@6.1.20": + version "6.1.20" + resolved "https://registry.yarnpkg.com/@storybook/core/-/core-6.1.20.tgz#57e8a86305f7da6cdc13185299c4c5f4b90b7308" + integrity sha512-cXca0s+ixoouXwPXeUoE9sB5OEkOUpkGAA78W8MLP4IHlI09ZBFCmLP989JdcCT2EjFBQ1V/UudNkQHMlyIl2A== dependencies: "@babel/core" "^7.12.3" "@babel/plugin-proposal-class-properties" "^7.12.1" @@ -2707,20 +3056,20 @@ "@babel/preset-react" "^7.12.1" "@babel/preset-typescript" "^7.12.1" "@babel/register" "^7.12.1" - "@storybook/addons" "6.1.16" - "@storybook/api" "6.1.16" - "@storybook/channel-postmessage" "6.1.16" - "@storybook/channels" "6.1.16" - "@storybook/client-api" "6.1.16" - "@storybook/client-logger" "6.1.16" - "@storybook/components" "6.1.16" - "@storybook/core-events" "6.1.16" + "@storybook/addons" "6.1.20" + "@storybook/api" "6.1.20" + "@storybook/channel-postmessage" "6.1.20" + "@storybook/channels" "6.1.20" + "@storybook/client-api" "6.1.20" + "@storybook/client-logger" "6.1.20" + "@storybook/components" "6.1.20" + "@storybook/core-events" "6.1.20" "@storybook/csf" "0.0.1" - "@storybook/node-logger" "6.1.16" - "@storybook/router" "6.1.16" + "@storybook/node-logger" "6.1.20" + "@storybook/router" "6.1.20" "@storybook/semver" "^7.3.2" - "@storybook/theming" "6.1.16" - "@storybook/ui" "6.1.16" + "@storybook/theming" "6.1.20" + "@storybook/ui" "6.1.20" "@types/glob-base" "^0.3.0" "@types/micromatch" "^4.0.1" "@types/node-fetch" "^2.5.4" @@ -2768,7 +3117,7 @@ pretty-hrtime "^1.0.3" qs "^6.6.0" raw-loader "^4.0.1" - react-dev-utils "^10.0.0" + react-dev-utils "^11.0.3" regenerator-runtime "^0.13.7" resolve-from "^5.0.0" serve-favicon "^2.5.0" @@ -2794,10 +3143,10 @@ dependencies: lodash "^4.17.15" -"@storybook/node-logger@6.1.16", "@storybook/node-logger@^6.1.16": - version "6.1.16" - resolved "https://registry.yarnpkg.com/@storybook/node-logger/-/node-logger-6.1.16.tgz#a7b25977f562eaaf3fcc4fed35298e22d08ac543" - integrity sha512-xwqAhhalwCs6FZvEyizgEhOkSAC+Ww+2bQ0FzHTAATTspp8DnRmSq6VmT8F8W9KJOMd1p4vYVy7C4dLhU2E/sQ== +"@storybook/node-logger@6.1.20", "@storybook/node-logger@^6.1.20": + version "6.1.20" + resolved "https://registry.yarnpkg.com/@storybook/node-logger/-/node-logger-6.1.20.tgz#40ec44bfd36e799089c831ecb86588c730023e6c" + integrity sha512-Z6337htb1mxIccvCx2Ai0v9LPDlBlmXzeWhap3q2Y6hg8g1p4+0W5Y6bG9RmXqJoXLaT1trO8uAXgGO7AN92yg== dependencies: "@types/npmlog" "^4.1.2" chalk "^4.0.0" @@ -2805,10 +3154,17 @@ npmlog "^4.1.2" pretty-hrtime "^1.0.3" -"@storybook/preset-create-react-app@^3.1.5": - version "3.1.5" - resolved "https://registry.yarnpkg.com/@storybook/preset-create-react-app/-/preset-create-react-app-3.1.5.tgz#af46c9d64c384980d458fe99c10dcbaa623f93fd" - integrity sha512-tzYcCRD5j22/HoDZ1tvsKaVnzyd4qqTE9sn3cx56Reb0XHcm4XkvG87jx0NvBGPCZrsThyBAtB3+XNxoFbI+9Q== +"@storybook/postinstall@6.1.20": + version "6.1.20" + resolved "https://registry.yarnpkg.com/@storybook/postinstall/-/postinstall-6.1.20.tgz#3e31f061f3f07f9a4955b970b963796734a45799" + integrity sha512-k9yLNN4T6KrvzWntU504NMesUQEg5YcsqKfNGjpTfKKRJjMR3+k74pbUZFC+XJEfMkCvSkWsJ2NRcE65bAMm3w== + dependencies: + core-js "^3.0.1" + +"@storybook/preset-create-react-app@^3.1.6": + version "3.1.6" + resolved "https://registry.yarnpkg.com/@storybook/preset-create-react-app/-/preset-create-react-app-3.1.6.tgz#fdcc332343085d26fe50891be484c830921f2688" + integrity sha512-hH8g5bBqNVU1zl2K19fXazncaWX2eiOX+ZHN3RtfQioAgHtXH/S1TS92xJj7uxoU40GYjldRmv7+fhzJCyRxNg== dependencies: "@types/babel__core" "^7.1.7" "@types/webpack" "^4.41.13" @@ -2817,17 +3173,17 @@ react-docgen-typescript-plugin "^0.6.2" semver "^7.3.2" -"@storybook/react@^6.1.16": - version "6.1.16" - resolved "https://registry.yarnpkg.com/@storybook/react/-/react-6.1.16.tgz#0714d69cade41b19f7ff5479c3c8aea4f73e7024" - integrity sha512-YHty7BIApRE6SHRHiDkIKiRv3LJkkSLGs2GArx1kZa6eZP0dL01GzHxGT7msClRO9iQTxUAkrj0O4EeF5WunbA== +"@storybook/react@^6.1.20": + version "6.1.20" + resolved "https://registry.yarnpkg.com/@storybook/react/-/react-6.1.20.tgz#0e2b858107fc139aa01a1d0fb2dca0611dd2224b" + integrity sha512-9NCWxLXJSjEy/DP9fC8Uj7zUljPA6eREjZuNElHGRI/Tg5R/QBuQnBJX9EagLic1lzpcUbsfWJ/+Bpa2qLXAEw== dependencies: "@babel/preset-flow" "^7.12.1" "@babel/preset-react" "^7.12.1" "@pmmmwh/react-refresh-webpack-plugin" "^0.4.2" - "@storybook/addons" "6.1.16" - "@storybook/core" "6.1.16" - "@storybook/node-logger" "6.1.16" + "@storybook/addons" "6.1.20" + "@storybook/core" "6.1.20" + "@storybook/node-logger" "6.1.20" "@storybook/semver" "^7.3.2" "@types/webpack-env" "^1.15.3" babel-plugin-add-react-displayname "^0.0.5" @@ -2837,17 +3193,17 @@ global "^4.3.2" lodash "^4.17.15" prop-types "^15.7.2" - react-dev-utils "^10.0.0" + react-dev-utils "^11.0.3" react-docgen-typescript-plugin "^0.6.2" react-refresh "^0.8.3" regenerator-runtime "^0.13.7" ts-dedent "^2.0.0" webpack "^4.44.2" -"@storybook/router@6.1.16": - version "6.1.16" - resolved "https://registry.yarnpkg.com/@storybook/router/-/router-6.1.16.tgz#3c92c87778e41609137d39b9c37877a2d8e31649" - integrity sha512-6z7t6uL+YKU9jSGR49jxrSKT4lkKwEd/+PE7ViMOk9r+BR+UsUitVnwQ2J4syFZ64EC9MepJOOfK0RVXILGuVQ== +"@storybook/router@6.1.20": + version "6.1.20" + resolved "https://registry.yarnpkg.com/@storybook/router/-/router-6.1.20.tgz#8d27379f53439762f503d77ce4ec2e9ac80644b4" + integrity sha512-hIJiy60znxu9fJgnFP3n5C9YdWr/bHk77vowf0nO0v+dd59EKlgh7ibiDi48Fe2PMU95pYGb6mCDouNS+boN0w== dependencies: "@reach/router" "^1.3.3" "@types/reach__router" "^1.3.7" @@ -2864,15 +3220,32 @@ core-js "^3.6.5" find-up "^4.1.0" -"@storybook/theming@6.1.16", "@storybook/theming@^6.1.16": - version "6.1.16" - resolved "https://registry.yarnpkg.com/@storybook/theming/-/theming-6.1.16.tgz#012711c0bd224cc6c089ec6c0239bc86c5b2842b" - integrity sha512-gFcqbTALWdLI6hwUSob3Gn7iIcyp9qp9Oib+8f00ZHDfZnWr8CDQPJ3PcZ322uf35gskDstk8eKqviSZSDUQug== +"@storybook/source-loader@6.1.20": + version "6.1.20" + resolved "https://registry.yarnpkg.com/@storybook/source-loader/-/source-loader-6.1.20.tgz#884f982430a422063a8a0edee0af1d72dabc75d2" + integrity sha512-rxfh+6JoPrw9RIB+yQ81VpRt586rlLC6mNeGthuwq1KLrw6j4B6Uk3VK0zE1mWdqVfVZZH3SuzM/KEGK86XlTg== + dependencies: + "@storybook/addons" "6.1.20" + "@storybook/client-logger" "6.1.20" + "@storybook/csf" "0.0.1" + core-js "^3.0.1" + estraverse "^4.2.0" + global "^4.3.2" + loader-utils "^2.0.0" + lodash "^4.17.15" + prettier "~2.0.5" + regenerator-runtime "^0.13.7" + source-map "^0.7.3" + +"@storybook/theming@6.1.20", "@storybook/theming@^6.1.20": + version "6.1.20" + resolved "https://registry.yarnpkg.com/@storybook/theming/-/theming-6.1.20.tgz#ed0b330a5c08bbe998e9df95e615f0e84a8d663f" + integrity sha512-yg56fa4uhXs+oNmwSHw/jAt1sWpAfq2k6aP1FOtWiEI372g7ZYddP/0ENoj07R+8jZxkvafLNhMI20aIxXpvTQ== dependencies: "@emotion/core" "^10.1.1" "@emotion/is-prop-valid" "^0.8.6" "@emotion/styled" "^10.0.23" - "@storybook/client-logger" "6.1.16" + "@storybook/client-logger" "6.1.20" core-js "^3.0.1" deep-object-diff "^1.1.0" emotion-theming "^10.0.19" @@ -2882,21 +3255,21 @@ resolve-from "^5.0.0" ts-dedent "^2.0.0" -"@storybook/ui@6.1.16": - version "6.1.16" - resolved "https://registry.yarnpkg.com/@storybook/ui/-/ui-6.1.16.tgz#b6e5615bdc89eba384f192df203da83c19b1be8a" - integrity sha512-6A45rCFhQTSIWmrlDpGFdT7SD6cUbFpmEKFLTeiPJWZJ4L+pqRxKh6J8X3QMeaD7GiacxiAfSdzk15jNxo+Qog== +"@storybook/ui@6.1.20": + version "6.1.20" + resolved "https://registry.yarnpkg.com/@storybook/ui/-/ui-6.1.20.tgz#ba585e2f600257e9168e8e5cb704c63593daeb69" + integrity sha512-XKsSgPjoThIzyxltJercXWRFErF99qOVJWYYCZ6/K0WuYHR4wncRPwN9/ur7BboWFJGWlCJll7fredFAmidP+g== dependencies: "@emotion/core" "^10.1.1" - "@storybook/addons" "6.1.16" - "@storybook/api" "6.1.16" - "@storybook/channels" "6.1.16" - "@storybook/client-logger" "6.1.16" - "@storybook/components" "6.1.16" - "@storybook/core-events" "6.1.16" - "@storybook/router" "6.1.16" + "@storybook/addons" "6.1.20" + "@storybook/api" "6.1.20" + "@storybook/channels" "6.1.20" + "@storybook/client-logger" "6.1.20" + "@storybook/components" "6.1.20" + "@storybook/core-events" "6.1.20" + "@storybook/router" "6.1.20" "@storybook/semver" "^7.3.2" - "@storybook/theming" "6.1.16" + "@storybook/theming" "6.1.20" "@types/markdown-to-jsx" "^6.11.0" copy-to-clipboard "^3.0.8" core-js "^3.0.1" @@ -3201,6 +3574,13 @@ dependencies: "@types/react" "*" +"@types/mdast@^3.0.0": + version "3.0.3" + resolved "https://registry.yarnpkg.com/@types/mdast/-/mdast-3.0.3.tgz#2d7d671b1cd1ea3deb306ea75036c2a0407d2deb" + integrity sha512-SXPBMnFVQg1s00dlMCc/jCdvPqdE4mXaMMCeRlxLDmTAEoegHT53xKtkDnzDTOcmMHUfcjyf36/YYZ6SxRdnsw== + dependencies: + "@types/unist" "*" + "@types/micromatch@^4.0.1": version "4.0.1" resolved "https://registry.yarnpkg.com/@types/micromatch/-/micromatch-4.0.1.tgz#9381449dd659fc3823fd2a4190ceacc985083bc7" @@ -3251,6 +3631,11 @@ resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.0.tgz#2f8bb441434d163b35fb8ffdccd7138927ffb8c0" integrity sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA== +"@types/parse5@^5.0.0": + version "5.0.3" + resolved "https://registry.yarnpkg.com/@types/parse5/-/parse5-5.0.3.tgz#e7b5aebbac150f8b5fdd4a46e7f0bd8e65e19109" + integrity sha512-kUNnecmtkunAoQ3CnjmMkzNU/gtxG8guhi+Fk2U/kOpIKjIMKnXGp4IJCgQJrXSgMsWYimYG4TGjz/UzbGEBTw== + "@types/prettier@^2.0.0": version "2.1.5" resolved "https://registry.yarnpkg.com/@types/prettier/-/prettier-2.1.5.tgz#b6ab3bba29e16b821d84e09ecfaded462b816b00" @@ -3352,7 +3737,7 @@ dependencies: source-map "^0.6.1" -"@types/unist@*": +"@types/unist@*", "@types/unist@^2.0.0", "@types/unist@^2.0.2", "@types/unist@^2.0.3": version "2.0.3" resolved "https://registry.yarnpkg.com/@types/unist/-/unist-2.0.3.tgz#9c088679876f374eb5983f150d4787aa6fb32d7e" integrity sha512-FvUupuM3rlRsRtCN+fDudtmytGO6iHJuuRKS1Ss0pG5z8oX0diNEw94UEL7hgDbpN94rgaK5R7sWm6RrSkZuAQ== @@ -3752,7 +4137,7 @@ acorn-import-meta@^1.0.0: resolved "https://registry.yarnpkg.com/acorn-import-meta/-/acorn-import-meta-1.1.0.tgz#c384423462ee7d4721d4de83231021a36cb09def" integrity sha512-pshgiVR5mhpjFVdizKTN+kAGRqjJFUOEB3TvpQ6kiAutb1lvHrIVVcGoe5xzMpJkVNifCeymMG7/tsDkWn8CdQ== -acorn-jsx@^5.2.0: +acorn-jsx@^5.1.0, acorn-jsx@^5.2.0: version "5.3.1" resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.1.tgz#fc8661e11b7ac1539c47dbfea2e72b3af34d267b" integrity sha512-K0Ptm/47OKfQRpNQ2J/oIN/3QYiK6FwW+eJbILhsdxh2WTLdl+30o8aGdTbm5JbffpFFAg/g+zi1E+jvJha5ng== @@ -4414,7 +4799,7 @@ aws4@^1.8.0: resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.11.0.tgz#d61f46d83b2519250e2784daf5b09479a8b41c59" integrity sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA== -axe-core@^4.0.2: +axe-core@^4.0.1, axe-core@^4.0.2: version "4.1.1" resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-4.1.1.tgz#70a7855888e287f7add66002211a423937063eaf" integrity sha512-5Kgy8Cz6LPC9DJcNb3yjAXTu3XihQgEdnIg50c//zOC/MyLP0Clg+Y8Sh9ZjjnvBrDZU4DgXS9C3T9r4/scGZQ== @@ -4559,6 +4944,14 @@ babel-plugin-add-react-displayname@^0.0.5: resolved "https://registry.yarnpkg.com/babel-plugin-add-react-displayname/-/babel-plugin-add-react-displayname-0.0.5.tgz#339d4cddb7b65fd62d1df9db9fe04de134122bd5" integrity sha1-M51M3be2X9YtHfnbn+BN4TQSK9U= +babel-plugin-apply-mdx-type-prop@1.6.22: + version "1.6.22" + resolved "https://registry.yarnpkg.com/babel-plugin-apply-mdx-type-prop/-/babel-plugin-apply-mdx-type-prop-1.6.22.tgz#d216e8fd0de91de3f1478ef3231e05446bc8705b" + integrity sha512-VefL+8o+F/DfK24lPZMtJctrCVOfgbqLAGZSkxwhazQv4VxPg3Za/i40fu22KR2m8eEda+IfSOlPLUSIiLcnCQ== + dependencies: + "@babel/helper-plugin-utils" "7.10.4" + "@mdx-js/util" "1.6.22" + babel-plugin-dynamic-import-node@^2.3.3: version "2.3.3" resolved "https://registry.yarnpkg.com/babel-plugin-dynamic-import-node/-/babel-plugin-dynamic-import-node-2.3.3.tgz#84fda19c976ec5c6defef57f9427b3def66e17a3" @@ -4582,6 +4975,13 @@ babel-plugin-emotion@^10.0.20, babel-plugin-emotion@^10.0.27: find-root "^1.1.0" source-map "^0.5.7" +babel-plugin-extract-import-names@1.6.22: + version "1.6.22" + resolved "https://registry.yarnpkg.com/babel-plugin-extract-import-names/-/babel-plugin-extract-import-names-1.6.22.tgz#de5f9a28eb12f3eb2578bf74472204e66d1a13dc" + integrity sha512-yJ9BsJaISua7d8zNT7oRG1ZLBJCIdZ4PZqmH8qa9N5AK01ifk3fnkc98AXhtzE7UkfCsEumvoQWgoYLhOnJ7jQ== + dependencies: + "@babel/helper-plugin-utils" "7.10.4" + babel-plugin-istanbul@^5.1.0: version "5.2.0" resolved "https://registry.yarnpkg.com/babel-plugin-istanbul/-/babel-plugin-istanbul-5.2.0.tgz#df4ade83d897a92df069c4d9a25cf2671293c854" @@ -4913,6 +5313,11 @@ babylon@~5.8.3: resolved "https://registry.yarnpkg.com/babylon/-/babylon-5.8.38.tgz#ec9b120b11bf6ccd4173a18bf217e60b79859ffd" integrity sha1-7JsSCxG/bM1Bc6GL8hfmC3mFn/0= +bail@^1.0.0: + version "1.0.5" + resolved "https://registry.yarnpkg.com/bail/-/bail-1.0.5.tgz#b6fa133404a392cbc1f8c4bf63f5953351e7a776" + integrity sha512-xFbRxM1tahm08yHBP16MMjVUAvDaBMD38zsM9EMAUN61omwLmKlOpB/Zku5QkjZ8TZ4vn53pj+t518cH0S03RQ== + balanced-match@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" @@ -5589,6 +5994,11 @@ camel-case@^4.1.1: pascal-case "^3.1.2" tslib "^2.0.3" +camelcase-css@2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/camelcase-css/-/camelcase-css-2.0.1.tgz#ee978f6947914cc30c6b44741b6ed1df7f043fd5" + integrity sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA== + camelcase-keys@^6.2.2: version "6.2.2" resolved "https://registry.yarnpkg.com/camelcase-keys/-/camelcase-keys-6.2.2.tgz#5e755d6ba51aa223ec7d3d52f25778210f9dc3c0" @@ -5647,6 +6057,11 @@ catharsis@^0.8.11: dependencies: lodash "^4.17.14" +ccount@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/ccount/-/ccount-1.1.0.tgz#246687debb6014735131be8abab2d93898f8d043" + integrity sha512-vlNK021QdI7PNeiUh/lKkC/mNHHfV0m/Ad5JoI0TYtlBnJAslM/JIkm/tGC88bkLIwO6OQ5uV6ztS6kVAtCDlg== + chai@^4.1.2: version "4.2.0" resolved "https://registry.yarnpkg.com/chai/-/chai-4.2.0.tgz#760aa72cf20e3795e84b12877ce0e83737aa29e5" @@ -6012,6 +6427,11 @@ coffeescript@1.12.7: resolved "https://registry.yarnpkg.com/coffeescript/-/coffeescript-1.12.7.tgz#e57ee4c4867cf7f606bfc4a0f2d550c0981ddd27" integrity sha512-pLXHFxQMPklVoEekowk8b3erNynC+DVJzChxS/LCBBgR6/8AJkHivkm//zbowcfc7BTCAjryuhx6gPqPRfsFoA== +collapse-white-space@^1.0.2: + version "1.0.6" + resolved "https://registry.yarnpkg.com/collapse-white-space/-/collapse-white-space-1.0.6.tgz#e63629c0016665792060dbbeb79c42239d2c5287" + integrity sha512-jEovNnrhMuqyCcjfEJA56v0Xq8SkIoPKDyaHahwo3POf4qcSXqMYuwNcOTzp74vTsR9Tn08z4MxWqAhcekogkQ== + collect-all@^1.0.3: version "1.0.4" resolved "https://registry.yarnpkg.com/collect-all/-/collect-all-1.0.4.tgz#50cd7119ac24b8e12a661f0f8c3aa0ea7222ddfc" @@ -7326,6 +7746,13 @@ destroy@~1.0.4: resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.0.4.tgz#978857442c44749e4206613e37946205826abd80" integrity sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA= +detab@2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/detab/-/detab-2.0.4.tgz#b927892069aff405fbb9a186fe97a44a92a94b43" + integrity sha512-8zdsQA5bIkoRECvCrNKPla84lyoR7DSAyf7p0YgXzBO9PDJx8KntPUay7NS6yp+KdxdVtiE5SpHKtbp2ZQyA9g== + dependencies: + repeat-string "^1.5.4" + detect-newline@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/detect-newline/-/detect-newline-2.1.0.tgz#f41f1c10be4b00e87b5f13da680759f2c5bfd3e2" @@ -7731,6 +8158,11 @@ emittery@^0.7.1: resolved "https://registry.yarnpkg.com/emittery/-/emittery-0.7.2.tgz#25595908e13af0f5674ab419396e2fb394cdfa82" integrity sha512-A8OG5SR/ij3SsJdWDJdkkSYUjQdCUx6APQXem0SaEePBSRg4eymGYwBkKo1Y6DU+af/Jn2dBQqDBvjnr9Vi8nQ== +"emoji-regex@>=6.0.0 <=6.1.1": + version "6.1.1" + resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-6.1.1.tgz#c6cd0ec1b0642e2a3c67a1137efc5e796da4f88e" + integrity sha1-xs0OwbBkLio8Z6ETfvxeeW2k+I4= + emoji-regex@^7.0.1: version "7.0.3" resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-7.0.3.tgz#933a04052860c85e83c122479c4748a8e4c72156" @@ -8047,7 +8479,7 @@ escape-string-regexp@^1.0.2, escape-string-regexp@^1.0.5: resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ= -escodegen@^1.14.1, escodegen@^1.9.1: +escodegen@^1.12.0, escodegen@^1.14.1, escodegen@^1.9.1: version "1.14.3" resolved "https://registry.yarnpkg.com/escodegen/-/escodegen-1.14.3.tgz#4e7b81fba61581dc97582ed78cab7f0e8d63f503" integrity sha512-qFcX0XJkdg+PB3xjZZG/wKSuT1PnQWx57+TVSjIMmILd2yC/6ByYElPwJnslDsuWuSAp4AwJGumarAAmJch5Kw== @@ -9295,6 +9727,13 @@ git-raw-commits@^2.0.0: split2 "^2.0.0" through2 "^4.0.0" +github-slugger@^1.0.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/github-slugger/-/github-slugger-1.3.0.tgz#9bd0a95c5efdfc46005e82a906ef8e2a059124c9" + integrity sha512-gwJScWVNhFYSRDvURk/8yhcFBee6aFjye2a7Lhb2bUyRulpIoek9p0I9Kt7PT67d/nUlZbFu8L9RLiA0woQN8Q== + dependencies: + emoji-regex ">=6.0.0 <=6.1.1" + glob-base@^0.3.0: version "0.3.0" resolved "https://registry.yarnpkg.com/glob-base/-/glob-base-0.3.0.tgz#dbb164f6221b1c0b1ccf82aea328b497df0ea3c4" @@ -9681,11 +10120,63 @@ hash.js@^1.0.0, hash.js@^1.0.3: inherits "^2.0.3" minimalistic-assert "^1.0.1" +hast-to-hyperscript@^9.0.0: + version "9.0.1" + resolved "https://registry.yarnpkg.com/hast-to-hyperscript/-/hast-to-hyperscript-9.0.1.tgz#9b67fd188e4c81e8ad66f803855334173920218d" + integrity sha512-zQgLKqF+O2F72S1aa4y2ivxzSlko3MAvxkwG8ehGmNiqd98BIN3JM1rAJPmplEyLmGLO2QZYJtIneOSZ2YbJuA== + dependencies: + "@types/unist" "^2.0.3" + comma-separated-tokens "^1.0.0" + property-information "^5.3.0" + space-separated-tokens "^1.0.0" + style-to-object "^0.3.0" + unist-util-is "^4.0.0" + web-namespaces "^1.0.0" + +hast-util-from-parse5@^6.0.0: + version "6.0.1" + resolved "https://registry.yarnpkg.com/hast-util-from-parse5/-/hast-util-from-parse5-6.0.1.tgz#554e34abdeea25ac76f5bd950a1f0180e0b3bc2a" + integrity sha512-jeJUWiN5pSxW12Rh01smtVkZgZr33wBokLzKLwinYOUfSzm1Nl/c3GUGebDyOKjdsRgMvoVbV0VpAcpjF4NrJA== + dependencies: + "@types/parse5" "^5.0.0" + hastscript "^6.0.0" + property-information "^5.0.0" + vfile "^4.0.0" + vfile-location "^3.2.0" + web-namespaces "^1.0.0" + hast-util-parse-selector@^2.0.0: version "2.2.5" resolved "https://registry.yarnpkg.com/hast-util-parse-selector/-/hast-util-parse-selector-2.2.5.tgz#d57c23f4da16ae3c63b3b6ca4616683313499c3a" integrity sha512-7j6mrk/qqkSehsM92wQjdIgWM2/BW61u/53G6xmC8i1OmEdKLHbk419QKQUjz6LglWsfqoiHmyMRkP1BGjecNQ== +hast-util-raw@6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/hast-util-raw/-/hast-util-raw-6.0.1.tgz#973b15930b7529a7b66984c98148b46526885977" + integrity sha512-ZMuiYA+UF7BXBtsTBNcLBF5HzXzkyE6MLzJnL605LKE8GJylNjGc4jjxazAHUtcwT5/CEt6afRKViYB4X66dig== + dependencies: + "@types/hast" "^2.0.0" + hast-util-from-parse5 "^6.0.0" + hast-util-to-parse5 "^6.0.0" + html-void-elements "^1.0.0" + parse5 "^6.0.0" + unist-util-position "^3.0.0" + vfile "^4.0.0" + web-namespaces "^1.0.0" + xtend "^4.0.0" + zwitch "^1.0.0" + +hast-util-to-parse5@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/hast-util-to-parse5/-/hast-util-to-parse5-6.0.0.tgz#1ec44650b631d72952066cea9b1445df699f8479" + integrity sha512-Lu5m6Lgm/fWuz8eWnrKezHtVY83JeRGaNQ2kn9aJgqaxvVkFCZQBEhgodZUDUvoodgyROHDb3r5IxAEdl6suJQ== + dependencies: + hast-to-hyperscript "^9.0.0" + property-information "^5.0.0" + web-namespaces "^1.0.0" + xtend "^4.0.0" + zwitch "^1.0.0" + hastscript@^6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/hastscript/-/hastscript-6.0.0.tgz#e8768d7eac56c3fdeac8a92830d58e811e5bf640" @@ -9814,6 +10305,16 @@ html-minifier-terser@^5.0.1: relateurl "^0.2.7" terser "^4.6.3" +html-tags@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/html-tags/-/html-tags-3.1.0.tgz#7b5e6f7e665e9fb41f30007ed9e0d41e97fb2140" + integrity sha512-1qYz89hW3lFDEazhjW0yVAV87lw8lVkrJocr72XmBkMKsoSVJCQx3W8BXsC7hO2qAt8BoVjYjtAcZ9perqGnNg== + +html-void-elements@^1.0.0: + version "1.0.5" + resolved "https://registry.yarnpkg.com/html-void-elements/-/html-void-elements-1.0.5.tgz#ce9159494e86d95e45795b166c2021c2cfca4483" + integrity sha512-uE/TxKuyNIcx44cIWnjr/rfIATDH7ZaOMmstu0CwhFG1Dunhlp4OC6/NMbhiwoq5BpW0ubi303qnEk/PZj614w== + html-webpack-plugin@4.5.0, html-webpack-plugin@^4.2.1: version "4.5.0" resolved "https://registry.yarnpkg.com/html-webpack-plugin/-/html-webpack-plugin-4.5.0.tgz#625097650886b97ea5dae331c320e3238f6c121c" @@ -10075,6 +10576,11 @@ immer@7.0.9: resolved "https://registry.yarnpkg.com/immer/-/immer-7.0.9.tgz#28e7552c21d39dd76feccd2b800b7bc86ee4a62e" integrity sha512-Vs/gxoM4DqNAYR7pugIxi0Xc8XAun/uy7AQu4fLLqaTBHxjOP9pJ266Q9MWA/ly4z6rAFZbvViOtihxUZ7O28A== +immer@8.0.1: + version "8.0.1" + resolved "https://registry.yarnpkg.com/immer/-/immer-8.0.1.tgz#9c73db683e2b3975c424fb0572af5889877ae656" + integrity sha512-aqXhGP7//Gui2+UrEtvxZxSquQVXTpZ7KDxfCcKAF3Vysvw0CViVaW9RZ1j1xlIYqaaaipBoqdqeibkc18PNvA== + import-cwd@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/import-cwd/-/import-cwd-2.1.0.tgz#aa6cf36e722761285cb371ec6519f53e2435b0a9" @@ -10173,7 +10679,7 @@ inflight@^1.0.4: once "^1.3.0" wrappy "1" -inherits@2, inherits@2.0.4, inherits@^2.0.1, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.1, inherits@~2.0.3: +inherits@2, inherits@2.0.4, inherits@^2.0.0, inherits@^2.0.1, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.1, inherits@~2.0.3: version "2.0.4" resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== @@ -10200,6 +10706,11 @@ inline-source-map@~0.6.0: dependencies: source-map "~0.5.3" +inline-style-parser@0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/inline-style-parser/-/inline-style-parser-0.1.1.tgz#ec8a3b429274e9c0a1f1c4ffa9453a7fef72cea1" + integrity sha512-7NXolsK4CAS5+xvdj5OMMbI962hU/wvwoxk+LWR9Ek9bVtyuuYScDN6eS0rUm6TxApFpw7CX1o4uJzcd4AyD3Q== + inquirer@7.0.4: version "7.0.4" resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-7.0.4.tgz#99af5bde47153abca23f5c7fc30db247f39da703" @@ -10308,7 +10819,7 @@ is-absolute-url@^2.0.0: resolved "https://registry.yarnpkg.com/is-absolute-url/-/is-absolute-url-2.1.0.tgz#50530dfb84fcc9aa7dbe7852e83a37b93b9f2aa6" integrity sha1-UFMN+4T8yap9vnhS6Do3uTufKqY= -is-absolute-url@^3.0.3: +is-absolute-url@^3.0.0, is-absolute-url@^3.0.3: version "3.0.3" resolved "https://registry.yarnpkg.com/is-absolute-url/-/is-absolute-url-3.0.3.tgz#96c6a22b6a23929b11ea0afb1836c36ad4a5d698" integrity sha512-opmNIX7uFnS96NtPmhWQgQx6/NYFgsUXYMllcfzwWKUMwfo8kku1TvE6hkNcH+Q1ts5cMVrsY7j0bxXQDciu9Q== @@ -10335,7 +10846,7 @@ is-accessor-descriptor@^1.0.0: dependencies: kind-of "^6.0.0" -is-alphabetical@^1.0.0: +is-alphabetical@1.0.4, is-alphabetical@^1.0.0: version "1.0.4" resolved "https://registry.yarnpkg.com/is-alphabetical/-/is-alphabetical-1.0.4.tgz#9e7d6b94916be22153745d184c298cbf986a686d" integrity sha512-DwzsA04LQ10FHTZuL0/grVDk4rFoVH1pjAToYwBrHSxcrBIGQuXrQMtD5U1b0U2XVgKZCTLLP8u2Qxqhy3l2Vg== @@ -10387,6 +10898,11 @@ is-buffer@^1.1.0, is-buffer@^1.1.5: resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be" integrity sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w== +is-buffer@^2.0.0: + version "2.0.5" + resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-2.0.5.tgz#ebc252e400d22ff8d77fa09888821a24a658c191" + integrity sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ== + is-callable@^1.1.4, is-callable@^1.1.5, is-callable@^1.2.2: version "1.2.2" resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.2.tgz#c7c6715cd22d4ddb48d3e19970223aceabb080d9" @@ -10470,6 +10986,14 @@ is-docker@^2.0.0: resolved "https://registry.yarnpkg.com/is-docker/-/is-docker-2.1.1.tgz#4125a88e44e450d384e09047ede71adc2d144156" integrity sha512-ZOoqiXfEwtGknTiuDEy8pN2CfE3TxMHprvNer1mXiqwkOT77Rw3YVrUQ52EqAOU3QAWDQ+bQdx7HJzrv7LS2Hw== +is-dom@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/is-dom/-/is-dom-1.1.0.tgz#af1fced292742443bb59ca3f76ab5e80907b4e8a" + integrity sha512-u82f6mvhYxRPKpw8V1N0W8ce1xXwOrQtgGcxl6UCL5zBmZu3is/18K0rR7uFCnMDuAsS/3W54mGL4vsaFUQlEQ== + dependencies: + is-object "^1.0.1" + is-window "^1.0.2" + is-extendable@^0.1.0, is-extendable@^0.1.1: version "0.1.1" resolved "https://registry.yarnpkg.com/is-extendable/-/is-extendable-0.1.1.tgz#62b110e289a471418e3ec36a617d472e301dfc89" @@ -10618,6 +11142,11 @@ is-obj@^2.0.0: resolved "https://registry.yarnpkg.com/is-obj/-/is-obj-2.0.0.tgz#473fb05d973705e3fd9620545018ca8e22ef4982" integrity sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w== +is-object@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-object/-/is-object-1.0.2.tgz#a56552e1c665c9e950b4a025461da87e72f86fcf" + integrity sha512-2rRIahhZr2UWb45fIOuvZGpFtz0TyOZLf32KxBbSoUCeZR495zCKlWUKKUByk3geS2eAs7ZAABt0Y/Rx0GiQGA== + is-observable@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/is-observable/-/is-observable-1.1.0.tgz#b3e986c8f44de950867cab5403f5a3465005975e" @@ -10661,6 +11190,11 @@ is-plain-obj@^1.0.0, is-plain-obj@^1.1.0: resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-1.1.0.tgz#71a50c8429dfca773c92a390a4a03b39fcd51d3e" integrity sha1-caUMhCnfync8kqOQpKA7OfzVHT4= +is-plain-obj@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-2.1.0.tgz#45e42e37fccf1f40da8e5f76ee21515840c09287" + integrity sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA== + is-plain-object@3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-3.0.1.tgz#662d92d24c0aa4302407b0d45d21f2251c85f85b" @@ -10787,11 +11321,26 @@ is-valid-glob@^1.0.0: resolved "https://registry.yarnpkg.com/is-valid-glob/-/is-valid-glob-1.0.0.tgz#29bf3eff701be2d4d315dbacc39bc39fe8f601aa" integrity sha1-Kb8+/3Ab4tTTFdusw5vDn+j2Aao= +is-whitespace-character@^1.0.0: + version "1.0.4" + resolved "https://registry.yarnpkg.com/is-whitespace-character/-/is-whitespace-character-1.0.4.tgz#0858edd94a95594c7c9dd0b5c174ec6e45ee4aa7" + integrity sha512-SDweEzfIZM0SJV0EUga669UTKlmL0Pq8Lno0QDQsPnvECB3IM2aP0gdx5TrU0A01MAPfViaZiI2V1QMZLaKK5w== + +is-window@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-window/-/is-window-1.0.2.tgz#2c896ca53db97de45d3c33133a65d8c9f563480d" + integrity sha1-LIlspT25feRdPDMTOmXYyfVjSA0= + is-windows@^1.0.1, is-windows@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-1.0.2.tgz#d1850eb9791ecd18e6182ce12a30f396634bb19d" integrity sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA== +is-word-character@^1.0.0: + version "1.0.4" + resolved "https://registry.yarnpkg.com/is-word-character/-/is-word-character-1.0.4.tgz#ce0e73216f98599060592f62ff31354ddbeb0230" + integrity sha512-5SMO8RVennx3nZrqtKwCGyyetPE9VDba5ugvKLaD4KopPG5kR4mQ7tNt/r7feL5yt5h3lpuBbIUmCOG2eSzXHA== + is-wsl@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-1.1.0.tgz#1f16e4aa22b04d1336b66188a66af3c600c3a66d" @@ -12480,7 +13029,7 @@ lodash.union@^4.6.0: resolved "https://registry.yarnpkg.com/lodash.union/-/lodash.union-4.6.0.tgz#48bb5088409f16f1821666641c44dd1aaae3cd88" integrity sha1-SLtQiECfFvGCFmZkHETdGqrjzYg= -lodash.uniq@^4.5.0: +lodash.uniq@4.5.0, lodash.uniq@^4.5.0: version "4.5.0" resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773" integrity sha1-0CJTc662Uq3BvILklFM5qEJ1R3M= @@ -12660,6 +13209,11 @@ map-visit@^1.0.0: dependencies: object-visit "^1.0.0" +markdown-escapes@^1.0.0: + version "1.0.4" + resolved "https://registry.yarnpkg.com/markdown-escapes/-/markdown-escapes-1.0.4.tgz#c95415ef451499d7602b91095f3c8e8975f78535" + integrity sha512-8z4efJYk43E0upd0NbVXwgSTQs6cT3T06etieCMEg7dRbzCbxUCK/GHlX8mhHRDcp+OLlHkPKsvqQTCvsRl2cg== + markdown-it-anchor@^5.2.7: version "5.3.0" resolved "https://registry.yarnpkg.com/markdown-it-anchor/-/markdown-it-anchor-5.3.0.tgz#d549acd64856a8ecd1bea58365ef385effbac744" @@ -12713,6 +13267,46 @@ md5.js@^1.3.4: inherits "^2.0.1" safe-buffer "^5.1.2" +mdast-squeeze-paragraphs@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/mdast-squeeze-paragraphs/-/mdast-squeeze-paragraphs-4.0.0.tgz#7c4c114679c3bee27ef10b58e2e015be79f1ef97" + integrity sha512-zxdPn69hkQ1rm4J+2Cs2j6wDEv7O17TfXTJ33tl/+JPIoEmtV9t2ZzBM5LPHE8QlHsmVD8t3vPKCyY3oH+H8MQ== + dependencies: + unist-util-remove "^2.0.0" + +mdast-util-definitions@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/mdast-util-definitions/-/mdast-util-definitions-2.0.1.tgz#2c931d8665a96670639f17f98e32c3afcfee25f3" + integrity sha512-Co+DQ6oZlUzvUR7JCpP249PcexxygiaKk9axJh+eRzHDZJk2julbIdKB4PXHVxdBuLzvJ1Izb+YDpj2deGMOuA== + dependencies: + unist-util-visit "^2.0.0" + +mdast-util-definitions@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/mdast-util-definitions/-/mdast-util-definitions-4.0.0.tgz#c5c1a84db799173b4dcf7643cda999e440c24db2" + integrity sha512-k8AJ6aNnUkB7IE+5azR9h81O5EQ/cTDXtWdMq9Kk5KcEW/8ritU5CeLg/9HhOC++nALHBlaogJ5jz0Ybk3kPMQ== + dependencies: + unist-util-visit "^2.0.0" + +mdast-util-to-hast@10.0.1: + version "10.0.1" + resolved "https://registry.yarnpkg.com/mdast-util-to-hast/-/mdast-util-to-hast-10.0.1.tgz#0cfc82089494c52d46eb0e3edb7a4eb2aea021eb" + integrity sha512-BW3LM9SEMnjf4HXXVApZMt8gLQWVNXc3jryK0nJu/rOXPOnlkUjmdkDlmxMirpbU9ILncGFIwLH/ubnWBbcdgA== + dependencies: + "@types/mdast" "^3.0.0" + "@types/unist" "^2.0.0" + mdast-util-definitions "^4.0.0" + mdurl "^1.0.0" + unist-builder "^2.0.0" + unist-util-generated "^1.0.0" + unist-util-position "^3.0.0" + unist-util-visit "^2.0.0" + +mdast-util-to-string@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/mdast-util-to-string/-/mdast-util-to-string-1.1.0.tgz#27055500103f51637bd07d01da01eb1967a43527" + integrity sha512-jVU0Nr2B9X3MU4tSK7JP1CMkSvOj7X5l/GboG1tKRw52lLF1x2Ju92Ms9tNetCcbfX3hzlM73zYo2NKkWSfF/A== + mdn-data@2.0.14: version "2.0.14" resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.0.14.tgz#7113fc4281917d63ce29b43446f701e68c25ba50" @@ -12723,7 +13317,7 @@ mdn-data@2.0.4: resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.0.4.tgz#699b3c38ac6f1d728091a64650b65d388502fd5b" integrity sha512-iV3XNKw06j5Q7mi6h+9vbx23Tv7JkjEVgKHW4pimwyDGWm0OIQntJJ+u1C6mg6mK1EaTv42XQ7w76yuzH7M2cA== -mdurl@^1.0.1: +mdurl@^1.0.0, mdurl@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/mdurl/-/mdurl-1.0.1.tgz#fe85b2ec75a59037f2adfec100fd6c601761152e" integrity sha1-/oWy7HWlkDfyrf7BAP1sYBdhFS4= @@ -13967,6 +14561,11 @@ parse5@^3.0.1: dependencies: "@types/node" "*" +parse5@^6.0.0: + version "6.0.1" + resolved "https://registry.yarnpkg.com/parse5/-/parse5-6.0.1.tgz#e1a1c085c569b3dc08321184f19a39cc27f7c30b" + integrity sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw== + parseurl@~1.3.2, parseurl@~1.3.3: version "1.3.3" resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4" @@ -14973,6 +15572,11 @@ prettier@^2.1.2: resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.2.1.tgz#795a1a78dd52f073da0cd42b21f9c91381923ff5" integrity sha512-PqyhM2yCjg/oKkFPtTGUojv7gnZAoG80ttl45O6x2Ug/rMJw4wcc9k6aaf2hibP7BGVCCM33gZoGjyvt9mm16Q== +prettier@~2.0.5: + version "2.0.5" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.0.5.tgz#d6d56282455243f2f92cc1716692c08aa31522d4" + integrity sha512-7PtVymN48hGcO4fGjybyBSIWDsLU4H4XlvOHfq91pz9kkGlonzwTfYkaIEwiRg/dAJF9YlbsduBAgtYLi+8cFg== + pretty-bytes@^5.3.0: version "5.4.1" resolved "https://registry.yarnpkg.com/pretty-bytes/-/pretty-bytes-5.4.1.tgz#cd89f79bbcef21e3d21eb0da68ffe93f803e884b" @@ -15097,7 +15701,7 @@ prop-types-exact@^1.2.0: object.assign "^4.1.0" reflect.ownkeys "^0.2.0" -prop-types@^15, prop-types@^15.5.10, prop-types@^15.6.0, prop-types@^15.6.1, prop-types@^15.6.2, prop-types@^15.7.2: +prop-types@^15, prop-types@^15.0.0, prop-types@^15.5.10, prop-types@^15.6.0, prop-types@^15.6.1, prop-types@^15.6.2, prop-types@^15.7.2: version "15.7.2" resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.7.2.tgz#52c41e75b8c87e72b9d9360e0206b99dcbffa6c5" integrity sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ== @@ -15106,7 +15710,7 @@ prop-types@^15, prop-types@^15.5.10, prop-types@^15.6.0, prop-types@^15.6.1, pro object-assign "^4.1.1" react-is "^16.8.1" -property-information@^5.0.0: +property-information@^5.0.0, property-information@^5.3.0: version "5.6.0" resolved "https://registry.yarnpkg.com/property-information/-/property-information-5.6.0.tgz#61675545fb23002f245c6540ec46077d4da3ed69" integrity sha512-YUHSPk+A30YPv+0Qf8i9Mbfe/C0hdPXk1s1jPVToV8pk8BQtpw10ct89Eo7OWkutrwqvT0eicAxlOg3dOAu8JA== @@ -15356,7 +15960,7 @@ react-color@^2.17.0: reactcss "^1.2.0" tinycolor2 "^1.4.1" -react-dev-utils@^10.0.0, react-dev-utils@^10.2.1: +react-dev-utils@^10.2.1: version "10.2.1" resolved "https://registry.yarnpkg.com/react-dev-utils/-/react-dev-utils-10.2.1.tgz#f6de325ae25fa4d546d09df4bb1befdc6dd19c19" integrity sha512-XxTbgJnYZmxuPtY3y/UV0D8/65NKkmaia4rXzViknVnZeVlklSh8u6TnaEYPfAi/Gh1TP4mEOXHI6jQOPbeakQ== @@ -15416,6 +16020,36 @@ react-dev-utils@^11.0.1: strip-ansi "6.0.0" text-table "0.2.0" +react-dev-utils@^11.0.3: + version "11.0.3" + resolved "https://registry.yarnpkg.com/react-dev-utils/-/react-dev-utils-11.0.3.tgz#b61ed499c7d74f447d4faddcc547e5e671e97c08" + integrity sha512-4lEA5gF4OHrcJLMUV1t+4XbNDiJbsAWCH5Z2uqlTqW6dD7Cf5nEASkeXrCI/Mz83sI2o527oBIFKVMXtRf1Vtg== + dependencies: + "@babel/code-frame" "7.10.4" + address "1.1.2" + browserslist "4.14.2" + chalk "2.4.2" + cross-spawn "7.0.3" + detect-port-alt "1.1.6" + escape-string-regexp "2.0.0" + filesize "6.1.0" + find-up "4.1.0" + fork-ts-checker-webpack-plugin "4.1.6" + global-modules "2.0.0" + globby "11.0.1" + gzip-size "5.1.1" + immer "8.0.1" + is-root "2.1.0" + loader-utils "2.0.0" + open "^7.0.2" + pkg-up "3.1.0" + prompts "2.4.0" + react-error-overlay "^6.0.9" + recursive-readdir "2.2.2" + shell-quote "1.7.2" + strip-ansi "6.0.0" + text-table "0.2.0" + react-docgen-typescript-loader@^3.7.2: version "3.7.2" resolved "https://registry.yarnpkg.com/react-docgen-typescript-loader/-/react-docgen-typescript-loader-3.7.2.tgz#45cb2305652c0602767242a8700ad1ebd66bbbbd" @@ -15510,6 +16144,11 @@ react-error-overlay@^6.0.7, react-error-overlay@^6.0.8: resolved "https://registry.yarnpkg.com/react-error-overlay/-/react-error-overlay-6.0.8.tgz#474ed11d04fc6bda3af643447d85e9127ed6b5de" integrity sha512-HvPuUQnLp5H7TouGq3kzBeioJmXms1wHy9EGjz2OURWBp4qZO6AfGEcnxts1D/CbwPLRAgTMPCEgYhA3sEM4vw== +react-error-overlay@^6.0.9: + version "6.0.9" + resolved "https://registry.yarnpkg.com/react-error-overlay/-/react-error-overlay-6.0.9.tgz#3c743010c9359608c375ecd6bc76f35d93995b0a" + integrity sha512-nQTTcUu+ATDbrSD1BZHr5kgSD4oF8OFjxun8uAaL8RwPBacGBNPf/yAuVVdx17N8XNzRDMrZ9XcKZHCjPW+9ew== + react-fast-compare@^3.0.1, react-fast-compare@^3.2.0: version "3.2.0" resolved "https://registry.yarnpkg.com/react-fast-compare/-/react-fast-compare-3.2.0.tgz#641a9da81b6a6320f270e89724fb45a0b39e43bb" @@ -15540,6 +16179,15 @@ react-hotkeys@2.0.0: dependencies: prop-types "^15.6.1" +react-inspector@^5.0.1: + version "5.1.0" + resolved "https://registry.yarnpkg.com/react-inspector/-/react-inspector-5.1.0.tgz#45a325e15f33e595be5356ca2d3ceffb7d6b8c3a" + integrity sha512-JAwswiengIcxi4X/Ssb8nf6suOuQsyit8Fxo04+iPKTnPNY3XIOuagjMZSzpJDDKkYcc/ARlySOYZZv626WUvA== + dependencies: + "@babel/runtime" "^7.0.0" + is-dom "^1.0.0" + prop-types "^15.0.0" + react-is@^16.13.1, react-is@^16.7.0, react-is@^16.8.1, react-is@^16.8.4, react-is@^16.8.6: version "16.13.1" resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" @@ -15643,7 +16291,7 @@ react-scripts@^4.0.0: optionalDependencies: fsevents "^2.1.3" -react-sizeme@^2.6.7: +react-sizeme@^2.5.2, react-sizeme@^2.6.7: version "2.6.12" resolved "https://registry.yarnpkg.com/react-sizeme/-/react-sizeme-2.6.12.tgz#ed207be5476f4a85bf364e92042520499455453e" integrity sha512-tL4sCgfmvapYRZ1FO2VmBmjPVzzqgHA7kI8lSJ6JS6L78jXFNRdOZFpXyK6P1NBZvKPPCZxReNgzZNUajAerZw== @@ -16012,6 +16660,74 @@ relateurl@^0.2.7: resolved "https://registry.yarnpkg.com/relateurl/-/relateurl-0.2.7.tgz#54dbf377e51440aca90a4cd274600d3ff2d888a9" integrity sha1-VNvzd+UUQKypCkzSdGANP/LYiKk= +remark-external-links@^6.0.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/remark-external-links/-/remark-external-links-6.1.0.tgz#1a545b3cf896eae00ec1732d90f595f75a329abe" + integrity sha512-dJr+vhe3wuh1+E9jltQ+efRMqtMDOOnfFkhtoArOmhnBcPQX6THttXMkc/H0kdnAvkXTk7f2QdOYm5qo/sGqdw== + dependencies: + extend "^3.0.0" + is-absolute-url "^3.0.0" + mdast-util-definitions "^2.0.0" + space-separated-tokens "^1.0.0" + unist-util-visit "^2.0.0" + +remark-footnotes@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/remark-footnotes/-/remark-footnotes-2.0.0.tgz#9001c4c2ffebba55695d2dd80ffb8b82f7e6303f" + integrity sha512-3Clt8ZMH75Ayjp9q4CorNeyjwIxHFcTkaektplKGl2A1jNGEUey8cKL0ZC5vJwfcD5GFGsNLImLG/NGzWIzoMQ== + +remark-mdx@1.6.22: + version "1.6.22" + resolved "https://registry.yarnpkg.com/remark-mdx/-/remark-mdx-1.6.22.tgz#06a8dab07dcfdd57f3373af7f86bd0e992108bbd" + integrity sha512-phMHBJgeV76uyFkH4rvzCftLfKCr2RZuF+/gmVcaKrpsihyzmhXjA0BEMDaPTXG5y8qZOKPVo83NAOX01LPnOQ== + dependencies: + "@babel/core" "7.12.9" + "@babel/helper-plugin-utils" "7.10.4" + "@babel/plugin-proposal-object-rest-spread" "7.12.1" + "@babel/plugin-syntax-jsx" "7.12.1" + "@mdx-js/util" "1.6.22" + is-alphabetical "1.0.4" + remark-parse "8.0.3" + unified "9.2.0" + +remark-parse@8.0.3: + version "8.0.3" + resolved "https://registry.yarnpkg.com/remark-parse/-/remark-parse-8.0.3.tgz#9c62aa3b35b79a486454c690472906075f40c7e1" + integrity sha512-E1K9+QLGgggHxCQtLt++uXltxEprmWzNfg+MxpfHsZlrddKzZ/hZyWHDbK3/Ap8HJQqYJRXP+jHczdL6q6i85Q== + dependencies: + ccount "^1.0.0" + collapse-white-space "^1.0.2" + is-alphabetical "^1.0.0" + is-decimal "^1.0.0" + is-whitespace-character "^1.0.0" + is-word-character "^1.0.0" + markdown-escapes "^1.0.0" + parse-entities "^2.0.0" + repeat-string "^1.5.4" + state-toggle "^1.0.0" + trim "0.0.1" + trim-trailing-lines "^1.0.0" + unherit "^1.0.4" + unist-util-remove-position "^2.0.0" + vfile-location "^3.0.0" + xtend "^4.0.1" + +remark-slug@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/remark-slug/-/remark-slug-6.0.0.tgz#2b54a14a7b50407a5e462ac2f376022cce263e2c" + integrity sha512-ln67v5BrGKHpETnm6z6adlJPhESFJwfuZZ3jrmi+lKTzeZxh2tzFzUfDD4Pm2hRGOarHLuGToO86MNMZ/hA67Q== + dependencies: + github-slugger "^1.0.0" + mdast-util-to-string "^1.0.0" + unist-util-visit "^2.0.0" + +remark-squeeze-paragraphs@4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/remark-squeeze-paragraphs/-/remark-squeeze-paragraphs-4.0.0.tgz#76eb0e085295131c84748c8e43810159c5653ead" + integrity sha512-8qRqmL9F4nuLPIgl92XUuxI3pFxize+F1H0e/W3llTk0UsjJaj01+RrirkMw7P21RKe4X6goQhYRSvNWX+70Rw== + dependencies: + mdast-squeeze-paragraphs "^4.0.0" + remove-bom-buffer@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/remove-bom-buffer/-/remove-bom-buffer-3.0.0.tgz#c2bf1e377520d324f623892e33c10cac2c252b53" @@ -16050,7 +16766,7 @@ repeat-element@^1.1.2: resolved "https://registry.yarnpkg.com/repeat-element/-/repeat-element-1.1.3.tgz#782e0d825c0c5a3bb39731f84efee6b742e6b1ce" integrity sha512-ahGq0ZnV5m5XtZLMb+vP76kcAM5nkLqk0lpqAuojSKGgQtn4eRi4ZZGm2olo2zKFH+sMsWaqOCW1dqAnOru72g== -repeat-string@^1.5.2, repeat-string@^1.6.1: +repeat-string@^1.5.2, repeat-string@^1.5.4, repeat-string@^1.6.1: version "1.6.1" resolved "https://registry.yarnpkg.com/repeat-string/-/repeat-string-1.6.1.tgz#8dcae470e1c88abc2d600fff4a776286da75e637" integrity sha1-jcrkcOHIirwtYA//Sndihtp15jc= @@ -17206,6 +17922,11 @@ stacktrace-js@^2.0.0: stack-generator "^2.0.5" stacktrace-gps "^3.0.4" +state-toggle@^1.0.0: + version "1.0.3" + resolved "https://registry.yarnpkg.com/state-toggle/-/state-toggle-1.0.3.tgz#e123b16a88e143139b09c6852221bc9815917dfe" + integrity sha512-d/5Z4/2iiCnHw6Xzghyhb+GcmF89bxwgXG60wjIiZaxnymbyOmI8Hk4VqHXiVVp6u2ysaskFfXg3ekCj4WNftQ== + static-extend@^0.1.1: version "0.1.2" resolved "https://registry.yarnpkg.com/static-extend/-/static-extend-0.1.2.tgz#60809c39cbff55337226fd5e0b520f341f1fb5c6" @@ -17229,10 +17950,10 @@ store2@^2.7.1: resolved "https://registry.yarnpkg.com/store2/-/store2-2.12.0.tgz#e1f1b7e1a59b6083b2596a8d067f6ee88fd4d3cf" integrity sha512-7t+/wpKLanLzSnQPX8WAcuLCCeuSHoWdQuh9SB3xD0kNOM38DNf+0Oa+wmvxmYueRzkmh6IcdKFtvTa+ecgPDw== -storybook-addon-jsx@^7.3.4: - version "7.3.4" - resolved "https://registry.yarnpkg.com/storybook-addon-jsx/-/storybook-addon-jsx-7.3.4.tgz#fdd0c47091fe51a86ac52c820bf9837ab45a6711" - integrity sha512-15bBSk4zl6yuIKWbJdQbl3MGbABzjhJf5cFx657nF8OmoOlXMpsz5+lxfc3c+AyRvUZxdJOXkvtRqBTmQ0mouQ== +storybook-addon-jsx@^7.3.6: + version "7.3.6" + resolved "https://registry.yarnpkg.com/storybook-addon-jsx/-/storybook-addon-jsx-7.3.6.tgz#ff51ec66d6af8323d64f7d4457f4da352bfc2aef" + integrity sha512-mbtelGqG5vSabRCEvz70dsdc1vDu/DMceb8pzCE6ng1Es5HAVubrKmvGpKM9g0o41FET7NThx0Fdz5ltIU07LA== dependencies: copy-to-clipboard "^3.0.8" js-beautify "^1.8.8" @@ -17557,6 +18278,13 @@ style-loader@1.3.0, style-loader@^1.2.1: loader-utils "^2.0.0" schema-utils "^2.7.0" +style-to-object@0.3.0, style-to-object@^0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/style-to-object/-/style-to-object-0.3.0.tgz#b1b790d205991cc783801967214979ee19a76e46" + integrity sha512-CzFnRRXhzWIdItT3OmF8SQfWyahHhjq3HwcMNCNLn+N7klOOqPjMeG/4JSu77D7ypZdGvSzvkrbyeTMizz2VrA== + dependencies: + inline-style-parser "0.1.1" + styled-jsx@^3.2.2, styled-jsx@^3.2.5, styled-jsx@^3.3.0: version "3.3.2" resolved "https://registry.yarnpkg.com/styled-jsx/-/styled-jsx-3.3.2.tgz#2474601a26670a6049fb4d3f94bd91695b3ce018" @@ -18189,6 +18917,21 @@ trim-off-newlines@^1.0.0: resolved "https://registry.yarnpkg.com/trim-off-newlines/-/trim-off-newlines-1.0.1.tgz#9f9ba9d9efa8764c387698bcbfeb2c848f11adb3" integrity sha1-n5up2e+odkw4dpi8v+sshI8RrbM= +trim-trailing-lines@^1.0.0: + version "1.1.4" + resolved "https://registry.yarnpkg.com/trim-trailing-lines/-/trim-trailing-lines-1.1.4.tgz#bd4abbec7cc880462f10b2c8b5ce1d8d1ec7c2c0" + integrity sha512-rjUWSqnfTNrjbB9NQWfPMH/xRK1deHeGsHoVfpxJ++XeYXE0d6B1En37AHfw3jtfTU7dzMzZL2jjpe8Qb5gLIQ== + +trim@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/trim/-/trim-0.0.1.tgz#5858547f6b290757ee95cccc666fb50084c460dd" + integrity sha1-WFhUf2spB1fulczMZm+1AITEYN0= + +trough@^1.0.0: + version "1.0.5" + resolved "https://registry.yarnpkg.com/trough/-/trough-1.0.5.tgz#b8b639cefad7d0bb2abd37d433ff8293efa5f406" + integrity sha512-rvuRbTarPXmMb79SmzEp8aqXNKcK+y0XaB298IXueQ8I2PsrATcPBCSPyK/dDNa2iWOhKlfNnOjdAOTBU/nkFA== + tryer@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/tryer/-/tryer-1.0.1.tgz#f2c85406800b9b0f74c9f7465b81eaad241252f8" @@ -18393,6 +19136,14 @@ unfetch@^4.1.0: resolved "https://registry.yarnpkg.com/unfetch/-/unfetch-4.2.0.tgz#7e21b0ef7d363d8d9af0fb929a5555f6ef97a3be" integrity sha512-F9p7yYCn6cIW9El1zi0HI6vqpeIvBsr3dSuRO6Xuppb1u5rXpCPmMvLSyECLhybr9isec8Ohl0hPekMVrEinDA== +unherit@^1.0.4: + version "1.1.3" + resolved "https://registry.yarnpkg.com/unherit/-/unherit-1.1.3.tgz#6c9b503f2b41b262330c80e91c8614abdaa69c22" + integrity sha512-Ft16BJcnapDKp0+J/rqFC3Rrk6Y/Ng4nzsC028k2jdDII/rdZ7Wd3pPT/6+vIIxRagwRc9K0IUX0Ra4fKvw+WQ== + dependencies: + inherits "^2.0.0" + xtend "^4.0.0" + unicode-canonical-property-names-ecmascript@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-1.0.4.tgz#2619800c4c825800efdd8343af7dd9933cbe2818" @@ -18416,6 +19167,18 @@ unicode-property-aliases-ecmascript@^1.0.4: resolved "https://registry.yarnpkg.com/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-1.1.0.tgz#dd57a99f6207bedff4628abefb94c50db941c8f4" integrity sha512-PqSoPh/pWetQ2phoj5RLiaqIk4kCNwoV3CI+LfGmWLKI3rE3kl1h59XpX2BjgDrmbxD9ARtQobPGU1SguCYuQg== +unified@9.2.0: + version "9.2.0" + resolved "https://registry.yarnpkg.com/unified/-/unified-9.2.0.tgz#67a62c627c40589edebbf60f53edfd4d822027f8" + integrity sha512-vx2Z0vY+a3YoTj8+pttM3tiJHCwY5UFbYdiWrwBEbHmK8pvsPj2rtAX2BFfgXen8T39CJWblWRDT4L5WGXtDdg== + dependencies: + bail "^1.0.0" + extend "^3.0.0" + is-buffer "^2.0.0" + is-plain-obj "^2.0.0" + trough "^1.0.0" + vfile "^4.0.0" + union-value@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/union-value/-/union-value-1.0.1.tgz#0b6fe7b835aecda61c6ea4d4f02c14221e109847" @@ -18465,6 +19228,64 @@ unique-string@^1.0.0: dependencies: crypto-random-string "^1.0.0" +unist-builder@2.0.3, unist-builder@^2.0.0: + version "2.0.3" + resolved "https://registry.yarnpkg.com/unist-builder/-/unist-builder-2.0.3.tgz#77648711b5d86af0942f334397a33c5e91516436" + integrity sha512-f98yt5pnlMWlzP539tPc4grGMsFaQQlP/vM396b00jngsiINumNmsY8rkXjfoi1c6QaM8nQ3vaGDuoKWbe/1Uw== + +unist-util-generated@^1.0.0: + version "1.1.6" + resolved "https://registry.yarnpkg.com/unist-util-generated/-/unist-util-generated-1.1.6.tgz#5ab51f689e2992a472beb1b35f2ce7ff2f324d4b" + integrity sha512-cln2Mm1/CZzN5ttGK7vkoGw+RZ8VcUH6BtGbq98DDtRGquAAOXig1mrBQYelOwMXYS8rK+vZDyyojSjp7JX+Lg== + +unist-util-is@^4.0.0: + version "4.0.4" + resolved "https://registry.yarnpkg.com/unist-util-is/-/unist-util-is-4.0.4.tgz#3e9e8de6af2eb0039a59f50c9b3e99698a924f50" + integrity sha512-3dF39j/u423v4BBQrk1AQ2Ve1FxY5W3JKwXxVFzBODQ6WEvccguhgp802qQLKSnxPODE6WuRZtV+ohlUg4meBA== + +unist-util-position@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/unist-util-position/-/unist-util-position-3.1.0.tgz#1c42ee6301f8d52f47d14f62bbdb796571fa2d47" + integrity sha512-w+PkwCbYSFw8vpgWD0v7zRCl1FpY3fjDSQ3/N/wNd9Ffa4gPi8+4keqt99N3XW6F99t/mUzp2xAhNmfKWp95QA== + +unist-util-remove-position@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/unist-util-remove-position/-/unist-util-remove-position-2.0.1.tgz#5d19ca79fdba712301999b2b73553ca8f3b352cc" + integrity sha512-fDZsLYIe2uT+oGFnuZmy73K6ZxOPG/Qcm+w7jbEjaFcJgbQ6cqjs/eSPzXhsmGpAsWPkqZM9pYjww5QTn3LHMA== + dependencies: + unist-util-visit "^2.0.0" + +unist-util-remove@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/unist-util-remove/-/unist-util-remove-2.0.1.tgz#fa13c424ff8e964f3aa20d1098b9a690c6bfaa39" + integrity sha512-YtuetK6o16CMfG+0u4nndsWpujgsHDHHLyE0yGpJLLn5xSjKeyGyzEBOI2XbmoUHCYabmNgX52uxlWoQhcvR7Q== + dependencies: + unist-util-is "^4.0.0" + +unist-util-stringify-position@^2.0.0: + version "2.0.3" + resolved "https://registry.yarnpkg.com/unist-util-stringify-position/-/unist-util-stringify-position-2.0.3.tgz#cce3bfa1cdf85ba7375d1d5b17bdc4cada9bd9da" + integrity sha512-3faScn5I+hy9VleOq/qNbAd6pAx7iH5jYBMS9I1HgQVijz/4mv5Bvw5iw1sC/90CODiKo81G/ps8AJrISn687g== + dependencies: + "@types/unist" "^2.0.2" + +unist-util-visit-parents@^3.0.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/unist-util-visit-parents/-/unist-util-visit-parents-3.1.1.tgz#65a6ce698f78a6b0f56aa0e88f13801886cdaef6" + integrity sha512-1KROIZWo6bcMrZEwiH2UrXDyalAa0uqzWCxCJj6lPOvTve2WkfgCytoDTPaMnodXh1WrXOq0haVYHj99ynJlsg== + dependencies: + "@types/unist" "^2.0.0" + unist-util-is "^4.0.0" + +unist-util-visit@2.0.3, unist-util-visit@^2.0.0: + version "2.0.3" + resolved "https://registry.yarnpkg.com/unist-util-visit/-/unist-util-visit-2.0.3.tgz#c3703893146df47203bb8a9795af47d7b971208c" + integrity sha512-iJ4/RczbJMkD0712mGktuGpm/U4By4FfDonL7N/9tATGIF4imikjOuagyMY53tnZq3NP6BcmlrHhEKAfGWjh7Q== + dependencies: + "@types/unist" "^2.0.0" + unist-util-is "^4.0.0" + unist-util-visit-parents "^3.0.0" + universalify@^0.1.0: version "0.1.2" resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66" @@ -18668,6 +19489,11 @@ uuid@^3.0.0, uuid@^3.1.0, uuid@^3.3.2, uuid@^3.4.0: resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee" integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A== +uuid@^8.0.0: + version "8.3.2" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" + integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg== + uuid@^8.3.0: version "8.3.1" resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.1.tgz#2ba2e6ca000da60fce5a196954ab241131e05a31" @@ -18719,11 +19545,29 @@ verror@1.10.0, verror@^1.9.0: core-util-is "1.0.2" extsprintf "^1.2.0" -vfile-location@^3.2.0: +vfile-location@^3.0.0, vfile-location@^3.2.0: version "3.2.0" resolved "https://registry.yarnpkg.com/vfile-location/-/vfile-location-3.2.0.tgz#d8e41fbcbd406063669ebf6c33d56ae8721d0f3c" integrity sha512-aLEIZKv/oxuCDZ8lkJGhuhztf/BW4M+iHdCwglA/eWc+vtuRFJj8EtgceYFX4LRjOhCAAiNHsKGssC6onJ+jbA== +vfile-message@^2.0.0: + version "2.0.4" + resolved "https://registry.yarnpkg.com/vfile-message/-/vfile-message-2.0.4.tgz#5b43b88171d409eae58477d13f23dd41d52c371a" + integrity sha512-DjssxRGkMvifUOJre00juHoP9DPWuzjxKuMDrhNbk2TdaYYBNMStsNhEOt3idrtI12VQYM/1+iM0KOzXi4pxwQ== + dependencies: + "@types/unist" "^2.0.0" + unist-util-stringify-position "^2.0.0" + +vfile@^4.0.0: + version "4.2.1" + resolved "https://registry.yarnpkg.com/vfile/-/vfile-4.2.1.tgz#03f1dce28fc625c625bc6514350fbdb00fa9e624" + integrity sha512-O6AE4OskCG5S1emQ/4gl8zK586RqA3srz3nfK/Viy0UPToBc5Trp9BVFb1u0CjsKrAWwnpr4ifM/KBXPWwJbCA== + dependencies: + "@types/unist" "^2.0.0" + is-buffer "^2.0.0" + unist-util-stringify-position "^2.0.0" + vfile-message "^2.0.0" + vinyl-fs@^3.0.1: version "3.0.3" resolved "https://registry.yarnpkg.com/vinyl-fs/-/vinyl-fs-3.0.3.tgz#c85849405f67428feabbbd5c5dbdd64f47d31bc7" @@ -18857,10 +19701,8 @@ watchpack@^1.7.4: resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-1.7.5.tgz#1267e6c55e0b9b5be44c2023aed5437a2c26c453" integrity sha512-9P3MWk6SrKjHsGkLT2KHXdQ/9SNkyoJbabxnKOoJepsvJjJG8uYTR3yTPxPQvNDI3w4Nz1xnE0TLHK4RIVe/MQ== dependencies: - chokidar "^3.4.1" graceful-fs "^4.1.2" neo-async "^2.5.0" - watchpack-chokidar2 "^2.0.1" optionalDependencies: chokidar "^3.4.1" watchpack-chokidar2 "^2.0.1" @@ -18872,6 +19714,11 @@ wbuf@^1.1.0, wbuf@^1.7.3: dependencies: minimalistic-assert "^1.0.0" +web-namespaces@^1.0.0: + version "1.1.4" + resolved "https://registry.yarnpkg.com/web-namespaces/-/web-namespaces-1.1.4.tgz#bc98a3de60dadd7faefc403d1076d529f5e030ec" + integrity sha512-wYxSGajtmoP4WxfejAPIr4l0fVh+jeMXZb08wNc0tMg6xsfZXj3cECqIK0G7ZAqUq0PP8WlMDtaOGVBTAWztNw== + webidl-conversions@^4.0.2: version "4.0.2" resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-4.0.2.tgz#a855980b1f0b6b359ba1d5d9fb39ae941faa63ad" @@ -19556,3 +20403,8 @@ zip-stream@^2.1.2: archiver-utils "^2.1.0" compress-commons "^2.1.1" readable-stream "^3.4.0" + +zwitch@^1.0.0: + version "1.0.5" + resolved "https://registry.yarnpkg.com/zwitch/-/zwitch-1.0.5.tgz#d11d7381ffed16b742f6af7b3f223d5cd9fe9920" + integrity sha512-V50KMwwzqJV0NpZIZFwfOD5/lyny3WlSzRiXgA0G7VUnRlqttta1L6UQIHzd6EuBY/cHGfwTIck7w1yH6Q5zUw==