radioGroupRating.tsx 2.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105
  1. import {useEffect, 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. useEffect(() => {
  41. if (!selectedOption) {
  42. return;
  43. }
  44. onChange?.(selectedOption);
  45. }, [selectedOption, onChange]);
  46. return (
  47. <Field {...fieldProps}>
  48. <Wrapper totalOptions={Object.keys(options).length}>
  49. {Object.entries(options).map(([key, option], index) => (
  50. <OptionWrapper key={key}>
  51. <Label
  52. selected={key === selectedOption}
  53. htmlFor={key}
  54. onClick={() => setSelectedOption(key)}
  55. aria-label={t('Select option %s', option.label)}
  56. >
  57. {index + 1}
  58. </Label>
  59. <HiddenInput type="radio" id={key} name={name} value={option.label} />
  60. <Description>{option.description}</Description>
  61. </OptionWrapper>
  62. ))}
  63. </Wrapper>
  64. </Field>
  65. );
  66. }
  67. const HiddenInput = styled('input')`
  68. display: none;
  69. `;
  70. const Wrapper = styled('div')<{totalOptions: number}>`
  71. display: grid;
  72. grid-template-columns: repeat(auto-fit, minmax(50px, 1fr));
  73. margin-top: ${space(0.5)};
  74. gap: ${space(1)};
  75. `;
  76. const OptionWrapper = styled('div')`
  77. display: flex;
  78. align-items: center;
  79. flex-direction: column;
  80. `;
  81. const Label = styled('label')<{'aria-label': string; selected: boolean}>`
  82. cursor: pointer;
  83. ${p => css`
  84. ${getButtonStyles({
  85. theme: p.theme,
  86. size: 'md',
  87. 'aria-label': p['aria-label'],
  88. priority: p.selected ? 'primary' : 'default',
  89. })}
  90. `}
  91. `;
  92. const Description = styled('div')`
  93. text-align: center;
  94. `;