diff --git a/apps/frontend/static_src/images/search.svg b/apps/frontend/static_src/images/search.svg index 590333d5..6302a70a 100644 --- a/apps/frontend/static_src/images/search.svg +++ b/apps/frontend/static_src/images/search.svg @@ -1 +1 @@ - + diff --git a/apps/frontend/static_src/js/main.js b/apps/frontend/static_src/js/main.js index 6382c6bb..eaa2e9e2 100644 --- a/apps/frontend/static_src/js/main.js +++ b/apps/frontend/static_src/js/main.js @@ -11,6 +11,11 @@ handleFeedback(); const searchInput = document.querySelector('[data-search-input]'); const searchIconButton = document.querySelector('[data-search-icon-button]'); +const searchModal = document.getElementById('search-modal'); +const resultsDiv = document.querySelector('[data-results]'); +const resultsCountContainer = document.querySelector( + '[data-results-count-container]', +); const removeExistingChildren = (parent) => { // eslint-disable-next-line no-param-reassign @@ -18,11 +23,6 @@ const removeExistingChildren = (parent) => { }; const injectResultsInHTML = (results) => { - const resultsDiv = document.querySelector('[data-results]'); - const resultsCountContainer = document.querySelector( - '[data-results-count-container]', - ); - removeExistingChildren(resultsDiv); removeExistingChildren(resultsCountContainer); @@ -38,7 +38,7 @@ const injectResultsInHTML = (results) => { resultsCountHeading.classList.add('autocomplete__count'); resultsCountContainer.appendChild(resultsCountHeading); - results.forEach((result) => { + results.forEach((result, index) => { const resultDiv = document.createElement('a'); const resultHeading = document.createElement('h3'); const resultDescription = document.createElement('div'); @@ -55,11 +55,22 @@ const injectResultsInHTML = (results) => { resultDiv.classList.add('autocomplete__row'); resultHeading.classList.add('autocomplete__heading'); resultsDiv.appendChild(resultDiv); + + requestAnimationFrame(() => { + setTimeout(() => { + resultDiv.classList.add('is-visible'); + }, index *400); + }); }); }; const onSearchInputChange = async (event) => { const query = event.target.value; + + document.querySelector('.search__container').classList.add('is-loading'); + + const minDelay = new Promise(resolve => setTimeout(resolve, 200)); + try { const res = await fetch( `${window.location.origin}${ @@ -75,16 +86,19 @@ const onSearchInputChange = async (event) => { console.log(err); // eslint-disable-next-line no-alert window.alert(`Error: ${err}`); + } finally { + await minDelay; // wait for 300ms to pass if fetch was faster + document.querySelector('.search__container').classList.remove('is-loading'); } }; searchInput.addEventListener('keyup', debounce(onSearchInputChange, 150)); -searchIconButton.addEventListener('click', () => { - const resultsDiv = document.querySelector('[data-results]'); - const resultsCountContainer = document.querySelector( - '[data-results-count-container]', - ); +searchModal.addEventListener('shown.bs.modal', () => { + searchInput.focus(); +}); +searchModal.addEventListener('hidden.bs.modal', () => { + searchInput.value = ''; removeExistingChildren(resultsDiv); removeExistingChildren(resultsCountContainer); }); diff --git a/apps/frontend/static_src/scss/components/autocomplete.scss b/apps/frontend/static_src/scss/components/autocomplete.scss index b076d096..25b250a3 100644 --- a/apps/frontend/static_src/scss/components/autocomplete.scss +++ b/apps/frontend/static_src/scss/components/autocomplete.scss @@ -5,14 +5,11 @@ &__count { @include fs(s); - position: absolute; - top: $gutter; - inset-inline-end: 0; + text-align: right; color: $meta-color; font-weight: 500; padding: 0 $row-spacing; margin-bottom: $gutter; - margin-top: $gutter; } &__row { @@ -20,7 +17,14 @@ padding: $gutter $row-spacing; border-top: 1px solid light-dark($color--light-grey, $color--grey); text-decoration: none; - transition: background-color $transition; + transition: background-color $transition, opacity 0.25s ease-out, transform 0.25s ease-out; + opacity: 0; + transform: translateY(8px); + + &.is-visible { + opacity: 1; + transform: translateY(0); + } &:hover, &:focus { diff --git a/apps/frontend/static_src/scss/components/search.scss b/apps/frontend/static_src/scss/components/search.scss index ed01a8b4..1c5b4116 100644 --- a/apps/frontend/static_src/scss/components/search.scss +++ b/apps/frontend/static_src/scss/components/search.scss @@ -5,6 +5,32 @@ display: flex; flex-direction: row; justify-content: stretch; + position: relative; + } + + &__container::after { + content: ''; + display: block; + position: absolute; + inset-inline-start:($gutter * 1.6); + inset-block-start: 50%; + transform: translateY(-50%); + width: 21px; + height: 21px; + background-image: url('../images/search.svg'); + background-size: contain; + background-repeat: no-repeat; + background-position: center; + pointer-events: none; + filter: light-dark(none, brightness(0) invert(1)); + } + + &__container.is-loading::after { + background-image: none; + border: 2px solid #ccc; + border-top-color: #333; + border-radius: 50%; + animation: spin 1s linear infinite; } &__input { @@ -12,6 +38,7 @@ border: 1px solid $color--black; width: 100%; padding: $gutter; + padding-inline-start: ($gutter * 2); border-radius: $border-radius--m; } @@ -20,6 +47,10 @@ } } +@keyframes spin { + to { transform: translateY(-50%) rotate(360deg); } +} + .search .modal-content { background-color: light-dark($color--white, $color--off-black); color: light-dark($color--off-black, $color--light-grey);