radioGroupRating.tsx 2.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106
  1. import {useCallback, useState} from 'react';
  2. import {css} from '@emotion/react';
  3. import styled from '@emotion/styled';
  4. import {getButtonStyles} from 'sentry/components/button';
  5. import Field, {FieldProps} from 'sentry/components/forms/field';
  6. import {t} from 'sentry/locale';
  7. import space from 'sentry/styles/space';
  8. type Option = {label: string; description?: string};
  9. export type RadioGroupRatingProps = Omit<FieldProps, 'children'> & {
  10. /**
  11. * Field name, used in all radio group elements
  12. */
  13. name: string;
  14. /**
  15. * An object of options, where the label is used for the aria-label,
  16. * the key is used for the selection and
  17. * the optional description to provide more context
  18. */
  19. options: Record<string, Option>;
  20. /**
  21. * The key of the option that should be selected by default
  22. */
  23. defaultValue?: string;
  24. /**
  25. * Callback function that is called when the selection changes.
  26. * its value is the key of the selected option
  27. */
  28. onChange?: (value: string) => void;
  29. };
  30. // Used to provide insights regarding opinions and experiences.
  31. // Currently limited to numeric options only, but can be updated to meet other needs.
  32. export function RadioGroupRating({
  33. options,
  34. name,
  35. onChange,
  36. defaultValue,
  37. ...fieldProps
  38. }: RadioGroupRatingProps) {
  39. const [selectedOption, setSelectedOption] = useState(defaultValue);
  40. const handleClickedOption = useCallback(
  41. (value: string) => {
  42. setSelectedOption(value);
  43. onChange?.(value);
  44. },
  45. [onChange]
  46. );
  47. return (
  48. <Field {...fieldProps}>
  49. <Wrapper totalOptions={Object.keys(options).length}>
  50. {Object.entries(options).map(([key, option], index) => (
  51. <OptionWrapper key={key}>
  52. <Label
  53. selected={key === selectedOption}
  54. htmlFor={key}
  55. onClick={() => handleClickedOption(key)}
  56. aria-label={t('Select option %s', option.label)}
  57. >
  58. {index + 1}
  59. </Label>
  60. <HiddenInput type="radio" id={key} name={name} value={option.label} />
  61. <Description>{option.description}</Description>
  62. </OptionWrapper>
  63. ))}
  64. </Wrapper>
  65. </Field>
  66. );
  67. }
  68. const HiddenInput = styled('input')`
  69. display: none;
  70. `;
  71. const Wrapper = styled('div')<{totalOptions: number}>`
  72. display: grid;
  73. grid-template-columns: repeat(auto-fit, minmax(50px, 1fr));
  74. margin-top: ${space(0.5)};
  75. gap: ${space(1)};
  76. `;
  77. const OptionWrapper = styled('div')`
  78. display: flex;
  79. align-items: center;
  80. flex-direction: column;
  81. `;
  82. const Label = styled('label')<{'aria-label': string; selected: boolean}>`
  83. cursor: pointer;
  84. ${p => css`
  85. ${getButtonStyles({
  86. theme: p.theme,
  87. size: 'md',
  88. 'aria-label': p['aria-label'],
  89. priority: p.selected ? 'primary' : 'default',
  90. })}
  91. `}
  92. `;
  93. const Description = styled('div')`
  94. text-align: center;
  95. `;