switchButton.tsx 2.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106
  1. import {forwardRef} from 'react';
  2. import styled from '@emotion/styled';
  3. type Props = {
  4. toggle: React.MouseEventHandler<HTMLButtonElement>;
  5. className?: string;
  6. /**
  7. * Toggle color is always active.
  8. */
  9. forceActiveColor?: boolean;
  10. forwardedRef?: React.Ref<HTMLButtonElement>;
  11. id?: string;
  12. isActive?: boolean;
  13. isDisabled?: boolean;
  14. isLoading?: boolean;
  15. name?: string;
  16. size?: 'sm' | 'lg';
  17. };
  18. const Switch = ({
  19. forwardedRef,
  20. size = 'sm',
  21. isActive,
  22. forceActiveColor,
  23. isLoading,
  24. isDisabled,
  25. toggle,
  26. id,
  27. name,
  28. className,
  29. ...props
  30. }: Props) => (
  31. <SwitchButton
  32. ref={forwardedRef}
  33. id={id}
  34. name={name}
  35. type="button"
  36. className={className}
  37. onClick={isDisabled ? undefined : toggle}
  38. role="checkbox"
  39. aria-checked={isActive}
  40. isLoading={isLoading}
  41. disabled={isDisabled}
  42. isActive={isActive}
  43. size={size}
  44. data-test-id="switch"
  45. {...props}
  46. >
  47. <Toggle
  48. isDisabled={isDisabled}
  49. isActive={isActive}
  50. forceActiveColor={forceActiveColor}
  51. size={size}
  52. />
  53. </SwitchButton>
  54. );
  55. type StyleProps = Partial<Props>;
  56. const getSize = (p: StyleProps) => (p.size === 'sm' ? 16 : 24);
  57. const getToggleSize = (p: StyleProps) => getSize(p) - (p.size === 'sm' ? 4 : 8);
  58. const getToggleTop = (p: StyleProps) => (p.size === 'sm' ? 1 : 3);
  59. const getTranslateX = (p: StyleProps) =>
  60. p.isActive ? getToggleTop(p) + getSize(p) * 0.875 : getToggleTop(p);
  61. const SwitchButton = styled('button')<StyleProps>`
  62. display: inline-block;
  63. background: none;
  64. padding: 0;
  65. border: 1px solid ${p => p.theme.border};
  66. position: relative;
  67. box-shadow: inset ${p => p.theme.dropShadowLight};
  68. height: ${getSize}px;
  69. width: ${p => getSize(p) * 1.875}px;
  70. border-radius: ${getSize}px;
  71. transition: border 0.1s, box-shadow 0.1s;
  72. &[disabled] {
  73. cursor: not-allowed;
  74. }
  75. &:focus,
  76. &.focus-visible {
  77. outline: none;
  78. border-color: ${p => p.theme.focusBorder};
  79. box-shadow: ${p => p.theme.focusBorder} 0 0 0 1px;
  80. }
  81. `;
  82. const Toggle = styled('span')<StyleProps>`
  83. display: block;
  84. position: absolute;
  85. border-radius: 50%;
  86. transition: 0.25s all ease;
  87. top: ${getToggleTop}px;
  88. transform: translateX(${getTranslateX}px);
  89. width: ${getToggleSize}px;
  90. height: ${getToggleSize}px;
  91. background: ${p =>
  92. p.isActive || p.forceActiveColor ? p.theme.active : p.theme.border};
  93. opacity: ${p => (p.isDisabled ? 0.4 : null)};
  94. `;
  95. export default forwardRef<HTMLButtonElement, Props>((props, ref) => (
  96. <Switch {...props} forwardedRef={ref} />
  97. ));