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.
 
 
 
 

176 rivejä
4.2 KiB

  1. import * as React from 'react'
  2. import * as PropTypes from 'prop-types'
  3. import styled from 'styled-components'
  4. import stringify from '../../services/stringify'
  5. const Base = styled('div')({
  6. display: 'block',
  7. })
  8. const CaptureArea = styled('label')({
  9. marginTop: '0.25rem',
  10. '::after': {
  11. content: '""',
  12. clear: 'both',
  13. },
  14. })
  15. CaptureArea.displayName = 'label'
  16. const Input = styled('input')({
  17. position: 'absolute',
  18. left: -999999,
  19. width: 1,
  20. height: 1,
  21. })
  22. Input.displayName = 'input'
  23. const IndicatorWrapper = styled('span')({
  24. borderColor: 'var(--color-accent, blue)',
  25. boxSizing: 'border-box',
  26. backgroundColor: 'transparent',
  27. borderRadius: '0.75rem',
  28. position: 'relative',
  29. width: '1.5rem',
  30. height: '1.5rem',
  31. minWidth: '1.5rem',
  32. maxWidth: '1.5rem',
  33. display: 'inline-flex',
  34. verticalAlign: 'top',
  35. justifyContent: 'center',
  36. alignItems: 'center',
  37. cursor: 'pointer',
  38. transitionProperty: 'border-color',
  39. [`${Input}:focus ~ &`]: {
  40. '--color-accent': 'var(--color-active, Highlight)',
  41. },
  42. [`${Input}:disabled ~ &`]: {
  43. cursor: 'not-allowed',
  44. opacity: 0.5,
  45. },
  46. })
  47. const Border = styled('span')({
  48. borderColor: 'var(--color-accent, blue)',
  49. boxSizing: 'border-box',
  50. display: 'inline-block',
  51. borderWidth: '0.125rem',
  52. borderStyle: 'solid',
  53. position: 'absolute',
  54. top: 0,
  55. left: 0,
  56. width: '100%',
  57. height: '100%',
  58. borderRadius: 'inherit',
  59. transitionProperty: 'border-color',
  60. '::before': {
  61. position: 'absolute',
  62. top: 0,
  63. left: 0,
  64. width: '100%',
  65. height: '100%',
  66. content: "''",
  67. borderRadius: '0.75rem',
  68. opacity: 0.5,
  69. pointerEvents: 'none',
  70. },
  71. [`${Base}:focus-within &::before`]: {
  72. boxShadow: '0 0 0 0.375rem var(--color-accent, blue)',
  73. },
  74. })
  75. const Indicator = styled('span')({
  76. backgroundColor: 'var(--color-accent, blue)',
  77. color: 'var(--color-bg, white)',
  78. width: 0,
  79. height: 0,
  80. opacity: 0,
  81. boxSizing: 'border-box',
  82. display: 'inline-grid',
  83. placeContent: 'center',
  84. borderRadius: '50%',
  85. transitionProperty: 'background-color, color, width, height, opacity',
  86. [`${Input}:checked + ${IndicatorWrapper} &`]: {
  87. width: `${(100 * 2) / 3}%`,
  88. height: `${(100 * 2) / 3}%`,
  89. opacity: 1,
  90. },
  91. })
  92. const Label = styled('span')({
  93. display: 'block',
  94. verticalAlign: 'top',
  95. float: 'right',
  96. width: 'calc(100% - 2.5rem)',
  97. fontFamily: 'var(--font-family-base, sans-serif)',
  98. pointerEvents: 'none',
  99. })
  100. const LabelContent = styled('span')({
  101. display: 'inline',
  102. pointerEvents: 'auto',
  103. })
  104. const propTypes = {
  105. /**
  106. * Group where the component belongs.
  107. */
  108. name: PropTypes.string.isRequired,
  109. /**
  110. * Short textual description indicating the nature of the component's value.
  111. */
  112. label: PropTypes.any,
  113. /**
  114. * Event handler triggered when the component is selected.
  115. */
  116. onChange: PropTypes.func,
  117. /**
  118. * Event handler triggered when the component receives focus.
  119. */
  120. onFocus: PropTypes.func,
  121. /**
  122. * Event handler triggered when the component loses focus.
  123. */
  124. onBlur: PropTypes.func,
  125. }
  126. type Props = PropTypes.InferProps<typeof propTypes>
  127. /**
  128. * Component for values which are to be selected from a few list of options.
  129. * @see {@link Checkbox} for a similar component on selecting values among very few choices.
  130. * @see {@link Select} for a similar component suitable for selecting more values.
  131. * @type {React.ComponentType<{readonly label?: string, readonly name?: string} & React.ClassAttributes<unknown>>}
  132. */
  133. const RadioButton = React.forwardRef<HTMLInputElement, Props>(
  134. ({ label = '', name, onChange, onFocus, onBlur }, ref) => (
  135. <Base>
  136. <CaptureArea>
  137. <Input
  138. ref={ref}
  139. name={name}
  140. type="radio"
  141. onChange={onChange as React.ChangeEventHandler}
  142. onFocus={onFocus as React.FocusEventHandler}
  143. onBlur={onBlur as React.FocusEventHandler}
  144. />
  145. <IndicatorWrapper>
  146. <Border />
  147. <Indicator />
  148. </IndicatorWrapper>
  149. {typeof label! !== 'undefined' && label !== null && ' '}
  150. <Label>
  151. <LabelContent>{stringify(label)}</LabelContent>
  152. </Label>
  153. </CaptureArea>
  154. </Base>
  155. ),
  156. )
  157. RadioButton.propTypes = propTypes
  158. RadioButton.displayName = 'RadioButton'
  159. export default RadioButton