Dropdown.jsx 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210
  1. import React, { useRef, useState, useCallback, useEffect } from "react";
  2. import { CSSTransition } from "react-transition-group";
  3. import flagLookup from "../../../domain/flagLookup";
  4. import styles from "./Dropdown.module.css";
  5. export const Item = ({ value, display, onSelect, children }) => {
  6. const onClick = () =>
  7. onSelect(value ?? children, display ?? value ?? children);
  8. return (
  9. <div
  10. tabIndex="0"
  11. role="menuitem"
  12. className={styles.item}
  13. onClick={onClick}
  14. onKeyDown={({ key }) => {
  15. if (key === "Enter") {
  16. onClick();
  17. }
  18. }}
  19. >
  20. {children ?? display ?? value}
  21. </div>
  22. );
  23. };
  24. export const Dropdown = ({ selected, open, onSelect, onClick, children }) => {
  25. const transitionRef = useRef(null);
  26. const [displayed, setDisplayed] = useState(null);
  27. const onSelectCallback = useCallback(
  28. (value, display) => {
  29. setDisplayed(display);
  30. onSelect(value);
  31. },
  32. [onSelect]
  33. );
  34. useEffect(() => {
  35. if (selected === undefined) {
  36. return;
  37. }
  38. let found = null;
  39. React.Children.toArray(children).forEach(element => {
  40. if (
  41. React.isValidElement(element) &&
  42. found === null &&
  43. element.props.value === selected
  44. ) {
  45. const { value, display } = element.props;
  46. found = display ?? value;
  47. }
  48. });
  49. setDisplayed(found);
  50. }, [children, selected]);
  51. return (
  52. <div className={styles.container}>
  53. <div
  54. className={styles.button}
  55. role="button"
  56. tabIndex="0"
  57. onClick={onClick}
  58. onKeyDown={({ key }) => {
  59. if (key === "Enter") {
  60. onClick();
  61. }
  62. }}
  63. >
  64. {displayed}
  65. </div>
  66. <CSSTransition
  67. nodeRef={transitionRef}
  68. in={open}
  69. timeout={200}
  70. mountOnEnter
  71. unmountOnExit
  72. classNames={{
  73. enter: styles["list-enter"],
  74. enterActive: styles["list-enter-active"],
  75. exit: styles["list-exit"],
  76. exitActive: styles["list-exit-active"],
  77. }}
  78. >
  79. <div className={styles.list} role="menu" ref={transitionRef}>
  80. {React.Children.toArray(children).map(child =>
  81. React.cloneElement(child, {
  82. onSelect: onSelectCallback,
  83. key: JSON.stringify(child.props.value),
  84. })
  85. )}
  86. </div>
  87. </CSSTransition>
  88. </div>
  89. );
  90. };
  91. export const CountryDropdown = ({
  92. countryLookup,
  93. selected,
  94. onSelect,
  95. onClick,
  96. open,
  97. }) => {
  98. const transitionRef = useRef(null);
  99. const [search, setSearch] = useState("");
  100. const found = countryLookup(search) ?? [];
  101. const onSelectCallback = useCallback(
  102. code => {
  103. setSearch("");
  104. onSelect(code);
  105. },
  106. [onSelect]
  107. );
  108. return (
  109. <div className={styles.container}>
  110. <div
  111. className={styles.button}
  112. role="button"
  113. tabIndex="0"
  114. onClick={onClick}
  115. onKeyDown={({ key }) => {
  116. if (key === "Enter") {
  117. onClick();
  118. }
  119. }}
  120. >
  121. {flagLookup(selected)}
  122. </div>
  123. <CSSTransition
  124. nodeRef={transitionRef}
  125. in={open}
  126. timeout={200}
  127. mountOnEnter
  128. unmountOnExit
  129. classNames={{
  130. enter: styles["list-enter"],
  131. enterActive: styles["list-enter-active"],
  132. exit: styles["list-exit"],
  133. exitActive: styles["list-exit-active"],
  134. }}
  135. >
  136. <div className={styles.list} role="menu" ref={transitionRef}>
  137. <input
  138. className={styles.search}
  139. autoFocus
  140. type="text"
  141. value={search}
  142. onChange={({ target }) => setSearch(target.value)}
  143. onKeyDown={({ key }) => {
  144. if (key === "Enter") {
  145. onSelectCallback(found?.[0]?.item?.alpha2);
  146. } else if (key === "Escape") {
  147. onSelectCallback(selected);
  148. }
  149. }}
  150. />
  151. {found.map(({ item: { country, alpha2 } }) => (
  152. <div
  153. role="button"
  154. tabIndex="0"
  155. key={alpha2}
  156. className={styles.item}
  157. onClick={() => onSelectCallback(alpha2)}
  158. onKeyDown={({ key }) => {
  159. if (key === "Enter") {
  160. onSelectCallback(alpha2);
  161. }
  162. }}
  163. >
  164. {flagLookup(alpha2)} - {country}
  165. </div>
  166. ))}
  167. <div
  168. className={styles.item}
  169. role="menuitem"
  170. tabIndex="0"
  171. onClick={() => onSelectCallback(null)}
  172. onKeyDown={({ key }) => {
  173. if (key === "Enter") {
  174. onSelectCallback(null);
  175. }
  176. }}
  177. >
  178. {flagLookup(null)} - All Countries
  179. </div>
  180. </div>
  181. </CSSTransition>
  182. </div>
  183. );
  184. };
  185. export const DropdownGroup = ({ children }) => {
  186. const [open, setOpen] = useState(null);
  187. return (
  188. <>
  189. {children.map(child =>
  190. React.cloneElement(child, {
  191. open: open === child.props.open,
  192. onClick: () =>
  193. setOpen(o => (o === child.props.open ? null : child.props.open)),
  194. onSelect: v => {
  195. child.props.onSelect(v);
  196. setOpen(null);
  197. },
  198. key: child.props.open,
  199. })
  200. )}
  201. </>
  202. );
  203. };