Skip to content

Commit a41d911

Browse files
authored
Component: tabs (#5)
* chore(tabs): initial documentation with API questions * chore(tabs): added more discussion points * chore(tabs): updated RFC, added detailed design * chore(tabs): reworked props
1 parent ac1996e commit a41d911

File tree

1 file changed

+181
-0
lines changed

1 file changed

+181
-0
lines changed

accepted/0003-component-tabs.md

Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
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

Comments
 (0)