numberInput.tsx 3.1 KB

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