radioGroup.tsx 3.5 KB

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