SearchModal.tsx 3.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119
  1. import React, {
  2. useState, useCallback, useEffect,
  3. } from 'react';
  4. import Downshift, { type DownshiftState, type StateChangeOptions } from 'downshift';
  5. import { useRouter } from 'next/router';
  6. import { Modal, ModalBody } from 'reactstrap';
  7. import type { DownshiftItem } from '../interfaces/downshift';
  8. import { useSearchModal } from '../stores/search';
  9. import { SearchForm } from './SearchForm';
  10. import { SearchHelp } from './SearchHelp';
  11. import { SearchMethodMenuItem } from './SearchMethodMenuItem';
  12. import { SearchResultMenuItem } from './SearchResultMenuItem';
  13. const SearchModal = (): JSX.Element => {
  14. const [searchKeyword, setSearchKeyword] = useState('');
  15. const { data: searchModalData, close: closeSearchModal } = useSearchModal();
  16. const router = useRouter();
  17. const changeSearchTextHandler = useCallback((searchText: string) => {
  18. setSearchKeyword(searchText);
  19. }, []);
  20. const selectSearchMenuItemHandler = useCallback((selectedItem: DownshiftItem) => {
  21. router.push(selectedItem.url);
  22. closeSearchModal();
  23. }, [closeSearchModal, router]);
  24. const submitHandler = useCallback(() => {
  25. router.push(`/_search?q=${searchKeyword}`);
  26. closeSearchModal();
  27. }, [closeSearchModal, router, searchKeyword]);
  28. const stateReducer = (state: DownshiftState<DownshiftItem>, changes: StateChangeOptions<DownshiftItem>) => {
  29. // Do not update highlightedIndex on mouse hover
  30. if (changes.type === Downshift.stateChangeTypes.itemMouseEnter) {
  31. return {
  32. ...changes,
  33. highlightedIndex: state.highlightedIndex,
  34. };
  35. }
  36. return changes;
  37. };
  38. useEffect(() => {
  39. if (!searchModalData?.isOpened) {
  40. return;
  41. }
  42. if (searchModalData?.searchKeyword == null) {
  43. setSearchKeyword('');
  44. }
  45. else {
  46. setSearchKeyword(searchModalData.searchKeyword);
  47. }
  48. }, [searchModalData?.isOpened, searchModalData?.searchKeyword]);
  49. return (
  50. <Modal size="lg" isOpen={searchModalData?.isOpened ?? false} toggle={closeSearchModal} data-testid="search-modal">
  51. <ModalBody className="pb-2">
  52. <Downshift
  53. onSelect={selectSearchMenuItemHandler}
  54. stateReducer={stateReducer}
  55. defaultIsOpen
  56. >
  57. {({
  58. getRootProps,
  59. getInputProps,
  60. getItemProps,
  61. getMenuProps,
  62. highlightedIndex,
  63. }) => (
  64. <div {...getRootProps({}, { suppressRefError: true })}>
  65. <div className="text-muted d-flex justify-content-center align-items-center p-1">
  66. <span className="material-symbols-outlined fs-4 me-3">search</span>
  67. <SearchForm
  68. searchKeyword={searchKeyword}
  69. onChange={changeSearchTextHandler}
  70. onSubmit={submitHandler}
  71. getInputProps={getInputProps}
  72. />
  73. <button
  74. type="button"
  75. className="btn border-0 d-flex justify-content-center p-0"
  76. onClick={closeSearchModal}
  77. >
  78. <span className="material-symbols-outlined fs-4 ms-3 py-0">close</span>
  79. </button>
  80. </div>
  81. <ul {...getMenuProps()} className="list-unstyled m-0">
  82. <div className="border-top mt-2 mb-2" />
  83. <SearchMethodMenuItem
  84. activeIndex={highlightedIndex}
  85. searchKeyword={searchKeyword}
  86. getItemProps={getItemProps}
  87. />
  88. <SearchResultMenuItem
  89. activeIndex={highlightedIndex}
  90. searchKeyword={searchKeyword}
  91. getItemProps={getItemProps}
  92. />
  93. <div className="border-top mt-2 mb-2" />
  94. </ul>
  95. </div>
  96. )}
  97. </Downshift>
  98. <SearchHelp />
  99. </ModalBody>
  100. </Modal>
  101. );
  102. };
  103. export default SearchModal;