Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions lib/src/toggle-group/ToggleGroup.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -53,14 +53,17 @@ const optionsWithIcon = [
{
value: 1,
icon: wifiSVG,
title: "WiFi connection",
},
{
value: 2,
icon: ethernetSVG,
title: "Ethernet connection",
},
{
value: 3,
icon: gMobileSVG,
title: "3G Mobile data connection",
},
];
const optionsWithIconAndLabel = [
Expand Down
31 changes: 22 additions & 9 deletions lib/src/toggle-group/ToggleGroup.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,19 @@ describe("Toggle group component tests", () => {
expect(getByText("Google")).toBeTruthy();
});

test("Toggle group renders with correct aria-label in only-icon scenario", () => {
const { getByRole } = render(
<DxcToggleGroup
label="Toggle group label"
helperText="Toggle group helper text"
options={[
{ value: 1, icon: "https://cdn.icon-icons.com/icons2/2645/PNG/512/mic_mute_icon_159965.png", title: "Mute" },
]}
/>
);
expect(getByRole("button").getAttribute("aria-label")).toBe("Mute");
});

test("Uncontrolled toggle group calls correct function on change with value", () => {
const onChange = jest.fn();
const { getByText } = render(<DxcToggleGroup options={options} onChange={onChange} />);
Expand Down Expand Up @@ -61,15 +74,15 @@ describe("Toggle group component tests", () => {
test("Uncontrolled multiple toggle group calls correct function on change with value when is multiple", () => {
const onChange = jest.fn();
const { getAllByRole } = render(<DxcToggleGroup options={options} onChange={onChange} multiple />);
const toggleOptions = getAllByRole("switch");
const toggleOptions = getAllByRole("button");
fireEvent.click(toggleOptions[0]);
expect(onChange).toHaveBeenCalledWith([1]);
fireEvent.click(toggleOptions[1]);
fireEvent.click(toggleOptions[3]);
expect(onChange).toHaveBeenCalledWith([1, 2, 4]);
expect(toggleOptions[0].getAttribute("aria-checked")).toBe("true");
expect(toggleOptions[1].getAttribute("aria-checked")).toBe("true");
expect(toggleOptions[3].getAttribute("aria-checked")).toBe("true");
expect(toggleOptions[0].getAttribute("aria-pressed")).toBe("true");
expect(toggleOptions[1].getAttribute("aria-pressed")).toBe("true");
expect(toggleOptions[3].getAttribute("aria-pressed")).toBe("true");
});

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

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

test("Multiple selection: Renders with correct default value", () => {
const { getAllByRole } = render(<DxcToggleGroup options={options} defaultValue={[2, 4]} multiple />);
const toggleOptions = getAllByRole("switch");
expect(toggleOptions[1].getAttribute("aria-checked")).toBe("true");
expect(toggleOptions[3].getAttribute("aria-checked")).toBe("true");
const toggleOptions = getAllByRole("button");
expect(toggleOptions[1].getAttribute("aria-pressed")).toBe("true");
expect(toggleOptions[3].getAttribute("aria-pressed")).toBe("true");
});
});
207 changes: 95 additions & 112 deletions lib/src/toggle-group/ToggleGroup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { spaces } from "../common/variables";
import useTheme from "../useTheme";
import ToggleGroupPropsType, { OptionLabel } from "./types";
import BackgroundColorContext, { BackgroundColors } from "../BackgroundColorContext";
import DxcFlex from "../flex/Flex";

const DxcToggleGroup = ({
label,
Expand All @@ -18,10 +19,10 @@ const DxcToggleGroup = ({
multiple = false,
tabIndex = 0,
}: ToggleGroupPropsType): JSX.Element => {
const colorsTheme = useTheme();
const [toggleGroupLabelId] = useState(`label-toggle-group-${uuidv4()}`);
const [selectedValue, setSelectedValue] = useState(defaultValue ?? (multiple ? [] : -1));
const [toggleGroupId] = useState(`toggle-group-${uuidv4()}`);

const colorsTheme = useTheme();
const backgroundType = useContext(BackgroundColorContext);

const handleToggleChange = (selectedOption) => {
Expand Down Expand Up @@ -49,22 +50,28 @@ const DxcToggleGroup = ({
onChange?.(multiple ? newSelectedOptions : selectedOption);
};

const handleKeyPress = (event, optionValue) => {
event.preventDefault();
if (!disabled && (event.nativeEvent.code === "Enter" || event.nativeEvent.code === "Space"))
handleToggleChange(optionValue);
const handleOnKeyDown = (event, optionValue) => {
switch (event.key) {
case "Enter":
case " ":
event.preventDefault();
handleToggleChange(optionValue);
}
};

return (
<ThemeProvider theme={colorsTheme.toggleGroup}>
<ToggleGroup margin={margin}>
<Label htmlFor={toggleGroupId} disabled={disabled}>
<Label id={toggleGroupLabelId} disabled={disabled}>
{label}
</Label>
<HelperText disabled={disabled}>{helperText}</HelperText>
<OptionsContainer id={toggleGroupId} role={multiple ? "group" : "radiogroup"}>
<OptionsContainer aria-labelledby={toggleGroupLabelId}>
{options.map((option, i) => (
<ToggleContainer
selected={
<ToggleButton
key={`toggle-${i}-${option.label}`}
aria-label={option.title}
aria-pressed={
multiple
? value
? Array.isArray(value) && value.includes(option.value)
Expand All @@ -73,9 +80,19 @@ const DxcToggleGroup = ({
? option.value === value
: option.value === selectedValue
}
role={multiple ? "switch" : "radio"}
disabled={disabled}
onClick={() => {
handleToggleChange(option.value);
}}
onKeyDown={(event) => {
handleOnKeyDown(event, option.value);
}}
tabIndex={!disabled ? tabIndex : -1}
title={option.title}
backgroundType={backgroundType}
aria-checked={
hasIcon={option.icon}
optionLabel={option.label}
selected={
multiple
? value
? Array.isArray(value) && value.includes(option.value)
Expand All @@ -84,33 +101,37 @@ const DxcToggleGroup = ({
? option.value === value
: option.value === selectedValue
}
tabIndex={!disabled ? tabIndex : -1}
onClick={() => !disabled && handleToggleChange(option.value)}
isLast={i === options.length - 1}
isIcon={option.icon}
optionLabel={option.label}
disabled={disabled}
onKeyPress={(event) => {
handleKeyPress(event, option.value);
}}
key={`toggle-${i}-${option.label}`}
>
<OptionContent>
<DxcFlex alignItems="center">
{option.icon && (
<IconContainer optionLabel={option.label}>
{typeof option.icon === "string" ? <Icon src={option.icon} /> : option.icon}
{typeof option.icon === "string" ? <img src={option.icon} /> : option.icon}
</IconContainer>
)}
{option.label && <LabelContainer>{option.label}</LabelContainer>}
</OptionContent>
</ToggleContainer>
</DxcFlex>
</ToggleButton>
))}
</OptionsContainer>
</ToggleGroup>
</ThemeProvider>
);
};

const ToggleGroup = styled.div<{ margin: ToggleGroupPropsType["margin"] }>`
display: inline-flex;
flex-direction: column;
margin: ${(props) => (props.margin && typeof props.margin !== "object" ? spaces[props.margin] : "0px")};
margin-top: ${(props) =>
props.margin && typeof props.margin === "object" && props.margin.top ? spaces[props.margin.top] : ""};
margin-right: ${(props) =>
props.margin && typeof props.margin === "object" && props.margin.right ? spaces[props.margin.right] : ""};
margin-bottom: ${(props) =>
props.margin && typeof props.margin === "object" && props.margin.bottom ? spaces[props.margin.bottom] : ""};
margin-left: ${(props) =>
props.margin && typeof props.margin === "object" && props.margin.left ? spaces[props.margin.left] : ""};
`;

const Label = styled.label<{ disabled: ToggleGroupPropsType["disabled"] }>`
color: ${(props) => (props.disabled ? props.theme.disabledLabelFontColor : props.theme.labelFontColor)};
font-family: ${(props) => props.theme.labelFontFamily};
Expand All @@ -129,100 +150,68 @@ const HelperText = styled.span<{ disabled: ToggleGroupPropsType["disabled"] }>`
line-height: ${(props) => props.theme.helperTextLineHeight};
`;

const ToggleGroup = styled.div<{ margin: ToggleGroupPropsType["margin"] }>`
display: inline-flex;
flex-direction: column;
margin: ${(props) => (props.margin && typeof props.margin !== "object" ? spaces[props.margin] : "0px")};
margin-top: ${(props) =>
props.margin && typeof props.margin === "object" && props.margin.top ? spaces[props.margin.top] : ""};
margin-right: ${(props) =>
props.margin && typeof props.margin === "object" && props.margin.right ? spaces[props.margin.right] : ""};
margin-bottom: ${(props) =>
props.margin && typeof props.margin === "object" && props.margin.bottom ? spaces[props.margin.bottom] : ""};
margin-left: ${(props) =>
props.margin && typeof props.margin === "object" && props.margin.left ? spaces[props.margin.left] : ""};
`;

const OptionsContainer = styled.div`
display: flex;
flex-direction: row;
gap: 0.25rem;
width: max-content;
opacity: 1;
height: calc(48px - 4px - 4px);
padding: 0.25rem;
border-width: ${(props) => props.theme.containerBorderThickness};
border-style: ${(props) => props.theme.containerBorderStyle};
border-radius: ${(props) => props.theme.containerBorderRadius};
border-color: ${(props) => props.theme.containerBorderColor};
background-color: ${(props) => props.theme.containerBackgroundColor};
padding: 4px;
margin-top: ${(props) => props.theme.containerMarginTop};
background-color: ${(props) => props.theme.containerBackgroundColor};
`;

const ToggleContainer = styled.div<{
const ToggleButton = styled.button<{
selected: boolean;
disabled: ToggleGroupPropsType["disabled"];
isLast: boolean;
isIcon: OptionLabel["icon"];
hasIcon: OptionLabel["icon"];
optionLabel: OptionLabel["label"];
backgroundType: BackgroundColors;
}>`
display: flex;
flex-direction: column;
justify-content: center;
margin-right: ${(props) => !props.isLast && "4px"};

${(props) => `
background-color: ${
props.selected
? props.disabled
? props.theme.selectedDisabledBackgroundColor
: props.theme.selectedBackgroundColor
: props.disabled
? props.theme.unselectedDisabledBackgroundColor
: props.theme.unselectedBackgroundColor
};
border-width: ${props.theme.optionBorderThickness};
border-style: ${props.theme.optionBorderStyle};
border-radius: ${props.theme.optionBorderRadius};
padding-left: ${
(props.optionLabel && props.isIcon) || (props.optionLabel && !props.isIcon)
? props.theme.labelPaddingLeft
: props.theme.iconPaddingLeft
};
padding-right: ${
(props.optionLabel && props.isIcon) || (props.optionLabel && !props.isIcon)
? props.theme.labelPaddingRight
: props.theme.iconPaddingRight
};
${
!props.disabled
? `:hover {
background-color: ${
props.selected ? props.theme.selectedHoverBackgroundColor : props.theme.unselectedHoverBackgroundColor
};
}
:active {
background-color: ${
props.selected ? props.theme.selectedActiveBackgroundColor : props.theme.unselectedActiveBackgroundColor
};
color: #ffffff;
}
:focus {
border-color: transparent;
box-shadow: 0 0 0 ${props.theme.optionFocusBorderThickness} ${
props.backgroundType === "dark" ? props.theme.focusColorOnDark : props.theme.focusColor
};
}
&:focus-visible {
outline: none;
}
cursor: pointer;
color: ${props.selected ? props.theme.selectedFontColor : props.theme.unselectedFontColor};
`
: `color: ${props.selected ? props.theme.selectedDisabledFontColor : props.theme.unselectedDisabledFontColor};
cursor: not-allowed;`
}
`}
padding-left: ${(props) =>
(props.optionLabel && props.hasIcon) || (props.optionLabel && !props.hasIcon)
? props.theme.labelPaddingLeft
: props.theme.iconPaddingLeft};
padding-right: ${(props) =>
(props.optionLabel && props.hasIcon) || (props.optionLabel && !props.hasIcon)
? props.theme.labelPaddingRight
: props.theme.iconPaddingRight};
border-width: ${(props) => props.theme.optionBorderThickness};
border-style: ${(props) => props.theme.optionBorderStyle};
border-radius: ${(props) => props.theme.optionBorderRadius};
background-color: ${(props) =>
props.selected ? props.theme.selectedBackgroundColor : props.theme.unselectedBackgroundColor};
color: ${(props) => (props.selected ? props.theme.selectedFontColor : props.theme.unselectedFontColor)};
cursor: pointer;

&:hover {
background-color: ${(props) =>
props.selected ? props.theme.selectedHoverBackgroundColor : props.theme.unselectedHoverBackgroundColor};
}
&:active {
background-color: ${(props) =>
props.selected ? props.theme.selectedActiveBackgroundColor : props.theme.unselectedActiveBackgroundColor};
color: #ffffff;
}
&:focus {
outline: none;
box-shadow: ${(props) =>
`0 0 0 ${props.theme.optionFocusBorderThickness} ${
props.backgroundType === "dark" ? props.theme.focusColorOnDark : props.theme.focusColor
}`};
}
&:disabled {
background-color: ${(props) =>
props.selected ? props.theme.selectedDisabledBackgroundColor : props.theme.unselectedDisabledBackgroundColor};
color: ${(props) =>
props.selected ? props.theme.selectedDisabledFontColor : props.theme.unselectedDisabledFontColor};
cursor: not-allowed;
}
`;

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

const OptionContent = styled.div`
display: flex;
flex-direction: row;
align-items: center;
`;

const Icon = styled.img``;

const IconContainer = styled.div<{ optionLabel: OptionLabel["label"] }>`
margin-right: ${(props) => props.optionLabel && props.theme.iconMarginRight};
display: flex;
height: 24px;
width: 24px;
margin-right: ${(props) => props.optionLabel && props.theme.iconMarginRight};
overflow: hidden;
display: flex;

img,
svg {
height: 100%;
width: 100%;
}
`;

export default DxcToggleGroup;
Loading