Skip to content

Commit

Permalink
Improve API documentation for input components (#2132)
Browse files Browse the repository at this point in the history
# 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

<!---
Detail the testing done to ensure this submission meets requirements. 

Include automated/manual test additions or modifications, testing done
on a local build, private CI run results, and additional testing not
covered by automatic pull request validation. If any functionality is
not covered by automated testing, provide justification.
-->

## ✅ Checklist

<!--- Review the list and put an x in the boxes that apply or ~~strike
through~~ around items that don't (along with an explanation). -->

- [x] I have updated the project documentation to reflect my changes or
determined no changes are needed.

---------

Co-authored-by: m-akinc <[email protected]>
  • Loading branch information
jattasNI and m-akinc authored May 24, 2024
1 parent 1cbdc2c commit 54232ae
Show file tree
Hide file tree
Showing 20 changed files with 457 additions and 100 deletions.
2 changes: 1 addition & 1 deletion packages/storybook/src/docs/component-apis.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
5 changes: 5 additions & 0 deletions packages/storybook/src/nimble/checkbox/checkbox.mdx
Original file line number Diff line number Diff line change
@@ -1,14 +1,19 @@
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';

<Meta of={checkboxStories} />
<Title of={checkboxStories} />

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 */}

Expand Down
32 changes: 29 additions & 3 deletions packages/storybook/src/nimble/checkbox/checkbox.stories.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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> = {
Expand All @@ -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: {
Expand Down
5 changes: 5 additions & 0 deletions packages/storybook/src/nimble/combobox/combobox.mdx
Original file line number Diff line number Diff line change
@@ -1,13 +1,18 @@
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} />
<Title of={comboboxStories} />
<Description of={comboboxStories} />

<Canvas of={comboboxStories.underlineCombobox} />

## API

<Controls of={comboboxStories.underlineCombobox} />
<ComponentApisLink />

{/* ## Styling */}

Expand Down
58 changes: 50 additions & 8 deletions packages/storybook/src/nimble/combobox/combobox.stories.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -24,6 +32,8 @@ interface ComboboxArgs {
currentValue: string;
appearance: string;
placeholder: string;
change: undefined;
input: undefined;
}

interface OptionArgs {
Expand Down Expand Up @@ -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',
Expand All @@ -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: {
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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 }
};
}
5 changes: 5 additions & 0 deletions packages/storybook/src/nimble/number-field/number-field.mdx
Original file line number Diff line number Diff line change
@@ -1,14 +1,19 @@
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} />

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 */}

Expand Down
53 changes: 44 additions & 9 deletions packages/storybook/src/nimble/number-field/number-field.stories.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -25,6 +25,8 @@ interface NumberFieldArgs extends LabelUserArgs {
disabled: boolean;
errorVisible: boolean;
errorText: string;
change: undefined;
input: undefined;
}

const metadata: Meta<NumberFieldArgs> = {
Expand Down Expand Up @@ -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: {
Expand Down
14 changes: 13 additions & 1 deletion packages/storybook/src/nimble/radio-group/radio-group.mdx
Original file line number Diff line number Diff line change
@@ -1,13 +1,23 @@
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} />

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 */}

Expand All @@ -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 */}
Expand Down
Loading

0 comments on commit 54232ae

Please sign in to comment.