Skip to content

Commit 9c2c553

Browse files
tdeekensemmenkoCarlosCortizasCT
authored
Add usage as controlled component to ViewSwitcher (#2240)
* feat(view-switcher): add uages as controlled component * docs: mix up in type * feat: warn if both props are passed * docs: add comment about useState * test: add test case for precedence * docs: add changeset * refactor(view-switcher): cleanup, generate readme, simplify test * refactor: typo Co-authored-by: Carlos Cortizas <[email protected]> Co-authored-by: Nicola Molinari <[email protected]> Co-authored-by: Carlos Cortizas <[email protected]>
1 parent 4735f79 commit 9c2c553

File tree

10 files changed

+258
-53
lines changed

10 files changed

+258
-53
lines changed

.changeset/sweet-wombats-knock.md

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
---
2+
"@commercetools-uikit/view-switcher": minor
3+
---
4+
5+
Add support for using the `<ViewSwitcher>` component in a controlled mode.
6+
7+
To make the component controlled you need to pass a prop `selectedValue` and `onChange` to the `<ViewSwitcher.Group>` component.
8+
9+
When the component is controlled, the parent must handle the state updates. This can be useful when the state is maintained for example in the URL.
10+
11+
See example usage:
12+
13+
```jsx
14+
const ControlledExample = () => {
15+
const [seletedValue, setSelectedValue] = useState('button 1');
16+
17+
return (
18+
<ViewSwitcher.Group
19+
selectedValue={seletedValue}
20+
onChange={setSelectedValue}
21+
>
22+
>
23+
<ViewSwitcher.Button isDisabled value="button 1">
24+
View 1
25+
</ViewSwitcher.Button>
26+
<ViewSwitcher.Button value="button 2">View 2</ViewSwitcher.Button>
27+
<ViewSwitcher.Button value="button 3">View 3</ViewSwitcher.Button>
28+
</ViewSwitcher.Group>
29+
);
30+
};
31+
```

packages/components/label/README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,12 +25,12 @@ import Label from '@commercetools-uikit/label';
2525
## Properties
2626

2727
| Props | Type | Required | Values | Default | Description |
28-
| ---------------------------- | -------------- | :------: | ------------------------- | ------- | --------------------------------------------------------------------------------------------------------------- |
28+
| ---------------------------- | -------------- | :------: | ------------------------- | ------- | --------------------------------------------------------------------------------------------------------------- | --- |
2929
| `tone` | `string` | - | `['primary', 'inverted']` | \_ | Indicates the tone to be applied to the label |
3030
| `children` | `node` | ✅ (\*) | - | - | Value of the label |
3131
| `intlMessage` | `intl message` | ✅ (\*) | - | - | An intl message object that will be rendered with `FormattedMessage` |
3232
| `isBold` | `bool` | - | - | `false` | Indicates if the label title should be in bold text |
33-
| `isRequiredIndicatorVisible` | `bool` | - | - | `false` | Indicates if the labeled field is required in a form | |
33+
| `isRequiredIndicatorVisible` | `bool` | - | - | `false` | Indicates if the labeled field is required in a form | |
3434
| `htmlFor` | `string` | - | - | - | The `for` HTML attribute, used to reference form elements with the related attribute `id` or `aria-labelledby`. |
3535
| `id` | `string` | - | - | - | The `id` HTML attribute, used to reference non-form elements with the related attribute `aria-labelledby`. |
3636

Lines changed: 53 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,28 @@
1+
<!-- THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. -->
2+
<!-- This file is created by the `yarn generate-readme` script. -->
3+
14
# ViewSwitcher
25

36
## Description
47

5-
ViewSwitchers allow users to toggle between two or more different views of the same, similar or related content items within the same space on screen.
8+
The `<ViewSwitcher>` component allow users to toggle between two or more different views of the same, similar or related content items within the same space on screen.
69

710
## When to use
811

9-
ViewSwitchers are frequently used to let users toggle between different formatting's, like with a grid view and a table view.
12+
Let users toggle between different formatting's, like with a grid view and a table view.
1013

1114
## When not to use
1215

13-
Do not use the ViewSwitcher as Tabs
16+
**Do not use the `<ViewSwitcher>` as tabs.**
1417
Tabs should be used when the content on the page is divided into related sections but without any overlap.
15-
See tabs as separate of content.
1618

17-
Do not use the ViewSwitcher as Toggle
18-
Toggles are used for yes/no or on/off binary decisions.
19+
**Do not use the `<ViewSwitcher>` as toggle.**
20+
Toggles are used for "yes/no" or "on/off" binary decisions.
1921

20-
## Do's and Don'ts
22+
## Do's and Don'ts
2123

22-
- If you use an icon within the ViewSwitcher, each switch needs to have an icon.
23-
- No colored icons are allowed. Only color-solid (black)
24+
- If you use an icon within the `<ViewSwitcher>`, each switch needs to have an icon.
25+
- No colored icons are allowed. Only `--color-solid` (black).
2426
- Do not use two lines of text in one switch field.
2527

2628
## Installation
@@ -46,11 +48,19 @@ npm --save install react
4648
## Usage
4749

4850
```jsx
51+
import { useState } from 'react';
4952
import ViewSwitcher from '@commercetools-uikit/view-switcher';
5053

51-
const Example = () => (
54+
/**
55+
* 1. Uncontrolled mode
56+
*
57+
* The component controls its own internal state and switching between options.
58+
* The `defaultSelected` value is only used on the initial render. Changes to that value
59+
* are not reflected in the component state.
60+
*/
61+
const UncontrolledExample = () => (
5262
<ViewSwitcher.Group
53-
defaultSelected="Button 2"
63+
defaultSelected="button 2"
5464
onChange={(value) => console.log(value)}
5565
>
5666
<ViewSwitcher.Button isDisabled value="button 1">
@@ -61,19 +71,41 @@ const Example = () => (
6171
</ViewSwitcher.Group>
6272
);
6373

64-
export default Example;
74+
/**
75+
* 2. Controlled mode
76+
*
77+
* The component is controlled from a parent component, using the prop `selectedValue`.
78+
* The component does not use an internal state and the parent must implement the `onChange` callback.
79+
*/
80+
const ControlledExample = () => {
81+
const [seletedValue, setSelectedValue] = useState('button 1');
82+
83+
return (
84+
<ViewSwitcher.Group
85+
selectedValue={seletedValue}
86+
onChange={setSelectedValue}
87+
>
88+
<ViewSwitcher.Button isDisabled value="button 1">
89+
View 1
90+
</ViewSwitcher.Button>
91+
<ViewSwitcher.Button value="button 2">View 2</ViewSwitcher.Button>
92+
<ViewSwitcher.Button value="button 3">View 3</ViewSwitcher.Button>
93+
</ViewSwitcher.Group>
94+
);
95+
};
96+
97+
export { UncontrolledExample, ControlledExample };
6598
```
6699

67100
## Properties
68101

69-
### ViewSwitcher.Group
70-
71-
| Props | Type | Required | Default | Description |
72-
| ----------------- | ---------------------------------------------------- | :------: | ------- | ------------------------------------------------------------------------------------------------------- |
73-
| `isCondensed` | `boolean` | | | Indicates that the view switcher can be reduced to save space |
74-
| `children` | `ReactNode` || | Pass one or more `ViewSwitcher.Button` components |
75-
| `onChange` | `Function`<br/>[See signature.](#signature-onChange) | | | Will be triggered whenever a `ViewSwitcher.Button` is clicked. Called with the ViewSwitcherButton value |
76-
| `defaultSelected` | `string` || | Indicates the default selected button |
102+
| Props | Type | Required | Default | Description |
103+
| ----------------- | ---------------------------------------------------- | :------: | ------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
104+
| `isCondensed` | `boolean` | | | Indicates that the view switcher can be reduced to save space. |
105+
| `children` | `ReactNode` || | Pass one or more `ViewSwitcher.Button` components. |
106+
| `onChange` | `Function`<br/>[See signature.](#signature-onChange) | | | Will be triggered whenever a `ViewSwitcher.Button` is selected. Called with the ViewSwitcherButton value.&#xA;This function is only required when the component is controlled. |
107+
| `defaultSelected` | `string` | | | Passing this prop makes the component an uncontrolled component.&#xA;Indicates the default selected button it is only used to as an initial state once, when the component mounts. |
108+
| `selectedValue` | `string` | | | Passing this prop makes the component an controlled component.&#xA;Controlled components also require to pass a `onChange` callback function. |
77109

78110
## Signatures
79111

@@ -83,23 +115,6 @@ export default Example;
83115
(value: string) => void
84116
```
85117

86-
### ViewSwitcher.Button
87-
88-
| Props | Type | Required | Default | Description |
89-
| ------------ | --------------------------------------------------- | :------: | ------- | ------------------------------------------------------------ |
90-
| `isDisabled` | `boolean` | | | If `true`, indicates that the button is in a disabled state. |
91-
| `children` | `ReactNode` || | Indicates the label of the `ViewSwitcher.Button`. |
92-
| `onClick` | `Function`<br/>[See signature.](#signature-onClick) | | | Will be triggered whenever a button is clicked. |
93-
| `value` | `string` || | The value identifying this `ViewSwitcher.Button`. |
94-
95-
## Signatures
96-
97-
### Signature `onClick`
98-
99-
```ts
100-
(value: string) => void
101-
```
102-
103118
## Invariants
104119

105-
1. The `ViewSwitcher.Group` must have at least one `ViewSwitcher.Button` element as `children`
120+
1. The `ViewSwitcher.Group` must have at least one `ViewSwitcher.Button` element as `children`
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
## Invariants
2+
3+
1. The `ViewSwitcher.Group` must have at least one `ViewSwitcher.Button` element as `children`
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
The `<ViewSwitcher>` component allow users to toggle between two or more different views of the same, similar or related content items within the same space on screen.
2+
3+
## When to use
4+
5+
Let users toggle between different formatting's, like with a grid view and a table view.
6+
7+
## When not to use
8+
9+
**Do not use the `<ViewSwitcher>` as tabs.**
10+
Tabs should be used when the content on the page is divided into related sections but without any overlap.
11+
12+
**Do not use the `<ViewSwitcher>` as toggle.**
13+
Toggles are used for "yes/no" or "on/off" binary decisions.
14+
15+
## Do's and Don'ts
16+
17+
- If you use an icon within the `<ViewSwitcher>`, each switch needs to have an icon.
18+
- No colored icons are allowed. Only `--color-solid` (black).
19+
- Do not use two lines of text in one switch field.
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import { useState } from 'react';
2+
import ViewSwitcher from '@commercetools-uikit/view-switcher';
3+
4+
/**
5+
* 1. Uncontrolled mode
6+
*
7+
* The component controls its own internal state and switching between options.
8+
* The `defaultSelected` value is only used on the initial render. Changes to that value
9+
* are not reflected in the component state.
10+
*/
11+
const UncontrolledExample = () => (
12+
<ViewSwitcher.Group
13+
defaultSelected="button 2"
14+
onChange={(value) => console.log(value)}
15+
>
16+
<ViewSwitcher.Button isDisabled value="button 1">
17+
View 1
18+
</ViewSwitcher.Button>
19+
<ViewSwitcher.Button value="button 2">View 2</ViewSwitcher.Button>
20+
<ViewSwitcher.Button value="button 3">View 3</ViewSwitcher.Button>
21+
</ViewSwitcher.Group>
22+
);
23+
24+
/**
25+
* 2. Controlled mode
26+
*
27+
* The component is controlled from a parent component, using the prop `selectedValue`.
28+
* The component does not use an internal state and the parent must implement the `onChange` callback.
29+
*/
30+
const ControlledExample = () => {
31+
const [seletedValue, setSelectedValue] = useState('button 1');
32+
33+
return (
34+
<ViewSwitcher.Group
35+
selectedValue={seletedValue}
36+
onChange={setSelectedValue}
37+
>
38+
<ViewSwitcher.Button isDisabled value="button 1">
39+
View 1
40+
</ViewSwitcher.Button>
41+
<ViewSwitcher.Button value="button 2">View 2</ViewSwitcher.Button>
42+
<ViewSwitcher.Button value="button 3">View 3</ViewSwitcher.Button>
43+
</ViewSwitcher.Group>
44+
);
45+
};
46+
47+
export { UncontrolledExample, ControlledExample };

packages/components/view-switcher/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
"@commercetools-uikit/utils": "15.1.2",
2727
"@emotion/react": "^11.4.0",
2828
"@emotion/styled": "^11.3.0",
29+
"lodash": "4.17.21",
2930
"prop-types": "15.8.1"
3031
},
3132
"devDependencies": {

packages/components/view-switcher/src/view-switcher.spec.js renamed to packages/components/view-switcher/src/view-switcher.spec.tsx

Lines changed: 62 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,24 @@
1+
import { useState } from 'react';
12
import { warning } from '@commercetools-uikit/utils';
23
import { screen, render } from '../../../../test/test-utils';
3-
import Group from './view-switcher';
4+
import Group, { type TViewSwitcherProps } from './view-switcher';
45
import Button from './view-switcher-button';
56

67
jest.mock('@commercetools-uikit/utils', () => ({
78
...jest.requireActual('@commercetools-uikit/utils'),
89
warning: jest.fn(),
910
}));
1011

11-
const createButtonTestProps = (index) => ({
12+
const createButtonTestProps = (index: number) => ({
1213
value: `test-button-${index}`,
1314
children: `test button ${index}`,
1415
isDisabled: false,
1516
});
1617

17-
const createGroupTestProps = (numberOfChildren = 3, custom) => {
18+
const createGroupTestProps = (
19+
numberOfChildren = 3,
20+
custom: Partial<TViewSwitcherProps> = {}
21+
): TViewSwitcherProps => {
1822
const buttonChildren = [...Array(numberOfChildren).keys()].map((i) => (
1923
<Button key={i} {...createButtonTestProps(i)} />
2024
));
@@ -27,7 +31,7 @@ const createGroupTestProps = (numberOfChildren = 3, custom) => {
2731
};
2832

2933
describe('rendering', () => {
30-
let props;
34+
let props: TViewSwitcherProps;
3135
beforeEach(() => {
3236
props = createGroupTestProps(3);
3337
});
@@ -131,10 +135,41 @@ describe('rendering', () => {
131135
screen.getByLabelText('Test Button 1').click();
132136
expect(handleClick).not.toHaveBeenCalled();
133137
});
138+
139+
it('should be controlled when selectedValue is passed', () => {
140+
const handleClick = jest.fn();
141+
function TestComponent(props: { defaultSelected: string }) {
142+
const [seletedValue, setSelectedValue] = useState(props.defaultSelected);
143+
144+
return (
145+
<Group selectedValue={seletedValue} onChange={setSelectedValue}>
146+
<Button value="test-button-1" onClick={handleClick}>
147+
Test Button 1
148+
</Button>
149+
<Button value="test-button-2" onClick={handleClick}>
150+
Test Button 2
151+
</Button>
152+
</Group>
153+
);
154+
}
155+
render(<TestComponent defaultSelected="test-button-1" />);
156+
157+
// test-button-1 is already active so onClick is not called.
158+
screen.getByLabelText('Test Button 1').click();
159+
expect(handleClick).not.toHaveBeenCalled();
160+
161+
// test-button-2 is not active so onClick is called.
162+
screen.getByLabelText('Test Button 2').click();
163+
expect(handleClick).toHaveBeenCalled();
164+
165+
// test-button-2 is now active so onClick is not called again.
166+
screen.getByLabelText('Test Button 2').click();
167+
expect(handleClick).toHaveBeenCalledTimes(1);
168+
});
134169
});
135170

136171
describe('warnings', () => {
137-
let props;
172+
let props: TViewSwitcherProps;
138173
beforeEach(() => {
139174
props = createGroupTestProps(0);
140175
});
@@ -145,4 +180,26 @@ describe('warnings', () => {
145180
'ViewSwitcher.Group must contain at least one ViewSwitcher.Button'
146181
);
147182
});
183+
it('should warn when selectedValue is passed but no onChange', () => {
184+
render(
185+
<Group selectedValue="test-button-1">
186+
<Button value="test-button-1">Test Button 1</Button>
187+
</Group>
188+
);
189+
expect(warning).toHaveBeenCalledWith(
190+
false,
191+
'ViewSwitcher.Group must contain at least one ViewSwitcher.Button'
192+
);
193+
});
194+
it('should warn when both defaultSelected and selectedValue as passed', () => {
195+
render(
196+
<Group selectedValue="test-button-1" defaultSelected="test-button-1">
197+
<Button value="test-button-1">Test Button 1</Button>
198+
</Group>
199+
);
200+
expect(warning).toHaveBeenCalledWith(
201+
false,
202+
'ui-kit/ViewSwitcher: passed both "selectedValue" (uncontrolled component) prop and "defaultSelected" (uncontrolled component). Please pass only one as the component can only be either controlled or uncontrolled.'
203+
);
204+
});
148205
});

0 commit comments

Comments
 (0)