|
| 1 | +- Start Date: 2022/04/11 |
| 2 | +- Related documents: |
| 3 | + - [acceptance criteria](https://wwnorton.atlassian.net/browse/NDS-235) |
| 4 | + - [documentation draft](https://docs.google.com/document/d/1D0MjvCYdCaaHDJGYcPk8u9pSREkVpAac8JqNYzviHc0) |
| 5 | + - [visual design](@TODO) |
| 6 | +- [RFC PR](https://github.com/wwnorton/design-system-rfcs/pull/5) |
| 7 | + |
| 8 | +## Summary |
| 9 | + |
| 10 | +`Tabs` allow the user to select layered sections of content to display within the same space. |
| 11 | + |
| 12 | +A list of tab headers is displayed using the `TabList` and `Tab` components, allowing the user to interact and select the tab content they want to see. The contents of each tab is contained in a `TabPanel` component, all wrapped within a `TabPanels` container. |
| 13 | + |
| 14 | +## Definitions |
| 15 | + |
| 16 | +- `Controlled/uncontrolled or managed/unmanaged state`: The only state this component is interested in is which tab is the currently selected one. The controlled/uncontrolled distinction is about _who_ is responsible for managing this state. |
| 17 | + - Controlled means that the parent (the component invoking the `Tabs` component) is responsible of managing this state, this way the user can **control the state however they want**. In order to use the component in controlled mode, the user must provide `selectedIndex` and `onChange` props to the `Tabs` component. |
| 18 | + - Uncontrolled means that the state is managed internally by the `Tabs` component, the parent does not need to provide any specific props and **can't control the state**. In uncontrolled mode, the only thing that the parent can control is the default state for the initial render, this is done through the optional `defaultSelectedIndex` prop. |
| 19 | +- `Position index`: this refers to the position of a component relative to their parent. If they are the first child their position index would be 0, if they are the second it would be 1 and so on. |
| 20 | + |
| 21 | +### Sub components |
| 22 | + |
| 23 | +- `<Tabs>` - The highest-order wrapper that contains all the other sub-components. Handles data management and distribution across it's children |
| 24 | +- `<TabList>` - Container for the `<Tab>` components |
| 25 | +- `<Tab>` - An interactive button within the `TabList`, allows the user to navigate between the different sections of content |
| 26 | +- `<TabPanels>` - Container for the `<TabPanel>` components |
| 27 | +- `<TabPanel>` - Container for the content of each section, contained within `TabPanels` |
| 28 | + |
| 29 | +## Detailed design |
| 30 | + |
| 31 | +A stateful component with both uncontrolled and controlled versions (state managed internally / state managed externally). |
| 32 | + |
| 33 | +The only state needed by this component is the currently active section of content, identified by its index. `Tab` components must be in order with its corresponding `TabPanel`s in order to assure that the `Tab` buttons activate the correct section of content |
| 34 | + |
| 35 | +### Tabs |
| 36 | + |
| 37 | +`<Tabs>` extends the `React.ComponentPropsWithRef<'div'>` interface and adds the following props: |
| 38 | + |
| 39 | +| Name | Type | Description | Required | Default | |
| 40 | +| ---------------------- | --------------------------------- | -------------------------------------------------------------------------------------------------------------------- | -------- | ----------- | |
| 41 | +| `selectedIndex` | `number` | The currently active tab, for controlled use only | `false` | `undefined` | |
| 42 | +| `onChange` | `(selectedIndex: number) => void` | Callback for when the user interacts with one of the `Tab` buttons, will pass the position index of the selected tab | `false` | `undefined` | |
| 43 | +| `defaultSelectedIndex` | `number` | Sets the default active tab, for uncontrolled use only. Will be ignored if the `selectedIndex` prop is defined | `false` | 0 | |
| 44 | + |
| 45 | +### TabList |
| 46 | + |
| 47 | +`<TabList>` extends the `React.ComponentPropsWithRef<'div'>` interface with a `role="tablist"` attribute. A simple wrapper for the `Tab` components. |
| 48 | + |
| 49 | +### Tab |
| 50 | + |
| 51 | +`<Tab>` extends the `React.ComponentPropsWithRef<'button'>` interface. |
| 52 | + |
| 53 | +The `Tab` component is responsible for rendering a `button` with the following (auto generated, the user does not need to provide these through props) attributes: |
| 54 | + |
| 55 | +- `type="button"` |
| 56 | +- `role="tab"` |
| 57 | +- `aria-selected`: `true` if this `Tab` refers to the currently active tab panel, else `false` |
| 58 | +- `aria-controls`: The value must be the `id` of the corresponding `TabPanel` this `Tab` controls. For example `tab-0` if this is the `Tab` with position index 0 |
| 59 | +- `id`: The value must be unique and will be referred to in the corresponding `TabPanel` through the `aria-labelledby` attribute. For example `tab-header-0` if this is the `Tab` with position index 0 |
| 60 | + |
| 61 | +### TabPanels |
| 62 | + |
| 63 | +`<TabPanels>` extends the `React.ComponentPropsWithRef<'div'>` interface. A simple wrapper for the `TabPanel` components. |
| 64 | + |
| 65 | +### TabPanel |
| 66 | + |
| 67 | +`<TabPanel>` extends the `React.ComponentPropsWithRef<'div'>` interface with `role="tabpanel"` and an autogenerated `id` attributes. |
| 68 | + |
| 69 | +The `TabPanel` component is responsible for rendering a `div` with the following (auto generated, the user does not need to provide these through props) attributes: |
| 70 | + |
| 71 | +- `role="tabpanel"` |
| 72 | +- `aria-labelledby`: The value must be the `id` of the corresponding `Tab` this `TabPanel` is controlled by. For example `tab-header-0` if this is the `TabPanel` with position index 0 |
| 73 | +- `id`: The value must be unique and will be referred to in the corresponding `Tab` through the `aria-controls` attribute. For example `tab-0` if this is the `TabPanel` with position index 0 |
| 74 | + |
| 75 | +### Simple Usage - Uncontrolled/State managed internally |
| 76 | + |
| 77 | +```tsx |
| 78 | +<Tabs defaultSelectedIndex={2}> |
| 79 | + <TabList> |
| 80 | + <Tab>Cats</Tab> |
| 81 | + <Tab>Dogs</Tab> |
| 82 | + <Tab>Horses</Tab> |
| 83 | + </TabList> |
| 84 | + <TabPanels> |
| 85 | + <TabPanel>Cats content</TabPanel> |
| 86 | + <TabPanel>Dogs content</TabPanel> |
| 87 | + <TabPanel>Horses content</TabPanel> |
| 88 | + </TabPanels> |
| 89 | +</Tabs> |
| 90 | +``` |
| 91 | + |
| 92 | +### Simple Usage - Controlled/State managed externally |
| 93 | + |
| 94 | +```tsx |
| 95 | +<Tabs selectedIndex={selectedTabIndex} onChange={handleSelectedTabIndexChange}> |
| 96 | + <TabList> |
| 97 | + <Tab>Cats</Tab> |
| 98 | + <Tab>Dogs</Tab> |
| 99 | + <Tab>Horses</Tab> |
| 100 | + </TabList> |
| 101 | + <TabPanels> |
| 102 | + <TabPanel>Cats content</TabPanel> |
| 103 | + <TabPanel>Dogs content</TabPanel> |
| 104 | + <TabPanel>Horses content</TabPanel> |
| 105 | + </TabPanels> |
| 106 | +</Tabs> |
| 107 | +``` |
| 108 | + |
| 109 | +### Simple example of final rendered HTML |
| 110 | + |
| 111 | +```tsx |
| 112 | +<div role="tablist"> |
| 113 | + <button |
| 114 | + type="button" |
| 115 | + role="tab" |
| 116 | + aria-selected="true" |
| 117 | + aria-controls="tab-0" |
| 118 | + id="tab-header-0" |
| 119 | + > |
| 120 | + Cats |
| 121 | + </button> |
| 122 | + <button |
| 123 | + type="button" |
| 124 | + role="tab" |
| 125 | + aria-selected="false" |
| 126 | + aria-controls="tab-1" |
| 127 | + id="tab-header-1" |
| 128 | + > |
| 129 | + Dogs |
| 130 | + </button> |
| 131 | + <button |
| 132 | + type="button" |
| 133 | + role="tab" |
| 134 | + aria-selected="false" |
| 135 | + aria-controls="tab-2" |
| 136 | + id="tab-header-2" |
| 137 | + > |
| 138 | + Horses |
| 139 | + </button> |
| 140 | + |
| 141 | + <div className="panel-container"> |
| 142 | + <div id="tab-0" role="tabpanel" aria-labelledby="tab-header-0"> |
| 143 | + Cats content |
| 144 | + </div> |
| 145 | + <div id="tab-1" role="tabpanel" aria-labelledby="tab-header-1" hidden=""> |
| 146 | + Dogs content |
| 147 | + </div> |
| 148 | + <div id="tab-2" role="tabpanel" aria-labelledby="tab-header-2" hidden=""> |
| 149 | + Horses content |
| 150 | + </div> |
| 151 | + </div> |
| 152 | +</div> |
| 153 | +``` |
| 154 | + |
| 155 | +## Alternatives |
| 156 | + |
| 157 | +Ant-design uses a simpler and more minimalistic API, which combines the `Tab` and `TabPanel` components into one and only a single `Tabs` wrapper. A clear disadvantage of this API is that it doesn't reflect the structure of the actual rendered HTML. Also it requires a more complex implementation due to the need to use Portals in order to elevate the tab content above its container in the final DOM. |
| 158 | + |
| 159 | +```tsx |
| 160 | +<Tabs defaultActiveKey="1" onChange={callback}> |
| 161 | + <TabPane tab="Tab 1" key="1"> |
| 162 | + Content of Tab Pane 1 |
| 163 | + </TabPane> |
| 164 | + <TabPane tab="Tab 2" key="2"> |
| 165 | + Content of Tab Pane 2 |
| 166 | + </TabPane> |
| 167 | + <TabPane tab="Tab 3" key="3"> |
| 168 | + Content of Tab Pane 3 |
| 169 | + </TabPane> |
| 170 | +</Tabs> |
| 171 | +``` |
| 172 | + |
| 173 | +## Unresolved questions |
| 174 | + |
| 175 | +- We need a way to identify each `Tab` and relate it to its corresponding `TabPanel`. This is needed in order to calculate the `aria-controls`, `aria-labelledby` and `id` attributes for the `Tab` and `TabPanel` components |
| 176 | +- Calculate these `id`s in a way that multiple tabs can be rendered in the same page at the same time and ensure the `id`s remain unique |
| 177 | + |
| 178 | +We can: |
| 179 | + |
| 180 | +1. Using the solution from NDS-22, generate an UUID to use as prefix for every `id`, that way we ensure that they are unique. For example a `Tab` would generate an `id="123autogenerated-tab-header-0"` for position index 0 and its corresponding `TabPanel` would have `id="123autogenerated-tab-0`, `aria-labelledby="123autogenerated-tab-header-0"` |
| 181 | +2. Don't use autogenerated UUIDs (in order to not depend on NDS-22) and ask the developer to manually give the parent `Tabs` component an unique `id` and use that as prefix. For example `<Tabs id="myAnimalsTabbedPanel" />` would generate `id="myAnimalsTabbedPanel-tab-header-0"`. As long as the developer doesn't give multiple `Tabs` the same `id` we can be sure that the generated `id`s will be unique |
0 commit comments