Skip to content

Commit b2ada7c

Browse files
committed
Typeahead component - wip
1 parent ded587c commit b2ada7c

File tree

8 files changed

+188
-143
lines changed

8 files changed

+188
-143
lines changed

components/src/demo/java/MenuDemo.java

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -59,12 +59,12 @@ public void searchDemo() {
5959
Menu menu = menu(MenuType.menu, click)
6060
.addSearch(menuSearch()
6161
.addSearchInput(menuSearchInput()
62-
.addSearchInput(searchInputGroup(""))))
63-
.onSearch((menuItem, value) ->
64-
menuItem.text().toLowerCase().contains(value.toLowerCase()))
65-
.onNoResults(value ->
66-
menuItem(Id.unique("no-results"), "No results found for \"" + value + "\"")
67-
.disabled())
62+
.addSearchInput(searchInputGroup(""))
63+
.onSearch((menuItem, value) ->
64+
menuItem.text().toLowerCase().contains(value.toLowerCase()))
65+
.onNoResults(value ->
66+
menuItem(Id.unique("no-results"), "No results found for \"" + value + "\"")
67+
.disabled())))
6868
.addDivider()
6969
.addContent(menuContent()
7070
.addList(menuList()

components/src/main/java/org/patternfly/component/Expandable.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@ default void expand() {
108108
void expand(boolean fireEvent);
109109

110110
/**
111-
* @return {@code true} if the elements class list has the modifier {@link Classes#expanded}, {@code false} otherwise.
111+
* @return {@code true} if the element's class list has the modifier {@link Classes#expanded}, {@code false} otherwise.
112112
*/
113113
default boolean expanded() {
114114
return expanded(element());

components/src/main/java/org/patternfly/component/menu/Menu.java

Lines changed: 5 additions & 99 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,6 @@
1818
import java.util.ArrayList;
1919
import java.util.Iterator;
2020
import java.util.List;
21-
import java.util.function.BiPredicate;
22-
import java.util.function.Function;
2321

2422
import org.gwtproject.event.shared.HandlerRegistration;
2523
import org.jboss.elemento.Attachable;
@@ -30,7 +28,6 @@
3028
import org.patternfly.component.BaseComponent;
3129
import org.patternfly.component.ComponentType;
3230
import org.patternfly.component.SelectionMode;
33-
import org.patternfly.component.textinputgroup.TextInputGroup;
3431
import org.patternfly.handler.MultiSelectHandler;
3532
import org.patternfly.handler.SelectHandler;
3633
import org.patternfly.style.Classes;
@@ -50,8 +47,7 @@
5047
import static elemental2.dom.DomGlobal.window;
5148
import static java.util.stream.Collectors.toList;
5249
import static org.jboss.elemento.Elements.div;
53-
import static org.jboss.elemento.Elements.failSafeRemoveFromParent;
54-
import static org.jboss.elemento.Elements.setVisible;
50+
import static org.jboss.elemento.Elements.isVisible;
5551
import static org.jboss.elemento.EventType.keydown;
5652
import static org.jboss.elemento.Key.ArrowDown;
5753
import static org.jboss.elemento.Key.ArrowLeft;
@@ -66,7 +62,6 @@
6662
import static org.patternfly.component.divider.DividerType.hr;
6763
import static org.patternfly.component.menu.MenuFooter.menuFooter;
6864
import static org.patternfly.component.menu.MenuHeader.menuHeader;
69-
import static org.patternfly.component.menu.MenuItem.menuItem;
7065
import static org.patternfly.style.Classes.component;
7166
import static org.patternfly.style.Classes.disabled;
7267
import static org.patternfly.style.Classes.divider;
@@ -106,13 +101,9 @@ public static Menu menu(MenuType menuType, SelectionMode selectionMode) {
106101
final SelectionMode selectionMode;
107102
final List<MenuActionHandler> actionHandler;
108103
boolean favorites;
109-
private MenuSearch search;
110-
private MenuContent content;
111-
private MenuItem noResultsItem;
104+
MenuContent content;
112105
private final List<SelectHandler<MenuItem>> selectHandler;
113106
private final List<MultiSelectHandler<Menu, MenuItem>> multiSelectHandler;
114-
private BiPredicate<MenuItem, String> searchFilter;
115-
private Function<String, MenuItem> noResultsProvider;
116107
private HandlerRegistration keyHandler;
117108

118109
Menu(MenuType menuType, SelectionMode selectionMode) {
@@ -123,7 +114,6 @@ public static Menu menu(MenuType menuType, SelectionMode selectionMode) {
123114
this.actionHandler = new ArrayList<>();
124115
this.selectHandler = new ArrayList<>();
125116
this.multiSelectHandler = new ArrayList<>();
126-
this.noResultsProvider = value -> menuItem(Id.unique("no-results"), "No results found").disabled();
127117
// TODO Without this workaround the menu "flickers" when showing.
128118
// This could be solved by replacing the show/hide alg with an add/remove alg in the Popper class
129119
componentVar(component(menu), "TransitionDuration").applyTo(this).set(0);
@@ -135,31 +125,6 @@ public static Menu menu(MenuType menuType, SelectionMode selectionMode) {
135125
public void attach(MutationRecord mutationRecord) {
136126
allowTabFirstItem();
137127
keyHandler = EventType.bind(window, keydown, this::keyHandler);
138-
if (searchFilter != null) {
139-
if (content == null) {
140-
logger.warn("Menu %o has a search filter, but no content was added.", element());
141-
} else if (!content.groups.isEmpty()) {
142-
logger.warn("Menu %o a search filter and groups. Search filters are not supported for grouped menus.",
143-
element());
144-
} else if (search == null) {
145-
logger.warn("Menu %o has a search filter, but no search menu was added.", element());
146-
} else if (search.searchInput() == null) {
147-
logger.warn("Menu %o has a search filter, but no search input was added.", element());
148-
} else if (search.searchInput().textInputGroup() == null) {
149-
logger.warn("Menu %o has a search filter, but no text input group was added.", element());
150-
} else {
151-
TextInputGroup textInputGroup = search.searchInput().textInputGroup();
152-
if (textInputGroup != null) {
153-
textInputGroup
154-
.onKeyup((event, tig, value) -> search(value))
155-
.onChange((event, tig, value) -> {
156-
if (value.isEmpty()) {
157-
clearSearch();
158-
}
159-
});
160-
}
161-
}
162-
}
163128
}
164129

165130
@Override
@@ -208,13 +173,6 @@ public Menu addSearch(MenuSearch search) {
208173
return add(search);
209174
}
210175

211-
// override to ensure internal wiring
212-
public Menu add(MenuSearch search) {
213-
this.search = search;
214-
add(search.element());
215-
return this;
216-
}
217-
218176
public Menu addDivider() {
219177
return add(divider(hr));
220178
}
@@ -261,28 +219,6 @@ public Menu onMultiSelect(MultiSelectHandler<Menu, MenuItem> selectHandler) {
261219
return this;
262220
}
263221

264-
/**
265-
* Configures a search behavior for the menu by applying a search filter. For this to work, you need to add a
266-
* {@link MenuSearch}, {@link MenuSearchInput} and {@link TextInputGroup}.
267-
*
268-
* <p>
269-
* {@snippet class = MenuDemo region = search}
270-
*
271-
* @param searchFilter a {@link BiPredicate} that defines the search logic. The first parameter is a {@link MenuItem}
272-
* representing a menu item, and the second parameter is a {@link String} representing the search query.
273-
* The predicate should return {@code true} for items matching the search.
274-
* @return the {@link Menu} instance for method chaining.
275-
*/
276-
public Menu onSearch(BiPredicate<MenuItem, String> searchFilter) {
277-
this.searchFilter = searchFilter;
278-
return this;
279-
}
280-
281-
public Menu onNoResults(Function<String, MenuItem> noResults) {
282-
this.noResultsProvider = noResults;
283-
return this;
284-
}
285-
286222
// ------------------------------------------------------ api
287223

288224
/**
@@ -443,42 +379,12 @@ private void unselectAllInGroup(MenuItem item) {
443379
}
444380
}
445381

446-
private void search(String value) {
447-
int visibleItems = 0;
448-
for (MenuItem menuItem : items()) {
449-
boolean visible = searchFilter.test(menuItem, value);
450-
setVisible(menuItem, visible);
451-
if (visible) {
452-
visibleItems++;
453-
}
454-
}
455-
failSafeRemoveFromParent(noResultsItem);
456-
if (visibleItems == 0) {
457-
if (content != null && content.list != null) {
458-
noResultsItem = noResultsProvider.apply(value);
459-
// Don't use content.list.add(noResultsItem) here
460-
// The no-result item should not be part of the item map
461-
content.list.add(noResultsItem.element());
462-
}
463-
} else {
464-
allowTabFirstItem();
465-
}
466-
}
467-
468-
private void clearSearch() {
469-
failSafeRemoveFromParent(noResultsItem);
470-
for (MenuItem menuItem : items()) {
471-
setVisible(menuItem, true);
472-
}
473-
allowTabFirstItem();
474-
}
475-
476382
// ------------------------------------------------------ keyboard navigation
477383

478384
private void keyHandler(KeyboardEvent event) {
479385
HTMLElement activeElement = (HTMLElement) document.activeElement;
480386
if (element().contains(((Node) event.target))) {
481-
JsArray<HTMLElement> navigableElements = getNavigableElement(element());
387+
JsArray<HTMLElement> navigableElements = navigableElement(element());
482388
if (navigableElements.length == 0) {
483389
logger.warn("Menu %o has no navigable elements. Keyboard navigation will be ignored.", element());
484390
}
@@ -572,10 +478,10 @@ private void handleArrows(KeyboardEvent event, HTMLElement activeElement, JsArra
572478
}
573479
}
574480

575-
private JsArray<HTMLElement> getNavigableElement(HTMLElement element) {
481+
private JsArray<HTMLElement> navigableElement(HTMLElement element) {
576482
JsArray<HTMLElement> elements = JsArray.from(element.querySelectorAll("li").values());
577483
return elements.filter((e, i) ->
578-
!(e.classList.contains(modifier(disabled)) || e.classList.contains(component(divider))));
484+
isVisible(e) && !(e.classList.contains(modifier(disabled)) || e.classList.contains(component(divider))));
579485
}
580486

581487
private HTMLElement getFocusableElement(HTMLElement navigableElement) {

components/src/main/java/org/patternfly/component/menu/MenuSearch.java

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,6 @@ public static MenuSearch menuSearch() {
3434
// ------------------------------------------------------ instance
3535

3636
static final String SUB_COMPONENT_NAME = "ms";
37-
private MenuSearchInput searchInput;
3837

3938
MenuSearch() {
4039
super(SUB_COMPONENT_NAME, div().css(component(menu, search)).element());
@@ -50,13 +49,6 @@ public MenuSearch that() {
5049
// ------------------------------------------------------ add
5150

5251
public MenuSearch addSearchInput(MenuSearchInput searchInput) {
53-
this.searchInput = searchInput;
5452
return add(searchInput);
5553
}
56-
57-
// ------------------------------------------------------ api
58-
59-
public MenuSearchInput searchInput() {
60-
return searchInput;
61-
}
6254
}

components/src/main/java/org/patternfly/component/menu/MenuSearchInput.java

Lines changed: 90 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,17 +15,27 @@
1515
*/
1616
package org.patternfly.component.menu;
1717

18+
import java.util.function.BiPredicate;
19+
import java.util.function.Function;
20+
21+
import org.jboss.elemento.Attachable;
22+
import org.jboss.elemento.Id;
23+
import org.jboss.elemento.logger.Logger;
1824
import org.patternfly.component.textinputgroup.TextInputGroup;
1925
import org.patternfly.style.Classes;
2026

2127
import elemental2.dom.HTMLElement;
28+
import elemental2.dom.MutationRecord;
2229

2330
import static org.jboss.elemento.Elements.div;
31+
import static org.jboss.elemento.Elements.failSafeRemoveFromParent;
32+
import static org.jboss.elemento.Elements.setVisible;
33+
import static org.patternfly.component.menu.MenuItem.menuItem;
2434
import static org.patternfly.style.Classes.component;
2535
import static org.patternfly.style.Classes.input;
2636
import static org.patternfly.style.Classes.search;
2737

28-
public class MenuSearchInput extends MenuSubComponent<HTMLElement, MenuSearchInput> {
38+
public class MenuSearchInput extends MenuSubComponent<HTMLElement, MenuSearchInput> implements Attachable {
2939

3040
// ------------------------------------------------------ factory
3141

@@ -36,15 +46,65 @@ public static MenuSearchInput menuSearchInput() {
3646

3747
// ------------------------------------------------------ instance
3848

49+
private static final Logger logger = Logger.getLogger(MenuSearch.class.getName());
3950
static final String SUB_COMPONENT_NAME = "msi";
51+
4052
private TextInputGroup textInputGroup;
53+
private MenuItem noResultsItem;
54+
private BiPredicate<MenuItem, String> searchFilter;
55+
private Function<String, MenuItem> noResultsProvider;
4156

4257
MenuSearchInput() {
4358
super(SUB_COMPONENT_NAME, div().css(component(Classes.menu, search, input)).element());
59+
this.noResultsProvider = value -> menuItem(Id.unique("no-results"), "No results found").disabled();
60+
Attachable.register(this, this);
61+
}
62+
63+
@Override
64+
public void attach(MutationRecord mutationRecord) {
65+
Menu menu = lookupComponent();
66+
if (searchFilter != null) {
67+
if (menu.content != null && !menu.content.groups.isEmpty()) {
68+
logger.warn("Menu %o has a search filter and groups. Search filters are not supported for grouped menus.",
69+
menu);
70+
}
71+
if (textInputGroup == null) {
72+
logger.warn("Menu %o has a search filter, but no text input group was added.", menu);
73+
} else {
74+
textInputGroup
75+
.onKeyup((event, tig, value) -> search(menu, value))
76+
.onChange((event, tig, value) -> {
77+
if (value.isEmpty()) {
78+
clearSearch(menu);
79+
}
80+
});
81+
}
82+
}
4483
}
4584

4685
// ------------------------------------------------------ builder
4786

87+
/**
88+
* Configures the search behavior for the search input you have added with {@link #addSearchInput(TextInputGroup)}.
89+
*
90+
* <p>
91+
* {@snippet class = MenuDemo region = search}
92+
*
93+
* @param searchFilter a {@link BiPredicate} that defines the search logic. The first parameter is a {@link MenuItem}
94+
* representing a menu item, and the second parameter is a {@link String} representing the search query.
95+
* The predicate should return {@code true} for items matching the search.
96+
* @return the {@link MenuSearchInput} instance for method chaining.
97+
*/
98+
public MenuSearchInput onSearch(BiPredicate<MenuItem, String> searchFilter) {
99+
this.searchFilter = searchFilter;
100+
return this;
101+
}
102+
103+
public MenuSearchInput onNoResults(Function<String, MenuItem> noResults) {
104+
this.noResultsProvider = noResults;
105+
return this;
106+
}
107+
48108
@Override
49109
public MenuSearchInput that() {
50110
return this;
@@ -57,9 +117,35 @@ public MenuSearchInput addSearchInput(TextInputGroup textInputGroup) {
57117
return add(textInputGroup);
58118
}
59119

60-
// ------------------------------------------------------ api
120+
// ------------------------------------------------------ internal
121+
122+
private void search(Menu menu, String value) {
123+
int visibleItems = 0;
124+
for (MenuItem menuItem : menu.items()) {
125+
boolean visible = searchFilter.test(menuItem, value);
126+
setVisible(menuItem, visible);
127+
if (visible) {
128+
visibleItems++;
129+
}
130+
}
131+
failSafeRemoveFromParent(noResultsItem);
132+
if (visibleItems == 0) {
133+
if (menu.content != null && menu.content.list != null) {
134+
noResultsItem = noResultsProvider.apply(value);
135+
// Don't use content.list.addItem(noResultsItem) here
136+
// The no-result item should not be part of the item map
137+
menu.content.list.add(noResultsItem.element());
138+
}
139+
} else {
140+
menu.allowTabFirstItem();
141+
}
142+
}
61143

62-
public TextInputGroup textInputGroup() {
63-
return textInputGroup;
144+
private void clearSearch(Menu menu) {
145+
failSafeRemoveFromParent(noResultsItem);
146+
for (MenuItem menuItem : menu.items()) {
147+
setVisible(menuItem, true);
148+
}
149+
menu.allowTabFirstItem();
64150
}
65151
}

0 commit comments

Comments
 (0)