Skip to content

Commit

Permalink
Merge pull request #1661 from dxc-technology/gomezivann-toggle-group-…
Browse files Browse the repository at this point in the history
…title

Add `title` prop to Toggle Group component
  • Loading branch information
Jialecl committed Jul 25, 2023
2 parents 41e66ee + 193e24d commit 6b2072d
Show file tree
Hide file tree
Showing 11 changed files with 308 additions and 206 deletions.
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

0 comments on commit 6b2072d

Please sign in to comment.