]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-19025 Add ToggleButton component
authorJeremy Davis <jeremy.davis@sonarsource.com>
Wed, 12 Apr 2023 12:54:18 +0000 (14:54 +0200)
committersonartech <sonartech@sonarsource.com>
Thu, 13 Apr 2023 20:03:05 +0000 (20:03 +0000)
server/sonar-web/design-system/src/components/ToggleButton.tsx [new file with mode: 0644]
server/sonar-web/design-system/src/components/__tests__/ToggleButton-test.tsx [new file with mode: 0644]
server/sonar-web/design-system/src/components/buttons.tsx
server/sonar-web/design-system/src/components/index.ts

diff --git a/server/sonar-web/design-system/src/components/ToggleButton.tsx b/server/sonar-web/design-system/src/components/ToggleButton.tsx
new file mode 100644 (file)
index 0000000..82dcc07
--- /dev/null
@@ -0,0 +1,115 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+
+import styled from '@emotion/styled';
+import tw from 'twin.macro';
+import { themeBorder, themeColor, themeContrast } from '../helpers/theme';
+import Badge from './Badge';
+import { ButtonSecondary } from './buttons';
+
+type ToggleButtonValueType = string | number | boolean;
+
+export interface ToggleButtonsOption<T extends ToggleButtonValueType> {
+  counter?: number;
+  disabled?: boolean;
+  label: string;
+  value: T;
+}
+
+export interface ButtonToggleProps<T extends ToggleButtonValueType> {
+  disabled?: boolean;
+  label?: string;
+  onChange: (value: T) => void;
+  options: Array<ToggleButtonsOption<T>>;
+  value?: T;
+}
+
+export function ToggleButton<T extends ToggleButtonValueType>(props: ButtonToggleProps<T>) {
+  const { disabled = false, label, options, value } = props;
+
+  return (
+    <Wrapper aria-label={label} role="radiogroup">
+      {options.map((option) => (
+        <OptionButton
+          aria-current={option.value === value}
+          data-value={option.value}
+          disabled={disabled || option.disabled}
+          key={option.value.toString()}
+          onClick={() => {
+            if (option.value !== value) {
+              props.onChange(option.value);
+            }
+          }}
+          role="radio"
+          selected={option.value === value}
+        >
+          {option.label}
+          {option.counter ? (
+            <Badge className="sw-ml-1" variant="counter">
+              {option.counter}
+            </Badge>
+          ) : null}
+        </OptionButton>
+      ))}
+    </Wrapper>
+  );
+}
+
+const Wrapper = styled.div`
+  border: ${themeBorder('default', 'toggleBorder')};
+
+  ${tw`sw-inline-flex`}
+  ${tw`sw-h-control`}
+  ${tw`sw-box-border`}
+  ${tw`sw-font-semibold`}
+  ${tw`sw-rounded-2`}
+`;
+
+const OptionButton = styled(ButtonSecondary)<{ selected: boolean }>`
+  background: ${(props) => (props.selected ? themeColor('toggleHover') : themeColor('toggle'))};
+  color: ${(props) => (props.selected ? themeContrast('toggleHover') : themeContrast('toggle'))};
+  border: none;
+  height: auto;
+  overflow: hidden;
+  ${tw`sw-rounded-0`};
+
+  &:first-of-type {
+    ${tw`sw-rounded-l-2`};
+  }
+
+  &:last-of-type {
+    ${tw`sw-rounded-r-2`};
+  }
+
+  &:not(:last-of-type) {
+    border-right: ${themeBorder('default', 'toggleBorder')};
+  }
+
+  &:hover {
+    background: ${themeColor('toggleHover')};
+    color: ${themeContrast('toggleHover')};
+  }
+
+  &:focus,
+  &:active {
+    outline: ${themeBorder('focus', 'toggleFocus')};
+    z-index: 1;
+  }
+`;
diff --git a/server/sonar-web/design-system/src/components/__tests__/ToggleButton-test.tsx b/server/sonar-web/design-system/src/components/__tests__/ToggleButton-test.tsx
new file mode 100644 (file)
index 0000000..d42cd87
--- /dev/null
@@ -0,0 +1,48 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import { screen } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import { render } from '../../helpers/testUtils';
+import { FCProps } from '../../types/misc';
+import { ToggleButton, ToggleButtonsOption } from '../ToggleButton';
+
+it('should render all options', async () => {
+  const user = userEvent.setup();
+
+  const onChange = jest.fn();
+
+  const options: Array<ToggleButtonsOption<number>> = [
+    { value: 1, label: 'first' },
+    { value: 2, label: 'disabled', disabled: true },
+    { value: 3, label: 'has counter', counter: 7 },
+  ];
+
+  renderToggleButtons({ onChange, options, value: 1 });
+
+  expect(screen.getAllByRole('radio')).toHaveLength(3);
+
+  await user.click(screen.getByText('has counter'));
+
+  expect(onChange).toHaveBeenCalledWith(3);
+});
+
+function renderToggleButtons(props: Partial<FCProps<typeof ToggleButton>> = {}) {
+  return render(<ToggleButton onChange={jest.fn()} options={[]} {...props} />);
+}
index ab0fcf9fb0cc26f4a9ead54807256edc1c63c81b..b939dbbe4c05447eb1e5187785056494eb084564 100644 (file)
@@ -29,7 +29,7 @@ import { BaseLink, LinkProps } from './Link';
 
 type AllowedButtonAttributes = Pick<
   React.ButtonHTMLAttributes<HTMLButtonElement>,
-  'aria-label' | 'autoFocus' | 'id' | 'name' | 'style' | 'title' | 'type'
+  'aria-label' | 'autoFocus' | 'id' | 'name' | 'role' | 'style' | 'title' | 'type'
 >;
 
 export interface ButtonProps extends AllowedButtonAttributes {
@@ -52,8 +52,6 @@ class Button extends React.PureComponent<ButtonProps> {
     const { disabled, onClick, stopPropagation = false, type } = this.props;
     const { preventDefault = type !== 'submit' } = this.props;
 
-    event.currentTarget.blur();
-
     if (preventDefault || disabled) {
       event.preventDefault();
     }
index 1a9eb75d615ab125b05f2687a37350af6d647d56..a9e95d2a208cac37823d1f6939dc927f06628ca4 100644 (file)
@@ -39,6 +39,7 @@ export * from './NavBarTabs';
 export { default as QualityGateIndicator } from './QualityGateIndicator';
 export * from './SonarQubeLogo';
 export * from './Text';
+export { ToggleButton } from './ToggleButton';
 export { TopBar } from './TopBar';
 export * from './buttons';
 export * from './icons';