123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123 |
- import {Fragment} from 'react';
- import isPropValid from '@emotion/is-prop-valid';
- import styled from '@emotion/styled';
- import Radio from 'sentry/components/radio';
- import Tooltip from 'sentry/components/tooltip';
- import space from 'sentry/styles/space';
- interface ContainerProps extends React.HTMLAttributes<HTMLDivElement> {
- orientInline?: boolean;
- }
- interface BaseRadioGroupProps<C extends string> {
- /**
- * The choices availiable in the group
- */
- choices: [id: C, label: React.ReactNode, description?: React.ReactNode][];
- /**
- * Labels the radio group.
- */
- label: string;
- onChange: (id: C, e: React.FormEvent<HTMLInputElement>) => void;
- value: string | number | null;
- disabled?: boolean;
- /**
- * An array of [choice id, disabled reason]
- */
- disabledChoices?: [C, React.ReactNode?][];
- /**
- * Switch the radio items to flow left to right, instead of vertically.
- */
- orientInline?: boolean;
- }
- export interface RadioGroupProps<C extends string>
- extends BaseRadioGroupProps<C>,
- Omit<ContainerProps, 'onChange'> {}
- const RadioGroup = <C extends string>({
- value,
- disabled: groupDisabled,
- disabledChoices = [],
- choices = [],
- label,
- onChange,
- orientInline,
- ...props
- }: RadioGroupProps<C>) => (
- <Container orientInline={orientInline} {...props} role="radiogroup" aria-label={label}>
- {choices.map(([id, name, description], index) => {
- const disabledChoice = disabledChoices.find(([choiceId]) => choiceId === id);
- const disabledChoiceReason = disabledChoice?.[1];
- const disabled = !!disabledChoice || groupDisabled;
- // TODO(epurkhiser): There should be a `name` and `label` attribute in
- // the options type to allow for the aria label to work correctly. For
- // now we slap a `toString` on there, but it may sometimes return
- // [object Object] if the name is a react node.
- return (
- <Tooltip
- key={index}
- disabled={!disabledChoiceReason}
- title={disabledChoiceReason}
- >
- <RadioLineItem index={index} aria-checked={value === id} disabled={disabled}>
- <Radio
- aria-label={name?.toString()}
- disabled={disabled}
- checked={value === id}
- onChange={(e: React.FormEvent<HTMLInputElement>) =>
- !disabled && onChange(id, e)
- }
- />
- <RadioLineText disabled={disabled}>{name}</RadioLineText>
- {description && (
- <Fragment>
- {/* If there is a description then we want to have a 2x2 grid so the first column width aligns with Radio Button */}
- <div />
- <Description>{description}</Description>
- </Fragment>
- )}
- </RadioLineItem>
- </Tooltip>
- );
- })}
- </Container>
- );
- const Container = styled('div')<ContainerProps>`
- display: flex;
- gap: ${p => space(p.orientInline ? 3 : 1)};
- flex-direction: ${p => (p.orientInline ? 'row' : 'column')};
- `;
- const shouldForwardProp = (p: PropertyKey) =>
- typeof p === 'string' && !['disabled', 'animate'].includes(p) && isPropValid(p);
- export const RadioLineItem = styled('label', {shouldForwardProp})<{
- index: number;
- disabled?: boolean;
- }>`
- display: grid;
- gap: 0.25em 0.5em;
- grid-template-columns: max-content auto;
- align-items: center;
- cursor: ${p => (p.disabled ? 'default' : 'pointer')};
- outline: none;
- font-weight: normal;
- margin: 0;
- `;
- const RadioLineText = styled('div', {shouldForwardProp})<{disabled?: boolean}>`
- opacity: ${p => (p.disabled ? 0.4 : null)};
- `;
- const Description = styled('div')`
- color: ${p => p.theme.gray300};
- font-size: ${p => p.theme.fontSizeRelativeSmall};
- line-height: 1.4em;
- `;
- export default RadioGroup;
|