radioGroupRating.tsx 2.9 KB

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