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.
 
 
 
 

222 lines
5.1 KiB

  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')(buttonStyles)
  51. Base.displayName = 'button'
  52. const LinkBase = styled('a')(buttonStyles)
  53. LinkBase.displayName = 'a'
  54. const DisabledLinkBase = styled('span')(disabledLinkButtonStyles)
  55. DisabledLinkBase.displayName = 'span'
  56. const Border = styled('span')({
  57. borderColor: 'var(--color-accent, blue)',
  58. boxSizing: 'border-box',
  59. display: 'inline-block',
  60. borderWidth: '0.125rem',
  61. borderStyle: 'solid',
  62. position: 'absolute',
  63. top: 0,
  64. left: 0,
  65. width: '100%',
  66. height: '100%',
  67. borderRadius: 'inherit',
  68. pointerEvents: 'none',
  69. transitionProperty: 'border-color',
  70. '::before': {
  71. position: 'absolute',
  72. top: 0,
  73. left: 0,
  74. width: '100%',
  75. height: '100%',
  76. content: "''",
  77. borderRadius: '0.125rem',
  78. opacity: 0.5,
  79. pointerEvents: 'none',
  80. },
  81. [`${Base}:focus &::before`]: {
  82. boxShadow: '0 0 0 0.375rem var(--color-accent, blue)',
  83. },
  84. })
  85. Border.displayName = 'span'
  86. const defaultVariantStyleSet: React.CSSProperties = {
  87. backgroundColor: 'transparent',
  88. color: 'var(--color-accent, blue)',
  89. }
  90. const variantStyleSets: Record<Variant, React.CSSProperties> = {
  91. outline: defaultVariantStyleSet,
  92. primary: {
  93. backgroundColor: 'var(--color-accent, blue)',
  94. color: 'var(--color-bg, white)',
  95. },
  96. }
  97. const propTypes = {
  98. /**
  99. * Size of the component.
  100. */
  101. size: PropTypes.oneOf<Size>(['small', 'medium', 'large']),
  102. /**
  103. * Variant of the component.
  104. */
  105. variant: PropTypes.oneOf<Variant>(['outline', 'primary']),
  106. /**
  107. * Text to identify the action associated upon activation of the component.
  108. */
  109. children: PropTypes.any,
  110. /**
  111. * Can the component be activated?
  112. */
  113. disabled: PropTypes.bool,
  114. /**
  115. * The corresponding HTML element of the component.
  116. */
  117. element: PropTypes.oneOf<ButtonElement>(['a', 'button']),
  118. /**
  119. * The URL of the page to navigate to, if element is set to "a".
  120. */
  121. href: PropTypes.string,
  122. /**
  123. * The target on where to display the page navigated to, if element is set to "a".
  124. */
  125. target: PropTypes.string,
  126. /**
  127. * The relationship of the current page to the referred page in "href", if element is set to "a".
  128. */
  129. rel: PropTypes.string,
  130. /**
  131. * The type of the button, if element is set to "button".
  132. */
  133. type: PropTypes.oneOf<ButtonType>(['submit', 'reset', 'button']),
  134. }
  135. type Props = PropTypes.InferProps<typeof propTypes>
  136. const Button = React.forwardRef<HTMLAnchorElement | HTMLButtonElement | HTMLSpanElement, Props>(
  137. (
  138. {
  139. size = 'medium',
  140. variant = 'outline',
  141. disabled = false,
  142. children,
  143. element = 'button',
  144. href,
  145. target,
  146. rel,
  147. type = 'button',
  148. },
  149. ref,
  150. ) => {
  151. const { [variant as Variant]: theVariantStyleSet = defaultVariantStyleSet } = variantStyleSets
  152. const commonButtonStyles: React.CSSProperties = {
  153. ...theVariantStyleSet,
  154. minHeight: MIN_HEIGHTS[size!],
  155. }
  156. const buttonContent = (
  157. <React.Fragment>
  158. <Border />
  159. {stringify(children)}
  160. </React.Fragment>
  161. )
  162. switch (element) {
  163. case 'button':
  164. return (
  165. <Base type={type!} ref={ref as React.Ref<HTMLButtonElement>} disabled={disabled!} style={commonButtonStyles}>
  166. {buttonContent}
  167. </Base>
  168. )
  169. case 'a':
  170. if (disabled) {
  171. return (
  172. <DisabledLinkBase ref={ref as React.Ref<HTMLSpanElement>} style={commonButtonStyles}>
  173. {buttonContent}
  174. </DisabledLinkBase>
  175. )
  176. }
  177. return (
  178. <LinkBase
  179. href={href!}
  180. target={target!}
  181. rel={rel!}
  182. ref={ref as React.Ref<HTMLAnchorElement>}
  183. style={commonButtonStyles}
  184. >
  185. {buttonContent}
  186. </LinkBase>
  187. )
  188. default:
  189. break
  190. }
  191. return null
  192. },
  193. )
  194. Button.propTypes = propTypes
  195. Button.displayName = 'Button'
  196. export default Button