diff --git a/.changeset/tough-coats-allow.md b/.changeset/tough-coats-allow.md new file mode 100644 index 00000000000..5b44c0537db --- /dev/null +++ b/.changeset/tough-coats-allow.md @@ -0,0 +1,5 @@ +--- +'@primer/react': patch +--- + +Fixes bugs in form components discovered while fixing/improving Storybook and docs. diff --git a/docs/content/Autocomplete.mdx b/docs/content/Autocomplete.mdx index 405d4b15d4f..ff8a08dfa84 100644 --- a/docs/content/Autocomplete.mdx +++ b/docs/content/Autocomplete.mdx @@ -4,7 +4,7 @@ title: Autocomplete status: Alpha description: Used to render a text input that allows a user to quickly filter through a list of options to pick one or more values. source: https://github.com/primer/react/tree/main/src/Autocomplete -storybook: '/react/storybook?path=/story/forms-autocomplete--single-select' +storybook: '/react/storybook?path=/story/form-controls-autocomplete--default' --- import {Autocomplete} from '@primer/react' @@ -56,7 +56,7 @@ A function may be passed to the `filterFn` prop if this default filtering behavi ```jsx live - Pick a branch + Pick a branch @@ -72,6 +72,7 @@ A function may be passed to the `filterFn` prop if this default filtering behavi {text: 'visual-design-tweaks', id: 7} ]} selectedItemIds={[]} + aria-labelledby="autocompleteLabel-basic" /> @@ -114,7 +115,7 @@ const CustomTextInputExample = () => { return ( - Pick options + Pick options @@ -132,6 +133,7 @@ const CustomTextInputExample = () => { selectedItemIds={selectedItemIds} onSelectedChange={onSelectedChange} selectionVariant="multiple" + aria-labelledby="autocompleteLabel-customInput" /> @@ -146,7 +148,7 @@ render() ```jsx live - Pick a branch + Pick a branch ) {text: 'visual-design-tweaks', id: 7} ]} selectedItemIds={[]} + aria-labelledby="autocompleteLabel-withoutOverlay" /> @@ -221,7 +224,7 @@ const CustomRenderedItemExample = () => { return ( - Pick labels + Pick labels { selectedItemIds={selectedItemIds} onSelectedChange={onSelectedChange} selectionVariant="multiple" - aria-labelledby="autocompleteLabel-issueLabels" + aria-labelledby="autocompleteLabel-customRenderedItem" /> @@ -279,9 +282,9 @@ const CustomSortAfterMenuClose = () => { return ( - Pick branches + Pick branches - + { return ( - Pick a branch + Pick a branch - + { )} > - Pick branches + + Pick branches + { ]} selectedItemIds={[]} customScrollContainerRef={scrollContainerRef} - aria-labelledby="autocompleteLabel" + aria-labelledby="autocompleteLabel-withCustomScrollRef" /> @@ -465,14 +470,14 @@ const MultiSelect = () => { return ( - Pick branches + Pick branches - + @@ -535,9 +540,9 @@ const MultiSelectAddNewItem = () => { return ( - Pick or add branches + Pick or add branches - + { selectedItemIds={selectedItemIds} onSelectedChange={onSelectedChange} selectionVariant="multiple" - aria-labelledby="autocompleteLabel" + aria-labelledby="autocompleteLabel-addItem" /> diff --git a/docs/content/Checkbox.mdx b/docs/content/Checkbox.mdx index 4cfe9901f8e..935b137176c 100644 --- a/docs/content/Checkbox.mdx +++ b/docs/content/Checkbox.mdx @@ -3,7 +3,7 @@ title: Checkbox description: Use checkboxes to toggle between checked and unchecked states in a list or as a standalone form field status: Alpha source: https://github.com/primer/react/blob/main/src/Checkbox.tsx -storybook: '/react/storybook?path=/story/forms-checkbox--default' +storybook: '/react/storybook?path=/story/form-controls-checkbox--default' componentId: checkbox --- @@ -87,15 +87,10 @@ An `indeterminate` checkbox state should be used if the input value is neither t type="boolean" description="Checks the input by default in uncontrolled mode" /> - } /> + + + Only used to inform ARIA attributes.
+ Individual checkboxes do not have validation styles. + + } + /> + + A unique value that is never shown to the user.
+ Used during form submission and to identify which checkbox inputs are selected. + + } + /> - + Creates a full width input element} + description="Creates a full width input element" /> + + Placeholder text to show when no option is selected.
+ This option is hidden from the dropdown menu when the 'required' prop is set + } + /> - + ) type="number" description="The number of tokens to display before truncating" /> + TextInput docs} + /> ### Adding and removing tokens @@ -464,6 +469,7 @@ There is no function that gets called to "add" a token, so the user needs to be fullTestCoverage: true, usedInProduction: false, usageExamplesDocumented: true, + hasStorybookStories: true, designReviewed: false, a11yReviewed: false, stableApi: false, diff --git a/docs/content/Textarea.mdx b/docs/content/Textarea.mdx index e5c25dba6ce..89ee395bf4f 100644 --- a/docs/content/Textarea.mdx +++ b/docs/content/Textarea.mdx @@ -4,7 +4,7 @@ title: Textarea description: Use Textarea for multi-line text input form fields status: Alpha source: https://github.com/primer/react/blob/main/src/Textarea.tsx -storybook: /react/storybook?path=/story/forms-textarea--default +storybook: '/react/storybook?path=/story/forms-form-controls--textarea-story' --- import {Textarea} from '@primer/react' @@ -158,10 +158,13 @@ By default, `Textarea` can be resized by the user vertically and horizontally. R elementType="textarea" refType="HTMLTextAreaElement" /> - MDN + + MDN + } /> diff --git a/src/Autocomplete/AutocompleteMenu.tsx b/src/Autocomplete/AutocompleteMenu.tsx index efe4bff8d25..2171279b64e 100644 --- a/src/Autocomplete/AutocompleteMenu.tsx +++ b/src/Autocomplete/AutocompleteMenu.tsx @@ -42,13 +42,12 @@ function getItemById(itemId: string | number, it // eslint-disable-next-line @typescript-eslint/no-explicit-any type AutocompleteItemProps> = AutocompleteMenuItem & {metadata?: T} +// TODO: we should make `aria-labelledby` required for a11y export type AutocompleteMenuInternalProps = { /** * A menu item that is used to allow users make a selection that is not available in the array passed to the `items` prop. * This menu item gets appended to the end of the list of options. */ - // TODO: rethink this part of the component API. this is kind of weird and confusing to use - // TODO: rethink `addNewItem` prop name addNewItem?: Omit & { handleAddItem: (item: Omit) => void } diff --git a/src/Checkbox.tsx b/src/Checkbox.tsx index 9eb881c4958..7607d2dfcb1 100644 --- a/src/Checkbox.tsx +++ b/src/Checkbox.tsx @@ -25,7 +25,7 @@ export type CheckboxProps = { */ required?: boolean /** - * Indicates whether the checkbox validation state + * Only used to inform ARIA attributes. Individual checkboxes do not have validation styles. */ validationStatus?: FormValidationStatus /** diff --git a/src/FormControl/_FormControlLabel.tsx b/src/FormControl/_FormControlLabel.tsx index 48876830259..e8fd00e7e99 100644 --- a/src/FormControl/_FormControlLabel.tsx +++ b/src/FormControl/_FormControlLabel.tsx @@ -11,11 +11,18 @@ export type Props = { visuallyHidden?: boolean } & SxProp -const FormControlLabel: React.FC<{htmlFor?: string} & Props> = ({children, htmlFor, visuallyHidden, sx}) => ( +const FormControlLabel: React.FC<{htmlFor?: string; id?: string} & Props> = ({ + children, + htmlFor, + id, + visuallyHidden, + sx +}) => ( - {({disabled, id, required}: FormControlContext) => ( + {({disabled, id: formControlId, required}: FormControlContext) => ( & diff --git a/src/TextInputWithTokens.tsx b/src/TextInputWithTokens.tsx index 8a6fee9b7b9..bc59c4e2eb8 100644 --- a/src/TextInputWithTokens.tsx +++ b/src/TextInputWithTokens.tsx @@ -351,7 +351,7 @@ function TextInputWithTokensInnerComponent )) : null} - {tokensAreTruncated ? ( + {tokensAreTruncated && tokens.length - visibleTokens.length ? ( +{tokens.length - visibleTokens.length} diff --git a/src/Textarea.tsx b/src/Textarea.tsx index 2bd60621588..6279ce91669 100644 --- a/src/Textarea.tsx +++ b/src/Textarea.tsx @@ -4,15 +4,15 @@ import {TextInputBaseWrapper} from './_TextInputWrapper' import {FormValidationStatus} from './utils/types/FormValidationStatus' import sx, {SxProp} from './sx' +export const DEFAULT_TEXTAREA_ROWS = 7 +export const DEFAULT_TEXTAREA_COLS = 30 +export const DEFAULT_TEXTAREA_RESIZE = 'both' + export type TextareaProps = { /** * Apply inactive visual appearance to the Textarea */ disabled?: boolean - /** - * Indicates whether the Textarea is a required form field - */ - required?: boolean /** * Indicates whether the Textarea validation state */ @@ -69,9 +69,9 @@ const Textarea = React.forwardRef( sx: sxProp, required, validationStatus, - rows = 7, - cols = 30, - resize = 'both', + rows = DEFAULT_TEXTAREA_ROWS, + cols = DEFAULT_TEXTAREA_COLS, + resize = DEFAULT_TEXTAREA_RESIZE, block, ...rest }: TextareaProps, diff --git a/src/_InputLabel.tsx b/src/_InputLabel.tsx index 2b6a51554bf..43ea1088bee 100644 --- a/src/_InputLabel.tsx +++ b/src/_InputLabel.tsx @@ -9,12 +9,13 @@ interface Props extends React.HTMLProps { visuallyHidden?: boolean } -const InputLabel: React.FC = ({children, disabled, required, visuallyHidden, htmlFor, sx}) => { +const InputLabel: React.FC = ({children, disabled, htmlFor, id, required, visuallyHidden, sx}) => { return ( { jest.resetAllMocks() cleanup() }) - behavesAsComponent({Component: Textarea, options: {skipAs: true}}) + behavesAsComponent({ + Component: Textarea, + options: {skipAs: true} + }) checkExports('Textarea', { - default: Textarea + default: Textarea, + DEFAULT_TEXTAREA_ROWS, + DEFAULT_TEXTAREA_COLS, + DEFAULT_TEXTAREA_RESIZE }) it('renders a valid textarea input', () => { diff --git a/src/stories/Autocomplete.stories.tsx b/src/stories/Autocomplete.stories.tsx index 62555212894..f62bce5bb85 100644 --- a/src/stories/Autocomplete.stories.tsx +++ b/src/stories/Autocomplete.stories.tsx @@ -1,11 +1,81 @@ import React, {ChangeEventHandler, RefObject, useCallback, useRef, useState} from 'react' import {Meta} from '@storybook/react' -import {BaseStyles, Box, Text, TextInput, ThemeProvider} from '..' +import {BaseStyles, Box, ThemeProvider} from '..' import TextInputTokens from '../TextInputWithTokens' import Autocomplete from '../Autocomplete/Autocomplete' import {AnchoredOverlay} from '../AnchoredOverlay' import {ButtonInvisible} from '../deprecated/Button' +import FormControl from '../FormControl' +import {ComponentProps} from '../utils/types' +import { + FormControlArgs, + formControlArgTypes, + getFormControlArgsByChildComponent, + getTextInputArgTypes, + textInputWithTokensArgTypes +} from '../utils/story-helpers' + +type AutocompleteOverlayArgs = ComponentProps +type AutocompleteMenuArgs = ComponentProps +type AutocompleteArgs = AutocompleteOverlayArgs & AutocompleteMenuArgs + +const excludedControlKeys = ['id', 'sx'] + +const getArgsByChildComponent = ({ + // Autocomplete.Menu + emptyStateText, + menuLoading, + selectionVariant, + + // Autocomplete.Overlay + anchorSide, + height, + overlayMaxHeight, + width, + + // TextInput + block, + contrast, + disabled, + inputSize, + loading, + loaderPosition, + placeholder, + validationStatus, + + // TextInputWithTokens + hideTokenRemoveButtons, + maxHeight: textInputWithTokensMaxHeight, + preventTokenWrapping, + size: tokenSize, + visibleTokenCount +}: AutocompleteArgs) => { + const textInputArgs = { + block, + contrast, + disabled, + inputSize, + loading, + loaderPosition, + placeholder, + validationStatus + } + return { + menuArgs: {emptyStateText, loading: menuLoading, selectionVariant}, + overlayArgs: {anchorSide, height, maxHeight: overlayMaxHeight, width}, + textInputArgs, + textInputWithTokensArgs: { + hideTokenRemoveButtons, + maxHeight: textInputWithTokensMaxHeight, + preventTokenWrapping, + size: tokenSize, + visibleTokenCount, + ...textInputArgs + // ...formControlArgTypes + } + } +} type ItemMetadata = { fillColor: React.CSSProperties['backgroundColor'] @@ -30,9 +100,8 @@ const items: Datum[] = [ const mockTokens: Datum[] = [...items].slice(0, 3) -export default { - title: 'Forms/Autocomplete', - +const autocompleteStoryMeta: Meta = { + title: 'Forms/Form Controls/Autocomplete', decorators: [ Story => { const [lastKey, setLastKey] = useState('none') @@ -55,33 +124,98 @@ export default { ) } - ] + ], + parameters: {controls: {exclude: excludedControlKeys}}, + argTypes: { + // Autocomplete.Menu + emptyStateText: { + defaultValue: 'No selectable options', + control: {type: 'text'}, + table: { + category: 'Autocomplete.Menu' + } + }, + menuLoading: { + name: 'loading', + defaultValue: false, + control: {type: 'boolean'}, + table: { + category: 'Autocomplete.Menu' + } + }, + selectionVariant: { + defaultValue: 'single', + control: { + type: 'radio', + options: ['single', 'multiple'] + }, + table: { + category: 'Autocomplete.Menu' + } + }, + + // Autocomplete.Overlay + anchorSide: { + defaultValue: undefined, + control: { + type: 'select', + options: [ + 'inside-top', + 'inside-bottom', + 'inside-left', + 'inside-right', + 'inside-center', + 'outside-top', + 'outside-bottom', + 'outside-left', + 'outside-right' + ] + }, + table: { + category: 'Autocomplete.Overlay' + } + }, + height: { + defaultValue: 'auto', + control: { + type: 'select', + options: ['auto', 'initial', 'small', 'medium', 'large', 'xlarge', 'xsmall'] + }, + table: { + category: 'Autocomplete.Overlay' + } + }, + // needs a key other than 'maxHeight' because TextInputWithTokens also has a maxHeight prop + overlayMaxHeight: { + name: 'maxHeight', + defaultValue: undefined, + control: { + type: 'select', + options: ['small', 'medium', 'large', 'xlarge', 'xsmall', undefined] + }, + table: { + category: 'Autocomplete.Overlay' + } + }, + width: { + defaultValue: 'auto', + control: { + type: 'select', + options: ['auto', 'small', 'medium', 'large', 'xlarge', 'xxlarge'] + }, + table: { + category: 'Autocomplete.Overlay' + } + }, + ...getTextInputArgTypes('TextInput props'), + ...formControlArgTypes + } } as Meta -export const SingleSelect = () => { - return ( - <> - - Pick a tag - - - - - - - - - ) -} - -export const MultiSelect = () => { +export const Default = (args: FormControlArgs) => { + const {parentArgs, labelArgs, captionArgs, validationArgs} = getFormControlArgsByChildComponent(args) + const {menuArgs, overlayArgs, textInputArgs} = getArgsByChildComponent(args) + const isMultiselect = menuArgs.selectionVariant === 'multiple' const [selectedItemIds, setSelectedItemIds] = useState>([]) const onSelectedChange = (newlySelectedItems: Datum | Datum[]) => { if (!Array.isArray(newlySelectedItems)) { @@ -91,47 +225,34 @@ export const MultiSelect = () => { setSelectedItemIds(newlySelectedItems.map(item => item.id)) } - const getItemById = (id: string | number) => items.find(item => item.id === id) - return ( - -
- - Pick a tag - + + + - - + + -
-
-
Selected items:
- - {selectedItemIds.map(selectedItemId => ( -
  • {getItemById(selectedItemId)?.text}
  • - ))} -
    -
    + {captionArgs.children && } + {validationArgs.children && validationArgs.variant && ( + + )} +
    ) } -export const MultiSelectWithTokenInput = () => { +export const WithTokenInput = (args: FormControlArgs) => { + const {parentArgs, labelArgs, captionArgs, validationArgs} = getFormControlArgsByChildComponent(args) + const {menuArgs, overlayArgs, textInputWithTokensArgs} = getArgsByChildComponent(args) const [tokens, setTokens] = useState([]) // [items[0], items[2]] const selectedTokenIds = tokens.map(token => token.id) const [selectedItemIds, setSelectedItemIds] = useState>(selectedTokenIds) @@ -161,40 +282,53 @@ export const MultiSelectWithTokenInput = () => { } return ( - <> - - Pick tags - - - - - + + + + - - - + + + +
    + {captionArgs.children && } + {validationArgs.children && validationArgs.variant && ( + + )} +
    +
    ) } +WithTokenInput.argTypes = { + ...autocompleteStoryMeta.argTypes, + ...getTextInputArgTypes('TextInput props'), + ...textInputWithTokensArgTypes +} +WithTokenInput.args = { + block: true, + selectionVariant: 'multiple' +} +WithTokenInput.parameters = { + controls: { + exclude: [...excludedControlKeys, 'size'] + } +} -export const MultiSelectAddNewItem = () => { +export const AddNewItem = (args: FormControlArgs) => { + const {parentArgs, labelArgs, captionArgs, validationArgs} = getFormControlArgsByChildComponent(args) + const {menuArgs, overlayArgs, textInputArgs} = getArgsByChildComponent(args) const [localItemsState, setLocalItemsState] = useState(items) const [filterVal, setFilterVal] = useState('') const [tokens, setTokens] = useState(mockTokens) @@ -238,84 +372,68 @@ export const MultiSelectAddNewItem = () => { } return ( - - - Pick tags - - - - - localItem.text).includes(filterVal) - ? { - text: `Add '${filterVal}'`, - handleAddItem: item => { - onItemSelect({ - ...item, - text: filterVal, - selected: true - }) - setFilterVal('') - } - } - : undefined - } - items={localItemsState} - selectedItemIds={selectedItemIds} - onSelectedChange={onSelectedChange} - selectionVariant="multiple" - aria-labelledby="autocompleteLabel" + + + + + - - + + localItem.text).includes(filterVal) + ? { + text: `Add '${filterVal}'`, + handleAddItem: item => { + onItemSelect({ + ...item, + text: filterVal, + selected: true + }) + setFilterVal('') + } + } + : undefined + } + items={localItemsState} + selectedItemIds={selectedItemIds} + onSelectedChange={onSelectedChange} + aria-labelledby="autocompleteLabel" + {...menuArgs} + /> + +
    + {captionArgs.children && } + {validationArgs.children && validationArgs.variant && ( + + )} +
    ) } - -export const CustomEmptyStateMessage = () => { - return ( - <> - - Pick a tag - - - - - - - - - ) +AddNewItem.args = { + block: true, + selectionVariant: 'multiple' +} +AddNewItem.argTypes = { + ...autocompleteStoryMeta.argTypes, + ...getTextInputArgTypes('TextInput props'), + ...textInputWithTokensArgTypes +} +AddNewItem.parameters = { + controls: { + exclude: [...excludedControlKeys, 'size'] + } } -export const CustomSearchFilter = () => { +export const CustomSearchFilterFn = (args: FormControlArgs) => { + const {parentArgs, labelArgs, captionArgs, validationArgs} = getFormControlArgsByChildComponent(args) + const {menuArgs, overlayArgs, textInputArgs} = getArgsByChildComponent(args) const [filterVal, setFilterVal] = useState('') const handleChange: ChangeEventHandler = e => { setFilterVal(e.currentTarget.value) @@ -323,36 +441,36 @@ export const CustomSearchFilter = () => { const customFilterFn = (item: Datum) => item.text.includes(filterVal) return ( - <> - - Pick a tag - - - - - - - - - Items in dropdown are filtered if their text has no part that matches the input value - - + + + + + + + + + + {captionArgs.children && } + {validationArgs.children && validationArgs.variant && ( + + )} + + ) } +CustomSearchFilterFn.args = { + captionChildren: 'Items in dropdown are filtered if their text has no part that matches the input value' +} -export const CustomSortAfterMenuClose = () => { +export const CustomSortAfterMenuClose = (args: FormControlArgs) => { + const {parentArgs, labelArgs, captionArgs, validationArgs} = getFormControlArgsByChildComponent(args) + const {menuArgs, overlayArgs, textInputArgs} = getArgsByChildComponent(args) const [selectedItemIds, setSelectedItemIds] = useState>([]) const isItemSelected = (itemId: string | number) => selectedItemIds.includes(itemId) const onSelectedChange = (newlySelectedItems: Datum | Datum[]) => { @@ -366,68 +484,63 @@ export const CustomSortAfterMenuClose = () => { isItemSelected(itemIdA) === isItemSelected(itemIdB) ? 0 : isItemSelected(itemIdA) ? 1 : -1 return ( - <> - - Pick a tag - - - - - - - - - When the dropdown closes, selected items are sorted to the end - - + + + + + + + + + + {captionArgs.children && } + {validationArgs.children && validationArgs.variant && ( + + )} + + ) } +CustomSortAfterMenuClose.args = { + captionChildren: 'When the dropdown closes, selected items are sorted to the end' +} -export const WithCallbackWhenOverlayOpenStateChanges = () => { +export const WithCallbackWhenOverlayOpenStateChanges = (args: FormControlArgs) => { + const {parentArgs, labelArgs, captionArgs, validationArgs} = getFormControlArgsByChildComponent(args) + const {menuArgs, overlayArgs, textInputArgs} = getArgsByChildComponent(args) const [isMenuOpen, setIsMenuOpen] = useState(false) const onOpenChange = (isOpen: boolean) => { setIsMenuOpen(isOpen) } return ( - -
    - - Pick a tag - + + + - - + + -
    + {captionArgs.children && } + {validationArgs.children && validationArgs.variant && ( + + )} +
    The menu is {isMenuOpen ? 'opened' : 'closed'}
    @@ -435,7 +548,9 @@ export const WithCallbackWhenOverlayOpenStateChanges = () => { ) } -export const AsyncLoadingOfItems = () => { +export const AsyncLoadingOfItems = (args: FormControlArgs) => { + const {parentArgs, labelArgs, captionArgs, validationArgs} = getFormControlArgsByChildComponent(args) + const {menuArgs, overlayArgs, textInputArgs} = getArgsByChildComponent(args) const [loadedItems, setLoadedItems] = useState([]) const onOpenChange = () => { setTimeout(() => { @@ -444,55 +559,61 @@ export const AsyncLoadingOfItems = () => { } return ( - <> - - Pick a tag - - - - - - - - + + + + + + + + + + {captionArgs.children && } + {validationArgs.children && validationArgs.variant && ( + + )} + + ) } +AsyncLoadingOfItems.parameters = {controls: {exclude: [...excludedControlKeys, 'loading']}} + +export const RenderingTheMenuOutsideAnOverlay = (args: FormControlArgs) => { + const {parentArgs, labelArgs, captionArgs, validationArgs} = getFormControlArgsByChildComponent(args) + const {menuArgs, textInputArgs} = getArgsByChildComponent(args) -export const RenderingTheMenuOutsideAnOverlay = () => { return ( - <> - - Pick a tag - - - - - - + + + + + + + + {captionArgs.children && } + {validationArgs.children && validationArgs.variant && ( + + )} + + ) } +RenderingTheMenuOutsideAnOverlay.parameters = { + controls: { + exclude: [...excludedControlKeys, 'anchorSide', 'height', 'maxHeight', 'width'] + } +} -export const CustomOverlayMenuAnchor = () => { +export const CustomOverlayMenuAnchor = (args: FormControlArgs) => { + const {parentArgs, labelArgs, captionArgs, validationArgs} = getFormControlArgsByChildComponent(args) + const {menuArgs, overlayArgs, textInputArgs} = getArgsByChildComponent(args) const menuAnchorRef = useRef(null) const anchorWrapperStyles = { display: 'flex', @@ -505,68 +626,45 @@ export const CustomOverlayMenuAnchor = () => { } return ( - <> - - Pick tags - - }> - - + + + }> + + - - - - - - - The overlay menu's position is anchored to the div with the black border instead of to the text input - - + padding: '0', + boxShadow: 'none', + ':focus-within': { + border: '0', + boxShadow: 'none' + } + }} + {...textInputArgs} + size={textInputArgs.inputSize} + /> + + + + + + {captionArgs.children && } + {validationArgs.children && validationArgs.variant && ( + + )} +
    + ) } - -export const WithCustomOverlayProps = () => { - return ( - <> - - Pick a tag - - - - - - - - - ) +CustomOverlayMenuAnchor.args = { + captionChildren: `The overlay menu's position is anchored to the div with the black border instead of to the text input` } -export const InOverlayWithCustomScrollContainerRef = () => { +export const InOverlayWithCustomScrollContainerRef = (args: FormControlArgs) => { + const {menuArgs, textInputArgs} = getArgsByChildComponent(args) const scrollContainerRef = useRef(null) const inputRef = useRef(null) @@ -577,74 +675,69 @@ export const InOverlayWithCustomScrollContainerRef = () => { } return ( - setIsOpen(false)} - width="large" - height="xsmall" - focusTrapSettings={{initialFocusRef: inputRef}} - side="inside-top" - renderAnchor={props => open overlay} - > - + setIsOpen(false)} + width="large" + height="xsmall" + focusTrapSettings={{initialFocusRef: inputRef}} + side="inside-top" + renderAnchor={props => open overlay} > - Pick tags - - - - - + Pick tags + + + + + - - }> - + paddingX: 3, + paddingY: 1, + boxShadow: 'none', + ':focus-within': { + border: '0', + boxShadow: 'none' + } + }} + {...textInputArgs} + size={textInputArgs.inputSize} + block + /> + + }> + + - - - + + + ) } + +InOverlayWithCustomScrollContainerRef.parameters = { + controls: { + exclude: [ + ...excludedControlKeys, + ...Object.keys(formControlArgTypes), + 'anchorSide', + 'height', + 'maxHeight', + 'width', + 'children' + ] + } +} + +export default autocompleteStoryMeta diff --git a/src/stories/Checkbox.stories.tsx b/src/stories/Checkbox.stories.tsx index dca7c024cf1..b9e69478789 100644 --- a/src/stories/Checkbox.stories.tsx +++ b/src/stories/Checkbox.stories.tsx @@ -1,14 +1,21 @@ import React, {useRef, useState} from 'react' import {Meta} from '@storybook/react' -import styled from 'styled-components' -import {BaseStyles, Box, Checkbox, CheckboxProps, Text, ThemeProvider} from '..' +import {BaseStyles, Box, Checkbox, CheckboxProps, ThemeProvider} from '..' import useLayoutEffect from '../utils/useIsomorphicLayoutEffect' import {action} from '@storybook/addon-actions' -import {get} from '../constants' +import FormControl from '../FormControl' +import { + FormControlArgs, + formControlArgTypesWithoutValidation, + getFormControlArgsByChildComponent +} from '../utils/story-helpers' +import {MarkGithubIcon} from '@primer/octicons-react' + +const excludedControlKeys = ['required', 'value', 'validationStatus', 'sx'] export default { - title: 'Forms/Checkbox', + title: 'Forms/Form Controls/Checkbox', component: Checkbox, decorators: [ Story => { @@ -21,36 +28,65 @@ export default { ) } ], + parameters: {controls: {exclude: excludedControlKeys}}, argTypes: { - sx: { - table: { - disable: true + checked: { + defaultValue: false, + control: { + type: 'boolean' } }, - disabled: { - name: 'Disabled', + indeterminate: { defaultValue: false, control: { type: 'boolean' } - } + }, + ...formControlArgTypesWithoutValidation } } as Meta -const StyledLabel = styled.label` - user-select: none; - font-weight: 600; - font-size: 14px; - line-height: 18px; - margin-left: 16px; -` +export const Default = ({value: _value, checked, ...args}: FormControlArgs) => { + const {parentArgs, labelArgs, captionArgs} = getFormControlArgsByChildComponent(args) + + return ( + + + + + {captionArgs.children && } + + + ) +} +Default.args = { + labelChildren: 'Default checkbox', + captionChildren: 'Always unchecked unless `checked` is set to true in Storybook controls' +} -const StyledSubLabel = styled(Text)` - color: ${get('colors.fg.muted')}; - font-size: 13px; -` +export const WithLeadingVisual = ({value: _value, checked, ...args}: FormControlArgs) => { + const {parentArgs, labelArgs, captionArgs} = getFormControlArgsByChildComponent(args) -export const Default = ({value: _value, ...args}: CheckboxProps) => { + return ( + + + + + + + + {captionArgs.children && } + + + ) +} +WithLeadingVisual.args = { + labelChildren: 'Default checkbox', + captionChildren: 'Always unchecked unless `checked` is set to true in Storybook controls' +} + +export const Controlled = (args: FormControlArgs) => { + const {parentArgs, labelArgs, captionArgs} = getFormControlArgsByChildComponent(args) const [isChecked, setChecked] = useState(false) const handleChange = (event: React.ChangeEvent) => { @@ -59,107 +95,43 @@ export const Default = ({value: _value, ...args}: CheckboxProps) => { } return ( - <> - - - - Default checkbox - controlled - - - - - - Always checked - checked="true" - - - - - - Always unchecked - checked="false" - - - - - - Inactive - disabled="true" - - - + + + + + {captionArgs.children && } + + ) } +Controlled.parameters = {controls: {exclude: [...excludedControlKeys, 'checked']}} +Controlled.args = { + labelChildren: 'Controlled checkbox', + captionChildren: 'Checked attribute is controlled by React state update on change' +} -export const Uncontrolled = ({value: _value, ...args}: CheckboxProps) => { +export const Uncontrolled = (args: FormControlArgs) => { + const {parentArgs, labelArgs, captionArgs} = getFormControlArgsByChildComponent(args) const checkboxRef = useRef(null) useLayoutEffect(() => { if (checkboxRef.current) { - checkboxRef.current.checked = true + checkboxRef.current.checked = false } }, []) return ( - - - - Uncontrolled checkbox - Checked by default - + + + + + {captionArgs.children && } + ) } - -export const Indeterminate = ({value: _value, ...args}: CheckboxProps) => { - const [checkboxes, setCheckboxes] = useState([false, false, false, false]) - - const handleChange = (_: React.ChangeEvent, index: number) => { - const newCheckboxes = [...checkboxes] - newCheckboxes[index] = !checkboxes[index] - setCheckboxes(newCheckboxes) - } - - const handleIndeterminateChange = () => { - if (checkboxes.every(checkbox => checkbox)) { - return setCheckboxes(checkboxes.map(() => false)) - } - - const newCheckboxes = checkboxes.map(() => true) - setCheckboxes(newCheckboxes) - } - - return ( - <> - - - - Default checkbox - controlled - - - - {checkboxes.map((field, index) => ( - - handleChange(event, index)} - {...args} - /> - - Checkbox {index + 1} - - - ))} - - ) +Uncontrolled.parameters = {controls: {exclude: [...excludedControlKeys, 'checked']}} +Uncontrolled.args = { + labelChildren: 'Uncontrolled checkbox', + captionChildren: 'Checked attribute is set in a useLayoutEffect hook' } diff --git a/src/stories/CheckboxGroup/examples.stories.tsx b/src/stories/CheckboxGroup/examples.stories.tsx index 97d41cecbfa..ce54db76c34 100644 --- a/src/stories/CheckboxGroup/examples.stories.tsx +++ b/src/stories/CheckboxGroup/examples.stories.tsx @@ -1,19 +1,64 @@ import React from 'react' import {Meta} from '@storybook/react' import {BaseStyles, Checkbox, CheckboxGroup, FormControl, ThemeProvider} from '../../' -import {ComponentProps} from '../../utils/types' - -type Args = ComponentProps +import {CheckboxOrRadioGroupArgs} from '../../utils/story-helpers' export default { title: 'Forms/CheckboxGroup/examples', component: CheckboxGroup, argTypes: { + // CheckboxGroup disabled: { - defaultValue: false + defaultValue: false, + type: 'boolean' }, required: { - defaultValue: false + defaultValue: false, + type: 'boolean' + }, + + // CheckboxGroup.Label + labelChildren: { + defaultValue: 'Choices', + type: 'string', + table: { + category: 'CheckboxGroup.Label' + } + }, + visuallyHidden: { + defaultValue: false, + type: 'boolean', + table: { + category: 'CheckboxGroup.Label' + } + }, + + // CheckboxGroup.Caption + captionChildren: { + defaultValue: '', + type: 'string', + table: { + category: 'CheckboxGroup.Caption' + } + }, + + // CheckboxGroup.Validation + validationChildren: { + defaultValue: '', + type: 'string', + table: { + category: 'CheckboxGroup.Validation' + } + }, + variant: { + defaultValue: 'error', + control: { + type: 'radio', + options: ['error', 'success', 'warning'] + }, + table: { + category: 'CheckboxGroup.Validation' + } } }, parameters: {controls: {exclude: ['aria-labelledby', 'id', 'onChange', 'sx']}}, @@ -30,40 +75,36 @@ export default { ] } as Meta -export const Basic = (args: Args) => ( - - Choices - - - Choice one - - - - Choice two - - - - Choice three - - -) +export const Default = ({ + disabled, + required, + labelChildren, + visuallyHidden, + captionChildren, + validationChildren, + variant +}: CheckboxOrRadioGroupArgs) => { + const parentArgs = {disabled, required} + const labelArgs = {children: labelChildren, visuallyHidden} + const validationArgs = {children: validationChildren, variant} -export const WithCaptionAndValidation = (args: Args) => ( - - Choices - You can pick any or all of these choices - - - Choice one - - - - Choice two - - - - Choice three - - Your choices are wrong - -) + return ( + + {labelArgs.children && } + {captionChildren && {captionChildren}} + + + Choice one + + + + Choice two + + + + Choice three + + {validationArgs.children && } + + ) +} diff --git a/src/stories/CheckboxGroup/fixtures.stories.tsx b/src/stories/CheckboxGroup/fixtures.stories.tsx index e80080b10e3..536df011fe7 100644 --- a/src/stories/CheckboxGroup/fixtures.stories.tsx +++ b/src/stories/CheckboxGroup/fixtures.stories.tsx @@ -1,22 +1,69 @@ import React from 'react' import {Meta} from '@storybook/react' import {BaseStyles, Box, Checkbox, CheckboxGroup, FormControl, ThemeProvider} from '../../' -import {ComponentProps} from '../../utils/types' +import {CheckboxOrRadioGroupArgs} from '../../utils/story-helpers' -type Args = ComponentProps +const excludedControlKeys = ['aria-labelledby', 'id', 'onChange', 'sx', 'visuallyHidden'] export default { title: 'Forms/CheckboxGroup/fixtures', component: CheckboxGroup, argTypes: { + // CheckboxGroup disabled: { - defaultValue: false + defaultValue: false, + type: 'boolean' }, required: { - defaultValue: false + defaultValue: false, + type: 'boolean' + }, + + // CheckboxGroup.Label + labelChildren: { + defaultValue: 'Choices', + type: 'string', + table: { + category: 'CheckboxGroup.Label' + } + }, + visuallyHidden: { + defaultValue: false, + type: 'boolean', + table: { + category: 'CheckboxGroup.Label' + } + }, + + // CheckboxGroup.Caption + captionChildren: { + defaultValue: '', + type: 'string', + table: { + category: 'CheckboxGroup.Caption' + } + }, + + // CheckboxGroup.Validation + validationChildren: { + defaultValue: '', + type: 'string', + table: { + category: 'CheckboxGroup.Validation' + } + }, + variant: { + defaultValue: 'error', + control: { + type: 'radio', + options: ['error', 'success', 'warning'] + }, + table: { + category: 'CheckboxGroup.Validation' + } } }, - parameters: {controls: {exclude: ['aria-labelledby', 'id', 'onChange', 'sx']}}, + parameters: {controls: {exclude: excludedControlKeys}}, decorators: [ Story => { return ( @@ -30,20 +77,68 @@ export default { ] } as Meta -export const WithExternalLabel = (args: Args) => ( - <> - - Choices - - +export const WithExternalLabel = ({ + disabled, + required, + labelChildren, + captionChildren, + validationChildren, + variant +}: CheckboxOrRadioGroupArgs) => { + const parentArgs = {disabled, required} + const validationArgs = {children: validationChildren, variant} + + return ( + <> + + {labelChildren} {parentArgs.required && '*'} + + + {captionChildren && {captionChildren}} + + + Choice one + + + + Choice two + + + + Choice three + + {validationArgs.children && } + + + ) +} +WithExternalLabel.parameters = {controls: {exclude: [...excludedControlKeys, 'visuallyHidden']}} + +export const WithHiddenLabel = ({ + disabled, + required, + labelChildren, + visuallyHidden, + captionChildren, + validationChildren, + variant +}: CheckboxOrRadioGroupArgs) => { + const parentArgs = {disabled, required} + const labelArgs = {children: labelChildren, visuallyHidden} + const validationArgs = {children: validationChildren, variant} + + return ( + + {labelArgs.children && } + {captionChildren && {captionChildren}} Choice one @@ -56,24 +151,10 @@ export const WithExternalLabel = (args: Args) => ( Choice three + {validationArgs.children && } - -) - -export const WithHiddenLabel = (args: Args) => ( - - Choices - - - Choice one - - - - Choice two - - - - Choice three - - -) + ) +} +WithHiddenLabel.args = { + visuallyHidden: true +} diff --git a/src/stories/FormControl.stories.tsx b/src/stories/FormControl.stories.tsx deleted file mode 100644 index 9f3fbb815c1..00000000000 --- a/src/stories/FormControl.stories.tsx +++ /dev/null @@ -1,181 +0,0 @@ -import React from 'react' -import {Meta} from '@storybook/react' -import {BaseStyles, Checkbox, Radio, Select, TextInput, TextInputWithTokens, ThemeProvider} from '..' -import FormControl from '../FormControl' -import {ComponentProps} from '../utils/types' -import Autocomplete from '../Autocomplete' -import {MarkGithubIcon} from '@primer/octicons-react' - -type Args = ComponentProps - -export default { - title: 'Forms/FormControl', - component: FormControl, - argTypes: { - required: { - defaultValue: false - }, - disabled: { - defaultValue: false - } - }, - parameters: {controls: {exclude: ['id']}}, - decorators: [ - Story => { - return ( - - - - - - ) - } - ] -} as Meta - -export const TextInputFormControl = (args: Args) => ( - - Name - - -) - -export const WithAVisuallyHiddenLabel = (args: Args) => ( - - Name - - -) - -export const WithCaption = (args: Args) => ( - - Name - - Hint: your first name - -) -export const WithValidation = (args: Args) => ( - - Name - - Your first name cannot contain spaces - -) -WithValidation.parameters = {controls: {exclude: ['id']}} - -export const WithValidationAndCaption = (args: Args) => ( - - Name - - Your first name cannot contain spaces - Hint: your first name - -) -WithValidationAndCaption.parameters = {controls: {exclude: ['id']}} - -export const UsingAutocompleteInput = (args: Args) => { - return ( - - Tags - - - - - - - - ) -} - -export const UsingTextInputWithTokens = (args: Args) => ( - - Tags - null} - /> - -) - -export const UsingSelectInput = (args: Args) => ( - - Preferred Primer component interface - - -) - -UsingTextInputWithTokens.storyName = 'Using TextInputWithTokens Input' - -export const UsingCheckboxInput = (args: Args) => ( - - - Selectable choice - -) - -export const UsingRadioInput = (args: Args) => ( - - - Selectable choice - -) - -export const UsingCustomInput = (args: Args) => ( - - Name - - Your first name - - Not a valid name - - -) - -export const WithLeadingVisual = (args: Args) => ( - - Selectable choice - - - - - -) -WithLeadingVisual.storyName = 'With LeadingVisual' - -export const WithCaptionAndLeadingVisual = (args: Args) => ( - - Selectable choice - - - - - This is an arbitrary choice - -) -WithCaptionAndLeadingVisual.storyName = 'With Caption and LeadingVisual' diff --git a/src/stories/Radio.stories.tsx b/src/stories/Radio.stories.tsx index 1a31c8e6d97..62c44e6cb92 100644 --- a/src/stories/Radio.stories.tsx +++ b/src/stories/Radio.stories.tsx @@ -1,12 +1,16 @@ -import React, {ChangeEvent, useState} from 'react' +import React from 'react' import {Meta} from '@storybook/react' -import styled from 'styled-components' +import {BaseStyles, Box, FormControl, Radio, RadioProps, ThemeProvider} from '..' +import { + FormControlArgs, + formControlArgTypesWithoutValidation, + getFormControlArgsByChildComponent +} from '../utils/story-helpers' -import {BaseStyles, Box, Radio, RadioProps, Text, ThemeProvider} from '..' -import {get} from '../constants' +const excludedControlKeys = ['required', 'value', 'name', 'validationStatus', 'sx'] export default { - title: 'Forms/Radio', + title: 'Forms/Form Controls/Radio', component: Radio, decorators: [ Story => { @@ -19,108 +23,31 @@ export default { ) } ], + parameters: {controls: {exclude: excludedControlKeys}}, argTypes: { - sx: { - table: { - disable: true - } - }, - disabled: { - defaultValue: false, - control: { - type: 'boolean' - } - }, checked: { defaultValue: false, control: { type: 'boolean' } - } + }, + ...formControlArgTypesWithoutValidation } } as Meta -const StyledLabel = styled.label` - user-select: none; - font-weight: 600; - font-size: 14px; - line-height: 18px; - margin-left: 8px; - display: flex; - cursor: pointer; - - &:first-child { - margin-left: 0; - } - - &[aria-disabled='true'] { - pointer-events: none; - cursor: not-allowed; - color: ${get('colors.primer.fg.disabled')}; - } -` - -export const Default = ({disabled, checked}: RadioProps) => { - const [isSelected, setSelected] = useState(checked || false) - const handleChange = () => { - setSelected(!isSelected) - } +export const Default = ({value: _value, ...args}: FormControlArgs) => { + const {parentArgs, labelArgs, captionArgs} = getFormControlArgsByChildComponent(args) return ( - <> - - - - Default radio button - - - + + + + + {captionArgs.children && } + + ) } - -export const MultipleRadios = ({disabled}: RadioProps) => { - const [activeSelection, setActiveSelection] = useState<'yes' | 'no' | undefined>() - - const handleChange = (event: ChangeEvent) => { - const target = event.target.value as 'yes' | 'no' - setActiveSelection(target) - } - - return ( - <> - - - - Yes - - - - - - No - - - - ) +Default.args = { + labelChildren: 'Default radio button' } diff --git a/src/stories/RadioGroup/examples.stories.tsx b/src/stories/RadioGroup/examples.stories.tsx index 2554583163a..0c6f3d2a835 100644 --- a/src/stories/RadioGroup/examples.stories.tsx +++ b/src/stories/RadioGroup/examples.stories.tsx @@ -1,22 +1,68 @@ import React from 'react' import {Meta} from '@storybook/react' -import {BaseStyles, Radio, RadioGroup, FormControl, ThemeProvider} from '../../' -import {ComponentProps} from '../../utils/types' - -type Args = ComponentProps +import {BaseStyles, RadioGroup, FormControl, ThemeProvider} from '../../' +import {CheckboxOrRadioGroupArgs} from '../../utils/story-helpers' +import Radio from '../../Radio' export default { title: 'Forms/RadioGroup/examples', component: RadioGroup, argTypes: { + // RadioGroup disabled: { - defaultValue: false + defaultValue: false, + type: 'boolean' }, required: { - defaultValue: false + defaultValue: false, + type: 'boolean' + }, + + // RadioGroup.Label + labelChildren: { + defaultValue: 'Choices', + type: 'string', + table: { + category: 'RadioGroup.Label' + } + }, + visuallyHidden: { + defaultValue: false, + type: 'boolean', + table: { + category: 'RadioGroup.Label' + } + }, + + // RadioGroup.Caption + captionChildren: { + defaultValue: '', + type: 'string', + table: { + category: 'RadioGroup.Caption' + } + }, + + // RadioGroup.Validation + validationChildren: { + defaultValue: '', + type: 'string', + table: { + category: 'RadioGroup.Validation' + } + }, + variant: { + defaultValue: 'error', + control: { + type: 'radio', + options: ['error', 'success', 'warning'] + }, + table: { + category: 'RadioGroup.Validation' + } } }, - parameters: {controls: {exclude: ['aria-labelledby', 'id', 'name', 'onChange', 'sx']}}, + parameters: {controls: {exclude: ['aria-labelledby', 'id', 'onChange', 'sx', 'name']}}, decorators: [ Story => { return ( @@ -30,40 +76,36 @@ export default { ] } as Meta -export const Basic = ({name: _name, ...args}: Args) => ( - - Choices - - - Choice one - - - - Choice two - - - - Choice three - - -) +export const Default = ({ + disabled, + required, + labelChildren, + visuallyHidden, + captionChildren, + validationChildren, + variant +}: CheckboxOrRadioGroupArgs) => { + const parentArgs = {disabled, required} + const labelArgs = {children: labelChildren, visuallyHidden} + const validationArgs = {children: validationChildren, variant} -export const WithCaptionAndValidation = ({name: _name, ...args}: Args) => ( - - Choices - You can pick any or all of these choices - - - Choice one - - - - Choice two - - - - Choice three - - Your choices are wrong - -) + return ( + + {labelArgs.children && } + {captionChildren && {captionChildren}} + + + Choice one + + + + Choice two + + + + Choice three + + {validationArgs.children && } + + ) +} diff --git a/src/stories/RadioGroup/fixtures.stories.tsx b/src/stories/RadioGroup/fixtures.stories.tsx index cb6b2b62d36..9f616a3d709 100644 --- a/src/stories/RadioGroup/fixtures.stories.tsx +++ b/src/stories/RadioGroup/fixtures.stories.tsx @@ -1,22 +1,69 @@ import React from 'react' import {Meta} from '@storybook/react' -import {BaseStyles, Box, Radio, RadioGroup, FormControl, ThemeProvider} from '../../' -import {ComponentProps} from '../../utils/types' +import {BaseStyles, Box, RadioGroup, FormControl, ThemeProvider, Radio} from '../../' +import {CheckboxOrRadioGroupArgs} from '../../utils/story-helpers' -type Args = ComponentProps +const excludedControlKeys = ['aria-labelledby', 'id', 'name', 'onChange', 'sx', 'visuallyHidden'] export default { title: 'Forms/RadioGroup/fixtures', component: RadioGroup, argTypes: { + // RadioGroup disabled: { - defaultValue: false + defaultValue: false, + type: 'boolean' }, required: { - defaultValue: false + defaultValue: false, + type: 'boolean' + }, + + // RadioGroup.Label + labelChildren: { + defaultValue: 'Choices', + type: 'string', + table: { + category: 'RadioGroup.Label' + } + }, + visuallyHidden: { + defaultValue: false, + type: 'boolean', + table: { + category: 'RadioGroup.Label' + } + }, + + // RadioGroup.Caption + captionChildren: { + defaultValue: '', + type: 'string', + table: { + category: 'RadioGroup.Caption' + } + }, + + // RadioGroup.Validation + validationChildren: { + defaultValue: '', + type: 'string', + table: { + category: 'RadioGroup.Validation' + } + }, + variant: { + defaultValue: 'error', + control: { + type: 'radio', + options: ['error', 'success', 'warning'] + }, + table: { + category: 'RadioGroup.Validation' + } } }, - parameters: {controls: {exclude: ['aria-labelledby', 'id', 'name', 'onChange', 'sx']}}, + parameters: {controls: {exclude: excludedControlKeys}}, decorators: [ Story => { return ( @@ -30,20 +77,68 @@ export default { ] } as Meta -export const WithExternalLabel = ({name: _name, ...args}: Args) => ( - <> - - Choices - - +export const WithExternalLabel = ({ + disabled, + required, + labelChildren, + captionChildren, + validationChildren, + variant +}: CheckboxOrRadioGroupArgs) => { + const parentArgs = {disabled, required} + const validationArgs = {children: validationChildren, variant} + + return ( + <> + + {labelChildren} {parentArgs.required && '*'} + + + {captionChildren && {captionChildren}} + + + Choice one + + + + Choice two + + + + Choice three + + {validationArgs.children && } + + + ) +} +WithExternalLabel.parameters = {controls: {exclude: [...excludedControlKeys, 'visuallyHidden']}} + +export const WithHiddenLabel = ({ + disabled, + required, + labelChildren, + visuallyHidden, + captionChildren, + validationChildren, + variant +}: CheckboxOrRadioGroupArgs) => { + const parentArgs = {disabled, required} + const labelArgs = {children: labelChildren, visuallyHidden} + const validationArgs = {children: validationChildren, variant} + + return ( + + {labelArgs.children && } + {captionChildren && {captionChildren}} Choice one @@ -56,24 +151,10 @@ export const WithExternalLabel = ({name: _name, ...args}: Args) => ( Choice three + {validationArgs.children && } - -) - -export const WithHiddenLabel = ({name: _name, ...args}: Args) => ( - - Choices - - - Choice one - - - - Choice two - - - - Choice three - - -) + ) +} +WithHiddenLabel.args = { + visuallyHidden: true +} diff --git a/src/stories/Select.stories.tsx b/src/stories/Select.stories.tsx index 48eae5e8c89..aa43d7d9934 100644 --- a/src/stories/Select.stories.tsx +++ b/src/stories/Select.stories.tsx @@ -1,11 +1,17 @@ import React from 'react' import {Meta} from '@storybook/react' -import {BaseStyles, Select, ThemeProvider, FormControl} from '..' +import {BaseStyles, Select, ThemeProvider, FormControl, Box} from '..' import {SelectProps} from '../Select' +import { + FormControlArgs, + formControlArgTypes, + getFormControlArgsByChildComponent, + inputWrapperArgTypes +} from '../utils/story-helpers' export default { - title: 'Forms/Select', + title: 'Forms/Form Controls/Select', component: Select, decorators: [ Story => { @@ -19,90 +25,73 @@ export default { } ], argTypes: { - block: { - defaultValue: false, - control: { - type: 'boolean' - } - }, - contrast: { - defaultValue: false, - control: { - type: 'boolean' - } - }, - disabled: { - defaultValue: false, - control: { - type: 'boolean' - } - }, required: { defaultValue: false, - control: { - type: 'boolean' - } - }, - sx: { - table: { - disable: true - } - }, - size: { - defaultValue: 'medium', - options: ['small', 'medium', 'large'], - control: {type: 'radio'} + type: 'boolean' }, - validationStatus: { - options: ['success', 'warning', 'error', undefined], - control: {type: 'radio'} - } + ...inputWrapperArgTypes, + ...formControlArgTypes }, - parameters: {controls: {exclude: ['contrast', 'hasTrailingAction', 'monospace', 'isInputFocused']}} + parameters: { + controls: { + exclude: ['contrast', 'hasTrailingAction', 'monospace', 'isInputFocused', 'sx', 'size'] + } + } } as Meta -export const Default = (args: SelectProps) => ( - - Choice - - -) +export const Default = (args: FormControlArgs) => { + const {parentArgs, labelArgs, captionArgs, validationArgs} = getFormControlArgsByChildComponent(args) -export const WithOptionGroups = (args: SelectProps) => ( - - Choice - - -) + return ( + + + + + {captionArgs.children && } + {validationArgs.children && validationArgs.variant && ( + + )} + + + ) +} +Default.args = { + labelChildren: 'Choice' +} -export const WithAPlaceholder = (args: SelectProps) => ( - - Choice - - -) +export const WithOptionGroups = (args: FormControlArgs) => { + const {parentArgs, labelArgs, captionArgs, validationArgs} = getFormControlArgsByChildComponent(args) + return ( + + + + + {captionArgs.children && } + {validationArgs.children && validationArgs.variant && ( + + )} + + + ) +} +WithOptionGroups.args = { + labelChildren: 'Choice' +} diff --git a/src/stories/TextInput.stories.tsx b/src/stories/TextInput.stories.tsx index f25182f7cf7..3132824d3a3 100644 --- a/src/stories/TextInput.stories.tsx +++ b/src/stories/TextInput.stories.tsx @@ -4,9 +4,16 @@ import {Meta} from '@storybook/react' import {BaseStyles, Box, ThemeProvider, FormControl} from '..' import TextInput, {TextInputProps} from '../TextInput' import {CalendarIcon, CheckIcon, XCircleFillIcon} from '@primer/octicons-react' +import { + FormControlArgs, + formControlArgTypes, + getFormControlArgsByChildComponent, + getTextInputArgTypes, + textInputExcludedControlKeys +} from '../utils/story-helpers' export default { - title: 'Forms/Text Input', + title: 'Forms/Form Controls/Text Input', component: TextInput, decorators: [ Story => { @@ -19,69 +26,21 @@ export default { ) } ], + parameters: {controls: {exclude: textInputExcludedControlKeys}}, argTypes: { - sx: { - table: { - disable: true - } - }, - block: { - name: 'Block', - defaultValue: false, - control: { - type: 'boolean' - } - }, - disabled: { - name: 'Disabled', - defaultValue: false, - control: { - type: 'boolean' - } - }, - loading: { - name: 'loading', - defaultValue: false, - control: { - type: 'boolean' - } - }, - loaderPosition: { - name: 'loaderPosition', - defaultValue: 'auto', - options: ['auto', 'leading', 'trailing'], - control: { - type: 'radio' - } - }, - monospace: { - name: 'Monospace', - defaultValue: false, - control: { - type: 'boolean' - } - }, - variant: { - name: 'Variants', - options: ['small', 'medium', 'large'], - control: {type: 'radio'} - }, - validationStatus: { - name: 'Validation Status', - options: ['warning', 'error', 'success', undefined], - control: {type: 'radio'} - }, - placeholder: { - name: 'Placeholder', - defaultValue: 'Hello!', + type: { + defaultValue: 'text', control: { type: 'text' } - } + }, + ...getTextInputArgTypes(), + ...formControlArgTypes } } as Meta -export const Default = (args: TextInputProps) => { +export const Default = (args: FormControlArgs) => { + const {parentArgs, labelArgs, captionArgs, validationArgs} = getFormControlArgsByChildComponent(args) const [value, setValue] = useState('') const handleChange = (event: React.ChangeEvent) => { @@ -89,16 +48,21 @@ export const Default = (args: TextInputProps) => { } return ( -
    - - Example label + + + + {captionArgs.children && } + {validationArgs.children && validationArgs.variant && ( + + )} - + ) } -export const WithLeadingVisual = (args: TextInputProps) => { +export const WithLeadingVisual = (args: FormControlArgs) => { + const {parentArgs, labelArgs, captionArgs, validationArgs} = getFormControlArgsByChildComponent(args) const [value, setValue] = useState('') const handleChange = (event: React.ChangeEvent) => { @@ -106,20 +70,29 @@ export const WithLeadingVisual = (args: TextInputProps) => { } return ( -
    - - Example label + + + + {captionArgs.children && } + {validationArgs.children && validationArgs.variant && ( + + )} Enter monies + {captionArgs.children && } + {validationArgs.children && validationArgs.variant && ( + + )} - + ) } -export const WithTrailingIcon = (args: TextInputProps) => { +export const WithTrailingIcon = (args: FormControlArgs) => { + const {parentArgs, labelArgs, captionArgs, validationArgs} = getFormControlArgsByChildComponent(args) const [value, setValue] = useState('') const handleChange = (event: React.ChangeEvent) => { @@ -127,20 +100,29 @@ export const WithTrailingIcon = (args: TextInputProps) => { } return ( -
    - - Example label + + + + {captionArgs.children && } + {validationArgs.children && validationArgs.variant && ( + + )} Enter monies + {captionArgs.children && } + {validationArgs.children && validationArgs.variant && ( + + )} - + ) } -export const WithTrailingAction = (args: TextInputProps) => { +export const WithTrailingAction = (args: FormControlArgs) => { + const {parentArgs, labelArgs, captionArgs, validationArgs} = getFormControlArgsByChildComponent(args) const [value, setValue] = useState('') const handleChange = (event: React.ChangeEvent) => { @@ -148,9 +130,9 @@ export const WithTrailingAction = (args: TextInputProps) => { } return ( -
    - - Icon action + + + { onChange={handleChange} {...args} /> + {captionArgs.children && } + {validationArgs.children && validationArgs.variant && ( + + )} - + ) } -export const WithLoadingIndicator = () => { - const [loading, setLoading] = React.useState(true) - - const toggleLoadingState = () => { - setLoading(!loading) - } - - return ( - <> - - - - -

    No visual

    - - - - - - - - - - -

    Leading visual

    - - - - - - - - - - -

    Trailing visual

    - - - - - - - - - - -

    Both visuals

    - - - - - - - - - - - ) +export const WithLoadingIndicator = (args: FormControlArgs) => ( + +

    No visual

    + + + + + + + + + + +

    Leading visual

    + + + + + + + + + + +

    Trailing visual

    + + + + + + + + + + +

    Both visuals

    + + + + + + + + + +
    +) + +WithLoadingIndicator.args = { + loading: true } - -WithLoadingIndicator.parameters = {controls: {exclude: ['loading']}} - -export const ContrastTextInput = (args: TextInputProps) => { - const [value, setValue] = useState('') - - const handleChange = (event: React.ChangeEvent) => { - setValue(event.target.value) +WithLoadingIndicator.parameters = { + controls: { + exclude: [...textInputExcludedControlKeys, 'loaderPosition', ...Object.keys(formControlArgTypes), 'children'] } - - return ( -
    - - Example label - - -
    - ) -} - -export const Password = (args: TextInputProps) => { - const [value, setValue] = useState('') - - const handleChange = (event: React.ChangeEvent) => { - setValue(event.target.value) - } - - return ( -
    - - Password - - -
    - ) -} - -export const TextInputInWarningState = (args: TextInputProps) => { - const [value, setValue] = useState('') - - const handleChange = (event: React.ChangeEvent) => { - setValue(event.target.value) - } - - return ( -
    - - Password - - -
    - ) } diff --git a/src/stories/TextInputWithTokens.stories.tsx b/src/stories/TextInputWithTokens.stories.tsx index f0d7225fa48..d94762617bb 100644 --- a/src/stories/TextInputWithTokens.stories.tsx +++ b/src/stories/TextInputWithTokens.stories.tsx @@ -5,11 +5,19 @@ import {CheckIcon, NumberIcon} from '@primer/octicons-react' import {BaseStyles, Box, FormControl, ThemeProvider} from '..' import TextInputWithTokens, {TextInputWithTokensProps} from '../TextInputWithTokens' import IssueLabelToken from '../Token/IssueLabelToken' +import { + FormControlArgs, + formControlArgTypes, + getFormControlArgsByChildComponent, + getTextInputArgTypes, + textInputExcludedControlKeys, + textInputWithTokensArgTypes +} from '../utils/story-helpers' -const excludedControls = ['tokens', 'onTokenRemove', 'tokenComponent'] +const excludedControls = ['tokens', 'onTokenRemove', 'tokenComponent', ...textInputExcludedControlKeys] export default { - title: 'Forms/Text Input with Tokens', + title: 'Forms/Form Controls/Text Input with Tokens', component: TextInputWithTokens, decorators: [ Story => { @@ -35,57 +43,9 @@ export default { } ], argTypes: { - maxHeight: { - defaultValue: undefined, - control: { - type: 'text' - } - }, - preventTokenWrapping: { - defaultValue: false, - control: { - type: 'boolean' - } - }, - loading: { - name: 'loading', - defaultValue: false, - control: { - type: 'boolean' - } - }, - loaderPosition: { - name: 'loaderPosition', - defaultValue: 'auto', - options: ['auto', 'leading', 'trailing'], - control: { - type: 'radio' - } - }, - size: { - name: 'size (token size)', - defaultValue: 'xlarge', - options: ['small', 'medium', 'large', 'xlarge'], - control: { - type: 'radio' - } - }, - hideTokenRemoveButtons: { - defaultValue: false, - control: { - type: 'boolean' - } - }, - visibleTokenCount: { - defaultValue: undefined, - control: { - type: 'number' - } - }, - validationStatus: { - options: ['warning', 'error', 'success', undefined], - control: {type: 'radio'} - } + ...getTextInputArgTypes('TextInput props'), + ...textInputWithTokensArgTypes, + ...formControlArgTypes }, parameters: {controls: {exclude: excludedControls}} } as Meta @@ -103,93 +63,108 @@ const mockTokens = [ {text: 'twentyone', id: 21} ] -export const Default = (args: TextInputWithTokensProps) => { +export const Default = (args: FormControlArgs) => { + const {parentArgs, labelArgs, captionArgs, validationArgs} = getFormControlArgsByChildComponent(args) const [tokens, setTokens] = useState([...mockTokens].slice(0, 3)) const onTokenRemove: (tokenId: string | number) => void = tokenId => { setTokens(tokens.filter(token => token.id !== tokenId)) } - return + return ( + + + + {captionArgs.children && } + {validationArgs.children && validationArgs.variant && ( + + )} + + ) } -Default.parameters = {controls: {exclude: [excludedControls, 'maxHeight']}} - -export const WithLeadingVisual = (args: TextInputWithTokensProps) => { +export const WithLeadingVisual = (args: FormControlArgs) => { + const {parentArgs, labelArgs, captionArgs, validationArgs} = getFormControlArgsByChildComponent(args) const [tokens, setTokens] = useState([...mockTokens].slice(0, 3)) const onTokenRemove: (tokenId: string | number) => void = tokenId => { setTokens(tokens.filter(token => token.id !== tokenId)) } - return + return ( + + + + {captionArgs.children && } + {validationArgs.children && validationArgs.variant && ( + + )} + + ) } -WithLeadingVisual.parameters = {controls: {exclude: [excludedControls, 'maxHeight']}} - -export const WithTrailingVisual = (args: TextInputWithTokensProps) => { +export const WithTrailingVisual = (args: FormControlArgs) => { + const {parentArgs, labelArgs, captionArgs, validationArgs} = getFormControlArgsByChildComponent(args) const [tokens, setTokens] = useState([...mockTokens].slice(0, 3)) const onTokenRemove: (tokenId: string | number) => void = tokenId => { setTokens(tokens.filter(token => token.id !== tokenId)) } - return + return ( + + + + + {captionArgs.children && } + {validationArgs.children && validationArgs.variant && ( + + )} + + + ) } -WithTrailingVisual.parameters = {controls: {exclude: [excludedControls, 'maxHeight']}} - -export const WithLoadingIndicator = (args: TextInputWithTokensProps) => { +export const WithLoadingIndicator = (args: FormControlArgs) => { const [tokens, setTokens] = useState([...mockTokens].slice(0, 3)) - const [loading, setLoading] = useState(true) const onTokenRemove: (tokenId: string | number) => void = tokenId => { setTokens(tokens.filter(token => token.id !== tokenId)) } - const toggleLoadingState = () => { - setLoading(!loading) - } return ( -
    - - - - - - - No visual - - - - - Leading visual - - - - - Both visuals - - - -
    + + + No visual + + + + + Leading visual + + + + + Both visuals + + + ) } -WithLoadingIndicator.parameters = {controls: {exclude: [excludedControls, 'maxHeight', 'loading']}} +WithLoadingIndicator.args = { + loading: true +} +WithLoadingIndicator.parameters = { + controls: { + exclude: [...excludedControls, 'loaderPosition', ...Object.keys(formControlArgTypes), 'children'] + } +} -export const UsingIssueLabelTokens = (args: TextInputWithTokensProps) => { +export const UsingIssueLabelTokens = (args: FormControlArgs) => { + const {parentArgs, labelArgs, captionArgs, validationArgs} = getFormControlArgsByChildComponent(args) const [tokens, setTokens] = useState([ {text: 'enhancement', id: 1, fillColor: '#a2eeef'}, {text: 'bug', id: 2, fillColor: '#d73a4a'}, @@ -200,50 +175,54 @@ export const UsingIssueLabelTokens = (args: TextInputWithTokensProps) => { } return ( - - ) -} - -UsingIssueLabelTokens.parameters = {controls: {exclude: [excludedControls, 'maxHeight']}} - -export const MaxHeight = (args: TextInputWithTokensProps) => { - const [tokens, setTokens] = useState(mockTokens) - const onTokenRemove: (tokenId: string | number) => void = tokenId => { - setTokens(tokens.filter(token => token.id !== tokenId)) - } - - return ( - - + + + + + {captionArgs.children && } + {validationArgs.children && validationArgs.variant && ( + + )} + ) } -MaxHeight.storyName = 'maxHeight 200px' - -export const Unstyled = (args: TextInputWithTokensProps) => { +export const Unstyled = (args: FormControlArgs) => { + const {parentArgs, labelArgs, captionArgs, validationArgs} = getFormControlArgsByChildComponent(args) const [tokens, setTokens] = useState([...mockTokens].slice(0, 2)) const onTokenRemove: (tokenId: string | number) => void = tokenId => { setTokens(tokens.filter(token => token.id !== tokenId)) } return ( - + + + + + {captionArgs.children && } + {validationArgs.children && validationArgs.variant && ( + + )} + + ) } -Unstyled.parameters = {controls: {exclude: [excludedControls, 'maxHeight', 'validationStatus']}} +Unstyled.parameters = { + controls: {exclude: [...excludedControls, 'maxHeight', 'validationStatus']} +} diff --git a/src/stories/Textarea.stories.tsx b/src/stories/Textarea.stories.tsx index 44c93c637ec..5c6883b9cbc 100644 --- a/src/stories/Textarea.stories.tsx +++ b/src/stories/Textarea.stories.tsx @@ -1,22 +1,12 @@ -import React, {ReactNode} from 'react' +import React from 'react' import {Meta} from '@storybook/react' -import styled from 'styled-components' -import {BaseStyles, Box, Textarea, TextareaProps, ThemeProvider} from '..' - -const StyledForm = styled.form` - padding: 20px; -` - -type LabelProps = {children: ReactNode; htmlFor: string} -const Label = ({children, htmlFor}: LabelProps) => ( - - {children} - -) +import {BaseStyles, Box, FormControl, Textarea, TextareaProps, ThemeProvider} from '..' +import {DEFAULT_TEXTAREA_COLS, DEFAULT_TEXTAREA_RESIZE, DEFAULT_TEXTAREA_ROWS} from '../Textarea' +import {FormControlArgs, formControlArgTypes, getFormControlArgsByChildComponent} from '../utils/story-helpers' export default { - title: 'Forms/Textarea', + title: 'Forms/Form Controls', component: Textarea, decorators: [ Story => { @@ -30,60 +20,55 @@ export default { } ], argTypes: { + block: { + defaultValue: false, + control: {type: 'boolean'} + }, + cols: { + defaultValue: DEFAULT_TEXTAREA_COLS, + control: {type: 'number'} + }, + disabled: { + defaultValue: false, + control: {type: 'boolean'} + }, + resize: { + defaultValue: DEFAULT_TEXTAREA_RESIZE, + options: ['none', 'both', 'horizontal', 'vertical'], + control: {type: 'radio'} + }, + rows: { + defaultValue: DEFAULT_TEXTAREA_ROWS, + control: {type: 'number'} + }, sx: { table: { disable: true } }, validationStatus: { - name: 'Validation Status', defaultValue: undefined, - options: ['success', 'error', undefined], + options: ['error', 'success', 'warning'], control: {type: 'radio'} }, - disabled: { - name: 'Disabled', - defaultValue: false, - control: { - type: 'boolean' - } - } + ...formControlArgTypes } } as Meta -export const Default = (args: TextareaProps) => { +export const TextareaStory = (args: FormControlArgs) => { + const {parentArgs, labelArgs, captionArgs, validationArgs} = getFormControlArgsByChildComponent(args) return ( - <> - - + + +