index.tsx 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219
  1. import {forwardRef as reactForwardRef, useEffect, useState} from 'react';
  2. import {useId} from '@react-aria/utils';
  3. import Input from 'sentry/components/input';
  4. import {t} from 'sentry/locale';
  5. import {defined} from 'sentry/utils';
  6. import Slider from './slider';
  7. import SliderAndInputWrapper from './sliderAndInputWrapper';
  8. import SliderLabel from './sliderLabel';
  9. type SliderProps = {
  10. name: string;
  11. /**
  12. * String is a valid type here only for empty string
  13. * Otherwise react complains:
  14. * "`value` prop on `input` should not be null. Consider using an empty string to clear the component or `undefined` for uncontrolled components."
  15. *
  16. * And we want this to be a controlled input when value is empty
  17. */
  18. value: number | '';
  19. /**
  20. * Array of allowed values. Make sure `value` is in this list.
  21. * THIS NEEDS TO BE SORTED
  22. */
  23. allowedValues?: number[];
  24. className?: string;
  25. disabled?: boolean;
  26. /**
  27. * Render prop for slider's label
  28. * Is passed the value as an argument
  29. */
  30. formatLabel?: (value: number | '') => React.ReactNode;
  31. forwardRef?: React.Ref<HTMLDivElement>;
  32. /**
  33. * max allowed value, not needed if using `allowedValues`
  34. */
  35. max?: number;
  36. /**
  37. * min allowed value, not needed if using `allowedValues`
  38. */
  39. min?: number;
  40. /**
  41. * This is called when *any* MouseUp or KeyUp event happens.
  42. * Used for "smart" Fields to trigger a "blur" event. `onChange` can
  43. * be triggered quite frequently
  44. */
  45. onBlur?: (
  46. event: React.MouseEvent<HTMLInputElement> | React.KeyboardEvent<HTMLInputElement>
  47. ) => void;
  48. onChange?: (
  49. value: SliderProps['value'],
  50. event: React.ChangeEvent<HTMLInputElement>
  51. ) => void;
  52. /**
  53. * Placeholder for custom input
  54. */
  55. placeholder?: string;
  56. /**
  57. * Show input control for custom values
  58. */
  59. showCustomInput?: boolean;
  60. /**
  61. * Show label with current value
  62. */
  63. showLabel?: boolean;
  64. step?: number;
  65. };
  66. function RangeSlider({
  67. value,
  68. allowedValues,
  69. showCustomInput,
  70. name,
  71. disabled,
  72. placeholder,
  73. formatLabel,
  74. className,
  75. onBlur,
  76. onChange,
  77. forwardRef,
  78. showLabel = true,
  79. ...props
  80. }: SliderProps) {
  81. const elementId = useId();
  82. const [sliderValue, setSliderValue] = useState(
  83. allowedValues ? allowedValues.indexOf(Number(value || 0)) : value
  84. );
  85. useEffect(() => {
  86. updateSliderValue();
  87. // eslint-disable-next-line react-hooks/exhaustive-deps
  88. }, [value]);
  89. function updateSliderValue() {
  90. if (!defined(value)) {
  91. return;
  92. }
  93. const newSliderValueIndex = allowedValues?.indexOf(Number(value || 0)) ?? -1;
  94. // If `allowedValues` is defined, then `sliderValue` represents index to `allowedValues`
  95. if (newSliderValueIndex > -1) {
  96. setSliderValue(newSliderValueIndex);
  97. return;
  98. }
  99. setSliderValue(value);
  100. }
  101. function getActualValue(newSliderValue: SliderProps['value']): SliderProps['value'] {
  102. if (!allowedValues) {
  103. return newSliderValue;
  104. }
  105. // If `allowedValues` is defined, then `sliderValue` represents index to `allowedValues`
  106. return allowedValues[newSliderValue];
  107. }
  108. function handleInput(e: React.ChangeEvent<HTMLInputElement>) {
  109. const newSliderValue = parseInt(e.target.value, 10);
  110. setSliderValue(newSliderValue);
  111. onChange?.(getActualValue(newSliderValue), e);
  112. }
  113. function handleCustomInputChange(e: React.ChangeEvent<HTMLInputElement>) {
  114. setSliderValue(parseInt(e.target.value, 10) || 0);
  115. }
  116. function handleBlur(
  117. e: React.MouseEvent<HTMLInputElement> | React.KeyboardEvent<HTMLInputElement>
  118. ) {
  119. if (typeof onBlur !== 'function') {
  120. return;
  121. }
  122. onBlur(e);
  123. }
  124. function getSliderData() {
  125. if (!allowedValues) {
  126. const {min, max, step} = props;
  127. return {
  128. min,
  129. max,
  130. step,
  131. actualValue: sliderValue,
  132. displayValue: sliderValue,
  133. };
  134. }
  135. const actualValue = allowedValues[sliderValue];
  136. return {
  137. step: 1,
  138. min: 0,
  139. max: allowedValues.length - 1,
  140. actualValue,
  141. displayValue: defined(actualValue) ? actualValue : t('Invalid value'),
  142. };
  143. }
  144. const {min, max, step, actualValue, displayValue} = getSliderData();
  145. return (
  146. <div className={className} ref={forwardRef}>
  147. {!showCustomInput && showLabel && (
  148. <SliderLabel htmlFor={elementId}>
  149. {formatLabel?.(actualValue) ?? displayValue}
  150. </SliderLabel>
  151. )}
  152. <SliderAndInputWrapper showCustomInput={showCustomInput}>
  153. <Slider
  154. type="range"
  155. id={elementId}
  156. name={name}
  157. min={min}
  158. max={max}
  159. step={step}
  160. disabled={disabled}
  161. onChange={handleInput}
  162. onInput={handleInput}
  163. onMouseUp={handleBlur}
  164. onKeyUp={handleBlur}
  165. value={sliderValue}
  166. hasLabel={!showCustomInput}
  167. />
  168. {showCustomInput && (
  169. <Input
  170. placeholder={placeholder}
  171. value={sliderValue}
  172. onChange={handleCustomInputChange}
  173. onBlur={handleInput}
  174. />
  175. )}
  176. </SliderAndInputWrapper>
  177. </div>
  178. );
  179. }
  180. const RangeSliderContainer = reactForwardRef(function RangeSliderContainer(
  181. props: SliderProps,
  182. ref: React.Ref<any>
  183. ) {
  184. return <RangeSlider {...props} forwardRef={ref} />;
  185. });
  186. export default RangeSliderContainer;
  187. export type {SliderProps};