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

Use standard dropdowns for combobox #9462

Merged
merged 8 commits into from
Oct 21, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
5 changes: 0 additions & 5 deletions core/src/main/resources/lib/form/combobox.jelly
Original file line number Diff line number Diff line change
Expand Up @@ -70,11 +70,6 @@ THE SOFTWARE.
</st:documentation>
<f:prepareDatabinding/>
${descriptor.calcFillSettings(field,attrs)} <!-- this figures out the 'fillUrl' and 'fillDependsOn' attribute -->
<st:once>
<script type="text/javascript" src="${request.contextPath}/scripts/utilities.js"/>
<script type="text/javascript" src="${request.contextPath}/scripts/combobox.js"/>
</st:once>
<st:adjunct includes="lib.form.combobox.combobox"/>

<!-- mostly pass-through all the attributes -->
<j:set var="name" value="${attrs.name ?: '_.'+attrs.field}"/>
Expand Down
2 changes: 2 additions & 0 deletions war/src/main/js/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import Tooltips from "@/components/tooltips";
import StopButtonLink from "@/components/stop-button-link";
import ConfirmationLink from "@/components/confirmation-link";
import Dialogs from "@/components/dialogs";
import ComboBox from "@/components/combo-box";

Dropdowns.init();
Notifications.init();
Expand All @@ -13,3 +14,4 @@ Tooltips.init();
StopButtonLink.init();
ConfirmationLink.init();
Dialogs.init();
ComboBox.init();
97 changes: 97 additions & 0 deletions war/src/main/js/components/combo-box/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import behaviorShim from "@/util/behavior-shim";
import Utils from "@/components/dropdowns/utils";

function init() {
function convertSuggestionToItem(suggestion, e) {
const confirm = () => {
e.value = suggestion.name;
e.focus();
timja marked this conversation as resolved.
Show resolved Hide resolved
};
return {
label: suggestion.name,
onClick: confirm,
onKeyPress: (evt) => {
if (evt.key === "Tab") {
confirm();
e.dropdown.hide();
evt.preventDefault();
}
},
};
}

function createAndShowDropdown(e, div, suggestions) {
const items = suggestions
.splice(0, 20)
.map((s) => convertSuggestionToItem(s, e));
if (!e.dropdown) {
Utils.generateDropdown(
div,
(instance) => {
e.dropdown = instance;
},
true,
);
}
e.dropdown.setContent(Utils.generateDropdownItems(items, true));
e.dropdown.show();
}

function updateSuggestions(e, div, items) {
const text = e.value.trim();

let filteredItems = text
? items.filter((item) => item.indexOf(text) === 0)
: items;

const suggestions = filteredItems
.filter((item) => item.indexOf(text) === 0)
.map((item) => {
return { name: item };
});
createAndShowDropdown(e, div, suggestions || []);
}

function debounce(callback) {
callback.running = false;
return () => {
if (!callback.running) {
callback.running = true;
setTimeout(() => {
callback();
callback.running = false;
}, 300);
}
};
}

behaviorShim.specify("INPUT.combobox2", "combobox", 100, function (e) {
// form field with auto-completion support
// insert the auto-completion container
const div = document.createElement("DIV");
e.parentNode.insertBefore(div, e.nextElementSibling);
e.style.position = "relative";

const url = e.getAttribute("fillUrl");
fetch(url)
.then((rsp) => (rsp.ok ? rsp.json() : {}))
.then((items) => {
e.addEventListener("focus", () => updateSuggestions(e, div, items));

// otherwise menu won't hide on tab with nothing selected
// needs delay as without that it blocks click selection of an item
e.addEventListener("focusout", () =>
setTimeout(() => e.dropdown.hide(), 200),
);

e.addEventListener(
"input",
debounce(() => {
updateSuggestions(e, div, items);
}),
);
});
});
}

export default { init };
4 changes: 3 additions & 1 deletion war/src/main/js/components/dropdowns/templates.js
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,9 @@ function menuItem(options) {
if (options.onClick) {
item.addEventListener("click", (event) => options.onClick(event));
}

if (options.onKeyPress) {
item.onkeypress = options.onKeyPress;
}
return item;
}

Expand Down
21 changes: 17 additions & 4 deletions war/src/main/js/components/dropdowns/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,12 @@ const SELECTED_ITEM_CLASS = "jenkins-dropdown__item--selected";
* @param element - the element to generate the dropdown for
* @param callback - called to retrieve the list of dropdown items
*/
function generateDropdown(element, callback) {
function generateDropdown(element, callback, immediate) {
tippy(
element,
Object.assign({}, Templates.dropdown(), {
onCreate(instance) {
instance.reference.addEventListener("mouseenter", () => {
const onload = () => {
if (instance.loaded) {
return;
}
Expand All @@ -26,7 +26,12 @@ function generateDropdown(element, callback) {
});

callback(instance);
});
};
if (immediate) {
onload();
} else {
instance.reference.addEventListener("mouseenter", onload);
}
},
}),
);
Expand Down Expand Up @@ -86,7 +91,11 @@ function generateDropdownItems(items, compact) {
menuItems,
() => menuItems.querySelectorAll(".jenkins-dropdown__item"),
SELECTED_ITEM_CLASS,
(selectedItem, key) => {
(selectedItem, key, evt) => {
if (!selectedItem) {
return;
}
Copy link
Member Author

Choose a reason for hiding this comment

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

occurs when navigating between potential text left and right without selecting an item (doesn't cause an issue just stacktraces in the console)

e.g. type North, then navigate left on the keyboard


switch (key) {
case "ArrowLeft": {
const root = selectedItem.closest("[data-tippy-root]");
Expand All @@ -110,6 +119,10 @@ function generateDropdownItems(items, compact) {
.classList.add(SELECTED_ITEM_CLASS);
break;
}
default:
if (selectedItem && selectedItem.onkeypress) {
selectedItem.onkeypress(evt);
}
}
},
(container) => {
Expand Down
2 changes: 1 addition & 1 deletion war/src/main/js/util/keyboard.js
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ export default function makeKeyboardNavigable(
selectedItem.click();
}
} else {
additionalBehaviours(selectedItem, e.key);
additionalBehaviours(selectedItem, e.key, e);
}
}
});
Expand Down
Loading