From e9540d6883e0eb8ccc03b07ab9cd5addc6b67329 Mon Sep 17 00:00:00 2001 From: "Ahmad K. Bawaneh" Date: Thu, 30 Nov 2023 11:48:48 +0300 Subject: [PATCH] fix #886 Select component improvements --- .../domino/ui/elements/InputElement.java | 16 +++ .../domino/ui/forms/FormsStyles.java | 2 + .../ui/forms/suggest/AbstractSelect.java | 71 ++++++++++- .../domino/ui/keyboard/AcceptKeyEvents.java | 36 +++++- .../ui/keyboard/KeyEventHandlerContext.java | 23 +++- .../ui/keyboard/KeyboardKeyListener.java | 47 ++++++- .../domino/ui/menu/AbstractMenuItem.java | 26 ++++ .../domino/ui/menu/CustomMenuItem.java | 8 +- .../org/dominokit/domino/ui/menu/Menu.java | 115 +++++++++++++++--- .../dominokit/domino/ui/menu/MenuItem.java | 22 +++- .../dominokit/domino/ui/menu/MenuStyles.java | 1 + .../dominokit/domino/ui/search/SearchBox.java | 9 +- .../domino/ui/utils/DelayedTextInput.java | 15 ++- .../domino/ui/utils/KeyboardNavigation.java | 53 +++++++- .../dui-components/domino-ui-forms.css | 23 ++++ .../dui-components/domino-ui-menu.css | 25 ++-- 16 files changed, 444 insertions(+), 48 deletions(-) diff --git a/domino-ui/src/main/java/org/dominokit/domino/ui/elements/InputElement.java b/domino-ui/src/main/java/org/dominokit/domino/ui/elements/InputElement.java index 27f94ea9f..400b5dce5 100644 --- a/domino-ui/src/main/java/org/dominokit/domino/ui/elements/InputElement.java +++ b/domino-ui/src/main/java/org/dominokit/domino/ui/elements/InputElement.java @@ -86,4 +86,20 @@ public InputElement setName(String name) { element.element().name = name; return this; } + + /** @return The String value of the input element */ + public String getValue() { + return element.element().value; + } + + /** + * Set the value for this input element. + * + * @param value String value + * @return Same InputElement instance + */ + public InputElement setValue(String value) { + element.element().value = value; + return this; + } } diff --git a/domino-ui/src/main/java/org/dominokit/domino/ui/forms/FormsStyles.java b/domino-ui/src/main/java/org/dominokit/domino/ui/forms/FormsStyles.java index 5a95964cb..876c06102 100644 --- a/domino-ui/src/main/java/org/dominokit/domino/ui/forms/FormsStyles.java +++ b/domino-ui/src/main/java/org/dominokit/domino/ui/forms/FormsStyles.java @@ -83,6 +83,8 @@ public interface FormsStyles { /** CSS class for a hidden input element within a form field. */ CssClass dui_hidden_input = () -> "dui-field-input-hidden"; + CssClass dui_auto_type_input = () -> "dui-auto-type-input"; + /** CSS class for a form switch component. */ CssClass dui_switch = () -> "dui-form-switch"; diff --git a/domino-ui/src/main/java/org/dominokit/domino/ui/forms/suggest/AbstractSelect.java b/domino-ui/src/main/java/org/dominokit/domino/ui/forms/suggest/AbstractSelect.java index a26452065..9ed020c2a 100644 --- a/domino-ui/src/main/java/org/dominokit/domino/ui/forms/suggest/AbstractSelect.java +++ b/domino-ui/src/main/java/org/dominokit/domino/ui/forms/suggest/AbstractSelect.java @@ -20,8 +20,10 @@ import static org.dominokit.domino.ui.utils.Domino.*; import elemental2.dom.DomGlobal; +import elemental2.dom.Event; import elemental2.dom.HTMLElement; import elemental2.dom.HTMLInputElement; +import elemental2.dom.KeyboardEvent; import java.util.*; import java.util.function.Consumer; import java.util.function.Function; @@ -29,6 +31,7 @@ import jsinterop.base.Js; import org.dominokit.domino.ui.IsElement; import org.dominokit.domino.ui.elements.DivElement; +import org.dominokit.domino.ui.elements.InputElement; import org.dominokit.domino.ui.elements.SpanElement; import org.dominokit.domino.ui.forms.AbstractFormElement; import org.dominokit.domino.ui.forms.AutoValidator; @@ -75,7 +78,8 @@ public abstract class AbstractSelect< protected Menu optionsMenu; protected DivElement fieldInput; private SpanElement placeHolderElement; - private DominoElement inputElement; + private InputElement inputElement; + private InputElement typingElement; /** * Default constructor which initializes the underlying structures, sets up event listeners, and @@ -85,12 +89,58 @@ public AbstractSelect() { placeHolderElement = span(); addCss(dui_form_select); wrapperElement + .addCss(dui_relative) .appendChild( fieldInput = div() .addCss(dui_field_input) .appendChild(placeHolderElement.addCss(dui_field_placeholder))) - .appendChild(inputElement = input(getType()).addCss(dui_hidden_input).toDominoElement()); + .appendChild(inputElement = input(getType()).addCss(dui_hidden_input)) + .appendChild( + typingElement = + input("text") + .addCss(dui_auto_type_input, dui_hidden) + .setTabIndex(-1) + .onKeyPress(keyEvents -> keyEvents.alphanumeric(Event::stopPropagation))); + + DelayedTextInput.create(typingElement, 1000) + .setDelayedAction( + () -> { + optionsMenu + .findOptionStarsWith(typingElement.getValue()) + .flatMap(OptionMeta::get) + .ifPresent( + meta -> onOptionSelected((O) meta.getOption(), isChangeListenersPaused())); + optionsMenu.focusFirstMatch(typingElement.getValue()); + typingElement.setValue(null).addCss(dui_hidden); + focus(); + }) + .setOnEnterAction( + () -> { + openOptionMenu(false); + String token = typingElement.getValue(); + typingElement.setValue(null).addCss(dui_hidden); + DomGlobal.setTimeout(p0 -> optionsMenu.focusFirstMatch(token), 0); + }); + + onKeyPress( + keyEvents -> { + keyEvents.alphanumeric( + evt -> { + KeyboardEvent keyboardEvent = Js.uncheckedCast(evt); + keyboardEvent.stopPropagation(); + keyboardEvent.preventDefault(); + String key = keyboardEvent.key; + if (nonNull(key) + && !optionsMenu.isOpened() + && (isNull(typingElement.getValue()) || typingElement.getValue().isEmpty())) { + typingElement.removeCss(dui_hidden); + typingElement.element().value = key; + typingElement.element().focus(); + } + }); + }); + labelForId(inputElement.getDominoId()); optionsMenu = @@ -123,7 +173,8 @@ public AbstractSelect() { getInputElement() .onKeyDown( keyEvents -> - keyEvents.onEnter(evt -> openOptionMenu()).onSpace(evt -> openOptionMenu())); + keyEvents.onEnter(evt -> openOptionMenu()).onSpace(evt -> openOptionMenu())) + .onKeyUp(keyEvents -> keyEvents.onArrowDown(evt -> openOptionMenu())); appendChild( PrimaryAddOn.of( @@ -163,13 +214,23 @@ public AbstractSelect() { * disabled. */ private void openOptionMenu() { + openOptionMenu(true); + } + + /** + * Opens the options menu allowing user to select an option, unless the select is read-only or + * disabled. + * + * @param focus a flag to decide if the menu should be focused on first element or not. + */ + private void openOptionMenu(boolean focus) { if (isReadOnly() || isDisabled()) { return; } if (optionsMenu.isOpened() && !optionsMenu.isContextMenu()) { optionsMenu.close(); } else { - optionsMenu.open(true); + optionsMenu.open(focus); } } @@ -439,7 +500,7 @@ public C setPlaceholder(String placeholder) { */ @Override public DominoElement getInputElement() { - return inputElement; + return inputElement.toDominoElement(); } /** diff --git a/domino-ui/src/main/java/org/dominokit/domino/ui/keyboard/AcceptKeyEvents.java b/domino-ui/src/main/java/org/dominokit/domino/ui/keyboard/AcceptKeyEvents.java index 2634c1eec..1163e04fa 100644 --- a/domino-ui/src/main/java/org/dominokit/domino/ui/keyboard/AcceptKeyEvents.java +++ b/domino-ui/src/main/java/org/dominokit/domino/ui/keyboard/AcceptKeyEvents.java @@ -15,9 +15,9 @@ */ package org.dominokit.domino.ui.keyboard; -import static org.dominokit.domino.ui.utils.Domino.*; - import elemental2.dom.EventListener; +import elemental2.dom.KeyboardEvent; +import java.util.function.Predicate; /** * The {@code AcceptKeyEvents} interface defines methods for handling keyboard events. @@ -246,7 +246,7 @@ public interface AcceptKeyEvents { AcceptKeyEvents on(String key, EventListener handler); /** - * Registers an event listener be called when ctrl + any key is pressed with options. + * Registers an event listener be called when any key is pressed with options. * * @param options The {@link org.dominokit.domino.ui.keyboard.KeyboardEventOptions}. * @param handler The {@link elemental2.dom.EventListener} to call when the event occurs. @@ -254,6 +254,36 @@ public interface AcceptKeyEvents { */ AcceptKeyEvents any(KeyboardEventOptions options, EventListener handler); + /** + * Registers an event listener be called when any alphanumeric key is pressed with options. + * + * @param options The {@link org.dominokit.domino.ui.keyboard.KeyboardEventOptions}. + * @param handler The {@link elemental2.dom.EventListener} to call when the event occurs. + * @return The same instance of {@code AcceptKeyEvents}. + */ + AcceptKeyEvents alphanumeric(KeyboardEventOptions options, EventListener handler); + + /** + * Registers an event listener be called when any alphanumeric key is pressed with default + * options. + * + * @param handler The {@link elemental2.dom.EventListener} to call when the event occurs. + * @return The same instance of {@code AcceptKeyEvents}. + */ + AcceptKeyEvents alphanumeric(EventListener handler); + + /** + * Registers an event listener be called when any key is pressed with options if the predicate + * condition is matched. + * + * @param options The {@link org.dominokit.domino.ui.keyboard.KeyboardEventOptions}. + * @param handler The {@link elemental2.dom.EventListener} to call when the event occurs. + * @param predicate A predicate to be executed to decide if the handler will be triggered or not. + * @return The same instance of {@code AcceptKeyEvents}. + */ + AcceptKeyEvents any( + KeyboardEventOptions options, EventListener handler, Predicate predicate); + /** * Registers an event listener be called when ctrl + any key is pressed. * diff --git a/domino-ui/src/main/java/org/dominokit/domino/ui/keyboard/KeyEventHandlerContext.java b/domino-ui/src/main/java/org/dominokit/domino/ui/keyboard/KeyEventHandlerContext.java index 3d8a3acab..1deaaedbc 100644 --- a/domino-ui/src/main/java/org/dominokit/domino/ui/keyboard/KeyEventHandlerContext.java +++ b/domino-ui/src/main/java/org/dominokit/domino/ui/keyboard/KeyEventHandlerContext.java @@ -15,9 +15,9 @@ */ package org.dominokit.domino.ui.keyboard; -import static org.dominokit.domino.ui.utils.Domino.*; - import elemental2.dom.EventListener; +import elemental2.dom.KeyboardEvent; +import java.util.function.Predicate; import java.util.function.Supplier; /** @@ -37,6 +37,8 @@ class KeyEventHandlerContext { /** The supplier for {@link KeyboardEventOptions} associated with this context. */ final Supplier options; + final Predicate predicate; + /** * Constructs a new {@code KeyEventHandlerContext} with the specified event listener handler and * options supplier. @@ -47,5 +49,22 @@ class KeyEventHandlerContext { public KeyEventHandlerContext(EventListener handler, Supplier options) { this.handler = handler; this.options = options; + this.predicate = keyboardEvent -> true; + } + + /** + * Constructs a new {@code KeyEventHandlerContext} with the specified event listener handler and + * options supplier. + * + * @param handler The event listener handler. + * @param options The supplier for {@link KeyboardEventOptions}. + */ + public KeyEventHandlerContext( + EventListener handler, + Supplier options, + Predicate predicate) { + this.handler = handler; + this.options = options; + this.predicate = predicate; } } diff --git a/domino-ui/src/main/java/org/dominokit/domino/ui/keyboard/KeyboardKeyListener.java b/domino-ui/src/main/java/org/dominokit/domino/ui/keyboard/KeyboardKeyListener.java index 0c70e53ac..4d38f9e02 100644 --- a/domino-ui/src/main/java/org/dominokit/domino/ui/keyboard/KeyboardKeyListener.java +++ b/domino-ui/src/main/java/org/dominokit/domino/ui/keyboard/KeyboardKeyListener.java @@ -15,8 +15,9 @@ */ package org.dominokit.domino.ui.keyboard; -import static org.dominokit.domino.ui.utils.Domino.*; +import static java.util.Objects.nonNull; +import elemental2.core.JsRegExp; import elemental2.dom.Event; import elemental2.dom.EventListener; import elemental2.dom.KeyboardEvent; @@ -24,6 +25,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.function.Predicate; import java.util.function.Supplier; import jsinterop.base.Js; import org.dominokit.domino.ui.events.HasDefaultEventOptions; @@ -106,7 +108,8 @@ private void callHandlers(List keyEventHandlerContexts, keyEventHandlerContexts.stream() .filter( context -> - context.options.get().withCtrlKey == keyboardEvent.ctrlKey + context.predicate.test(keyboardEvent) + && context.options.get().withCtrlKey == keyboardEvent.ctrlKey && context.options.get().withAltKey == keyboardEvent.altKey && context.options.get().withShiftKey == keyboardEvent.shiftKey && context.options.get().withMetaKey == keyboardEvent.metaKey @@ -273,6 +276,39 @@ public AcceptKeyEvents any(KeyboardEventOptions options, EventListener handler) return addGlobalHandler(contextOf(handler, () -> options)); } + /** {@inheritDoc} */ + @Override + public AcceptKeyEvents alphanumeric(KeyboardEventOptions options, EventListener handler) { + return any( + options, + handler, + keyboardEvent -> + nonNull(keyboardEvent.key) + && new JsRegExp( + "^[a-zA-Z0-9\\u0600-\\u06FF\\u0660-\\u0669\\u06F0-\\u06F9 _.-]{1}$", "g") + .test(keyboardEvent.key)); + } + + /** {@inheritDoc} */ + @Override + public AcceptKeyEvents alphanumeric(EventListener handler) { + return any( + hasDefaultEventOptions.getOptions(), + handler, + keyboardEvent -> + nonNull(keyboardEvent.key) + && new JsRegExp( + "^[a-zA-Z0-9\\u0600-\\u06FF\\u0660-\\u0669\\u06F0-\\u06F9 _.-]{1}$", "g") + .test(keyboardEvent.key)); + } + + /** {@inheritDoc} */ + @Override + public AcceptKeyEvents any( + KeyboardEventOptions options, EventListener handler, Predicate predicate) { + return addGlobalHandler(contextOf(handler, () -> options, predicate)); + } + /** {@inheritDoc} */ @Override public AcceptKeyEvents any(EventListener handler) { @@ -297,6 +333,13 @@ private KeyEventHandlerContext contextOf( return new KeyEventHandlerContext(handler, options); } + private KeyEventHandlerContext contextOf( + EventListener handler, + Supplier options, + Predicate predicate) { + return new KeyEventHandlerContext(handler, options, predicate); + } + /** {@inheritDoc} */ @Override public AcceptKeyEvents clearAll() { diff --git a/domino-ui/src/main/java/org/dominokit/domino/ui/menu/AbstractMenuItem.java b/domino-ui/src/main/java/org/dominokit/domino/ui/menu/AbstractMenuItem.java index 81970669e..b6b40a267 100644 --- a/domino-ui/src/main/java/org/dominokit/domino/ui/menu/AbstractMenuItem.java +++ b/domino-ui/src/main/java/org/dominokit/domino/ui/menu/AbstractMenuItem.java @@ -79,6 +79,8 @@ public class AbstractMenuItem extends BaseDominoElement false; + /** Default constructor to create a menu item. */ public AbstractMenuItem() { root = li().addCss(dui_menu_item); @@ -580,6 +582,30 @@ public > T appendChild(PrefixAddOn prefixAddOn) return (T) this; } + /** + * Retrieves the current {@link MenuSearchFilter} used for search operations. + * + * @return the current {@link MenuSearchFilter} + */ + public MenuSearchFilter getSearchFilter() { + return searchFilter; + } + + /** + * Sets the {@link MenuSearchFilter} to be used during search operations. + * + * @param searchFilter the search filter to set + * @return this Menu item instance for chaining + */ + public > T setSearchFilter(MenuSearchFilter searchFilter) { + this.searchFilter = searchFilter; + return (T) this; + } + + public boolean startsWith(String character) { + return false; + } + /** * Returns the underlying DOM element. * diff --git a/domino-ui/src/main/java/org/dominokit/domino/ui/menu/CustomMenuItem.java b/domino-ui/src/main/java/org/dominokit/domino/ui/menu/CustomMenuItem.java index 33e393c7c..e624643ee 100644 --- a/domino-ui/src/main/java/org/dominokit/domino/ui/menu/CustomMenuItem.java +++ b/domino-ui/src/main/java/org/dominokit/domino/ui/menu/CustomMenuItem.java @@ -33,8 +33,6 @@ */ public class CustomMenuItem extends AbstractMenuItem { - private MenuSearchFilter searchFilter = (token, caseSensitive) -> false; - /** * Creates a new instance of {@link CustomMenuItem}. * @@ -44,12 +42,16 @@ public static CustomMenuItem create() { return new CustomMenuItem<>(); } + public CustomMenuItem() { + this.searchFilter = (token, caseSensitive) -> false; + } + /** * Invoked during a search operation. Displays the menu item if the token is found using the * provided {@link MenuSearchFilter}. * * @param token the search token - * @param caseSensitive indicates if the search should be case sensitive or not + * @param caseSensitive indicates if the search should be case-sensitive or not * @return true if the token matches; false otherwise */ @Override diff --git a/domino-ui/src/main/java/org/dominokit/domino/ui/menu/Menu.java b/domino-ui/src/main/java/org/dominokit/domino/ui/menu/Menu.java index da0e03ade..e6930ed7d 100644 --- a/domino-ui/src/main/java/org/dominokit/domino/ui/menu/Menu.java +++ b/domino-ui/src/main/java/org/dominokit/domino/ui/menu/Menu.java @@ -76,6 +76,7 @@ public class Menu extends BaseDominoElement> private final LazyChild menuSubHeader; private final UListElement menuItemsList; private final DivElement menuBody; + private final LazyChild menuFooter; private final LazyChild createMissingElement; private final LazyChild backIcon; private LazyChild noResultElement; @@ -183,13 +184,31 @@ public Menu() { addClickListener(evt -> evt.stopPropagation()); + onKeyDown( + keyEvents -> { + keyEvents.alphanumeric( + evt -> { + KeyboardEvent keyboardEvent = Js.uncheckedCast(evt); + focusFirstMatch(keyboardEvent.key); + }); + }); + + menuSubHeader = LazyChild.of(div().addCss(dui_menu_sub_header), menuElement); + + menuItemsList = ul().addCss(dui_menu_items_list); + noResultElement = LazyChild.of(li().addCss(dui_menu_no_results, dui_order_last), menuItemsList); + menuBody = div().addCss(dui_menu_body); + menuElement.appendChild(menuBody.appendChild(menuItemsList)); + + menuFooter = LazyChild.of(div().addCss(dui_menu_footer), menuBody); + createMissingElement = LazyChild.of( a("#") .setAttribute("tabindex", "0") .setAttribute("aria-expanded", "true") .addCss(dui_menu_create_missing), - menuSearchContainer); + menuFooter); createMissingElement.whenInitialized( () -> { createMissingElement @@ -207,10 +226,28 @@ public Menu() { .onTab(evt -> keyboardNavigation.focusTopFocusableItem()) .onArrowDown( evt -> { - keyboardNavigation.focusTopFocusableItem(); + evt.stopPropagation(); + evt.preventDefault(); + if (isSearchable()) { + this.searchBox + .get() + .getTextBox() + .getInputElement() + .element() + .focus(); + } else { + keyboardNavigation.focusTopFocusableItem(); + } + }) + .onArrowUp( + evt -> { + evt.stopPropagation(); + evt.preventDefault(); + keyboardNavigation.focusBottomFocusableItem(); }); }); }); + searchBox.whenInitialized( () -> { searchBox.element().addSearchListener(this::onSearch); @@ -223,27 +260,38 @@ public Menu() { keyEvents .onArrowDown( evt -> { - String searchToken = searchBox.element().getTextBox().getValue(); - boolean tokenPresent = - nonNull(searchToken) && !searchToken.trim().isEmpty(); + evt.stopPropagation(); + evt.preventDefault(); + Optional> topFocusableItem = + keyboardNavigation.getTopFocusableItem(); + if (topFocusableItem.isPresent()) { + keyboardNavigation.focusTopFocusableItem(); + } else { + if (isAllowCreateMissing() + && createMissingElement.element().isAttached()) { + createMissingElement.get().element().focus(); + } + } + }) + .onArrowUp( + evt -> { + evt.stopPropagation(); + evt.preventDefault(); if (isAllowCreateMissing() - && createMissingElement.element().isAttached() - && tokenPresent) { + && createMissingElement.element().isAttached()) { createMissingElement.get().element().focus(); } else { - keyboardNavigation.focusTopFocusableItem(); + keyboardNavigation.focusBottomFocusableItem(); } }) - .onEscape(evt -> close())); + .onEscape(evt -> close()) + .onEnter( + evt -> + keyboardNavigation + .getTopFocusableItem() + .ifPresent(AbstractMenuItem::select))); }); - menuSubHeader = LazyChild.of(div().addCss(dui_menu_sub_header), menuElement); - - menuItemsList = ul().addCss(dui_menu_items_list); - noResultElement = LazyChild.of(li().addCss(dui_menu_no_results, dui_order_last), menuItemsList); - menuBody = div().addCss(dui_menu_body); - menuElement.appendChild(menuBody.appendChild(menuItemsList)); - keyboardNavigation = KeyboardNavigation.create(menuItems) .setTabOptions(new KeyboardNavigation.EventOptions(false, true)) @@ -280,7 +328,29 @@ public Menu() { item.focus(); } }) - .onEscape(this::close); + .onEscape(this::close) + .setOnEndReached( + navigation -> { + if (isAllowCreateMissing() && createMissingElement.element().isAttached()) { + createMissingElement.get().element().focus(); + } else if (isSearchable()) { + this.searchBox.get().getTextBox().getInputElement().element().focus(); + } else { + navigation.focusTopFocusableItem(); + } + }) + .setOnStartReached( + navigation -> { + if (isSearchable()) { + this.searchBox.get().getTextBox().getInputElement().element().focus(); + } else if (isAllowCreateMissing() + && createMissingElement.element().isAttached()) { + createMissingElement.get().element().focus(); + } else { + navigation.focusBottomFocusableItem(); + } + }); + ; element.addEventListener("keydown", keyboardNavigation); @@ -333,6 +403,17 @@ public Menu() { }; } + public void focusFirstMatch(String token) { + findOptionStarsWith(token).ifPresent(AbstractMenuItem::focus); + } + + public Optional> findOptionStarsWith(String token) { + return this.menuItems.stream() + .filter(menuItem -> !menuItem.isGrouped()) + .filter(dropDownItem -> dropDownItem.startsWith(token)) + .findFirst(); + } + /** * Handles the behavior when an expected menu item is missing. * diff --git a/domino-ui/src/main/java/org/dominokit/domino/ui/menu/MenuItem.java b/domino-ui/src/main/java/org/dominokit/domino/ui/menu/MenuItem.java index f1783ca61..41734ffd2 100644 --- a/domino-ui/src/main/java/org/dominokit/domino/ui/menu/MenuItem.java +++ b/domino-ui/src/main/java/org/dominokit/domino/ui/menu/MenuItem.java @@ -67,6 +67,8 @@ public MenuItem(String text) { textElement = span().addCss(dui_menu_item_content).setTextContent(text); appendChild(textElement); } + + this.searchFilter = this::containsToken; } /** @@ -84,6 +86,20 @@ public MenuItem(String text, String description) { } } + @Override + public boolean startsWith(String character) { + String textContent = + Arrays.asList(Optional.ofNullable(textElement), Optional.ofNullable(descriptionElement)) + .stream() + .filter(Optional::isPresent) + .map(element -> element.get().getTextContent()) + .collect(Collectors.joining(" ")); + if (isNull(textContent) || textContent.isEmpty()) { + return false; + } + return textContent.toLowerCase().startsWith(character.toLowerCase()); + } + /** * Retrieves the description element of the menu item. * @@ -111,11 +127,15 @@ public SpanElement getTextElement() { */ @Override public boolean onSearch(String token, boolean caseSensitive) { + return onSearch(token, caseSensitive, getSearchFilter()); + } + + private boolean onSearch(String token, boolean caseSensitive, MenuSearchFilter searchFilter) { if (isNull(token) || token.isEmpty()) { this.show(); return true; } - if (searchable && containsToken(token, caseSensitive)) { + if (searchable && searchFilter.onSearch(token, caseSensitive)) { if (this.isHidden()) { this.show(); } diff --git a/domino-ui/src/main/java/org/dominokit/domino/ui/menu/MenuStyles.java b/domino-ui/src/main/java/org/dominokit/domino/ui/menu/MenuStyles.java index be55ca2f0..927331d65 100644 --- a/domino-ui/src/main/java/org/dominokit/domino/ui/menu/MenuStyles.java +++ b/domino-ui/src/main/java/org/dominokit/domino/ui/menu/MenuStyles.java @@ -29,6 +29,7 @@ public interface MenuStyles { CssClass dui_menu_search_box = () -> "dui-menu-search-box"; CssClass dui_menu_sub_header = () -> "dui-menu-subheader"; CssClass dui_menu_body = () -> "dui-menu-body"; + CssClass dui_menu_footer = () -> "dui-menu-footer"; CssClass dui_menu_items_list = () -> "dui-menu-items-list"; CssClass dui_menu_item = () -> "dui-menu-item"; CssClass dui_menu_item_anchor = () -> "dui-menu-item-anchor"; diff --git a/domino-ui/src/main/java/org/dominokit/domino/ui/search/SearchBox.java b/domino-ui/src/main/java/org/dominokit/domino/ui/search/SearchBox.java index b053a4804..5d3ccca96 100644 --- a/domino-ui/src/main/java/org/dominokit/domino/ui/search/SearchBox.java +++ b/domino-ui/src/main/java/org/dominokit/domino/ui/search/SearchBox.java @@ -129,7 +129,14 @@ public SearchBox() { .setPlaceholder(getLabels().defaultQuickSearchPlaceHolder()) .appendChild(PrefixAddOn.of(searchIcon)) .appendChild(PostfixAddOn.of(clearIcon)) - .addCss(dui_m_0); + .addCss(dui_m_0) + .withInputElement( + (parent, input) -> { + input.onKeyDown( + keyEvents -> + keyEvents.any( + KeyboardEventOptions.create().setStopPropagation(true), evt -> {})); + }); root.appendChild(textBox.element()); diff --git a/domino-ui/src/main/java/org/dominokit/domino/ui/utils/DelayedTextInput.java b/domino-ui/src/main/java/org/dominokit/domino/ui/utils/DelayedTextInput.java index cd71081e8..f21244c57 100644 --- a/domino-ui/src/main/java/org/dominokit/domino/ui/utils/DelayedTextInput.java +++ b/domino-ui/src/main/java/org/dominokit/domino/ui/utils/DelayedTextInput.java @@ -16,10 +16,10 @@ package org.dominokit.domino.ui.utils; import static java.util.Objects.isNull; -import static org.dominokit.domino.ui.utils.Domino.*; import elemental2.dom.HTMLInputElement; import jsinterop.base.Js; +import org.dominokit.domino.ui.elements.InputElement; import org.dominokit.domino.ui.events.EventType; import org.gwtproject.timer.client.Timer; @@ -84,6 +84,18 @@ public static DelayedTextInput create(DominoElement inputEleme return create(inputElement.element(), delay); } + /** + * Creates a {@code DelayedTextInput} instance for the given InputElement with a specified delay. + * + * @param inputElement The DominoElement wrapping the HTML input element to monitor for text input + * changes. + * @param delay The delay in milliseconds before triggering the action. + * @return A {@code DelayedTextInput} instance. + */ + public static DelayedTextInput create(InputElement inputElement, int delay) { + return create(inputElement.element(), delay); + } + /** * Constructs a {@code DelayedTextInput} instance for the given HTML input element with a * specified delay. @@ -133,6 +145,7 @@ public void run() { EventType.keypress.getName(), evt -> { if (ElementUtil.isEnterKey(Js.uncheckedCast(evt))) { + autoActionTimer.cancel(); DelayedTextInput.this.onEnterAction.doAction(); } }); diff --git a/domino-ui/src/main/java/org/dominokit/domino/ui/utils/KeyboardNavigation.java b/domino-ui/src/main/java/org/dominokit/domino/ui/utils/KeyboardNavigation.java index a60c5e3c9..a6744ccc9 100644 --- a/domino-ui/src/main/java/org/dominokit/domino/ui/utils/KeyboardNavigation.java +++ b/domino-ui/src/main/java/org/dominokit/domino/ui/utils/KeyboardNavigation.java @@ -34,6 +34,8 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Optional; +import java.util.function.Consumer; import jsinterop.base.Js; import org.dominokit.domino.ui.IsElement; @@ -58,6 +60,9 @@ public class KeyboardNavigation> implements EventListener private EventOptions enterOptions = new EventOptions(true, true); private EventOptions tabOptions = new EventOptions(true, true); private EventOptions spaceOptions = new EventOptions(true, true); + private Consumer> onEndReached = (navigation) -> focusTopFocusableItem(); + private Consumer> onStartReached = + (navigation) -> focusBottomFocusableItem(); /** * Creates a new `KeyboardNavigation` instance for the given list of items. @@ -266,7 +271,7 @@ public void focusNext(V item) { int nextIndex = items.indexOf(item) + 1; int size = items.size(); if (nextIndex >= size) { - focusTopFocusableItem(); + onEndReached.accept(this); } else { for (int i = nextIndex; i < size; i++) { V itemToFocus = items.get(i); @@ -275,7 +280,7 @@ public void focusNext(V item) { return; } } - focusTopFocusableItem(); + onEndReached.accept(this); } } @@ -311,8 +316,22 @@ public void focusTopFocusableItem() { } } + /** + * Get the first focusable item oif exists + * + * @return Optional of the first focusable item. + */ + public Optional getTopFocusableItem() { + for (V item : items) { + if (shouldFocus(item)) { + return Optional.of(item); + } + } + return Optional.empty(); + } + /** Focuses on the last focusable item in the list. */ - private void focusBottomFocusableItem() { + public void focusBottomFocusableItem() { for (int i = items.size() - 1; i >= 0; i--) { V itemToFocus = items.get(i); if (shouldFocus(itemToFocus)) { @@ -330,7 +349,7 @@ private void focusBottomFocusableItem() { public void focusPrevious(V item) { int nextIndex = items.indexOf(item) - 1; if (nextIndex < 0) { - focusBottomFocusableItem(); + onStartReached.accept(this); } else { for (int i = nextIndex; i >= 0; i--) { V itemToFocus = items.get(i); @@ -339,7 +358,7 @@ public void focusPrevious(V item) { return; } } - focusBottomFocusableItem(); + onStartReached.accept(this); } } @@ -400,6 +419,30 @@ public KeyboardNavigation removeNavigationHandler( return this; } + /** + * Use to change the behavior of navigating to the next item when navigating away from the last + * item down by default this will focus the first focusable item in the list + * + * @param onEndReached a function for the new desired behavior. + * @return Same KeyboardNavigation instance + */ + public KeyboardNavigation setOnEndReached(Consumer> onEndReached) { + this.onEndReached = onEndReached; + return this; + } + + /** + * Use to change the behavior of navigating to the previous item when navigating away from the + * first up item by default this will focus the last focusable item in the list + * + * @param onStartReached a function for the new desired behavior. + * @return Same KeyboardNavigation instance + */ + public KeyboardNavigation setOnStartReached(Consumer> onStartReached) { + this.onStartReached = onStartReached; + return this; + } + /** * A functional interface for handling focus on items. * diff --git a/domino-ui/src/main/resources/org/dominokit/domino/ui/public/css/domino-ui/dui-components/domino-ui-forms.css b/domino-ui/src/main/resources/org/dominokit/domino/ui/public/css/domino-ui/dui-components/domino-ui-forms.css index a7d48951b..a7cd59269 100644 --- a/domino-ui/src/main/resources/org/dominokit/domino/ui/public/css/domino-ui/dui-components/domino-ui-forms.css +++ b/domino-ui/src/main/resources/org/dominokit/domino/ui/public/css/domino-ui/dui-components/domino-ui-forms.css @@ -698,4 +698,27 @@ textarea.dui-field-input { } .dui-form-field.dui-form-text-area .dui-field-input:not([data-scroll='0']) { border-top: var(--dui-form-scrolled-text-area-border); +} + +.dui-auto-type-input { + position: absolute; + top: 0; + left: 0; + height: 100%; + width: inherit; + max-width: 100%; + min-height: var(--dui-form-field-input-height); + border: none; + border-radius: var(--dui-form-field-wrapper-radius); + font: var(--dui-form-field-input-line-font); + line-height: var(--dui-form-field-input-line-hieght); + overflow: var(--dui-form-input-overflow, hidden); + white-space: nowrap; + text-overflow: ellipsis; + background-color: var(--dui-bg); + color: inherit; +} + +.dui-auto-type-input:focus{ + outline: none; } \ No newline at end of file diff --git a/domino-ui/src/main/resources/org/dominokit/domino/ui/public/css/domino-ui/dui-components/domino-ui-menu.css b/domino-ui/src/main/resources/org/dominokit/domino/ui/public/css/domino-ui/dui-components/domino-ui-menu.css index bca65a192..71b6819c3 100644 --- a/domino-ui/src/main/resources/org/dominokit/domino/ui/public/css/domino-ui/dui-components/domino-ui-menu.css +++ b/domino-ui/src/main/resources/org/dominokit/domino/ui/public/css/domino-ui/dui-components/domino-ui-menu.css @@ -30,15 +30,15 @@ } .dui-menu-back { - order: 1; + order: 10; } .dui-menu-icon { - order: 2; + order: 20; } .dui-menu-title { - order: 3; + order: 30; flex-grow: 1; } @@ -47,7 +47,7 @@ } .dui-menu-search { - order: 2; + order: 20; padding: var(--dui-menu-search-box-padding); border-width: var(--dui-menu-search-bar-border-width); border-style: var(--dui-menu-search-bar-border-style); @@ -65,7 +65,7 @@ } .dui-menu-subheader { - order: 3; + order: 30; padding: var(--dui-menu-subheader-padding); border-width: var(--dui-menu-subheader-border-width); border-style: var(--dui-menu-subheader-border-style); @@ -77,7 +77,7 @@ } .dui-menu-body { - order: 4; + order: 40; flex-grow: 1; } @@ -129,12 +129,12 @@ } .dui-menu-item-icon { - order: 1; + order: 10; min-width: var(--dui-menu-item-icon-min-height); } .dui-menu-item-utility { - order: 3; + order: 30; } .dui-menu-indicator { @@ -268,4 +268,13 @@ a.dui-menu-group-header:focus, a.dui-menu-group-header { text-decoration: none; outline: none; +} + +.dui-menu-items-list .li:last-child { + border-bottom-right-radius: var(--dui-menu-radius); + border-bottom-left-radius: var(--dui-menu-radius); +} + +.dui-menu-footer { + order: 50; } \ No newline at end of file