Common front-end components for Web using the Tesseract design system, written for React. https://make.modal.sh/tesseract/web/react/common
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

Button.tsx 6.0 KiB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255
  1. import * as React from 'react'
  2. import * as PropTypes from 'prop-types'
  3. import styled, { CSSObject } from 'styled-components'
  4. import { Size, SizeMap } from '../../services/utilities'
  5. import stringify from '../../services/stringify'
  6. export type Variant = 'outline' | 'primary'
  7. export type ButtonElement = 'a' | 'button'
  8. type ButtonType = 'submit' | 'reset' | 'button'
  9. const MIN_HEIGHTS: SizeMap<string | number> = {
  10. small: '2.5rem',
  11. medium: '3rem',
  12. large: '4rem',
  13. }
  14. const disabledButtonStyles: CSSObject = {
  15. opacity: 0.5,
  16. cursor: 'not-allowed',
  17. }
  18. const buttonStyles: CSSObject = {
  19. display: 'grid',
  20. appearance: 'none',
  21. padding: '0 1rem',
  22. font: 'inherit',
  23. fontFamily: 'var(--font-family-base, sans-serif)',
  24. textTransform: 'uppercase',
  25. fontWeight: 'bolder',
  26. borderRadius: '0.25rem',
  27. placeContent: 'center',
  28. position: 'relative',
  29. cursor: 'pointer',
  30. border: 0,
  31. userSelect: 'none',
  32. textDecoration: 'none',
  33. transitionProperty: 'background-color, color',
  34. whiteSpace: 'nowrap',
  35. lineHeight: 1,
  36. ':focus': {
  37. '--color-accent': 'var(--color-active, Highlight)',
  38. outline: 0,
  39. },
  40. ':disabled': disabledButtonStyles,
  41. '::-moz-focus-inner': {
  42. outline: 0,
  43. border: 0,
  44. },
  45. }
  46. const disabledLinkButtonStyles: CSSObject = {
  47. ...buttonStyles,
  48. ...disabledButtonStyles,
  49. }
  50. const Base = styled('button')({
  51. ...buttonStyles,
  52. width: '100%',
  53. })
  54. Base.displayName = 'button'
  55. const LinkBase = styled('a')(buttonStyles)
  56. LinkBase.displayName = 'a'
  57. const DisabledLinkBase = styled('span')(disabledLinkButtonStyles)
  58. DisabledLinkBase.displayName = 'span'
  59. const Border = styled('span')({
  60. borderColor: 'var(--color-accent, blue)',
  61. boxSizing: 'border-box',
  62. display: 'inline-block',
  63. borderWidth: '0.125rem',
  64. borderStyle: 'solid',
  65. position: 'absolute',
  66. top: 0,
  67. left: 0,
  68. width: '100%',
  69. height: '100%',
  70. borderRadius: 'inherit',
  71. pointerEvents: 'none',
  72. transitionProperty: 'border-color',
  73. '::before': {
  74. position: 'absolute',
  75. top: 0,
  76. left: 0,
  77. width: '100%',
  78. height: '100%',
  79. content: "''",
  80. borderRadius: '0.125rem',
  81. opacity: 0.5,
  82. pointerEvents: 'none',
  83. },
  84. [`${Base}:focus &::before`]: {
  85. boxShadow: '0 0 0 0.375rem var(--color-accent, blue)',
  86. },
  87. })
  88. Border.displayName = 'span'
  89. const defaultVariantStyleSet: React.CSSProperties = {
  90. backgroundColor: 'transparent',
  91. color: 'var(--color-accent, blue)',
  92. }
  93. const variantStyleSets: Record<Variant, React.CSSProperties> = {
  94. outline: defaultVariantStyleSet,
  95. primary: {
  96. backgroundColor: 'var(--color-accent, blue)',
  97. color: 'var(--color-bg, white)',
  98. },
  99. }
  100. const propTypes = {
  101. /**
  102. * Size of the component.
  103. */
  104. size: PropTypes.oneOf<Size>(['small', 'medium', 'large']),
  105. /**
  106. * Variant of the component.
  107. */
  108. variant: PropTypes.oneOf<Variant>(['outline', 'primary']),
  109. /**
  110. * Text to identify the action associated upon activation of the component.
  111. */
  112. children: PropTypes.any,
  113. /**
  114. * Can the component be activated?
  115. */
  116. disabled: PropTypes.bool,
  117. /**
  118. * The corresponding HTML element of the component.
  119. */
  120. element: PropTypes.oneOf<ButtonElement>(['a', 'button']),
  121. /**
  122. * The URL of the page to navigate to, if element is set to "a".
  123. */
  124. href: PropTypes.string,
  125. /**
  126. * The target on where to display the page navigated to, if element is set to "a".
  127. */
  128. target: PropTypes.string,
  129. /**
  130. * The relationship of the current page to the referred page in "href", if element is set to "a".
  131. */
  132. rel: PropTypes.string,
  133. /**
  134. * The type of the button, if element is set to "button".
  135. */
  136. type: PropTypes.oneOf<ButtonType>(['submit', 'reset', 'button']),
  137. /**
  138. * Does the button display a border?
  139. */
  140. border: PropTypes.bool,
  141. /**
  142. * Event handler triggered when the component is clicked.
  143. */
  144. onClick: PropTypes.func,
  145. /**
  146. * Event handler triggered when the component receives focus.
  147. */
  148. onFocus: PropTypes.func,
  149. /**
  150. * Event handler triggered when the component loses focus.
  151. */
  152. onBlur: PropTypes.func,
  153. }
  154. type Props = PropTypes.InferProps<typeof propTypes>
  155. const Button = React.forwardRef<HTMLAnchorElement | HTMLButtonElement | HTMLSpanElement, Props>(
  156. (
  157. {
  158. size = 'medium',
  159. variant = 'outline',
  160. disabled = false,
  161. children,
  162. element = 'button',
  163. href,
  164. target,
  165. rel,
  166. type = 'button',
  167. border = false,
  168. onClick,
  169. onFocus,
  170. onBlur,
  171. },
  172. ref,
  173. ) => {
  174. const { [variant as Variant]: theVariantStyleSet = defaultVariantStyleSet } = variantStyleSets
  175. const commonButtonStyles: React.CSSProperties = {
  176. ...theVariantStyleSet,
  177. minHeight: MIN_HEIGHTS[size!],
  178. }
  179. const buttonContent = (
  180. <React.Fragment>
  181. {border && <Border />}
  182. {stringify(children)}
  183. </React.Fragment>
  184. )
  185. switch (element) {
  186. case 'button':
  187. return (
  188. <Base
  189. onClick={onClick as React.EventHandler<React.SyntheticEvent>}
  190. type={type!}
  191. ref={ref as React.Ref<HTMLButtonElement>}
  192. disabled={disabled!}
  193. style={commonButtonStyles}
  194. onFocus={onFocus as React.FocusEventHandler}
  195. onBlur={onBlur as React.FocusEventHandler}
  196. >
  197. {buttonContent}
  198. </Base>
  199. )
  200. case 'a':
  201. if (disabled) {
  202. return (
  203. <DisabledLinkBase ref={ref as React.Ref<HTMLSpanElement>} style={commonButtonStyles}>
  204. {buttonContent}
  205. </DisabledLinkBase>
  206. )
  207. }
  208. return (
  209. <LinkBase
  210. onClick={onClick as React.EventHandler<React.SyntheticEvent>}
  211. href={href!}
  212. target={target!}
  213. rel={rel!}
  214. ref={ref as React.Ref<HTMLAnchorElement>}
  215. style={commonButtonStyles}
  216. onFocus={onFocus as React.FocusEventHandler}
  217. onBlur={onBlur as React.FocusEventHandler}
  218. >
  219. {buttonContent}
  220. </LinkBase>
  221. )
  222. default:
  223. break
  224. }
  225. return null
  226. },
  227. )
  228. Button.propTypes = propTypes
  229. Button.displayName = 'Button'
  230. export default Button