From 54232ae573d4aa72ad72ae7d1f8bf535c6ac5afc Mon Sep 17 00:00:00 2001 From: Jesse Attas Date: Fri, 24 May 2024 15:48:19 -0500 Subject: [PATCH] Improve API documentation for input components (#2132) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # Pull Request ## ๐Ÿคจ Rationale Ongoing efforts towards #824. This PR covers input components: - select - combobox - radio group - text area - text field - number field - checkbox - switch ## ๐Ÿ‘ฉโ€๐Ÿ’ป Implementation Generally follow patterns from previous PRs like #2126 and #2117. A few interesting notes: 1. components have different behaviors regarding whether they sync their `value` property to an attribute. I documented the behavior I observed by adding a new table category for properties that don't have attributes (also used by checkbox indeterminate property). 2. not yet documenting a recommendation to use form association / CVAs instead of change events and value properties. Until I do so, the `placeholder` docs for select are outside any table category. 3. added another new table category for localizable strings. 4. not yet documenting list option API in the select and combobox stories. We (probably Meyer) are going to tackle this in a follow up after #2111. ## ๐Ÿงช Testing ## โœ… Checklist - [x] I have updated the project documentation to reflect my changes or determined no changes are needed. --------- Co-authored-by: m-akinc <7282195+m-akinc@users.noreply.github.com> --- .../storybook/src/docs/component-apis.mdx | 2 +- .../src/nimble/checkbox/checkbox.mdx | 5 ++ .../src/nimble/checkbox/checkbox.stories.ts | 32 ++++++- .../src/nimble/combobox/combobox.mdx | 5 ++ .../src/nimble/combobox/combobox.stories.ts | 58 ++++++++++-- .../base/label-user-stories-utils.ts | 6 +- .../src/nimble/number-field/number-field.mdx | 5 ++ .../number-field/number-field.stories.ts | 53 +++++++++-- .../src/nimble/radio-group/radio-group.mdx | 14 ++- .../nimble/radio-group/radio-group.stories.ts | 88 +++++++++++++++---- .../storybook/src/nimble/select/select.mdx | 5 ++ .../src/nimble/select/select.stories.ts | 70 +++++++++------ .../storybook/src/nimble/switch/switch.mdx | 5 ++ .../src/nimble/switch/switch.stories.ts | 33 ++++++- .../src/nimble/text-area/text-area.mdx | 5 ++ .../src/nimble/text-area/text-area.stories.ts | 70 +++++++++++---- .../src/nimble/text-field/text-field.mdx | 5 ++ .../nimble/text-field/text-field.stories.ts | 81 ++++++++++++++--- .../toggle-button/toggle-button.stories.ts | 3 +- packages/storybook/src/utilities/storybook.ts | 12 +++ 20 files changed, 457 insertions(+), 100 deletions(-) diff --git a/packages/storybook/src/docs/component-apis.mdx b/packages/storybook/src/docs/component-apis.mdx index ea1a7970e0..23c5727154 100644 --- a/packages/storybook/src/docs/component-apis.mdx +++ b/packages/storybook/src/docs/component-apis.mdx @@ -45,7 +45,7 @@ Or in a named slot: ## Attributes and Properties Configure components from HTML using attributes or from code using properties. Attributes and properties typically -correspond to each other one-to-one; Nimble documentation refers to the attribute name. +correspond to each other one-to-one; Nimble documentation refers to the attribute name unless otherwise specified. ### Attributes diff --git a/packages/storybook/src/nimble/checkbox/checkbox.mdx b/packages/storybook/src/nimble/checkbox/checkbox.mdx index 51ebc12fe1..f74563e147 100644 --- a/packages/storybook/src/nimble/checkbox/checkbox.mdx +++ b/packages/storybook/src/nimble/checkbox/checkbox.mdx @@ -1,6 +1,7 @@ import { Canvas, Meta, Controls, Title } from '@storybook/blocks'; import { NimbleCheckbox } from './checkbox.react'; import * as checkboxStories from './checkbox.stories'; +import ComponentApisLink from '../../docs/component-apis-link.mdx'; @@ -8,7 +9,11 @@ import * as checkboxStories from './checkbox.stories'; Per [W3C](https://www.w3.org/WAI/ARIA/apg/patterns/checkbox/) - The dual-state checkbox is the most common type, as it allows the user to toggle between two choices: checked and not checked. <Canvas of={checkboxStories.checkbox} /> + +## API + <Controls of={checkboxStories.checkbox} /> +<ComponentApisLink /> {/* ## Styling */} diff --git a/packages/storybook/src/nimble/checkbox/checkbox.stories.ts b/packages/storybook/src/nimble/checkbox/checkbox.stories.ts index 77d12fb9b0..9fc08aa0bd 100644 --- a/packages/storybook/src/nimble/checkbox/checkbox.stories.ts +++ b/packages/storybook/src/nimble/checkbox/checkbox.stories.ts @@ -2,13 +2,15 @@ import { html } from '@microsoft/fast-element'; import { withActions } from '@storybook/addon-actions/decorator'; import type { HtmlRenderer, Meta, StoryObj } from '@storybook/html'; import { checkboxTag } from '../../../../nimble-components/src/checkbox'; -import { createUserSelectedThemeStory } from '../../utilities/storybook'; +import { apiCategory, createUserSelectedThemeStory, disabledDescription, slottedLabelDescription } from '../../utilities/storybook'; interface CheckboxArgs { label: string; checked: boolean; + checkedProperty: undefined; indeterminate: boolean; disabled: boolean; + change: undefined; } const metadata: Meta<CheckboxArgs> = { @@ -29,13 +31,37 @@ const metadata: Meta<CheckboxArgs> = { </${checkboxTag}> `), argTypes: { + label: { + name: 'default', + description: slottedLabelDescription({ componentName: 'checkbox' }), + table: { category: apiCategory.slots } + }, + checked: { + description: 'Whether the checkbox is initially checked. Setting this attribute after the checkbox initializes will not affect its visual state. Note that the `checked` property behaves differently than the `checked` attribute.', + table: { category: apiCategory.attributes } + }, + checkedProperty: { + name: 'checked', + description: 'Whether the checkbox is checked. Setting this property affects the checkbox visual state and interactively changing the checkbox state affects this property. Note that the `checked` property behaves differently than the `checked` attribute.', + table: { category: apiCategory.nonAttributeProperties } + }, indeterminate: { description: `Whether the checkbox is in the indeterminate (i.e. partially checked) state. Configured programmatically, not by attribute. <details> <summary>Usage details</summary> -The \`indeterminate\` state is not automatically changed when the user changes the \`checked\` state. Client applications that use \`indeterminate\` state are responsible for subscribing to the \`change\` event to respond to this situation. -</details>` +The \`indeterminate\` state is not automatically changed when the user interactively changes the checked state. Client applications that use \`indeterminate\` state are responsible for subscribing to the \`change\` event to respond to this situation. +</details>`, + table: { category: apiCategory.nonAttributeProperties } + }, + disabled: { + description: disabledDescription({ componentName: 'checkbox' }), + table: { category: apiCategory.attributes } + }, + change: { + description: 'Event emitted when the user checks or unchecks the checkbox.', + table: { category: apiCategory.events }, + control: false } }, args: { diff --git a/packages/storybook/src/nimble/combobox/combobox.mdx b/packages/storybook/src/nimble/combobox/combobox.mdx index 905f6f331a..b97c3e7a16 100644 --- a/packages/storybook/src/nimble/combobox/combobox.mdx +++ b/packages/storybook/src/nimble/combobox/combobox.mdx @@ -1,5 +1,6 @@ import { Controls, Canvas, Meta, Title, Description } from '@storybook/blocks'; import * as comboboxStories from './combobox.stories'; +import ComponentApisLink from '../../docs/component-apis-link.mdx'; import { listOptionTag } from '../../../../nimble-components/src/list-option/'; <Meta of={comboboxStories} /> @@ -7,7 +8,11 @@ import { listOptionTag } from '../../../../nimble-components/src/list-option/'; <Description of={comboboxStories} /> <Canvas of={comboboxStories.underlineCombobox} /> + +## API + <Controls of={comboboxStories.underlineCombobox} /> +<ComponentApisLink /> {/* ## Styling */} diff --git a/packages/storybook/src/nimble/combobox/combobox.stories.ts b/packages/storybook/src/nimble/combobox/combobox.stories.ts index 3cb2be4ed1..9842dce5cf 100644 --- a/packages/storybook/src/nimble/combobox/combobox.stories.ts +++ b/packages/storybook/src/nimble/combobox/combobox.stories.ts @@ -10,8 +10,16 @@ import { DropdownPosition } from '../../../../nimble-components/src/patterns/dropdown/types'; import { + apiCategory, + appearanceDescription, createUserSelectedThemeStory, - disableStorybookZoomTransform + disableStorybookZoomTransform, + disabledDescription, + dropdownPositionDescription, + errorTextDescription, + errorVisibleDescription, + optionsDescription, + placeholderDescription } from '../../utilities/storybook'; interface ComboboxArgs { @@ -24,6 +32,8 @@ interface ComboboxArgs { currentValue: string; appearance: string; placeholder: string; + change: undefined; + input: undefined; } interface OptionArgs { @@ -117,22 +127,43 @@ const metadata: Meta<ComboboxArgs> = { description: `- inline: Automatically matches the first option that matches the start of the entered text. - list: Filters the dropdown to options that start with the entered text. - both: Automatically matches and filters list to options that start with the entered text. -- none: No autocomplete (default).` +- none: No autocomplete (default).`, + table: { category: apiCategory.attributes } }, dropDownPosition: { + name: 'position', options: [DropdownPosition.above, DropdownPosition.below], - control: { type: 'select' } + control: { type: 'select' }, + description: dropdownPositionDescription({ componentName: 'combobox' }), + table: { category: apiCategory.attributes } }, appearance: { options: Object.values(DropdownAppearance), - control: { type: 'radio' } + control: { type: 'radio' }, + description: appearanceDescription({ componentName: 'combobox' }), + table: { category: apiCategory.attributes } + }, + disabled: { + description: disabledDescription({ componentName: 'combobox' }), + table: { category: apiCategory.attributes } }, errorText: { - description: - 'A message to be displayed when the text field is in the invalid state explaining why the value is invalid' + name: 'error-text', + description: errorTextDescription, + table: { category: apiCategory.attributes } + }, + errorVisible: { + name: 'error-visible', + description: errorVisibleDescription, + table: { category: apiCategory.attributes } + }, + placeholder: { + description: placeholderDescription({ componentName: 'combobox' }), + table: { category: apiCategory.attributes } }, optionsType: { - name: 'options', + name: 'default', + description: optionsDescription, options: Object.values(ExampleOptionsType), control: { type: 'radio', @@ -141,7 +172,18 @@ const metadata: Meta<ComboboxArgs> = { [ExampleOptionsType.wideOptions]: 'Wide options', [ExampleOptionsType.manyOptions]: 'Many options' } - } + }, + table: { category: apiCategory.slots } + }, + change: { + description: 'Emitted when the user changes the selected option, either by selecting an item from the dropdown or by committing a typed value.', + table: { category: apiCategory.events }, + control: false + }, + input: { + description: 'Emitted when the user types in the combobox. Use this event if you need to update the list of options based on the text input.', + table: { category: apiCategory.events }, + control: false } }, args: { diff --git a/packages/storybook/src/nimble/label-provider/base/label-user-stories-utils.ts b/packages/storybook/src/nimble/label-provider/base/label-user-stories-utils.ts index c8910e30f6..11fe9458e6 100644 --- a/packages/storybook/src/nimble/label-provider/base/label-user-stories-utils.ts +++ b/packages/storybook/src/nimble/label-provider/base/label-user-stories-utils.ts @@ -1,5 +1,6 @@ import type { Meta } from '@storybook/html'; import type { DesignToken } from '@microsoft/fast-foundation'; +import { apiCategory } from '../../../utilities/storybook'; export interface LabelUserArgs { usedLabels: null; @@ -26,8 +27,9 @@ export function addLabelUseMetadata<TArgs extends LabelUserArgs>( description: `Label Provider:\`${labelProviderTag}\` ${tokenContent} -See the "Tokens/Label Providers" docs page for more information. +See the [Tokens/Label Providers docs page](./?path=/docs/tokens-label-providers--docs) for more information. `, - control: false + control: false, + table: { category: apiCategory.localizableLabels } }; } diff --git a/packages/storybook/src/nimble/number-field/number-field.mdx b/packages/storybook/src/nimble/number-field/number-field.mdx index 36afea60a9..9cd985ff36 100644 --- a/packages/storybook/src/nimble/number-field/number-field.mdx +++ b/packages/storybook/src/nimble/number-field/number-field.mdx @@ -1,6 +1,7 @@ import { Canvas, Meta, Controls, Title } from '@storybook/blocks'; import { NimbleNumberField } from './number-field.react'; import * as numberFieldStories from './number-field.stories'; +import ComponentApisLink from '../../docs/component-apis-link.mdx'; <Meta of={numberFieldStories} /> <Title of={numberFieldStories} /> @@ -8,7 +9,11 @@ import * as numberFieldStories from './number-field.stories'; Similar to a single line text box but only used for numeric data. The controls allow the user to increment and decrement the value. <Canvas of={numberFieldStories.underlineNumberField} /> + +## API + <Controls of={numberFieldStories.underlineNumberField} /> +<ComponentApisLink /> {/* ## Styling */} diff --git a/packages/storybook/src/nimble/number-field/number-field.stories.ts b/packages/storybook/src/nimble/number-field/number-field.stories.ts index 0a7f83867e..c6c9e43444 100644 --- a/packages/storybook/src/nimble/number-field/number-field.stories.ts +++ b/packages/storybook/src/nimble/number-field/number-field.stories.ts @@ -12,7 +12,7 @@ import { addLabelUseMetadata, type LabelUserArgs } from '../label-provider/base/label-user-stories-utils'; -import { createUserSelectedThemeStory } from '../../utilities/storybook'; +import { apiCategory, appearanceDescription, createUserSelectedThemeStory, disabledDescription, errorTextDescription, errorVisibleDescription, slottedLabelDescription } from '../../utilities/storybook'; interface NumberFieldArgs extends LabelUserArgs { label: string; @@ -25,6 +25,8 @@ interface NumberFieldArgs extends LabelUserArgs { disabled: boolean; errorVisible: boolean; errorText: string; + change: undefined; + input: undefined; } const metadata: Meta<NumberFieldArgs> = { @@ -52,30 +54,63 @@ const metadata: Meta<NumberFieldArgs> = { </${numberFieldTag}> `), argTypes: { + label: { + name: 'default', + description: `${slottedLabelDescription({ componentName: 'number field' })}`, + table: { category: apiCategory.slots } + }, + value: { + description: 'The number displayed in the number field. Note that the property value is not synced to an attribute.', + table: { category: apiCategory.nonAttributeProperties } + }, appearance: { options: Object.values(NumberFieldAppearance), - control: { type: 'radio' } + control: { type: 'radio' }, + description: appearanceDescription({ componentName: 'number field' }), + table: { category: apiCategory.attributes } + }, + disabled: { + description: disabledDescription({ componentName: 'number field' }), + table: { category: apiCategory.attributes } }, step: { description: - 'The amount to increase or decrease the value when a step button is pressed.' + 'The amount to increase or decrease the value when a step button is pressed.', + table: { category: apiCategory.attributes } }, hideStep: { name: 'hide-step', description: - 'Configures the visibility of the increment and decrement step buttons. Consider hiding the buttons if the input values will commonly have varied levels of precision (for example both integers and decimal numbers).' + 'Configures the visibility of the increment and decrement step buttons. Consider hiding the buttons if the input values will commonly have varied levels of precision (for example both integers and decimal numbers).', + table: { category: apiCategory.attributes } }, min: { - description: 'The minimum value that can be set.' + description: 'The minimum value that can be set.', + table: { category: apiCategory.attributes } }, max: { - description: 'The maximum value that can be set.' + description: 'The maximum value that can be set.', + table: { category: apiCategory.attributes } + }, + errorVisible: { + name: 'error-visible', + description: errorVisibleDescription, + table: { category: apiCategory.attributes } }, errorText: { - name: 'error-text' + name: 'error-text', + description: errorTextDescription, + table: { category: apiCategory.attributes } }, - errorVisible: { - name: 'error-visible' + change: { + description: 'Event emitted when the user commits a new value to the number field.', + table: { category: apiCategory.events }, + control: false + }, + input: { + description: 'Event emitted on each user keystroke within the number field.', + table: { category: apiCategory.events }, + control: false } }, args: { diff --git a/packages/storybook/src/nimble/radio-group/radio-group.mdx b/packages/storybook/src/nimble/radio-group/radio-group.mdx index 544a458456..4757996328 100644 --- a/packages/storybook/src/nimble/radio-group/radio-group.mdx +++ b/packages/storybook/src/nimble/radio-group/radio-group.mdx @@ -1,5 +1,6 @@ import { Controls, Canvas, Meta, Title } from '@storybook/blocks'; import * as radioGroupStories from './radio-group.stories'; +import ComponentApisLink from '../../docs/component-apis-link.mdx'; <Meta of={radioGroupStories} /> <Title of={radioGroupStories} /> @@ -7,7 +8,16 @@ import * as radioGroupStories from './radio-group.stories'; Per [W3C](https://www.w3.org/WAI/ARIA/apg/patterns/radio/) - A radio group is a set of checkable buttons, known as radio buttons, where no more than one of the buttons can be checked at a time. Some implementations may initialize the set with all buttons in the unchecked state in order to force the user to check one of the buttons before moving past a certain point in the workflow. <Canvas of={radioGroupStories.radioGroup} /> + +## API + <Controls of={radioGroupStories.radioGroup} /> +<ComponentApisLink /> + +### Radio + +<Canvas of={radioGroupStories.radio} /> +<Controls of={radioGroupStories.radio} /> {/* ## Styling */} @@ -17,7 +27,9 @@ Per [W3C](https://www.w3.org/WAI/ARIA/apg/patterns/radio/) - A radio group is a ### Angular Usage -The Angular CVA for the radio button group ignores the value of `callSetDisabledState` configured on the form module. +When using radio buttons in an Angular form, you must explicitly set either `name` or `formControlName` on each radio button. In that scenario, setting `name` on the group is ineffective. + +The Angular control value accessor for the radio button group ignores the value of `callSetDisabledState` configured on the form module. Instead, it always uses the default value of `'always'`. {/* ## Accessibility */} diff --git a/packages/storybook/src/nimble/radio-group/radio-group.stories.ts b/packages/storybook/src/nimble/radio-group/radio-group.stories.ts index ee53aefbd3..8fa5a613cf 100644 --- a/packages/storybook/src/nimble/radio-group/radio-group.stories.ts +++ b/packages/storybook/src/nimble/radio-group/radio-group.stories.ts @@ -4,7 +4,7 @@ import { withActions } from '@storybook/addon-actions/decorator'; import type { HtmlRenderer, Meta, StoryObj } from '@storybook/html'; import { radioTag } from '../../../../nimble-components/src/radio'; import { radioGroupTag } from '../../../../nimble-components/src/radio-group'; -import { createUserSelectedThemeStory } from '../../utilities/storybook'; +import { apiCategory, createUserSelectedThemeStory, disabledDescription, slottedLabelDescription } from '../../utilities/storybook'; interface RadioGroupArgs { label: string; @@ -12,10 +12,26 @@ interface RadioGroupArgs { disabled: boolean; name: string; value: string; + buttons: undefined; + change: undefined; +} + +interface RadioArgs { + label: string; + value: string; + disabled: boolean; + name: string; } const metadata: Meta<RadioGroupArgs> = { title: 'Components/Radio Group', +}; + +export default metadata; + +const nameDescription = 'Radio buttons whose values are mutually exclusive should set the same `name` attribute. Setting the name on the group sets it on all child radio buttons. When using radio buttons in an Angular form, you must explicitly set either `name` or `formControlName` on each radio button. In that scenario, setting `name` on the group is ineffective.'; + +export const radioGroup: StoryObj<RadioGroupArgs> = { decorators: [withActions<HtmlRenderer>], parameters: { actions: { @@ -47,32 +63,72 @@ const metadata: Meta<RadioGroupArgs> = { options: ['none', 'apple', 'mango', 'orange'], control: { type: 'radio' - } + }, + description: 'The currently selected radio button. Each button should specify its unique value using its `value` attribute.', + table: { category: apiCategory.attributes } }, label: { description: - 'You must provide a `label` element with `slot="label"` as content of the `nimble-radio-group`.' + 'A `label` element containing text that describes the group of options.', + table: { category: apiCategory.slots } }, orientation: { options: Object.values(Orientation), control: { - type: 'radio', - labels: { - [Orientation.horizontal]: 'Horizontal', - [Orientation.vertical]: 'Vertical' - } + type: 'radio' }, - table: { - defaultValue: { summary: 'Horizontal' } - } + description: 'The orientation of the radio buttons.', + table: { category: apiCategory.attributes } + }, + disabled: { + description: disabledDescription({ componentName: 'radio group' }), + table: { category: apiCategory.attributes } }, name: { - description: - 'Radio buttons whose values are mutually exclusive should set the same `name` attribute. Setting the name on the group sets it on all child radio buttons. When using radio buttons in an Angular form, you must explicitly set either `name` or `formControlName` on each radio button. In that scenario, setting `name` on the group is ineffective.' + description: nameDescription, + table: { category: apiCategory.attributes } + }, + buttons: { + name: 'default', + description: `The \`${radioTag}\` elements to display in the group.`, + control: false, + table: { category: apiCategory.slots } + }, + change: { + description: 'Event emitted when the user selects a new value in the radio group.', + table: { category: apiCategory.events }, + control: false } } }; -export default metadata; - -export const radioGroup: StoryObj<RadioGroupArgs> = {}; +export const radio: StoryObj<RadioArgs> = { + render: createUserSelectedThemeStory(html` + <${radioTag} value="${x => x.value}" ?disabled="${x => x.disabled}">${x => x.label}</${radioTag}> + `), + args: { + disabled: false, + name: 'fruit', + label: 'Apple', + value: 'none' + }, + argTypes: { + value: { + control: false, + description: 'The value of the radio button. Used by the radio group `value` attribute to determine the selected radio button.', + table: { category: apiCategory.attributes } + }, + label: { + description: slottedLabelDescription({ componentName: 'radio button' }), + table: { category: apiCategory.slots } + }, + disabled: { + description: disabledDescription({ componentName: 'radio button' }), + table: { category: apiCategory.attributes } + }, + name: { + description: nameDescription, + table: { category: apiCategory.attributes } + }, + } +}; diff --git a/packages/storybook/src/nimble/select/select.mdx b/packages/storybook/src/nimble/select/select.mdx index 7cc9b5e2c2..cbcffab33e 100644 --- a/packages/storybook/src/nimble/select/select.mdx +++ b/packages/storybook/src/nimble/select/select.mdx @@ -1,6 +1,7 @@ import { Canvas, Meta, Controls, Title } from '@storybook/blocks'; import { NimbleSelect } from './select.react'; import * as selectStories from './select.stories'; +import ComponentApisLink from '../../docs/component-apis-link.mdx'; import { listOptionTag } from '../../../../nimble-components/src/list-option'; <Meta of={selectStories} /> @@ -9,7 +10,11 @@ import { listOptionTag } from '../../../../nimble-components/src/list-option'; Select is a control for selecting amongst a set of options. Its value comes from the `value` of the currently selected <Tag name={listOptionTag}/>, or, if no value exists for that option, the option's content. Upon clicking on the element, the other options are visible. <Canvas of={selectStories.underlineSelect} /> + +## API + <Controls of={selectStories.underlineSelect} /> +<ComponentApisLink /> {/* ## Styling */} diff --git a/packages/storybook/src/nimble/select/select.stories.ts b/packages/storybook/src/nimble/select/select.stories.ts index edaf4264d9..eea0bfb5df 100644 --- a/packages/storybook/src/nimble/select/select.stories.ts +++ b/packages/storybook/src/nimble/select/select.stories.ts @@ -8,8 +8,15 @@ import { ExampleOptionsType } from '../../../../nimble-components/src/select/tes import { DropdownAppearance } from '../../../../nimble-components/src/patterns/dropdown/types'; import { + apiCategory, + appearanceDescription, createUserSelectedThemeStory, - disableStorybookZoomTransform + disableStorybookZoomTransform, + disabledDescription, + dropdownPositionDescription, + errorTextDescription, + errorVisibleDescription, + optionsDescription } from '../../utilities/storybook'; interface SelectArgs { @@ -22,6 +29,7 @@ interface SelectArgs { filterMode: keyof typeof FilterMode; placeholder: boolean; clearable: boolean; + change: undefined; } interface OptionArgs { @@ -67,32 +75,20 @@ const optionSets = { [ExampleOptionsType.manyOptions]: manyOptions } as const; -const dropdownPositionDescription = ` -The \`dropDownPosition\` attribute controls the position of the dropdown relative to the \`Select\`. The default is \`below\`, which will display the dropdown below the \`Select\`. The \`above\` setting will display the dropdown above the \`Select\`. -`; - -const appearanceDescription = ` -This attribute affects the appearance of the \`Select\`. The default appearance is \`underline\`, which displays a line beneath the selected value. The \`outline\` appearance displays a border around the entire component. The \`block\` appearance applies a background for the entire component. -`; - -const errorTextDescription = ` -A message to be displayed when the text field is in the invalid state explaining why the value is invalid. -`; - const filterModeDescription = ` -This attribute controls the filtering behavior of the \`Select\`. The default of \`none\` results in a dropdown with no input for filtering. A non-'none' setting results in a search input placed at the top or the bottom of the dropdown when opened (depending on where the popup is shown relative to the component). The \`standard\` setting will perform a case-insensitive and diacritic-insensitive filtering of the available options anywhere within the text of each option. +Controls the filtering behavior of the select. The default of \`none\` results in a dropdown with no input for filtering. A non-'none' setting results in a search input placed at the top or the bottom of the dropdown when opened (depending on where the dropdown is shown relative to the component). The \`standard\` setting will perform a case-insensitive and diacritic-insensitive filtering of the available options anywhere within the text of each option. -It is recommended that if the \`Select\` has 15 or fewer options that you use the \`none\` setting for the \`filter-mode\`. +It is recommended that if the select has 15 or fewer options that you use the \`none\` setting for the \`filter-mode\`. `; const placeholderDescription = ` -To display placeholder text within the \`Select\` you must provide an option that has the \`disabled\`, \`selected\` and \`hidden\` attributes set. This option will not be available in the dropdown, and its contents will be used as the placeholder text. Note that giving the placeholder an initial \`selected\` state is only necessary to display the placeholder initially. If another option is selected initially the placeholder will be displayed upon clearing the current value. +To display placeholder text within the select you must provide an option that has the \`disabled\`, \`selected\` and \`hidden\` attributes set. This option will not be available in the dropdown, and its contents will be used as the placeholder text. Note that giving the placeholder an initial \`selected\` state is only necessary to display the placeholder initially. If another option is selected initially the placeholder will be displayed upon clearing the current value. -Any \`Select\` without a default selected option should provide placeholder text. Placeholder text should always follow the pattern "Select [thing(s)]", for example "Select country". Use sentence casing and don't include punctuation at the end of the prompt. +Any select without a default selected option should provide placeholder text. Placeholder text should always follow the pattern "Select [thing(s)]", for example "Select country". Use sentence casing and don't include punctuation at the end of the prompt. `; const clearableDescription = ` -When the \`clearable\` attribute is set, a clear button will be displayed in the \`Select\` when a value is selected. Clicking the clear button will clear the selected value and display the placeholder text, if available, or will result in a blank display. +When the \`clearable\` attribute is set, a clear button will be displayed in the select when a value is selected. Clicking the clear button will clear the selected value and display the placeholder text, if available, or will result in a blank display. `; const metadata: Meta<SelectArgs> = { @@ -139,38 +135,52 @@ const metadata: Meta<SelectArgs> = { `), argTypes: { dropDownPosition: { + name: 'position', options: ['above', 'below'], control: { type: 'select' }, - description: dropdownPositionDescription + description: dropdownPositionDescription({ componentName: 'select' }), + table: { category: apiCategory.attributes } }, appearance: { options: Object.values(DropdownAppearance), control: { type: 'radio' }, - description: appearanceDescription + description: appearanceDescription({ componentName: 'select' }), + table: { category: apiCategory.attributes } }, filterMode: { options: Object.keys(FilterMode), control: { type: 'radio' }, name: 'filter-mode', - description: filterModeDescription + description: filterModeDescription, + table: { category: apiCategory.attributes } + }, + disabled: { + description: disabledDescription({ componentName: 'select' }), + table: { category: apiCategory.attributes } }, errorText: { name: 'error-text', - description: errorTextDescription + description: errorTextDescription, + table: { category: apiCategory.attributes } }, errorVisible: { - name: 'error-visible' + name: 'error-visible', + description: errorVisibleDescription, + table: { category: apiCategory.attributes } }, placeholder: { name: 'placeholder', - description: placeholderDescription + description: placeholderDescription, + // TODO: move this to a list-option story or create a table category to indicate there isn't a single 'placeholder' attribute }, clearable: { name: 'clearable', - description: clearableDescription + description: clearableDescription, + table: { category: apiCategory.attributes } }, optionsType: { - name: 'options', + name: 'default', + description: optionsDescription, options: Object.values(ExampleOptionsType), control: { type: 'radio', @@ -179,7 +189,13 @@ const metadata: Meta<SelectArgs> = { [ExampleOptionsType.manyOptions]: 'Many options', [ExampleOptionsType.wideOptions]: 'Wide options' } - } + }, + table: { category: apiCategory.slots } + }, + change: { + description: 'Emitted when the user changes the selected option.', + table: { category: apiCategory.events }, + control: false } }, args: { diff --git a/packages/storybook/src/nimble/switch/switch.mdx b/packages/storybook/src/nimble/switch/switch.mdx index 4a7c671fe0..1b7cbea2d9 100644 --- a/packages/storybook/src/nimble/switch/switch.mdx +++ b/packages/storybook/src/nimble/switch/switch.mdx @@ -1,6 +1,7 @@ import { Canvas, Meta, Controls, Title } from '@storybook/blocks'; import { NimbleSwitch } from './switch.react'; import * as switchStories from './switch.stories'; +import ComponentApisLink from '../../docs/component-apis-link.mdx'; <Meta of={switchStories} /> <Title of={switchStories} /> @@ -13,7 +14,11 @@ be checked or not checked and can optionally also allow for a partially checked pressed or not pressed and can optionally allow for a partially pressed state. <Canvas of={switchStories.switchStory} /> + +## API + <Controls of={switchStories.switchStory} /> +<ComponentApisLink /> {/* ## Styling */} diff --git a/packages/storybook/src/nimble/switch/switch.stories.ts b/packages/storybook/src/nimble/switch/switch.stories.ts index 1b183b32da..6c034fbae0 100644 --- a/packages/storybook/src/nimble/switch/switch.stories.ts +++ b/packages/storybook/src/nimble/switch/switch.stories.ts @@ -2,7 +2,7 @@ import { html, when } from '@microsoft/fast-element'; import { withActions } from '@storybook/addon-actions/decorator'; import type { HtmlRenderer, Meta, StoryObj } from '@storybook/html'; import { switchTag } from '../../../../nimble-components/src/switch'; -import { createUserSelectedThemeStory } from '../../utilities/storybook'; +import { apiCategory, createUserSelectedThemeStory, disabledDescription, slottedLabelDescription } from '../../utilities/storybook'; interface SwitchArgs { label: string; @@ -10,6 +10,7 @@ interface SwitchArgs { disabled: boolean; checkedMessage: string; uncheckedMessage: string; + change: undefined; } const metadata: Meta<SwitchArgs> = { @@ -31,6 +32,36 @@ const metadata: Meta<SwitchArgs> = { ${when(x => x.uncheckedMessage, html<SwitchArgs>`<span slot="unchecked-message">${x => x.uncheckedMessage}</span>`)} </${switchTag}> `), + argTypes: { + label: { + name: 'default', + description: `${slottedLabelDescription({ componentName: 'switch' })}`, + table: { category: apiCategory.slots } + }, + checked: { + description: 'Whether the switch is toggled on.', + table: { category: apiCategory.attributes } + }, + disabled: { + description: disabledDescription({ componentName: 'switch' }), + table: { category: apiCategory.attributes } + }, + checkedMessage: { + name: 'checked-message', + description: 'A `span` element containing the message to display when the switch is toggled on.', + table: { category: apiCategory.slots } + }, + uncheckedMessage: { + name: 'unchecked-message', + description: 'A `span` element containing the message to display when the switch is toggled off.', + table: { category: apiCategory.slots } + }, + change: { + description: 'Event emitted when the user toggles the switch.', + table: { category: apiCategory.events }, + control: false + } + }, args: { label: 'Switch', checked: true, diff --git a/packages/storybook/src/nimble/text-area/text-area.mdx b/packages/storybook/src/nimble/text-area/text-area.mdx index 5dff1d0092..95f8412d35 100644 --- a/packages/storybook/src/nimble/text-area/text-area.mdx +++ b/packages/storybook/src/nimble/text-area/text-area.mdx @@ -1,6 +1,7 @@ import { Canvas, Meta, Controls, Title } from '@storybook/blocks'; import { NimbleTextArea } from './text-area.react'; import * as textAreaStories from './text-area.stories'; +import ComponentApisLink from '../../docs/component-apis-link.mdx'; <Meta of={textAreaStories} /> <Title of={textAreaStories} /> @@ -10,7 +11,11 @@ A multi-line text input control. The text area is often used in a form to collec If you configure your text area to be resizable (with the `resize` attribute) in a certain dimension, do not set an explicit size for that dimension (via `height` and/or `width` `style` properties), or you may experience unexpected resize behavior. If you want to set the initial size of a resizable text area, use the `rows` and/or `cols` attribute(s). <Canvas of={textAreaStories.outlineTextArea} /> + +## API + <Controls of={textAreaStories.outlineTextArea} /> +<ComponentApisLink /> {/* ## Styling */} diff --git a/packages/storybook/src/nimble/text-area/text-area.stories.ts b/packages/storybook/src/nimble/text-area/text-area.stories.ts index 1acc8e8625..16736c3873 100644 --- a/packages/storybook/src/nimble/text-area/text-area.stories.ts +++ b/packages/storybook/src/nimble/text-area/text-area.stories.ts @@ -3,7 +3,7 @@ import { withActions } from '@storybook/addon-actions/decorator'; import type { HtmlRenderer, Meta, StoryObj } from '@storybook/html'; import { textAreaTag } from '../../../../nimble-components/src/text-area'; import { TextAreaAppearance, TextAreaResize } from '../../../../nimble-components/src/text-area/types'; -import { createUserSelectedThemeStory } from '../../utilities/storybook'; +import { apiCategory, appearanceDescription, createUserSelectedThemeStory, disabledDescription, errorTextDescription, errorVisibleDescription, placeholderDescription, slottedLabelDescription } from '../../utilities/storybook'; import { loremIpsum } from '../../utilities/lorem-ipsum'; interface TextAreaArgs { @@ -20,6 +20,7 @@ interface TextAreaArgs { rows: number; cols: number; maxlength: number; + change: undefined; } const metadata: Meta<TextAreaArgs> = { @@ -52,40 +53,71 @@ const metadata: Meta<TextAreaArgs> = { appearance: { options: Object.values(TextAreaAppearance), control: { type: 'radio' }, - table: { - defaultValue: { summary: 'outline' } - } + description: appearanceDescription({ componentName: 'text area' }), + table: { category: apiCategory.attributes } + }, + label: { + name: 'default', + description: `${slottedLabelDescription({ componentName: 'text area' })}`, + table: { category: apiCategory.slots } + }, + placeholder: { + description: placeholderDescription({ componentName: 'text area' }), + table: { category: apiCategory.attributes } + }, + value: { + description: 'The string displayed in the text area. Note that the property value is not synced to an attribute.', + table: { category: apiCategory.nonAttributeProperties } + }, + readonly: { + description: 'Disallows input on the text area while maintaining enabled appearance.', + table: { category: apiCategory.attributes } + }, + disabled: { + description: disabledDescription({ componentName: 'text area' }), + table: { category: apiCategory.attributes } + }, + errorText: { + name: 'error-text', + description: errorTextDescription, + table: { category: apiCategory.attributes } + }, + errorVisible: { + name: 'error-visible', + description: errorVisibleDescription, + table: { category: apiCategory.attributes } + }, + spellcheck: { + description: 'Specifies whether the text area is subject to spell checking by the underlying browser/OS.', + table: { category: apiCategory.attributes } }, resize: { description: 'Direction(s) the text area is sizeable by the user. Setting a fixed `height` and `width` on the text area is not supported while it is sizeable. You may instead use `rows` and `cols` to set an initial size.', options: Object.values(TextAreaResize), control: { type: 'select' }, - table: { - defaultValue: { summary: 'none' } - } + table: { category: apiCategory.attributes } + }, rows: { - description: 'Number of visible rows of text.' + description: 'Number of visible rows of text.', + table: { category: apiCategory.attributes } }, cols: { description: 'Visible width of the text, in average character widths', - table: { - defaultValue: { summary: '20' } - } + table: { category: apiCategory.attributes } + }, maxlength: { description: - 'Maximum number of characters that may be entered by the user' - }, - errorVisible: { - description: - 'Whether the text area should be styled to indicate that it is in an invalid state' + 'Maximum number of characters that may be entered by the user', + table: { category: apiCategory.attributes } }, - errorText: { - description: - 'A message to be displayed when the text area is in the invalid state explaining why the value is invalid' + change: { + description: 'Event emitted when the user commits a new value to the text area.', + table: { category: apiCategory.events }, + control: false } }, args: { diff --git a/packages/storybook/src/nimble/text-field/text-field.mdx b/packages/storybook/src/nimble/text-field/text-field.mdx index 3a0ca2a25c..658158cafc 100644 --- a/packages/storybook/src/nimble/text-field/text-field.mdx +++ b/packages/storybook/src/nimble/text-field/text-field.mdx @@ -1,6 +1,7 @@ import { Canvas, Meta, Controls, Title } from '@storybook/blocks'; import { NimbleTextField } from './text-field.react'; import * as textFieldStories from './text-field.stories'; +import ComponentApisLink from '../../docs/component-apis-link.mdx'; <Meta of={textFieldStories} /> <Title of={textFieldStories} /> @@ -8,7 +9,11 @@ import * as textFieldStories from './text-field.stories'; A single-line text field. <Canvas of={textFieldStories.underlineTextField} /> + +## API + <Controls of={textFieldStories.underlineTextField} /> +<ComponentApisLink /> {/* ## Styling */} diff --git a/packages/storybook/src/nimble/text-field/text-field.stories.ts b/packages/storybook/src/nimble/text-field/text-field.stories.ts index b0d2211939..893f2530ff 100644 --- a/packages/storybook/src/nimble/text-field/text-field.stories.ts +++ b/packages/storybook/src/nimble/text-field/text-field.stories.ts @@ -6,25 +6,29 @@ import { iconPencilTag } from '../../../../nimble-components/src/icons/pencil'; import { iconTagTag } from '../../../../nimble-components/src/icons/tag'; import { textFieldTag } from '../../../../nimble-components/src/text-field'; import { TextFieldAppearance, TextFieldType } from '../../../../nimble-components/src/text-field/types'; -import { createUserSelectedThemeStory } from '../../utilities/storybook'; +import { apiCategory, appearanceDescription, createUserSelectedThemeStory, disabledDescription, errorTextDescription, errorVisibleDescription, placeholderDescription, slottedLabelDescription } from '../../utilities/storybook'; interface TextFieldArgs { label: string; + placeholder: string; type: TextFieldType; appearance: string; fullBleed: boolean; value: string; + valueAttribute: string; readonly: boolean; disabled: boolean; errorVisible: boolean; errorText: string; actionButton: boolean; leftIcon: boolean; + change: undefined; + input: undefined; } -const leftIconDescription = 'To place an icon at the far-left of the text-field, set `slot="start"` on the icon.'; +const leftIconDescription = 'An icon to display at the start of the text field.'; -const actionButtonDescription = `To place content such as a button at the far-right of the text-field, set \`slot="actions"\` on the content. +const actionButtonDescription = `Content such as a button at the end of the text field. Note: The content in the \`actions\` slot will not adjust based on the state of the text-field (e.g. disabled or readonly). It is the responsibility of the consuming application to make any necessary adjustments. For example, if the buttons should be disabled when the text-field is disabled, the @@ -42,10 +46,10 @@ const metadata: Meta<TextFieldArgs> = { // prettier-ignore render: createUserSelectedThemeStory(html` <${textFieldTag} - placeholder="${x => x.label}" + placeholder="${x => x.placeholder}" + :value="${x => x.value}" type="${x => x.type}" appearance="${x => x.appearance}" - value="${x => x.value}" ?readonly="${x => x.readonly}" ?disabled="${x => x.disabled}" error-text="${x => x.errorText}" @@ -65,31 +69,84 @@ const metadata: Meta<TextFieldArgs> = { </${textFieldTag}> `), argTypes: { + label: { + name: 'default', + description: `${slottedLabelDescription({ componentName: 'text field' })}`, + table: { category: apiCategory.slots } + }, + placeholder: { + description: placeholderDescription({ componentName: 'text field' }), + table: { category: apiCategory.attributes } + }, type: { options: Object.values(TextFieldType), - control: { type: 'select' } + control: { type: 'radio' }, + description: 'They type of input to accept and render in the text field. This corresponds to [the `type` attribute of the native `input` element](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#type) though only a subset of values are supported.', + table: { category: apiCategory.attributes } }, appearance: { options: Object.values(TextFieldAppearance), - control: { type: 'radio' } + control: { type: 'radio' }, + description: appearanceDescription({ componentName: 'text field' }), + table: { category: apiCategory.attributes } }, fullBleed: { + name: 'full-bleed', description: - 'Remove the start and end margins causing the text to stretch across the full control width. Only applies to the frameless appearance.' + 'Remove the start and end margins causing the text to stretch across the full control width. Only applies to the frameless appearance.', + table: { category: apiCategory.attributes } + }, + value: { + description: 'The string displayed in the text field. Note that the property and attribute behave differently.', + table: { category: apiCategory.nonAttributeProperties } + }, + valueAttribute: { + name: 'value', + description: 'The initial string displayed in the text field. Changing this after the text field initializes has no effect. Note that the property behave differently.', + table: { category: apiCategory.attributes } + }, + readonly: { + description: 'Disallows input on the text field while maintaining enabled appearance.', + table: { category: apiCategory.attributes } + }, + disabled: { + description: disabledDescription({ componentName: 'text field' }), + table: { category: apiCategory.attributes } + }, + errorVisible: { + name: 'error-visible', + description: errorVisibleDescription, + table: { category: apiCategory.attributes } }, errorText: { - description: - 'A message to be displayed when the text field is in the invalid state explaining why the value is invalid' + name: 'error-text', + description: errorTextDescription, + table: { category: apiCategory.attributes } }, actionButton: { - description: actionButtonDescription + name: 'actions', + description: actionButtonDescription, + table: { category: apiCategory.slots } }, leftIcon: { - description: leftIconDescription + name: 'start', + description: leftIconDescription, + table: { category: apiCategory.slots } + }, + change: { + description: 'Event emitted when the user commits a new value to the text field.', + table: { category: apiCategory.events }, + control: false + }, + input: { + description: 'Event emitted on each user keystroke within the text field.', + table: { category: apiCategory.events }, + control: false } }, args: { label: 'default label', + placeholder: 'Enter text...', type: TextFieldType.text, appearance: 'underline', fullBleed: false, diff --git a/packages/storybook/src/nimble/toggle-button/toggle-button.stories.ts b/packages/storybook/src/nimble/toggle-button/toggle-button.stories.ts index e57d15a8e3..64ca61d926 100644 --- a/packages/storybook/src/nimble/toggle-button/toggle-button.stories.ts +++ b/packages/storybook/src/nimble/toggle-button/toggle-button.stories.ts @@ -83,7 +83,8 @@ const metadata: Meta<ToggleButtonArgs> = { }, change: { description: 'Fires when the toggle button is pressed via mouse or keyboard.', - table: { category: apiCategory.events } + table: { category: apiCategory.events }, + control: false } }, // prettier-ignore diff --git a/packages/storybook/src/utilities/storybook.ts b/packages/storybook/src/utilities/storybook.ts index 0bff54dabb..5e94feaff8 100644 --- a/packages/storybook/src/utilities/storybook.ts +++ b/packages/storybook/src/utilities/storybook.ts @@ -2,6 +2,7 @@ import { html, ViewTemplate } from '@microsoft/fast-element'; import { themeProviderTag } from '../../../nimble-components/src/theme-provider'; import { bodyFont } from '../../../nimble-components/src/theme-provider/design-tokens'; import type { Theme } from '../../../nimble-components/src/theme-provider/types'; +import { listOptionTag } from '../../../nimble-components/src/list-option'; import { BackgroundState, backgroundStates, @@ -165,10 +166,21 @@ export const disableStorybookZoomTransform = ` export const apiCategory = { attributes: 'Attributes', events: 'Events', + localizableLabels: 'Localizable Labels', methods: 'Methods', + nonAttributeProperties: 'Properties', slots: 'Slots' } as const; +export const appearanceDescription = (options: { componentName: string }): string => `This attribute affects the appearance of the ${options.componentName}.`; export const iconDescription = 'Set `slot="start"` to include an icon before the text content.'; export const disabledDescription = (options: { componentName: string }): string => `Styles the ${options.componentName} as disabled and prevents focus and user interaction.`; +export const slottedLabelDescription = (options: { componentName: string }): string => `Label text to display adjacent to the ${options.componentName} describing its purpose to the user.`; export const textContentDescription = (options: { componentName: string }): string => `The text content of the ${options.componentName}.`; +export const placeholderDescription = (options: { componentName: string }): string => `Placeholder text to display when no value has been entered in the ${options.componentName}.`; + +export const errorTextDescription = 'A message to be displayed explaining why the value is invalid. Only visible when `error-visible` is set.'; +export const errorVisibleDescription = 'When set to `true`, the `error-text` message will be displayed.'; + +export const dropdownPositionDescription = (options: { componentName: string }): string => `Controls the position of the dropdown relative to the ${options.componentName}.`; +export const optionsDescription = `The \`${listOptionTag}\` items for the user to select from.`;