Skip to content

Commit 6b2072d

Browse files
authored
Merge pull request #1661 from dxc-technology/gomezivann-toggle-group-title
Add `title` prop to Toggle Group component
2 parents 41e66ee + 193e24d commit 6b2072d

File tree

11 files changed

+308
-206
lines changed

11 files changed

+308
-206
lines changed

lib/src/toggle-group/ToggleGroup.stories.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,14 +53,17 @@ const optionsWithIcon = [
5353
{
5454
value: 1,
5555
icon: wifiSVG,
56+
title: "WiFi connection",
5657
},
5758
{
5859
value: 2,
5960
icon: ethernetSVG,
61+
title: "Ethernet connection",
6062
},
6163
{
6264
value: 3,
6365
icon: gMobileSVG,
66+
title: "3G Mobile data connection",
6467
},
6568
];
6669
const optionsWithIconAndLabel = [

lib/src/toggle-group/ToggleGroup.test.js

Lines changed: 22 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,19 @@ describe("Toggle group component tests", () => {
3434
expect(getByText("Google")).toBeTruthy();
3535
});
3636

37+
test("Toggle group renders with correct aria-label in only-icon scenario", () => {
38+
const { getByRole } = render(
39+
<DxcToggleGroup
40+
label="Toggle group label"
41+
helperText="Toggle group helper text"
42+
options={[
43+
{ value: 1, icon: "https://cdn.icon-icons.com/icons2/2645/PNG/512/mic_mute_icon_159965.png", title: "Mute" },
44+
]}
45+
/>
46+
);
47+
expect(getByRole("button").getAttribute("aria-label")).toBe("Mute");
48+
});
49+
3750
test("Uncontrolled toggle group calls correct function on change with value", () => {
3851
const onChange = jest.fn();
3952
const { getByText } = render(<DxcToggleGroup options={options} onChange={onChange} />);
@@ -61,15 +74,15 @@ describe("Toggle group component tests", () => {
6174
test("Uncontrolled multiple toggle group calls correct function on change with value when is multiple", () => {
6275
const onChange = jest.fn();
6376
const { getAllByRole } = render(<DxcToggleGroup options={options} onChange={onChange} multiple />);
64-
const toggleOptions = getAllByRole("switch");
77+
const toggleOptions = getAllByRole("button");
6578
fireEvent.click(toggleOptions[0]);
6679
expect(onChange).toHaveBeenCalledWith([1]);
6780
fireEvent.click(toggleOptions[1]);
6881
fireEvent.click(toggleOptions[3]);
6982
expect(onChange).toHaveBeenCalledWith([1, 2, 4]);
70-
expect(toggleOptions[0].getAttribute("aria-checked")).toBe("true");
71-
expect(toggleOptions[1].getAttribute("aria-checked")).toBe("true");
72-
expect(toggleOptions[3].getAttribute("aria-checked")).toBe("true");
83+
expect(toggleOptions[0].getAttribute("aria-pressed")).toBe("true");
84+
expect(toggleOptions[1].getAttribute("aria-pressed")).toBe("true");
85+
expect(toggleOptions[3].getAttribute("aria-pressed")).toBe("true");
7386
});
7487

7588
test("Controlled multiple toggle returns always same values", () => {
@@ -85,14 +98,14 @@ describe("Toggle group component tests", () => {
8598

8699
test("Single selection: Renders with correct default value", () => {
87100
const { getAllByRole } = render(<DxcToggleGroup options={options} defaultValue={2} />);
88-
const toggleOptions = getAllByRole("radio");
89-
expect(toggleOptions[1].getAttribute("aria-checked")).toBe("true");
101+
const toggleOptions = getAllByRole("button");
102+
expect(toggleOptions[1].getAttribute("aria-pressed")).toBe("true");
90103
});
91104

92105
test("Multiple selection: Renders with correct default value", () => {
93106
const { getAllByRole } = render(<DxcToggleGroup options={options} defaultValue={[2, 4]} multiple />);
94-
const toggleOptions = getAllByRole("switch");
95-
expect(toggleOptions[1].getAttribute("aria-checked")).toBe("true");
96-
expect(toggleOptions[3].getAttribute("aria-checked")).toBe("true");
107+
const toggleOptions = getAllByRole("button");
108+
expect(toggleOptions[1].getAttribute("aria-pressed")).toBe("true");
109+
expect(toggleOptions[3].getAttribute("aria-pressed")).toBe("true");
97110
});
98111
});

lib/src/toggle-group/ToggleGroup.tsx

Lines changed: 95 additions & 112 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { spaces } from "../common/variables";
55
import useTheme from "../useTheme";
66
import ToggleGroupPropsType, { OptionLabel } from "./types";
77
import BackgroundColorContext, { BackgroundColors } from "../BackgroundColorContext";
8+
import DxcFlex from "../flex/Flex";
89

910
const DxcToggleGroup = ({
1011
label,
@@ -18,10 +19,10 @@ const DxcToggleGroup = ({
1819
multiple = false,
1920
tabIndex = 0,
2021
}: ToggleGroupPropsType): JSX.Element => {
21-
const colorsTheme = useTheme();
22+
const [toggleGroupLabelId] = useState(`label-toggle-group-${uuidv4()}`);
2223
const [selectedValue, setSelectedValue] = useState(defaultValue ?? (multiple ? [] : -1));
23-
const [toggleGroupId] = useState(`toggle-group-${uuidv4()}`);
2424

25+
const colorsTheme = useTheme();
2526
const backgroundType = useContext(BackgroundColorContext);
2627

2728
const handleToggleChange = (selectedOption) => {
@@ -49,22 +50,28 @@ const DxcToggleGroup = ({
4950
onChange?.(multiple ? newSelectedOptions : selectedOption);
5051
};
5152

52-
const handleKeyPress = (event, optionValue) => {
53-
event.preventDefault();
54-
if (!disabled && (event.nativeEvent.code === "Enter" || event.nativeEvent.code === "Space"))
55-
handleToggleChange(optionValue);
53+
const handleOnKeyDown = (event, optionValue) => {
54+
switch (event.key) {
55+
case "Enter":
56+
case " ":
57+
event.preventDefault();
58+
handleToggleChange(optionValue);
59+
}
5660
};
61+
5762
return (
5863
<ThemeProvider theme={colorsTheme.toggleGroup}>
5964
<ToggleGroup margin={margin}>
60-
<Label htmlFor={toggleGroupId} disabled={disabled}>
65+
<Label id={toggleGroupLabelId} disabled={disabled}>
6166
{label}
6267
</Label>
6368
<HelperText disabled={disabled}>{helperText}</HelperText>
64-
<OptionsContainer id={toggleGroupId} role={multiple ? "group" : "radiogroup"}>
69+
<OptionsContainer aria-labelledby={toggleGroupLabelId}>
6570
{options.map((option, i) => (
66-
<ToggleContainer
67-
selected={
71+
<ToggleButton
72+
key={`toggle-${i}-${option.label}`}
73+
aria-label={option.title}
74+
aria-pressed={
6875
multiple
6976
? value
7077
? Array.isArray(value) && value.includes(option.value)
@@ -73,9 +80,19 @@ const DxcToggleGroup = ({
7380
? option.value === value
7481
: option.value === selectedValue
7582
}
76-
role={multiple ? "switch" : "radio"}
83+
disabled={disabled}
84+
onClick={() => {
85+
handleToggleChange(option.value);
86+
}}
87+
onKeyDown={(event) => {
88+
handleOnKeyDown(event, option.value);
89+
}}
90+
tabIndex={!disabled ? tabIndex : -1}
91+
title={option.title}
7792
backgroundType={backgroundType}
78-
aria-checked={
93+
hasIcon={option.icon}
94+
optionLabel={option.label}
95+
selected={
7996
multiple
8097
? value
8198
? Array.isArray(value) && value.includes(option.value)
@@ -84,33 +101,37 @@ const DxcToggleGroup = ({
84101
? option.value === value
85102
: option.value === selectedValue
86103
}
87-
tabIndex={!disabled ? tabIndex : -1}
88-
onClick={() => !disabled && handleToggleChange(option.value)}
89-
isLast={i === options.length - 1}
90-
isIcon={option.icon}
91-
optionLabel={option.label}
92-
disabled={disabled}
93-
onKeyPress={(event) => {
94-
handleKeyPress(event, option.value);
95-
}}
96-
key={`toggle-${i}-${option.label}`}
97104
>
98-
<OptionContent>
105+
<DxcFlex alignItems="center">
99106
{option.icon && (
100107
<IconContainer optionLabel={option.label}>
101-
{typeof option.icon === "string" ? <Icon src={option.icon} /> : option.icon}
108+
{typeof option.icon === "string" ? <img src={option.icon} /> : option.icon}
102109
</IconContainer>
103110
)}
104111
{option.label && <LabelContainer>{option.label}</LabelContainer>}
105-
</OptionContent>
106-
</ToggleContainer>
112+
</DxcFlex>
113+
</ToggleButton>
107114
))}
108115
</OptionsContainer>
109116
</ToggleGroup>
110117
</ThemeProvider>
111118
);
112119
};
113120

121+
const ToggleGroup = styled.div<{ margin: ToggleGroupPropsType["margin"] }>`
122+
display: inline-flex;
123+
flex-direction: column;
124+
margin: ${(props) => (props.margin && typeof props.margin !== "object" ? spaces[props.margin] : "0px")};
125+
margin-top: ${(props) =>
126+
props.margin && typeof props.margin === "object" && props.margin.top ? spaces[props.margin.top] : ""};
127+
margin-right: ${(props) =>
128+
props.margin && typeof props.margin === "object" && props.margin.right ? spaces[props.margin.right] : ""};
129+
margin-bottom: ${(props) =>
130+
props.margin && typeof props.margin === "object" && props.margin.bottom ? spaces[props.margin.bottom] : ""};
131+
margin-left: ${(props) =>
132+
props.margin && typeof props.margin === "object" && props.margin.left ? spaces[props.margin.left] : ""};
133+
`;
134+
114135
const Label = styled.label<{ disabled: ToggleGroupPropsType["disabled"] }>`
115136
color: ${(props) => (props.disabled ? props.theme.disabledLabelFontColor : props.theme.labelFontColor)};
116137
font-family: ${(props) => props.theme.labelFontFamily};
@@ -129,100 +150,68 @@ const HelperText = styled.span<{ disabled: ToggleGroupPropsType["disabled"] }>`
129150
line-height: ${(props) => props.theme.helperTextLineHeight};
130151
`;
131152

132-
const ToggleGroup = styled.div<{ margin: ToggleGroupPropsType["margin"] }>`
133-
display: inline-flex;
134-
flex-direction: column;
135-
margin: ${(props) => (props.margin && typeof props.margin !== "object" ? spaces[props.margin] : "0px")};
136-
margin-top: ${(props) =>
137-
props.margin && typeof props.margin === "object" && props.margin.top ? spaces[props.margin.top] : ""};
138-
margin-right: ${(props) =>
139-
props.margin && typeof props.margin === "object" && props.margin.right ? spaces[props.margin.right] : ""};
140-
margin-bottom: ${(props) =>
141-
props.margin && typeof props.margin === "object" && props.margin.bottom ? spaces[props.margin.bottom] : ""};
142-
margin-left: ${(props) =>
143-
props.margin && typeof props.margin === "object" && props.margin.left ? spaces[props.margin.left] : ""};
144-
`;
145-
146153
const OptionsContainer = styled.div`
147154
display: flex;
148-
flex-direction: row;
155+
gap: 0.25rem;
149156
width: max-content;
150-
opacity: 1;
151157
height: calc(48px - 4px - 4px);
158+
padding: 0.25rem;
152159
border-width: ${(props) => props.theme.containerBorderThickness};
153160
border-style: ${(props) => props.theme.containerBorderStyle};
154161
border-radius: ${(props) => props.theme.containerBorderRadius};
155162
border-color: ${(props) => props.theme.containerBorderColor};
156-
background-color: ${(props) => props.theme.containerBackgroundColor};
157-
padding: 4px;
158163
margin-top: ${(props) => props.theme.containerMarginTop};
164+
background-color: ${(props) => props.theme.containerBackgroundColor};
159165
`;
160166

161-
const ToggleContainer = styled.div<{
167+
const ToggleButton = styled.button<{
162168
selected: boolean;
163-
disabled: ToggleGroupPropsType["disabled"];
164-
isLast: boolean;
165-
isIcon: OptionLabel["icon"];
169+
hasIcon: OptionLabel["icon"];
166170
optionLabel: OptionLabel["label"];
167171
backgroundType: BackgroundColors;
168172
}>`
169173
display: flex;
170174
flex-direction: column;
171175
justify-content: center;
172-
margin-right: ${(props) => !props.isLast && "4px"};
173-
174-
${(props) => `
175-
background-color: ${
176-
props.selected
177-
? props.disabled
178-
? props.theme.selectedDisabledBackgroundColor
179-
: props.theme.selectedBackgroundColor
180-
: props.disabled
181-
? props.theme.unselectedDisabledBackgroundColor
182-
: props.theme.unselectedBackgroundColor
183-
};
184-
border-width: ${props.theme.optionBorderThickness};
185-
border-style: ${props.theme.optionBorderStyle};
186-
border-radius: ${props.theme.optionBorderRadius};
187-
padding-left: ${
188-
(props.optionLabel && props.isIcon) || (props.optionLabel && !props.isIcon)
189-
? props.theme.labelPaddingLeft
190-
: props.theme.iconPaddingLeft
191-
};
192-
padding-right: ${
193-
(props.optionLabel && props.isIcon) || (props.optionLabel && !props.isIcon)
194-
? props.theme.labelPaddingRight
195-
: props.theme.iconPaddingRight
196-
};
197-
${
198-
!props.disabled
199-
? `:hover {
200-
background-color: ${
201-
props.selected ? props.theme.selectedHoverBackgroundColor : props.theme.unselectedHoverBackgroundColor
202-
};
203-
}
204-
:active {
205-
background-color: ${
206-
props.selected ? props.theme.selectedActiveBackgroundColor : props.theme.unselectedActiveBackgroundColor
207-
};
208-
color: #ffffff;
209-
}
210-
:focus {
211-
border-color: transparent;
212-
box-shadow: 0 0 0 ${props.theme.optionFocusBorderThickness} ${
213-
props.backgroundType === "dark" ? props.theme.focusColorOnDark : props.theme.focusColor
214-
};
215-
}
216-
&:focus-visible {
217-
outline: none;
218-
}
219-
cursor: pointer;
220-
color: ${props.selected ? props.theme.selectedFontColor : props.theme.unselectedFontColor};
221-
`
222-
: `color: ${props.selected ? props.theme.selectedDisabledFontColor : props.theme.unselectedDisabledFontColor};
223-
cursor: not-allowed;`
224-
}
225-
`}
176+
padding-left: ${(props) =>
177+
(props.optionLabel && props.hasIcon) || (props.optionLabel && !props.hasIcon)
178+
? props.theme.labelPaddingLeft
179+
: props.theme.iconPaddingLeft};
180+
padding-right: ${(props) =>
181+
(props.optionLabel && props.hasIcon) || (props.optionLabel && !props.hasIcon)
182+
? props.theme.labelPaddingRight
183+
: props.theme.iconPaddingRight};
184+
border-width: ${(props) => props.theme.optionBorderThickness};
185+
border-style: ${(props) => props.theme.optionBorderStyle};
186+
border-radius: ${(props) => props.theme.optionBorderRadius};
187+
background-color: ${(props) =>
188+
props.selected ? props.theme.selectedBackgroundColor : props.theme.unselectedBackgroundColor};
189+
color: ${(props) => (props.selected ? props.theme.selectedFontColor : props.theme.unselectedFontColor)};
190+
cursor: pointer;
191+
192+
&:hover {
193+
background-color: ${(props) =>
194+
props.selected ? props.theme.selectedHoverBackgroundColor : props.theme.unselectedHoverBackgroundColor};
195+
}
196+
&:active {
197+
background-color: ${(props) =>
198+
props.selected ? props.theme.selectedActiveBackgroundColor : props.theme.unselectedActiveBackgroundColor};
199+
color: #ffffff;
200+
}
201+
&:focus {
202+
outline: none;
203+
box-shadow: ${(props) =>
204+
`0 0 0 ${props.theme.optionFocusBorderThickness} ${
205+
props.backgroundType === "dark" ? props.theme.focusColorOnDark : props.theme.focusColor
206+
}`};
207+
}
208+
&:disabled {
209+
background-color: ${(props) =>
210+
props.selected ? props.theme.selectedDisabledBackgroundColor : props.theme.unselectedDisabledBackgroundColor};
211+
color: ${(props) =>
212+
props.selected ? props.theme.selectedDisabledFontColor : props.theme.unselectedDisabledFontColor};
213+
cursor: not-allowed;
214+
}
226215
`;
227216

228217
const LabelContainer = styled.span`
@@ -232,24 +221,18 @@ const LabelContainer = styled.span`
232221
font-weight: ${(props) => props.theme.optionLabelFontWeight};
233222
`;
234223

235-
const OptionContent = styled.div`
236-
display: flex;
237-
flex-direction: row;
238-
align-items: center;
239-
`;
240-
241-
const Icon = styled.img``;
242-
243224
const IconContainer = styled.div<{ optionLabel: OptionLabel["label"] }>`
244-
margin-right: ${(props) => props.optionLabel && props.theme.iconMarginRight};
225+
display: flex;
245226
height: 24px;
246227
width: 24px;
228+
margin-right: ${(props) => props.optionLabel && props.theme.iconMarginRight};
247229
overflow: hidden;
248-
display: flex;
230+
249231
img,
250232
svg {
251233
height: 100%;
252234
width: 100%;
253235
}
254236
`;
237+
255238
export default DxcToggleGroup;

0 commit comments

Comments
 (0)