Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Apply modal pattern to search box pop-up #1932

Merged
merged 8 commits into from
Jul 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 44 additions & 17 deletions src/pydata_sphinx_theme/assets/scripts/pydata-sphinx-theme.js
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,7 @@ var findSearchInput = () => {
} else {
// must be at least one persistent form, use the first persistent one
form = document.querySelector(
"div:not(.search-button__search-container) > form.bd-search",
":not(#pst-search-dialog) > form.bd-search",
);
}
return form.querySelector("input");
Expand All @@ -208,22 +208,30 @@ var findSearchInput = () => {
*/
var toggleSearchField = () => {
// Find the search input to highlight
let input = findSearchInput();
const input = findSearchInput();

// if the input field is the hidden one (the one associated with the
// search button) then toggle the button state (to show/hide the field)
let searchPopupWrapper = document.querySelector(".search-button__wrapper");
let hiddenInput = searchPopupWrapper.querySelector("input");
const searchDialog = document.getElementById("pst-search-dialog");
const hiddenInput = searchDialog.querySelector("input");
if (input === hiddenInput) {
searchPopupWrapper.classList.toggle("show");
}
// when toggling off the search field, remove its focus
if (document.activeElement === input) {
input.blur();
if (searchDialog.open) {
searchDialog.close();
} else {
// Note: browsers should focus the input field inside the modal dialog
// automatically when it is opened.
searchDialog.showModal();
}
} else {
input.focus();
input.select();
input.scrollIntoView({ block: "center" });
// if the input field is not the hidden one, then toggle its focus state
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On the left side, the logic in this else-branch was always run. That was because the old way of opening the search pop-up did not automatically focus the search input field. However, when the search input field is inside a dialog, then when you open the dialog, it is automatically focussed. So if you then run the code below, you get a buggy sequence of actions:

  1. Call to searchDialog.showModal() puts focus on search input field
  2. Call to input.blur() removes focus from search input field

So the solution is to put this toggle logic in an else-branch to handle the case when the search field is on already on the page (for example, the search results page, or a page with a persistent search bar).


if (document.activeElement === input) {
input.blur();
} else {
input.focus();
input.select();
input.scrollIntoView({ block: "center" });
}
}
};

Expand Down Expand Up @@ -295,11 +303,30 @@ var setupSearchButtons = () => {
btn.onclick = toggleSearchField;
});

// Add the search button overlay event callback
let overlay = document.querySelector(".search-button__overlay");
if (overlay) {
overlay.onclick = toggleSearchField;
}
// If user clicks outside the search modal dialog, then close it.
Copy link
Collaborator Author

@gabalafou gabalafou Jul 17, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The default behavior for modal dialogs is to stay on the screen even if you click on the dialog's backdrop.

To me the old behavior seemed natural though - i.e., if the user clicks the backdrop, we remove the pop-up search bar. So I restore that behavior here.

const searchDialog = document.getElementById("pst-search-dialog");
// Dialog click handler includes clicks on dialog ::backdrop.
searchDialog.addEventListener("click", (event) => {
if (!searchDialog.open) {
return;
}

// Dialog.getBoundingClientRect() does not include ::backdrop. (This is the
// trick that allows us to determine if click was inside or outside of the
// dialog: click handler includes backdrop, getBoundingClientRect does not.)
const { left, right, top, bottom } = searchDialog.getBoundingClientRect();

// 0, 0 means top left
const clickWasOutsideDialog =
event.clientX < left ||
right < event.clientX ||
event.clientY < top ||
bottom < event.clientY;

if (clickWasOutsideDialog) {
searchDialog.close();
}
});
Carreau marked this conversation as resolved.
Show resolved Hide resolved
};

