switchButton.tsx 2.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108
  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. function 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. return (
  32. <SwitchButton
  33. ref={forwardedRef}
  34. id={id}
  35. name={name}
  36. type="button"
  37. className={className}
  38. onClick={isDisabled ? undefined : toggle}
  39. role="checkbox"
  40. aria-checked={isActive}
  41. isLoading={isLoading}
  42. disabled={isDisabled}
  43. isActive={isActive}
  44. size={size}
  45. data-test-id="switch"
  46. {...props}
  47. >
  48. <Toggle
  49. isDisabled={isDisabled}
  50. isActive={isActive}
  51. forceActiveColor={forceActiveColor}
  52. size={size}
  53. />
  54. </SwitchButton>
  55. );
  56. }
  57. type StyleProps = Partial<Props>;
  58. const getSize = (p: StyleProps) => (p.size === 'sm' ? 16 : 24);
  59. const getToggleSize = (p: StyleProps) => getSize(p) - (p.size === 'sm' ? 4 : 8);
  60. const getToggleTop = (p: StyleProps) => (p.size === 'sm' ? 1 : 3);
  61. const getTranslateX = (p: StyleProps) =>
  62. p.isActive ? getToggleTop(p) + getSize(p) * 0.875 : getToggleTop(p);
  63. const SwitchButton = styled('button')<StyleProps>`
  64. display: inline-block;
  65. background: none;
  66. padding: 0;
  67. border: 1px solid ${p => p.theme.border};
  68. position: relative;
  69. box-shadow: inset ${p => p.theme.dropShadowMedium};
  70. height: ${getSize}px;
  71. width: ${p => getSize(p) * 1.875}px;
  72. border-radius: ${getSize}px;
  73. transition: border 0.1s, box-shadow 0.1s;
  74. &[disabled] {
  75. cursor: not-allowed;
  76. }
  77. &:focus,
  78. &.focus-visible {
  79. outline: none;
  80. border-color: ${p => p.theme.focusBorder};
  81. box-shadow: ${p => p.theme.focusBorder} 0 0 0 1px;
  82. }
  83. `;
  84. const Toggle = styled('span')<StyleProps>`
  85. display: block;
  86. position: absolute;
  87. border-radius: 50%;
  88. transition: 0.25s all ease;
  89. top: ${getToggleTop}px;
  90. transform: translateX(${getTranslateX}px);
  91. width: ${getToggleSize}px;
  92. height: ${getToggleSize}px;
  93. background: ${p =>
  94. p.isActive || p.forceActiveColor ? p.theme.active : p.theme.border};
  95. opacity: ${p => (p.isDisabled ? 0.4 : null)};
  96. `;
  97. export default forwardRef<HTMLButtonElement, Props>((props, ref) => (
  98. <Switch {...props} forwardedRef={ref} />
  99. ));