numberInput.tsx 3.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116
  1. import {forwardRef, ForwardRefRenderFunction, useRef} from 'react';
  2. import styled from '@emotion/styled';
  3. import {useButton} from '@react-aria/button';
  4. import {useLocale} from '@react-aria/i18n';
  5. import {useNumberField} from '@react-aria/numberfield';
  6. import {useNumberFieldState} from '@react-stately/numberfield';
  7. import {AriaNumberFieldProps} from '@react-types/numberfield';
  8. import {Button} from 'sentry/components/button';
  9. import {InputStylesProps} from 'sentry/components/input';
  10. import {InputGroup} from 'sentry/components/inputGroup';
  11. import {IconChevron} from 'sentry/icons/iconChevron';
  12. import {space} from 'sentry/styles/space';
  13. import mergeRefs from 'sentry/utils/mergeRefs';
  14. import {FormSize} from 'sentry/utils/theme';
  15. export interface NumberInputProps
  16. extends InputStylesProps,
  17. AriaNumberFieldProps,
  18. Pick<
  19. React.InputHTMLAttributes<HTMLInputElement>,
  20. 'name' | 'disabled' | 'readOnly' | 'required' | 'className'
  21. > {
  22. max?: number;
  23. min?: number;
  24. }
  25. const BaseNumberInput: ForwardRefRenderFunction<HTMLInputElement, NumberInputProps> = (
  26. {
  27. disabled,
  28. readOnly,
  29. monospace,
  30. min,
  31. max,
  32. size,
  33. placeholder,
  34. nativeSize,
  35. className,
  36. ...props
  37. },
  38. forwardedRef
  39. ) => {
  40. const ref = useRef<HTMLInputElement>(null);
  41. const ariaProps = {
  42. isDisabled: disabled,
  43. isReadOnly: readOnly,
  44. minValue: min,
  45. maxValue: max,
  46. placeholder,
  47. ...props,
  48. };
  49. const {locale} = useLocale();
  50. const state = useNumberFieldState({locale, ...ariaProps});
  51. const {groupProps, inputProps, incrementButtonProps, decrementButtonProps} =
  52. useNumberField(ariaProps, state, ref);
  53. const incrementButtonRef = useRef<HTMLButtonElement>(null);
  54. const {buttonProps: incrementProps} = useButton(
  55. incrementButtonProps,
  56. incrementButtonRef
  57. );
  58. const decrementButtonRef = useRef<HTMLButtonElement>(null);
  59. const {buttonProps: decrementProps} = useButton(
  60. decrementButtonProps,
  61. decrementButtonRef
  62. );
  63. return (
  64. <InputGroup {...groupProps}>
  65. <InputGroup.Input
  66. {...inputProps}
  67. ref={mergeRefs([ref, forwardedRef])}
  68. placeholder={placeholder}
  69. size={size}
  70. nativeSize={nativeSize}
  71. monospace={monospace}
  72. className={className}
  73. />
  74. <InputGroup.TrailingItems>
  75. <StepWrap size={size}>
  76. <StepButton ref={incrementButtonRef} size="zero" borderless {...incrementProps}>
  77. <StyledIconChevron direction="up" />
  78. </StepButton>
  79. <StepButton ref={decrementButtonRef} size="zero" borderless {...decrementProps}>
  80. <StyledIconChevron direction="down" />
  81. </StepButton>
  82. </StepWrap>
  83. </InputGroup.TrailingItems>
  84. </InputGroup>
  85. );
  86. };
  87. const NumberInput = forwardRef(BaseNumberInput);
  88. export default NumberInput;
  89. const StepWrap = styled('div')<{size?: FormSize}>`
  90. display: flex;
  91. flex-direction: column;
  92. align-items: center;
  93. width: ${space(1.5)};
  94. height: ${p => (p.size === 'xs' ? '1rem' : '1.25rem')};
  95. `;
  96. const StepButton = styled(Button)`
  97. display: flex;
  98. height: 50%;
  99. padding: 0 ${space(0.25)};
  100. color: ${p => p.theme.subText};
  101. `;
  102. const StyledIconChevron = styled(IconChevron)`
  103. width: 0.5rem;
  104. height: 0.5rem;
  105. `;