radioGroup.tsx 3.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123
  1. import {Fragment} from 'react';
  2. import isPropValid from '@emotion/is-prop-valid';
  3. import styled from '@emotion/styled';
  4. import Radio from 'sentry/components/radio';
  5. import Tooltip from 'sentry/components/tooltip';
  6. import space from 'sentry/styles/space';
  7. interface ContainerProps extends React.HTMLAttributes<HTMLDivElement> {
  8. orientInline?: boolean;
  9. }
  10. interface BaseRadioGroupProps<C extends string> {
  11. /**
  12. * The choices availiable in the group
  13. */
  14. choices: [id: C, label: React.ReactNode, description?: React.ReactNode][];
  15. /**
  16. * Labels the radio group.
  17. */
  18. label: string;
  19. onChange: (id: C, e: React.FormEvent<HTMLInputElement>) => void;
  20. value: string | number | null;
  21. disabled?: boolean;
  22. /**
  23. * An array of [choice id, disabled reason]
  24. */
  25. disabledChoices?: [C, React.ReactNode?][];
  26. /**
  27. * Switch the radio items to flow left to right, instead of vertically.
  28. */
  29. orientInline?: boolean;
  30. }
  31. export interface RadioGroupProps<C extends string>
  32. extends BaseRadioGroupProps<C>,
  33. Omit<ContainerProps, 'onChange'> {}
  34. const RadioGroup = <C extends string>({
  35. value,
  36. disabled: groupDisabled,
  37. disabledChoices = [],
  38. choices = [],
  39. label,
  40. onChange,
  41. orientInline,
  42. ...props
  43. }: RadioGroupProps<C>) => (
  44. <Container orientInline={orientInline} {...props} role="radiogroup" aria-label={label}>
  45. {choices.map(([id, name, description], index) => {
  46. const disabledChoice = disabledChoices.find(([choiceId]) => choiceId === id);
  47. const disabledChoiceReason = disabledChoice?.[1];
  48. const disabled = !!disabledChoice || groupDisabled;
  49. // TODO(epurkhiser): There should be a `name` and `label` attribute in
  50. // the options type to allow for the aria label to work correctly. For
  51. // now we slap a `toString` on there, but it may sometimes return
  52. // [object Object] if the name is a react node.
  53. return (
  54. <Tooltip
  55. key={index}
  56. disabled={!disabledChoiceReason}
  57. title={disabledChoiceReason}
  58. >
  59. <RadioLineItem index={index} aria-checked={value === id} disabled={disabled}>
  60. <Radio
  61. aria-label={name?.toString()}
  62. disabled={disabled}
  63. checked={value === id}
  64. onChange={(e: React.FormEvent<HTMLInputElement>) =>
  65. !disabled && onChange(id, e)
  66. }
  67. />
  68. <RadioLineText disabled={disabled}>{name}</RadioLineText>
  69. {description && (
  70. <Fragment>
  71. {/* If there is a description then we want to have a 2x2 grid so the first column width aligns with Radio Button */}
  72. <div />
  73. <Description>{description}</Description>
  74. </Fragment>
  75. )}
  76. </RadioLineItem>
  77. </Tooltip>
  78. );
  79. })}
  80. </Container>
  81. );
  82. const Container = styled('div')<ContainerProps>`
  83. display: flex;
  84. gap: ${p => space(p.orientInline ? 3 : 1)};
  85. flex-direction: ${p => (p.orientInline ? 'row' : 'column')};
  86. `;
  87. const shouldForwardProp = (p: PropertyKey) =>
  88. typeof p === 'string' && !['disabled', 'animate'].includes(p) && isPropValid(p);
  89. export const RadioLineItem = styled('label', {shouldForwardProp})<{
  90. index: number;
  91. disabled?: boolean;
  92. }>`
  93. display: grid;
  94. gap: 0.25em 0.5em;
  95. grid-template-columns: max-content auto;
  96. align-items: center;
  97. cursor: ${p => (p.disabled ? 'default' : 'pointer')};
  98. outline: none;
  99. font-weight: normal;
  100. margin: 0;
  101. `;
  102. const RadioLineText = styled('div', {shouldForwardProp})<{disabled?: boolean}>`
  103. opacity: ${p => (p.disabled ? 0.4 : null)};
  104. `;
  105. const Description = styled('div')`
  106. color: ${p => p.theme.gray300};
  107. font-size: ${p => p.theme.fontSizeRelativeSmall};
  108. line-height: 1.4em;
  109. `;
  110. export default RadioGroup;