/*******************************************************************************
Expand Down
87 changes: 42 additions & 45 deletions src/pydata_sphinx_theme/assets/styles/components/_search.scss
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,15 @@
color: var(--pst-color-text-muted);
}

// Hoist the focus ring from the input field to its parent
&:focus-within {
box-shadow: $focus-ring-box-shadow;

input:focus {
box-shadow: none;
}
}

.icon {
position: absolute;
color: var(--pst-color-border);
Expand All @@ -28,7 +37,11 @@
color: var(--pst-color-text-muted);
}

input {
input.form-control {
background-color: var(--pst-color-background);
color: var(--pst-color-text-base);
border: none;

// Inner-text of the search bar
&::placeholder {
color: var(--pst-color-text-muted);
Expand All @@ -39,46 +52,36 @@
&::-webkit-search-decoration {
appearance: none;
}

&:focus,
&:focus-visible {
color: var(--pst-color-text-muted);
}
}

// Shows off the keyboard shortcuts for the button
.search-button__kbd-shortcut {
display: flex;
position: absolute;
Copy link
Collaborator Author

@gabalafou gabalafou Jul 17, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think position: absolute was actually a bad idea because it allows the search terms in the input box to bleed into the span displaying the keyboard shortcut (think narrow screen rather than super long search string).

right: 0.5rem;
margin-inline-end: 0.5rem;
color: var(--pst-color-border);
}
}

.form-control {
Copy link
Collaborator Author

@gabalafou gabalafou Jul 17, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I merged these rules in with the input selector above. As far as I can tell, I searched the code base, and the only place I could find this class being used is on the input element in the search-field.html component.

background-color: var(--pst-color-background);
color: var(--pst-color-text-base);

&:focus,
&:focus-visible {
border: none;
background-color: var(--pst-color-background);
color: var(--pst-color-text-muted);
}
}

/**
* Search button - located in the navbar
*/

// Search link icon should be a bit bigger since it is separate from icon links
.search-button i {
// Search link icon should be a bit bigger since it is separate from icon links
font-size: 1.3rem;
}

// __search-container will only show up when we use the search pop-up bar
.search-button__search-container,
.search-button__overlay {
/**
* The search modal <dialog>
*/
#pst-search-dialog {
display: none;
}

.search-button__wrapper.show {
.search-button__search-container {
&[open] {
display: flex;

// Center in middle of screen just underneath header
Expand All @@ -91,30 +94,24 @@
margin-top: 0.5rem;
width: 90%;
max-width: 800px;
}
background-color: transparent;
padding: $focus-ring-width;
border: none;
Carreau marked this conversation as resolved.
Show resolved Hide resolved

.search-button__overlay {
display: flex;
position: fixed;
z-index: $zindex-modal-backdrop;
background-color: black;
opacity: 0.5;
width: 100%;
height: 100%;
top: 0;
left: 0;
}
&::backdrop {
background-color: black;
opacity: 0.5;
}

form.bd-search {
flex-grow: 1;
padding-top: 0;
padding-bottom: 0;
Comment on lines -110 to -111
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These padding rules are not needed. The form already has 0 top and bottom padding.

}
form.bd-search {
flex-grow: 1;

// Font and input text a bit bigger
svg,
input {
font-size: var(--pst-font-size-icon);
// Font and input text a bit bigger
svg,
input {
font-size: var(--pst-font-size-icon);
}
}
}
}

Expand All @@ -141,7 +138,7 @@
border-radius: $search-button-border-radius;
}

// The keyboard shotcut text
// The keyboard shortcut text
.search-button__default-text {
font-size: var(--bs-nav-link-font-size);
font-weight: var(--bs-nav-link-font-weight);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
<input type="search"
class="form-control"
name="q"
id="search-input"
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's misleading to put an id on this element because this template can currently be rendered on the same page more than once. This definitely happens in the PST docs, for example, on the search results page.

placeholder="{{ theme_search_bar_text }}"
aria-label="{{ theme_search_bar_text }}"
autocomplete="off"
Expand Down
7 changes: 3 additions & 4 deletions src/pydata_sphinx_theme/theme/pydata_sphinx_theme/layout.html
Original file line number Diff line number Diff line change
Expand Up @@ -63,10 +63,9 @@
id="pst-secondary-sidebar-checkbox"/>
<label class="overlay overlay-secondary" for="pst-secondary-sidebar-checkbox"></label>
{# A search field pop-up that will only show when the search button is clicked #}
<div class="search-button__wrapper">
<div class="search-button__overlay"></div>
<div class="search-button__search-container">{% include "../components/search-field.html" %}</div>
</div>
<dialog id="pst-search-dialog">
{% include "../components/search-field.html" %}
Carreau marked this conversation as resolved.
Show resolved Hide resolved
</dialog>

{% include "sections/announcement.html" %}

Expand Down
Loading