You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

Select.tsx 5.1KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193
  1. import React, { ChangeEvent } from 'react';
  2. import { makeStyles } from 'tss-react/mui';
  3. import { isMobileBrowser } from '../../../environment/utils';
  4. import Icon from '../../../icons/components/Icon';
  5. import { IconArrowDown } from '../../../icons/svg';
  6. import { withPixelLineHeight } from '../../../styles/functions.web';
  7. interface ISelectProps {
  8. /**
  9. * Helper text to be displayed below the select.
  10. */
  11. bottomLabel?: string;
  12. /**
  13. * Class name for additional styles.
  14. */
  15. className?: string;
  16. /**
  17. * Whether or not the select is disabled.
  18. */
  19. disabled?: boolean;
  20. /**
  21. * Whether or not the select is in the error state.
  22. */
  23. error?: boolean;
  24. /**
  25. * Id of the <select> element.
  26. * Necessary for screen reader users, to link the label and error to the select.
  27. */
  28. id: string;
  29. /**
  30. * Label to be displayed above the select.
  31. */
  32. label?: string;
  33. /**
  34. * Change handler.
  35. */
  36. onChange: (e: ChangeEvent<HTMLSelectElement>) => void;
  37. /**
  38. * The options of the select.
  39. */
  40. options: Array<{
  41. label: string;
  42. value: number | string;
  43. }>;
  44. /**
  45. * The value of the select.
  46. */
  47. value: number | string;
  48. }
  49. const useStyles = makeStyles()(theme => {
  50. return {
  51. container: {
  52. display: 'flex',
  53. flexDirection: 'column'
  54. },
  55. label: {
  56. color: theme.palette.text01,
  57. ...withPixelLineHeight(theme.typography.bodyShortRegular),
  58. marginBottom: theme.spacing(2),
  59. '&.is-mobile': {
  60. ...withPixelLineHeight(theme.typography.bodyShortRegularLarge)
  61. }
  62. },
  63. selectContainer: {
  64. position: 'relative'
  65. },
  66. select: {
  67. backgroundColor: theme.palette.ui03,
  68. borderRadius: `${theme.shape.borderRadius}px`,
  69. width: '100%',
  70. ...withPixelLineHeight(theme.typography.bodyShortRegular),
  71. color: theme.palette.text01,
  72. padding: '10px 16px',
  73. paddingRight: '42px',
  74. border: 0,
  75. appearance: 'none',
  76. overflow: 'hidden',
  77. whiteSpace: 'nowrap',
  78. textOverflow: 'ellipsis',
  79. '&:focus': {
  80. outline: 0,
  81. boxShadow: `0px 0px 0px 2px ${theme.palette.focus01}`
  82. },
  83. '&:disabled': {
  84. color: theme.palette.text03
  85. },
  86. '&.is-mobile': {
  87. ...withPixelLineHeight(theme.typography.bodyShortRegularLarge),
  88. padding: '12px 16px',
  89. paddingRight: '46px'
  90. },
  91. '&.error': {
  92. boxShadow: `0px 0px 0px 2px ${theme.palette.textError}`
  93. }
  94. },
  95. icon: {
  96. position: 'absolute',
  97. top: '8px',
  98. right: '8px',
  99. pointerEvents: 'none',
  100. '&.is-mobile': {
  101. top: '12px',
  102. right: '12px'
  103. }
  104. },
  105. bottomLabel: {
  106. marginTop: theme.spacing(2),
  107. ...withPixelLineHeight(theme.typography.labelRegular),
  108. color: theme.palette.text02,
  109. '&.is-mobile': {
  110. ...withPixelLineHeight(theme.typography.bodyShortRegular)
  111. },
  112. '&.error': {
  113. color: theme.palette.textError
  114. }
  115. }
  116. };
  117. });
  118. const Select = ({
  119. bottomLabel,
  120. className,
  121. disabled,
  122. error,
  123. id,
  124. label,
  125. onChange,
  126. options,
  127. value }: ISelectProps) => {
  128. const { classes, cx, theme } = useStyles();
  129. const isMobile = isMobileBrowser();
  130. return (
  131. <div className = { classes.container }>
  132. {label && <label
  133. className = { cx(classes.label, isMobile && 'is-mobile') }
  134. htmlFor = { id } >
  135. {label}
  136. </label>}
  137. <div className = { classes.selectContainer }>
  138. <select
  139. aria-describedby = { bottomLabel ? `${id}-description` : undefined }
  140. className = { cx(classes.select, isMobile && 'is-mobile', className, error && 'error') }
  141. disabled = { disabled }
  142. id = { id }
  143. onChange = { onChange }
  144. value = { value }>
  145. {options.map(option => (<option
  146. key = { option.value }
  147. value = { option.value }>{option.label}</option>))}
  148. </select>
  149. <Icon
  150. className = { cx(classes.icon, isMobile && 'is-mobile') }
  151. color = { disabled ? theme.palette.icon03 : theme.palette.icon01 }
  152. size = { 22 }
  153. src = { IconArrowDown } />
  154. </div>
  155. {bottomLabel && (
  156. <span
  157. className = { cx(classes.bottomLabel, isMobile && 'is-mobile', error && 'error') }
  158. id = { `${id}-description` }>
  159. {bottomLabel}
  160. </span>
  161. )}
  162. </div>
  163. );
  164. };
  165. export default Select;