Skip to content

Commit

Permalink
refactor(TabBar)!: remove TabBar uncontrolled mode
Browse files Browse the repository at this point in the history
BREAKING CHANGE: removed onItemClick and defaultSelected props

refs: CDS-60 (#126)
  • Loading branch information
CataldoMazzilli authored Sep 29, 2022
1 parent d086d90 commit f69777e
Show file tree
Hide file tree
Showing 3 changed files with 113 additions and 87 deletions.
95 changes: 32 additions & 63 deletions src/components/navigation/TabBar.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,41 +6,6 @@ SPDX-License-Identifier: AGPL-3.0-only

The TabBar is a customizable navigation component, which can be used for in-page navigation between different tabs.

### Uncontrolled Plain TabBar
```jsx
import {useState} from 'react';
import {Container, Divider, Text} from '@zextras/carbonio-design-system';
const items = [
{ id: 'tab-one', label: 'First Tab' },
{ id: 'tab-two', label: 'Second Tab' },
{ id: 'tab-three', label: 'Disabled', disabled: true }
];
const [change, setChange] = useState('');
const [click, setClick] = useState('');
<>
<TabBar
items={items}
defaultSelected="tab-one"
onChange={setChange}
onItemClick={setClick}
width={512}
height={48}
/>
<Container
background="gray4"
width={512}
padding={{ all: 'small'}}
crossAlignment="flex-start"
>
<Text style={{ fontFamily: 'monospace' }}>
{`Change Event: '${change}'`}
</Text>
<Text style={{ fontFamily: 'monospace' }}>
{`ClickEvent.selectedItemId: '${click.selectedItemId}'`}
</Text>
</Container>
</>
```
### Controlled Plain TabBar
```jsx
import {useState} from 'react';
Expand All @@ -54,8 +19,11 @@ const [selected, setSelected] = useState('tab-one');
<TabBar
items={items}
selected={selected}
onChange={console.log}
onItemClick={(ev) => setSelected(ev.selectedItemId)}
onChange={(ev, selectedId) => {
console.log(ev);
console.log(selectedId);
setSelected(selectedId);
}}
width={512}
height={48}
/>
Expand All @@ -68,9 +36,6 @@ const [selected, setSelected] = useState('tab-one');
<Text size="large">
{`Selected: '${selected}'`}
</Text>
<Text style={{ fontFamily: 'monospace' }}>
{`ClickEvent.selectedItemId: '${selected}'`}
</Text>
<Row>
{items.map(
(item) => (
Expand Down Expand Up @@ -105,16 +70,22 @@ const [selected, setSelected] = useState('tab-one');
<TabBar
items={items}
selected={selected}
onChange={console.log}
onItemClick={(ev) => setSelected(ev.selectedItemId)}
onChange={(ev, selectedId) => {
console.log(ev);
console.log(selectedId);
setSelected(selectedId);
}}
width={512}
height={48}
/>
<TabBar
items={items}
selected={selected}
onChange={console.log}
onItemClick={(ev) => setSelected(ev.selectedItemId)}
onChange={(ev, selectedId) => {
console.log(ev);
console.log(selectedId);
setSelected(selectedId);
}}
width={512}
height={48}
forceWidthEquallyDistributed
Expand Down Expand Up @@ -184,14 +155,16 @@ const items = [
{ id: 'tab-three', label: 'Another Tab', CustomComponent },
{ id: 'tab-four', label: 'Car Tab', CustomComponent: ReusedDefaultTabBar, icon: 'CarOutline' }
];
const [change, setChange] = useState('');
const [click, setClick] = useState('');
const [selected, setSelected] = useState('');
<>
<TabBar
items={items}
defaultSelected="tab-one"
onChange={setChange}
onItemClick={setClick}
onChange={(ev, selectedId) => {
console.log(ev);
console.log(selectedId);
setSelected(selectedId);
}}
selected={selected}
width={512}
height={48}
/>
Expand All @@ -202,10 +175,7 @@ const [click, setClick] = useState('');
crossAlignment="flex-start"
>
<Text style={{ fontFamily: 'monospace' }}>
{`Change Event: '${change}'`}
</Text>
<Text style={{ fontFamily: 'monospace' }}>
{`ClickEvent.selectedItemId: '${click.selectedItemId}'`}
{`selected: '${selected}'`}
</Text>
</Container>
</>
Expand Down Expand Up @@ -246,15 +216,17 @@ const items = [
{ id: 'four', label: 'Hello', CustomComponent, icon: 'SmilingFaceOutline' },
{ id: 'five', label: 'Hello', disabled: true }
];
const [change, setChange] = useState('');
const [click, setClick] = useState('');
const [selected, setSelected] = useState('tab-one');
<>
<TabBar
items={items}
defaultSelected="tab-one"
onChange={setChange}
onItemClick={setClick}
width={512}
selected={selected}
onChange={(ev, selectedId) => {
console.log(ev);
console.log(selectedId);
setSelected(selectedId);
}}
width={512}
height={48}
underlineColor="success"
/>
Expand All @@ -265,10 +237,7 @@ const [click, setClick] = useState('');
crossAlignment="flex-start"
>
<Text style={{ fontFamily: 'monospace' }}>
{`Change Event: '${change}'`}
</Text>
<Text style={{ fontFamily: 'monospace' }}>
{`ClickEvent.selectedItemId: '${click.selectedItemId}'`}
{`selected: '${selected}'`}
</Text>
</Container>
</>
Expand Down
71 changes: 71 additions & 0 deletions src/components/navigation/TabBar.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
/*
* SPDX-FileCopyrightText: 2021 Zextras <https://www.zextras.com>
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
/* eslint-disable import/no-extraneous-dependencies */

import { act, within } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import React from 'react';
import { screen } from '@testing-library/dom';
import { render } from '../../test-utils';
import { DefaultTabBarItem, DefaultTabBarItemProps, TabBar } from './TabBar';
import { Text } from '../basic/Text';

describe('TabBar', () => {
test('The visually selected element always reflects the selected TabBar prop', () => {
const ReusedDefaultTabBar = ({
item,
selected,
onClick,
forceWidthEquallyDistributed,
...rest
}: DefaultTabBarItemProps): JSX.Element => (
<DefaultTabBarItem
item={item}
selected={selected}
onClick={onClick}
orientation="horizontal"
forceWidthEquallyDistributed={forceWidthEquallyDistributed}
{...rest}
background="secondary"
underlineColor="primary"
>
<Text size="large">{`${item.label} is ${selected ? 'selected' : 'not selected'}`} </Text>
</DefaultTabBarItem>
);

const items = [
{ id: 'tab-one', label: 'First Tab', CustomComponent: ReusedDefaultTabBar },
{ id: 'tab-two', label: 'Second Tab', CustomComponent: ReusedDefaultTabBar }
];

const changeFn = jest.fn();
const { rerender } = render(
<TabBar items={items} selected={'tab-one'} onChange={changeFn} background="secondary" />
);
const teb0 = screen.getByTestId('tab0');
expect(within(teb0).getByText('First Tab is selected')).toBeVisible();

const tab1 = screen.getByTestId('tab1');
expect(within(tab1).getByText('Second Tab is not selected')).toBeVisible();

act(() => {
userEvent.click(tab1);
});
expect(changeFn).toBeCalled();

// the second tab is clicked but is not selected until the new selected TabBar prop is provided
expect(within(teb0).getByText('First Tab is selected')).toBeVisible();
expect(within(tab1).getByText('Second Tab is not selected')).toBeVisible();

// simulate the change of selected item
rerender(
<TabBar items={items} selected={'tab-two'} onChange={changeFn} background="secondary" />
);

expect(within(teb0).getByText('First Tab is not selected')).toBeVisible();
expect(within(tab1).getByText('Second Tab is selected')).toBeVisible();
});
});
34 changes: 10 additions & 24 deletions src/components/navigation/TabBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/

import React, { useState, useEffect, useCallback, useMemo, HTMLAttributes } from 'react';
import React, { useCallback, useMemo, HTMLAttributes } from 'react';
import styled, { css, SimpleInterpolation } from 'styled-components';
import { map } from 'lodash';
import { getColor } from '../../theme/theme-utils';
Expand Down Expand Up @@ -61,14 +61,9 @@ interface TabBarProps extends Omit<ContainerProps, 'onChange'> {
/** List of elements, can have extra attributes to pass down to the CustomComponent */
items: Array<Item>;
/** id of the selected item */
selected?: string;
/** id of the default selected item */
defaultSelected?: string;
selected: string;
/** change callback, is called with the new selected id */
onChange: (selectedId: string) => void;
/** click (or also keyboard in the default component) event,
* the selectedItemId field is added to the event object */
onItemClick: (ev: React.SyntheticEvent & { selectedItemId: string }) => void;
onChange: (ev: React.SyntheticEvent, selectedId: string) => void;
/** background color of the tabBar */
background: string | keyof ThemeObj['palette'];
/** underline color of the selected tab */
Expand Down Expand Up @@ -144,31 +139,20 @@ const TabBar = React.forwardRef<HTMLDivElement, TabBarProps>(function TabBarFn(
{
items,
selected,
defaultSelected,
onChange,
onItemClick,
background,
underlineColor = 'primary',
forceWidthEquallyDistributed = false,
...rest
},
ref
) {
const [currentSelection, setCurrentSelection] = useState(defaultSelected);
useEffect(() => {
if (typeof selected !== 'undefined') {
setCurrentSelection(selected);
onChange(selected);
}
}, [selected, onChange]);
const onItemClickCb = useCallback(
(id: string) =>
(ev: React.SyntheticEvent): void => {
setCurrentSelection(id);
onChange(id);
onItemClick({ ...ev, selectedItemId: id });
onChange(ev, id);
},
[onChange, onItemClick]
[onChange]
);
return (
<Container
Expand All @@ -178,12 +162,13 @@ const TabBar = React.forwardRef<HTMLDivElement, TabBarProps>(function TabBarFn(
mainAlignment="flex-start"
{...rest}
>
{map(items, (item) =>
{map(items, (item, index) =>
item.CustomComponent ? (
<item.CustomComponent
data-testid={`tab${index}`}
key={item.id}
item={item}
selected={item.id === currentSelection}
selected={item.id === selected}
onClick={onItemClickCb(item.id)}
tabIndex={item.disabled ? undefined : 0}
background={background}
Expand All @@ -192,9 +177,10 @@ const TabBar = React.forwardRef<HTMLDivElement, TabBarProps>(function TabBarFn(
/>
) : (
<DefaultTabBarItem
data-testid={`tab${index}`}
key={item.id}
item={item}
selected={item.id === currentSelection}
selected={item.id === selected}
background={background}
onClick={onItemClickCb(item.id)}
tabIndex={item.disabled ? undefined : 0}
Expand Down

0 comments on commit f69777e

Please sign in to comment.