switchButton.tsx 2.6 KB

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