Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add title prop to Toggle Group component #1661

Merged
merged 1 commit into from
Jul 25, 2023
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.
Jump to
Jump to file
Failed to load files.
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;