@transferwise/components 46.7.0 → 46.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build/index.esm.js +154 -76
- package/build/index.esm.js.map +1 -1
- package/build/index.js +155 -76
- package/build/index.js.map +1 -1
- package/build/main.css +101 -0
- package/build/styles/main.css +101 -0
- package/build/styles/segmentedControl/SegmentedControl.css +101 -0
- package/build/types/checkboxOption/CheckboxOption.d.ts +2 -2
- package/build/types/checkboxOption/CheckboxOption.d.ts.map +1 -1
- package/build/types/index.d.ts +4 -0
- package/build/types/index.d.ts.map +1 -1
- package/build/types/segmentedControl/SegmentedControl.d.ts +31 -0
- package/build/types/segmentedControl/SegmentedControl.d.ts.map +1 -0
- package/build/types/segmentedControl/index.d.ts +3 -0
- package/build/types/segmentedControl/index.d.ts.map +1 -0
- package/build/types/snackbar/Snackbar.d.ts +30 -22
- package/build/types/snackbar/Snackbar.d.ts.map +1 -1
- package/build/types/snackbar/SnackbarContext.d.ts +7 -2
- package/build/types/snackbar/SnackbarContext.d.ts.map +1 -1
- package/build/types/snackbar/SnackbarProvider.d.ts +7 -12
- package/build/types/snackbar/SnackbarProvider.d.ts.map +1 -1
- package/build/types/snackbar/index.d.ts +2 -0
- package/build/types/snackbar/index.d.ts.map +1 -0
- package/build/types/snackbar/useSnackbar.d.ts +1 -1
- package/build/types/snackbar/useSnackbar.d.ts.map +1 -1
- package/build/types/withNextPortal/withNextPortal.d.ts +1 -1
- package/build/types/withNextPortal/withNextPortal.d.ts.map +1 -1
- package/package.json +9 -18
- package/src/checkboxOption/CheckboxOption.tsx +2 -2
- package/src/index.ts +4 -0
- package/src/main.css +101 -0
- package/src/main.less +1 -0
- package/src/segmentedControl/SegmentedControl.css +101 -0
- package/src/segmentedControl/SegmentedControl.less +101 -0
- package/src/segmentedControl/SegmentedControl.spec.tsx +106 -0
- package/src/segmentedControl/SegmentedControl.story.tsx +55 -0
- package/src/segmentedControl/SegmentedControl.tsx +175 -0
- package/src/segmentedControl/index.ts +2 -0
- package/src/snackbar/{Snackbar.story.js → Snackbar.story.tsx} +2 -1
- package/src/snackbar/{Snackbar.js → Snackbar.tsx} +31 -32
- package/src/snackbar/SnackbarContext.ts +11 -0
- package/src/snackbar/SnackbarProvider.tsx +39 -0
- package/src/ssr.spec.js +17 -0
- package/src/withDisplayFormat/WithDisplayFormat.spec.js +1 -1
- package/src/withDisplayFormat/WithDisplayFormat.tsx +1 -1
- package/src/withNextPortal/withNextPortal.tsx +1 -1
- package/src/snackbar/SnackbarContext.js +0 -4
- package/src/snackbar/SnackbarProvider.js +0 -51
- /package/src/snackbar/{index.js → index.ts} +0 -0
- /package/src/snackbar/{useSnackbar.js → useSnackbar.ts} +0 -0
|
@@ -13,7 +13,7 @@ export type CheckboxOptionProps = Omit<BaseOptionProps, 'onChange'> & {
|
|
|
13
13
|
/**
|
|
14
14
|
* Function to call when the checkbox option is clicked
|
|
15
15
|
*/
|
|
16
|
-
onChange
|
|
16
|
+
onChange?: (value: boolean) => void;
|
|
17
17
|
};
|
|
18
18
|
|
|
19
19
|
const CheckboxOption = forwardRef<ReferenceType, CheckboxOptionProps>(
|
|
@@ -27,7 +27,7 @@ const CheckboxOption = forwardRef<ReferenceType, CheckboxOptionProps>(
|
|
|
27
27
|
<CheckboxButton
|
|
28
28
|
checked={checked}
|
|
29
29
|
disabled={disabled}
|
|
30
|
-
onChange={() => onChange(!checked)}
|
|
30
|
+
onChange={() => onChange?.(!checked)}
|
|
31
31
|
/>
|
|
32
32
|
}
|
|
33
33
|
/>
|
package/src/index.ts
CHANGED
|
@@ -16,6 +16,8 @@ export type {
|
|
|
16
16
|
} from './inputs/SelectInput';
|
|
17
17
|
export type { TextAreaProps } from './inputs/TextArea';
|
|
18
18
|
export type { PhoneNumberInputProps } from './phoneNumberInput/PhoneNumberInput';
|
|
19
|
+
export type { SnackbarProps } from './snackbar/Snackbar';
|
|
20
|
+
export type { SnackbarContextType } from './snackbar/SnackbarContext';
|
|
19
21
|
export type { TextareaWithDisplayFormatProps } from './textareaWithDisplayFormat';
|
|
20
22
|
export type { UploadedFile, UploadError, UploadResponse } from './uploadInput/types';
|
|
21
23
|
export type { ModalProps } from './modal';
|
|
@@ -33,6 +35,7 @@ export type {
|
|
|
33
35
|
LinkTypes,
|
|
34
36
|
DisplayTypes,
|
|
35
37
|
} from './common';
|
|
38
|
+
export type { SegmentedControlProps } from './segmentedControl';
|
|
36
39
|
|
|
37
40
|
/**
|
|
38
41
|
* Components
|
|
@@ -107,6 +110,7 @@ export { default as RadioGroup } from './radioGroup';
|
|
|
107
110
|
export { default as RadioOption } from './radioOption';
|
|
108
111
|
export { default as Section } from './section';
|
|
109
112
|
export { default as Select } from './select';
|
|
113
|
+
export { default as SegmentedControl } from './segmentedControl';
|
|
110
114
|
export { default as SlidingPanel } from './slidingPanel';
|
|
111
115
|
export { default as SnackbarPortal } from './snackbar/Snackbar';
|
|
112
116
|
export { default as SnackbarProvider } from './snackbar/SnackbarProvider';
|
package/src/main.css
CHANGED
|
@@ -4629,6 +4629,107 @@ html:not([dir="rtl"]) .np-navigation-option {
|
|
|
4629
4629
|
.np-theme-personal .np-dropdown-menu .np-dropdown-item--focused {
|
|
4630
4630
|
box-shadow: inset 0 0 0 2px var(--color-interactive-primary);
|
|
4631
4631
|
}
|
|
4632
|
+
.segmented-control {
|
|
4633
|
+
box-sizing: border-box;
|
|
4634
|
+
--segment-highlight-width: 0;
|
|
4635
|
+
--segment-highlight-x: var(--size-4);
|
|
4636
|
+
}
|
|
4637
|
+
.segmented-control__segments {
|
|
4638
|
+
display: inline-flex;
|
|
4639
|
+
position: relative;
|
|
4640
|
+
padding: 4px;
|
|
4641
|
+
padding: var(--size-4);
|
|
4642
|
+
width: 100%;
|
|
4643
|
+
justify-content: center;
|
|
4644
|
+
align-items: center;
|
|
4645
|
+
background: rgba(134,167,189,0.10196);
|
|
4646
|
+
background: var(--color-background-neutral);
|
|
4647
|
+
border-radius: 24px;
|
|
4648
|
+
border-radius: var(--size-24);
|
|
4649
|
+
transition: outline 300ms;
|
|
4650
|
+
outline: 2px solid transparent;
|
|
4651
|
+
}
|
|
4652
|
+
.segmented-control--input:has(:focus-visible) > .segmented-control__segments::after {
|
|
4653
|
+
outline: 2px solid var(--color-interactive-primary);
|
|
4654
|
+
}
|
|
4655
|
+
.segmented-control__segments::after {
|
|
4656
|
+
content: "";
|
|
4657
|
+
position: absolute;
|
|
4658
|
+
width: var(--segment-highlight-width);
|
|
4659
|
+
top: 4px;
|
|
4660
|
+
top: var(--size-4);
|
|
4661
|
+
bottom: 4px;
|
|
4662
|
+
bottom: var(--size-4);
|
|
4663
|
+
left: var(--segment-highlight-x);
|
|
4664
|
+
z-index: 0;
|
|
4665
|
+
background: #ffffff;
|
|
4666
|
+
background: var(--color-background-screen);
|
|
4667
|
+
border-radius: 24px;
|
|
4668
|
+
border-radius: var(--size-24);
|
|
4669
|
+
transition: left 300ms;
|
|
4670
|
+
}
|
|
4671
|
+
.segmented-control__segments--no-animate::after {
|
|
4672
|
+
transition: none !important;
|
|
4673
|
+
}
|
|
4674
|
+
.segmented-control__segment {
|
|
4675
|
+
position: relative;
|
|
4676
|
+
flex: 1 1 100%;
|
|
4677
|
+
flex-flow: column;
|
|
4678
|
+
padding: 8px 16px;
|
|
4679
|
+
padding: var(--size-8) var(--size-16);
|
|
4680
|
+
margin: 0 0 0 4px;
|
|
4681
|
+
margin: 0 0 0 var(--size-4);
|
|
4682
|
+
min-width: 0;
|
|
4683
|
+
line-height: inherit;
|
|
4684
|
+
align-items: center;
|
|
4685
|
+
text-align: center;
|
|
4686
|
+
vertical-align: middle;
|
|
4687
|
+
border-radius: 24px;
|
|
4688
|
+
border-radius: var(--size-24);
|
|
4689
|
+
z-index: 1;
|
|
4690
|
+
cursor: pointer;
|
|
4691
|
+
transition: background 300ms;
|
|
4692
|
+
color: var(--color-interactive-primary);
|
|
4693
|
+
}
|
|
4694
|
+
.segmented-control__segment:first-child {
|
|
4695
|
+
margin-left: 0;
|
|
4696
|
+
}
|
|
4697
|
+
.segmented-control__segment:hover {
|
|
4698
|
+
background: rgba(0,0,0,0.10196);
|
|
4699
|
+
background: var(--color-background-overlay);
|
|
4700
|
+
}
|
|
4701
|
+
.segmented-control__radio-input {
|
|
4702
|
+
position: fixed;
|
|
4703
|
+
opacity: 0;
|
|
4704
|
+
pointer-events: none;
|
|
4705
|
+
}
|
|
4706
|
+
.segmented-control__button {
|
|
4707
|
+
width: 100%;
|
|
4708
|
+
height: 100%;
|
|
4709
|
+
background: none;
|
|
4710
|
+
-webkit-appearance: none;
|
|
4711
|
+
-moz-appearance: none;
|
|
4712
|
+
appearance: none;
|
|
4713
|
+
border: none;
|
|
4714
|
+
outline: none;
|
|
4715
|
+
font: inherit;
|
|
4716
|
+
outline: 2px solid transparent;
|
|
4717
|
+
}
|
|
4718
|
+
.segmented-control__button:focus {
|
|
4719
|
+
outline-offset: 0px;
|
|
4720
|
+
}
|
|
4721
|
+
.segmented-control__button:focus-visible {
|
|
4722
|
+
outline-color: var(--color-interactive-primary);
|
|
4723
|
+
}
|
|
4724
|
+
.segmented-control__selected-segment:hover {
|
|
4725
|
+
background: transparent;
|
|
4726
|
+
}
|
|
4727
|
+
.segmented-control__text {
|
|
4728
|
+
word-wrap: break-word;
|
|
4729
|
+
word-break: break-word;
|
|
4730
|
+
color: var(--color-interactive-primary);
|
|
4731
|
+
transition: font-weight 300ms;
|
|
4732
|
+
}
|
|
4632
4733
|
.np-summary {
|
|
4633
4734
|
min-width: 280px;
|
|
4634
4735
|
}
|
package/src/main.less
CHANGED
|
@@ -53,6 +53,7 @@
|
|
|
53
53
|
@import "./statusIcon/StatusIcon.less";
|
|
54
54
|
@import "./stepper/Stepper.less";
|
|
55
55
|
@import "./select/Select.less";
|
|
56
|
+
@import "./segmentedControl/SegmentedControl.less";
|
|
56
57
|
@import "./summary/Summary.less";
|
|
57
58
|
@import "./switch/Switch.less";
|
|
58
59
|
@import "./tabs/Tabs.less";
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
.segmented-control {
|
|
2
|
+
box-sizing: border-box;
|
|
3
|
+
--segment-highlight-width: 0;
|
|
4
|
+
--segment-highlight-x: var(--size-4);
|
|
5
|
+
}
|
|
6
|
+
.segmented-control__segments {
|
|
7
|
+
display: inline-flex;
|
|
8
|
+
position: relative;
|
|
9
|
+
padding: 4px;
|
|
10
|
+
padding: var(--size-4);
|
|
11
|
+
width: 100%;
|
|
12
|
+
justify-content: center;
|
|
13
|
+
align-items: center;
|
|
14
|
+
background: rgba(134,167,189,0.10196);
|
|
15
|
+
background: var(--color-background-neutral);
|
|
16
|
+
border-radius: 24px;
|
|
17
|
+
border-radius: var(--size-24);
|
|
18
|
+
transition: outline 300ms;
|
|
19
|
+
outline: 2px solid transparent;
|
|
20
|
+
}
|
|
21
|
+
.segmented-control--input:has(:focus-visible) > .segmented-control__segments::after {
|
|
22
|
+
outline: 2px solid var(--color-interactive-primary);
|
|
23
|
+
}
|
|
24
|
+
.segmented-control__segments::after {
|
|
25
|
+
content: "";
|
|
26
|
+
position: absolute;
|
|
27
|
+
width: var(--segment-highlight-width);
|
|
28
|
+
top: 4px;
|
|
29
|
+
top: var(--size-4);
|
|
30
|
+
bottom: 4px;
|
|
31
|
+
bottom: var(--size-4);
|
|
32
|
+
left: var(--segment-highlight-x);
|
|
33
|
+
z-index: 0;
|
|
34
|
+
background: #ffffff;
|
|
35
|
+
background: var(--color-background-screen);
|
|
36
|
+
border-radius: 24px;
|
|
37
|
+
border-radius: var(--size-24);
|
|
38
|
+
transition: left 300ms;
|
|
39
|
+
}
|
|
40
|
+
.segmented-control__segments--no-animate::after {
|
|
41
|
+
transition: none !important;
|
|
42
|
+
}
|
|
43
|
+
.segmented-control__segment {
|
|
44
|
+
position: relative;
|
|
45
|
+
flex: 1 1 100%;
|
|
46
|
+
flex-flow: column;
|
|
47
|
+
padding: 8px 16px;
|
|
48
|
+
padding: var(--size-8) var(--size-16);
|
|
49
|
+
margin: 0 0 0 4px;
|
|
50
|
+
margin: 0 0 0 var(--size-4);
|
|
51
|
+
min-width: 0;
|
|
52
|
+
line-height: inherit;
|
|
53
|
+
align-items: center;
|
|
54
|
+
text-align: center;
|
|
55
|
+
vertical-align: middle;
|
|
56
|
+
border-radius: 24px;
|
|
57
|
+
border-radius: var(--size-24);
|
|
58
|
+
z-index: 1;
|
|
59
|
+
cursor: pointer;
|
|
60
|
+
transition: background 300ms;
|
|
61
|
+
color: var(--color-interactive-primary);
|
|
62
|
+
}
|
|
63
|
+
.segmented-control__segment:first-child {
|
|
64
|
+
margin-left: 0;
|
|
65
|
+
}
|
|
66
|
+
.segmented-control__segment:hover {
|
|
67
|
+
background: rgba(0,0,0,0.10196);
|
|
68
|
+
background: var(--color-background-overlay);
|
|
69
|
+
}
|
|
70
|
+
.segmented-control__radio-input {
|
|
71
|
+
position: fixed;
|
|
72
|
+
opacity: 0;
|
|
73
|
+
pointer-events: none;
|
|
74
|
+
}
|
|
75
|
+
.segmented-control__button {
|
|
76
|
+
width: 100%;
|
|
77
|
+
height: 100%;
|
|
78
|
+
background: none;
|
|
79
|
+
-webkit-appearance: none;
|
|
80
|
+
-moz-appearance: none;
|
|
81
|
+
appearance: none;
|
|
82
|
+
border: none;
|
|
83
|
+
outline: none;
|
|
84
|
+
font: inherit;
|
|
85
|
+
outline: 2px solid transparent;
|
|
86
|
+
}
|
|
87
|
+
.segmented-control__button:focus {
|
|
88
|
+
outline-offset: 0px;
|
|
89
|
+
}
|
|
90
|
+
.segmented-control__button:focus-visible {
|
|
91
|
+
outline-color: var(--color-interactive-primary);
|
|
92
|
+
}
|
|
93
|
+
.segmented-control__selected-segment:hover {
|
|
94
|
+
background: transparent;
|
|
95
|
+
}
|
|
96
|
+
.segmented-control__text {
|
|
97
|
+
word-wrap: break-word;
|
|
98
|
+
word-break: break-word;
|
|
99
|
+
color: var(--color-interactive-primary);
|
|
100
|
+
transition: font-weight 300ms;
|
|
101
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
.segmented-control {
|
|
2
|
+
box-sizing: border-box;
|
|
3
|
+
--segment-highlight-width: 0;
|
|
4
|
+
--segment-highlight-x: var(--size-4);
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
.segmented-control__segments {
|
|
8
|
+
display: inline-flex;
|
|
9
|
+
position: relative;
|
|
10
|
+
padding: var(--size-4);
|
|
11
|
+
width: 100%;
|
|
12
|
+
justify-content: center;
|
|
13
|
+
align-items: center;
|
|
14
|
+
background: var(--color-background-neutral);
|
|
15
|
+
border-radius: var(--size-24);
|
|
16
|
+
transition: outline 300ms;
|
|
17
|
+
outline: 2px solid transparent;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
.segmented-control--input:has(:focus-visible) > .segmented-control__segments::after {
|
|
21
|
+
outline: 2px solid var(--color-interactive-primary);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
.segmented-control__segments::after {
|
|
25
|
+
content: "";
|
|
26
|
+
position: absolute;
|
|
27
|
+
width: var(--segment-highlight-width);
|
|
28
|
+
top: var(--size-4);
|
|
29
|
+
bottom: var(--size-4);
|
|
30
|
+
left: var(--segment-highlight-x);
|
|
31
|
+
z-index: 0;
|
|
32
|
+
background: var(--color-background-screen);
|
|
33
|
+
border-radius: var(--size-24);
|
|
34
|
+
transition: left 300ms;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
.segmented-control__segments--no-animate::after {
|
|
38
|
+
transition: none !important;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
.segmented-control__segment {
|
|
42
|
+
position: relative;
|
|
43
|
+
flex: 1 1 100%;
|
|
44
|
+
flex-flow: column;
|
|
45
|
+
padding: var(--size-8) var(--size-16);
|
|
46
|
+
margin: 0 0 0 var(--size-4);
|
|
47
|
+
min-width: 0;
|
|
48
|
+
line-height: inherit;
|
|
49
|
+
align-items: center;
|
|
50
|
+
text-align: center;
|
|
51
|
+
vertical-align: middle;
|
|
52
|
+
border-radius: var(--size-24);
|
|
53
|
+
z-index: 1;
|
|
54
|
+
cursor: pointer;
|
|
55
|
+
transition: background 300ms;
|
|
56
|
+
color: var(--color-interactive-primary);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
.segmented-control__segment:first-child {
|
|
60
|
+
margin-left: 0;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
.segmented-control__segment:hover {
|
|
64
|
+
background: var(--color-background-overlay);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
.segmented-control__radio-input {
|
|
68
|
+
position: fixed;
|
|
69
|
+
opacity: 0;
|
|
70
|
+
pointer-events: none;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
.segmented-control__button {
|
|
74
|
+
width: 100%;
|
|
75
|
+
height: 100%;
|
|
76
|
+
background: none;
|
|
77
|
+
appearance: none;
|
|
78
|
+
border: none;
|
|
79
|
+
outline: none;
|
|
80
|
+
font: inherit;
|
|
81
|
+
outline: 2px solid transparent;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
.segmented-control__button:focus {
|
|
85
|
+
outline-offset: 0px;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
.segmented-control__button:focus-visible {
|
|
89
|
+
outline-color: var(--color-interactive-primary);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
.segmented-control__selected-segment:hover {
|
|
93
|
+
background: transparent;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
.segmented-control__text {
|
|
97
|
+
word-wrap: break-word;
|
|
98
|
+
word-break: break-word;
|
|
99
|
+
color: var(--color-interactive-primary);
|
|
100
|
+
transition: font-weight 300ms;
|
|
101
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import '@testing-library/jest-dom';
|
|
2
|
+
import { render, screen, userEvent } from '../test-utils';
|
|
3
|
+
|
|
4
|
+
import SegmentedControl, { SegmentedControlProps } from './SegmentedControl';
|
|
5
|
+
|
|
6
|
+
const defaultSegments = [
|
|
7
|
+
{
|
|
8
|
+
id: '1',
|
|
9
|
+
value: 'accounting',
|
|
10
|
+
label: 'Accounting',
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
id: '2',
|
|
14
|
+
value: 'payroll',
|
|
15
|
+
label: 'Payroll',
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
id: '3',
|
|
19
|
+
value: 'reporting',
|
|
20
|
+
label: 'Reporting',
|
|
21
|
+
},
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
const defaultSegmentsWithControls = defaultSegments.map((segment, index) => ({
|
|
25
|
+
...segment,
|
|
26
|
+
controls: `aControlId${index}`,
|
|
27
|
+
}));
|
|
28
|
+
|
|
29
|
+
const onChange = jest.fn();
|
|
30
|
+
|
|
31
|
+
const defaultProps: SegmentedControlProps = {
|
|
32
|
+
name: 'segmentedControl',
|
|
33
|
+
defaultValue: defaultSegments[0].value,
|
|
34
|
+
mode: 'input',
|
|
35
|
+
segments: defaultSegments,
|
|
36
|
+
onChange,
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const renderSegmentedControl = (overrides: Partial<SegmentedControlProps> = {}) => {
|
|
40
|
+
return render(<SegmentedControl {...defaultProps} {...(overrides as SegmentedControlProps)} />);
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
describe('SegmentedControl', () => {
|
|
44
|
+
it('dynamically adds inline style props for animated segment overlay', () => {
|
|
45
|
+
renderSegmentedControl();
|
|
46
|
+
|
|
47
|
+
const segmentedControls = screen.getByTestId('segmented-control');
|
|
48
|
+
|
|
49
|
+
expect(segmentedControls).toHaveStyle(
|
|
50
|
+
'--segment-highlight-width: 0px; --segment-highlight-x: 0px;',
|
|
51
|
+
);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('default value is selected', () => {
|
|
55
|
+
renderSegmentedControl({ mode: 'view', segments: defaultSegmentsWithControls });
|
|
56
|
+
|
|
57
|
+
const accountingTab = screen.getByRole('tab', { name: 'Accounting' });
|
|
58
|
+
expect(accountingTab).toHaveAttribute('aria-selected', 'true');
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('lets the user pick through the segments when it is set to input', () => {
|
|
62
|
+
renderSegmentedControl();
|
|
63
|
+
|
|
64
|
+
const payroll = screen.getByRole('radio', { name: 'Payroll' });
|
|
65
|
+
userEvent.click(payroll);
|
|
66
|
+
|
|
67
|
+
expect(onChange).toHaveBeenCalledWith('payroll');
|
|
68
|
+
|
|
69
|
+
const reporting = screen.getByRole('radio', { name: 'Reporting' });
|
|
70
|
+
userEvent.click(reporting);
|
|
71
|
+
|
|
72
|
+
expect(onChange).toHaveBeenCalledWith('reporting');
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('lets the user pick through the segments when it is set to view', () => {
|
|
76
|
+
renderSegmentedControl({ mode: 'view', segments: defaultSegmentsWithControls });
|
|
77
|
+
|
|
78
|
+
const payroll = screen.getByRole('tab', { name: 'Payroll' });
|
|
79
|
+
userEvent.click(payroll);
|
|
80
|
+
|
|
81
|
+
expect(onChange).toHaveBeenCalledWith('payroll');
|
|
82
|
+
|
|
83
|
+
const reporting = screen.getByRole('tab', { name: 'Reporting' });
|
|
84
|
+
userEvent.click(reporting);
|
|
85
|
+
|
|
86
|
+
expect(onChange).toHaveBeenCalledWith('reporting');
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('throws error if user tries to add too many segments', () => {
|
|
90
|
+
expect(() => {
|
|
91
|
+
renderSegmentedControl({
|
|
92
|
+
mode: 'input',
|
|
93
|
+
segments: [
|
|
94
|
+
...defaultSegments,
|
|
95
|
+
{
|
|
96
|
+
id: '4',
|
|
97
|
+
value: 'anotherOne',
|
|
98
|
+
label: 'Another One',
|
|
99
|
+
},
|
|
100
|
+
],
|
|
101
|
+
});
|
|
102
|
+
}).toThrow(
|
|
103
|
+
'SegmentedControl only supports up to 3 segments. Please refer to: https://wise.design/components/segmented-control',
|
|
104
|
+
);
|
|
105
|
+
});
|
|
106
|
+
});
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { StoryFn } from '@storybook/react';
|
|
2
|
+
import React from 'react';
|
|
3
|
+
|
|
4
|
+
import SegmentedControl, { Segments } from './SegmentedControl';
|
|
5
|
+
|
|
6
|
+
export default {
|
|
7
|
+
component: SegmentedControl,
|
|
8
|
+
title: 'Forms/SegmentedControl',
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
const segments: Segments = [
|
|
12
|
+
{ id: 'CUPCAKE', label: 'Cupcakes', value: 'cupcakes' },
|
|
13
|
+
{ id: 'SPONGECAKE', label: 'Sponge cake', value: 'spongecake' },
|
|
14
|
+
{ id: 'CARROT_CAKE', label: 'Carrot cake', value: 'carrotcake' },
|
|
15
|
+
];
|
|
16
|
+
|
|
17
|
+
const segmentsWithControls: Segments = [
|
|
18
|
+
{ id: 'CUPCAKE', label: 'Cupcakes', value: 'cupcakes', controls: 'aControlId' },
|
|
19
|
+
{ id: 'SPONGECAKE', label: 'Sponge cake', value: 'spongecake', controls: 'aControlId' },
|
|
20
|
+
{ id: 'CARROT_CAKE', label: 'Carrot cake', value: 'carrotcake', controls: 'aControlId' },
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
const Template: StoryFn = (args) => {
|
|
24
|
+
const [selectedValue, setSelectedValue] = React.useState(segments[0].value);
|
|
25
|
+
|
|
26
|
+
return (
|
|
27
|
+
<div className="p-a-2">
|
|
28
|
+
<SegmentedControl
|
|
29
|
+
name="aSegmentedControl"
|
|
30
|
+
defaultValue={selectedValue}
|
|
31
|
+
onChange={setSelectedValue}
|
|
32
|
+
{...(args.mode === 'view'
|
|
33
|
+
? { segments: segmentsWithControls, mode: 'view', controls: 'aControlId' }
|
|
34
|
+
: { segments, mode: 'input' })}
|
|
35
|
+
/>
|
|
36
|
+
<div className="m-a-2" id="aControlId">
|
|
37
|
+
<p>Selected value: {selectedValue}</p>
|
|
38
|
+
</div>
|
|
39
|
+
</div>
|
|
40
|
+
);
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export const SegmentedControlDefault = {
|
|
44
|
+
render: Template,
|
|
45
|
+
args: {
|
|
46
|
+
mode: 'input',
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
export const SegmentedControlView = {
|
|
51
|
+
render: Template,
|
|
52
|
+
args: {
|
|
53
|
+
mode: 'view',
|
|
54
|
+
},
|
|
55
|
+
};
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import classNames from 'classnames';
|
|
2
|
+
import { createRef, useEffect, useRef, useState } from 'react';
|
|
3
|
+
|
|
4
|
+
import Body from '../body';
|
|
5
|
+
import { Typography } from '../common';
|
|
6
|
+
|
|
7
|
+
type SegmentBase = { id: string; label: string; value: string };
|
|
8
|
+
|
|
9
|
+
type Segment = SegmentBase & { controls?: never };
|
|
10
|
+
type SegmentWithControls = SegmentBase & { controls: string };
|
|
11
|
+
|
|
12
|
+
export type Segments = Segment[] | SegmentWithControls[];
|
|
13
|
+
|
|
14
|
+
type SegmentedControlPropsBase = {
|
|
15
|
+
name: string;
|
|
16
|
+
defaultValue: string;
|
|
17
|
+
mode: 'input' | 'view';
|
|
18
|
+
onChange: (value: string) => void;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
type SegmentedControlViewProps = {
|
|
22
|
+
mode: 'view';
|
|
23
|
+
segments: SegmentWithControls[];
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
type SegmentedControlInputProps = {
|
|
27
|
+
mode: 'input';
|
|
28
|
+
segments: Segment[];
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export type SegmentedControlProps = SegmentedControlPropsBase &
|
|
32
|
+
(SegmentedControlViewProps | SegmentedControlInputProps);
|
|
33
|
+
|
|
34
|
+
const SegmentedControl = ({
|
|
35
|
+
name,
|
|
36
|
+
defaultValue,
|
|
37
|
+
mode = 'input',
|
|
38
|
+
segments,
|
|
39
|
+
onChange,
|
|
40
|
+
}: SegmentedControlProps) => {
|
|
41
|
+
const [selectedValue, setSelectedValue] = useState(defaultValue || segments[0].value);
|
|
42
|
+
const [animate, setAnimate] = useState(false);
|
|
43
|
+
|
|
44
|
+
const segmentsRef = useRef<HTMLDivElement>(null);
|
|
45
|
+
|
|
46
|
+
if (segments.length > 3) {
|
|
47
|
+
throw new Error(
|
|
48
|
+
'SegmentedControl only supports up to 3 segments. Please refer to: https://wise.design/components/segmented-control',
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const segmentsWithRefs = segments.map((segment) => ({
|
|
53
|
+
...segment,
|
|
54
|
+
ref: createRef<HTMLLabelElement | HTMLButtonElement>(),
|
|
55
|
+
}));
|
|
56
|
+
|
|
57
|
+
const updateSegmentPosition = () => {
|
|
58
|
+
const selectedSegmentRef = segmentsWithRefs.find(
|
|
59
|
+
(segment) => segment.value === selectedValue,
|
|
60
|
+
)?.ref;
|
|
61
|
+
|
|
62
|
+
// We grab the active segments style object from the ref
|
|
63
|
+
// and set the css variables to the selected segments width and x position.
|
|
64
|
+
// This is so we can animate the highlight to the selected segment
|
|
65
|
+
if (selectedSegmentRef?.current && segmentsRef.current) {
|
|
66
|
+
const { style } = segmentsRef.current;
|
|
67
|
+
style.setProperty('--segment-highlight-width', `${selectedSegmentRef.current.offsetWidth}px`);
|
|
68
|
+
style.setProperty('--segment-highlight-x', `${selectedSegmentRef.current.offsetLeft}px`);
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
useEffect(() => {
|
|
73
|
+
updateSegmentPosition();
|
|
74
|
+
|
|
75
|
+
const handleWindowSizeChange = () => {
|
|
76
|
+
setAnimate(false);
|
|
77
|
+
updateSegmentPosition();
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
window.addEventListener('resize', handleWindowSizeChange);
|
|
81
|
+
return () => {
|
|
82
|
+
window.removeEventListener('resize', handleWindowSizeChange);
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
86
|
+
}, [segmentsWithRefs, selectedValue]);
|
|
87
|
+
|
|
88
|
+
useEffect(() => {
|
|
89
|
+
onChange(selectedValue);
|
|
90
|
+
}, [onChange, selectedValue]);
|
|
91
|
+
|
|
92
|
+
return (
|
|
93
|
+
<div
|
|
94
|
+
ref={segmentsRef}
|
|
95
|
+
data-testid="segmented-control"
|
|
96
|
+
className={classNames('segmented-control', {
|
|
97
|
+
'segmented-control--input': mode === 'input',
|
|
98
|
+
})}
|
|
99
|
+
>
|
|
100
|
+
<div
|
|
101
|
+
className={classNames('segmented-control__segments', {
|
|
102
|
+
'segmented-control__segments--no-animate': !animate,
|
|
103
|
+
})}
|
|
104
|
+
>
|
|
105
|
+
{segmentsWithRefs.map((segment) =>
|
|
106
|
+
mode === 'input' ? (
|
|
107
|
+
<label
|
|
108
|
+
ref={segment.ref as React.RefObject<HTMLLabelElement>}
|
|
109
|
+
key={segment.id}
|
|
110
|
+
htmlFor={segment.id}
|
|
111
|
+
className={classNames('segmented-control__segment', {
|
|
112
|
+
'segmented-control__selected-segment': selectedValue === segment.value,
|
|
113
|
+
})}
|
|
114
|
+
>
|
|
115
|
+
<input
|
|
116
|
+
type="radio"
|
|
117
|
+
className="segmented-control__radio-input"
|
|
118
|
+
id={segment.id}
|
|
119
|
+
name={name}
|
|
120
|
+
value={segment.value}
|
|
121
|
+
checked={selectedValue === segment.value}
|
|
122
|
+
onChange={() => {
|
|
123
|
+
setAnimate(true);
|
|
124
|
+
setSelectedValue(segment.value);
|
|
125
|
+
}}
|
|
126
|
+
/>
|
|
127
|
+
<Body
|
|
128
|
+
className="segmented-control__text"
|
|
129
|
+
as="span"
|
|
130
|
+
type={
|
|
131
|
+
selectedValue === segment.value
|
|
132
|
+
? Typography.BODY_DEFAULT_BOLD
|
|
133
|
+
: Typography.BODY_DEFAULT
|
|
134
|
+
}
|
|
135
|
+
>
|
|
136
|
+
{segment.label}
|
|
137
|
+
</Body>
|
|
138
|
+
</label>
|
|
139
|
+
) : (
|
|
140
|
+
<button
|
|
141
|
+
ref={segment.ref as React.RefObject<HTMLButtonElement>}
|
|
142
|
+
key={segment.id}
|
|
143
|
+
type="button"
|
|
144
|
+
role="tab"
|
|
145
|
+
id={segment.id}
|
|
146
|
+
aria-controls={segment.controls}
|
|
147
|
+
aria-selected={selectedValue === segment.value}
|
|
148
|
+
className={classNames('segmented-control__segment', 'segmented-control__button', {
|
|
149
|
+
'segmented-control__selected-segment': selectedValue === segment.value,
|
|
150
|
+
})}
|
|
151
|
+
onClick={() => {
|
|
152
|
+
setAnimate(true);
|
|
153
|
+
setSelectedValue(segment.value);
|
|
154
|
+
}}
|
|
155
|
+
>
|
|
156
|
+
<Body
|
|
157
|
+
as="span"
|
|
158
|
+
className="segmented-control__text"
|
|
159
|
+
type={
|
|
160
|
+
selectedValue === segment.value
|
|
161
|
+
? Typography.BODY_DEFAULT_BOLD
|
|
162
|
+
: Typography.BODY_DEFAULT
|
|
163
|
+
}
|
|
164
|
+
>
|
|
165
|
+
{segment.label}
|
|
166
|
+
</Body>
|
|
167
|
+
</button>
|
|
168
|
+
),
|
|
169
|
+
)}
|
|
170
|
+
</div>
|
|
171
|
+
</div>
|
|
172
|
+
);
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
export default SegmentedControl;
|