]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-20878 Create Switch component into Design System
authorKevin Silva <kevin.silva@sonarsource.com>
Wed, 25 Oct 2023 12:59:52 +0000 (14:59 +0200)
committersonartech <sonartech@sonarsource.com>
Thu, 26 Oct 2023 20:02:58 +0000 (20:02 +0000)
server/sonar-web/design-system/src/components/Switch.tsx [new file with mode: 0644]
server/sonar-web/design-system/src/components/__tests__/Switch-test.tsx [new file with mode: 0644]
server/sonar-web/design-system/src/components/index.ts

diff --git a/server/sonar-web/design-system/src/components/Switch.tsx b/server/sonar-web/design-system/src/components/Switch.tsx
new file mode 100644 (file)
index 0000000..7ff7b0a
--- /dev/null
@@ -0,0 +1,119 @@
+/*
+ * 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, themeShadow } from '../helpers';
+import { CheckIcon } from './icons';
+
+interface Props {
+  disabled?: boolean;
+  labels: {
+    off: string;
+    on: string;
+  };
+  name?: string;
+  onChange?: (value: boolean) => void;
+  value: boolean | string;
+}
+
+const getValue = (value: boolean | string) => {
+  return typeof value === 'string' ? value === 'true' : value;
+};
+
+export function Switch(props: Readonly<Props>) {
+  const { disabled, onChange, name, labels } = props;
+  const value = getValue(props.value);
+
+  const handleClick = () => {
+    if (!disabled && onChange) {
+      const value = getValue(props.value);
+      onChange(!value);
+    }
+  };
+
+  return (
+    <StyledSwitch
+      active={value}
+      aria-checked={value}
+      aria-label={value ? labels.on : labels.off}
+      disabled={disabled}
+      name={name}
+      onClick={handleClick}
+      role="switch"
+    >
+      <CheckIconContainer active={value} disabled={disabled}>
+        {value && <CheckIcon fill="currentColor" />}
+      </CheckIconContainer>
+    </StyledSwitch>
+  );
+}
+
+interface StyledProps {
+  active: boolean;
+  disabled?: boolean;
+}
+
+const CheckIconContainer = styled.div<StyledProps>`
+  ${tw`sw-rounded-pill`}
+  ${tw`sw-flex sw-items-center sw-justify-center`}
+  ${tw`sw-w-4 sw-h-4`}
+  color: ${({ disabled }) =>
+    disabled ? themeContrast('switchButtonDisabled') : themeContrast('switchButton')};
+  background: ${({ disabled }) =>
+    disabled ? themeColor('switchButtonDisabled') : themeColor('switchButton')};
+  border: none;
+  box-shadow: ${themeShadow('xs')};
+  transform: ${({ active }) => (active ? 'translateX(1rem)' : 'translateX(0)')};
+  cursor: inherit;
+  transition: transform 0.3s ease;
+`;
+
+const StyledSwitch = styled.button<StyledProps>`
+  ${tw`sw-flex sw-flex-row`}
+  ${tw`sw-rounded-pill`}
+  ${tw`sw-p-1/2`}
+  ${tw`sw-cursor-pointer`}
+  width: 2.25rem;
+  height: 1.25rem;
+  background: ${({ active }) => (active ? themeColor('switchActive') : themeColor('switch'))};
+  border: none;
+  transition: 0.3s ease;
+  transition-property: background, outline;
+
+  &:hover:not(:disabled),
+  &:active:not(:disabled),
+  &:focus:not(:disabled) {
+    background: ${({ active }) =>
+      active ? themeColor('switchHoverActive') : themeColor('switchHover')};
+    ${CheckIconContainer} {
+      color: ${themeContrast('switchHover')};
+    }
+  }
+
+  &:disabled {
+    background: ${themeColor('switchDisabled')};
+  }
+
+  &:focus:not(:disabled),
+  &:active:not(:disabled) {
+    outline: ${({ active }) =>
+      active ? themeBorder('focus', 'switchActive') : themeBorder('focus', 'switch')};
+  }
+`;
diff --git a/server/sonar-web/design-system/src/components/__tests__/Switch-test.tsx b/server/sonar-web/design-system/src/components/__tests__/Switch-test.tsx
new file mode 100644 (file)
index 0000000..ed43459
--- /dev/null
@@ -0,0 +1,64 @@
+/*
+ * 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 { Switch } from '../Switch';
+
+const defaultProps = {
+  labels: {
+    off: 'Off',
+    on: 'On',
+  },
+  value: false,
+};
+
+it('renders switch correctly if value is false and change to true on click', async () => {
+  const user = userEvent.setup();
+  const onChange = jest.fn();
+
+  render(<Switch {...defaultProps} onChange={onChange} />);
+  const switchContainer = screen.getByRole('switch');
+  expect(switchContainer).not.toBeChecked();
+
+  await user.click(switchContainer);
+
+  expect(onChange).toHaveBeenCalledWith(true);
+});
+
+it('renders switch correctly if value is true and change to false on click', async () => {
+  const user = userEvent.setup();
+  const onChange = jest.fn();
+
+  render(<Switch {...defaultProps} onChange={onChange} value />);
+  const switchContainer = screen.getByRole('switch');
+  expect(switchContainer).toBeChecked();
+
+  await user.click(switchContainer);
+
+  expect(onChange).toHaveBeenCalledWith(false);
+});
+
+it('renders switch correctly if value is true and disabled', () => {
+  render(<Switch {...defaultProps} disabled value />);
+  const switchContainer = screen.getByRole('switch');
+  expect(switchContainer).toBeDisabled();
+});
index 9064185c0b470085d6b299847cbe659a899473a1..4e1032292874c36df1cca4feb20ca4d6a7f53637 100644 (file)
@@ -70,6 +70,7 @@ export * from './SonarCodeColorizer';
 export * from './SonarQubeLogo';
 export { Spinner } from './Spinner';
 export * from './SpotlightTour';
+export * from './Switch';
 export * from './Table';
 export * from './Tags';
 export * from './Text';