diff --git a/pkg/lib/cockpit-components-multi-typeahead-select.tsx b/pkg/lib/cockpit-components-multi-typeahead-select.tsx
index a8bc7dc236da..b5709e52d3b3 100644
--- a/pkg/lib/cockpit-components-multi-typeahead-select.tsx
+++ b/pkg/lib/cockpit-components-multi-typeahead-select.tsx
@@ -156,7 +156,9 @@ export const MultiTypeaheadSelectBase: React.FunctionComponent.
+ */
+
+import cockpit from "cockpit";
+
+import React, { useState } from "react";
+import { createRoot, Container } from 'react-dom/client';
+
+import { Checkbox } from '@patternfly/react-core';
+import { MultiTypeaheadSelect, MultiTypeaheadSelectOption } from "cockpit-components-multi-typeahead-select";
+
+const MultiTypeaheadDemo = ({ options } : { options: MultiTypeaheadSelectOption[] }) => {
+ const [notFoundIsString, setNotFoundIsString] = useState(false);
+ const [selected, setSelected] = useState<(string | number)[]>([]);
+ const [toggles, setToggles] = useState(0);
+ const [changes, setChanges] = useState(0);
+
+ function add(val: string | number) {
+ setSelected(selected.concat([val]));
+ }
+
+ function rem(val: string | number) {
+ setSelected(selected.filter(v => v != val));
+ }
+
+ return (
+
+
cockpit.format("'$0' not found", val) }
+ options={options}
+ selected={selected}
+ onAdd={add}
+ onRemove={rem}
+ onToggle={() => setToggles(val => val + 1)}
+ onInputChange={() => setChanges(val => val + 1)}
+ />
+ Selected: {JSON.stringify(selected)}
+ Toggles: {toggles}
+ Changes: {changes}
+ setNotFoundIsString(checked)}
+ />
+
+ );
+};
+
+export function showMultiTypeaheadDemo(rootElement: Container) {
+ const flavors: string[] = [
+ "Alumni Swirl",
+ "Apple Cobbler Crunch",
+ "Arboretum Breeze",
+ "August Pie",
+ "Autumn Delight",
+ "Bavarian Raspberry Crunch",
+ "Berkey Brickle",
+ "Birthday Bash",
+ "Bittersweet Mint",
+ "Black Cow",
+ "Black Raspberry",
+ "Blueberry Cheesecake",
+ "Butter Pecan",
+ "Candy Bar/Snickers",
+ "Caramel Critters",
+ "Centennial Vanilla Bean",
+ "Cherry Cheesecake",
+ "Cherry Chip",
+ "Cherry Quist",
+ "Cherry Sherbet",
+ "Chocolate",
+ "Chocolate Cherry Cordia",
+ "Chocolate Chip",
+ "Chocolate Chip Cheesecake",
+ "Chocolate Chip Cookie Dough",
+ "Chocolate Chocolate Nut",
+ "Chocolate Marble",
+ "Chocolate Marshmallow",
+ "Chocolate Pretzel Crunch",
+ "Chunky Chocolate",
+ "Chunky Chocolate- Vanilla",
+ "Coconut Chip",
+ "Coffee Mocha Fudge",
+ "Coffee w/Cream and Sugar",
+ "Crazy Charlie Sundae Swirl",
+ "Death By Chocolate",
+ "Egg Nog",
+ "Espresso Fudge Pie",
+ "German Chocolate Cake",
+ "Golden Chocolate Pecan",
+ "Goo Goo Cluster",
+ "Grape Sherbet",
+ "Happy Happy Joy Joy",
+ "Heath Bar Candy",
+ "Just Fudge",
+ "Kenney Beany Chocolate",
+ "Lion Tracks",
+ "LionS'more",
+ "Mallo Cup",
+ "Maple Nut",
+ "Mint Nittany",
+ "Monster Mash",
+ "Orange Vanilla Sundae",
+ "Palmer Mousseum With Almonds",
+ "Peachy Paterno",
+ "Peanut Butter Cup",
+ "Peanut Butter Fudge Cluster",
+ "Peanut Butter Marshmallow",
+ "Peanut Butter Swirl",
+ "Pecan Apple Danish",
+ "Peppermint Stick",
+ "Pistachio",
+ "Pralines N Cream",
+ "Pumpkin Pie",
+ "Raspberry Fudge Torte",
+ "Raspberry Parfait",
+ "Rum Raisin",
+ "Russ 'Digs' Roseberry",
+ "Santa Fe Banana",
+ "Scholar's Chip",
+ "Sea Salt Chocolate Caramel",
+ "Somerset Shortcake",
+ "Southern Chocolate Pie",
+ "Southern Pecan Cheesecake",
+ "Strawberry",
+ "Strawberry Cheesecake",
+ "Teaberry",
+ "Tin Roof Sundae",
+ "Toasted Almond",
+ "Toasted Almond Fudge",
+ "Turtle Creek",
+ "Vanilla",
+ "White House",
+ "Wicked Caramel Sundae",
+ "WPSU Coffee Break",
+ ];
+
+ const options: MultiTypeaheadSelectOption[] = flavors.map((f, i) => ({ value: i + 1, content: f }));
+
+ const root = createRoot(rootElement);
+ root.render();
+}
diff --git a/pkg/playground/react-patterns.html b/pkg/playground/react-patterns.html
index 378d9ffd3ec5..0ef97bb56b14 100644
--- a/pkg/playground/react-patterns.html
+++ b/pkg/playground/react-patterns.html
@@ -23,6 +23,11 @@ Typeahead
+
+
Dialogs
diff --git a/pkg/playground/react-patterns.js b/pkg/playground/react-patterns.js
index 712d059f0fa2..f8835d4987d8 100644
--- a/pkg/playground/react-patterns.js
+++ b/pkg/playground/react-patterns.js
@@ -31,6 +31,7 @@ import { showCardsDemo } from "./react-demo-cards.jsx";
import { showUploadDemo } from "./react-demo-file-upload.jsx";
import { showFileAcDemo, showFileAcDemoPreselected } from "./react-demo-file-autocomplete.jsx";
import { showTypeaheadDemo } from "./react-demo-typeahead.jsx";
+import { showMultiTypeaheadDemo } from "./react-demo-multi-typeahead.jsx";
/* -----------------------------------------------------------------------------
Modal Dialog
@@ -130,6 +131,9 @@ document.addEventListener("DOMContentLoaded", function() {
// Plain typeahead select with headers and dividers
showTypeaheadDemo(document.getElementById('demo-typeahead'));
+ // Multi typeahead
+ showMultiTypeaheadDemo(document.getElementById('demo-multi-typeahead'));
+
// Cards
showCardsDemo(document.getElementById('demo-cards'));
diff --git a/test/reference b/test/reference
index b8704b853a9e..d71be06ea649 160000
--- a/test/reference
+++ b/test/reference
@@ -1 +1 @@
-Subproject commit b8704b853a9ebd0f137c55ea95bc2e8b818a3148
+Subproject commit d71be06ea64970157dc2a196b4d664ae66d9067d
diff --git a/test/verify/check-lib b/test/verify/check-lib
index 9fa192947517..3c951e23e39c 100755
--- a/test/verify/check-lib
+++ b/test/verify/check-lib
@@ -131,6 +131,112 @@ class TestLib(testlib.MachineCase):
b.wait_text("#toggles", "14")
b.wait_text("#changes", "26")
+ def testMultiTypeaheadSelect(self):
+ b = self.browser
+
+ # Login
+
+ self.login_and_go("/playground/react-patterns")
+
+ # No clear button
+
+ b.wait_not_visible("#demo-multi-typeahead .pf-v5-c-text-input-group__utilities button")
+
+ # Open menu, pixel test
+
+ b.click("#demo-multi-typeahead .pf-v5-c-menu-toggle__button")
+ b.assert_pixels("#multi-typeahead-widget", "menu")
+ b.wait_text("#multi-toggles", "1")
+ b.wait_text("#multi-changes", "0")
+
+ # Select from menu (with mouse)
+
+ b.click("#multi-typeahead-widget .pf-v5-c-menu__item:contains(Strawberry Cheesecake)")
+ b.wait_not_present("#multi-typeahead-widget")
+ b.assert_pixels("#demo-multi-typeahead .pf-v5-c-menu-toggle", "input")
+ b.wait_text("#multi-value", "[76]")
+ b.wait_text("#multi-toggles", "2")
+ b.wait_text("#multi-changes", "0")
+
+ # Select another one
+
+ b.click("#demo-multi-typeahead .pf-v5-c-menu-toggle__button")
+ b.wait_visible("#multi-typeahead-widget")
+ b.click("#multi-typeahead-widget .pf-v5-c-menu__item:contains(Coconut Chip)")
+ b.wait_not_present("#multi-typeahead-widget")
+ b.wait_text("#multi-value", "[76,32]")
+ b.wait_text("#multi-toggles", "4")
+ b.wait_text("#multi-changes", "0")
+
+ # Remove one
+
+ b.click("#demo-multi-typeahead .pf-v5-c-label:contains(Strawberry Cheesecake) button")
+ b.wait_text("#multi-value", "[32]")
+ b.wait_text("#multi-toggles", "4")
+ b.wait_text("#multi-changes", "0")
+
+ # Open by clicking into input, close with ESC
+
+ b.click("#demo-multi-typeahead .pf-v5-c-text-input-group__text-input")
+ b.wait_visible("#multi-typeahead-widget")
+ b.key("Escape")
+ b.wait_not_present("#multi-typeahead-widget")
+ b.wait_text("#multi-toggles", "6")
+ b.wait_text("#multi-changes", "0")
+
+ # Select with keys
+
+ b.click("#demo-multi-typeahead .pf-v5-c-menu-toggle__button")
+ b.wait_visible("#multi-typeahead-widget")
+ b.key("ArrowDown") # Alumni Swirl
+ b.key("ArrowUp") # Wraps, WPSU Coffee break
+ b.key("ArrowDown") # Wraps
+ b.key("ArrowDown")
+ b.key("ArrowDown")
+ b.key("ArrowDown")
+ b.key("ArrowDown") # Autumn Delight
+ b.key("Enter")
+ b.key("Escape")
+ b.wait_not_present("#multi-typeahead-widget")
+ b.wait_text("#multi-value", "[32,5]")
+ b.wait_text("#multi-toggles", "8")
+ b.wait_text("#multi-changes", "0")
+
+ b.wait_visible("#demo-multi-typeahead .pf-v5-c-label:contains(Coconut Chip)")
+ b.wait_visible("#demo-multi-typeahead .pf-v5-c-label:contains(Autumn Delight)")
+
+ # Search for non-existent
+
+ b.click("#demo-multi-typeahead .pf-v5-c-text-input-group__text-input")
+ b.set_input_text("#demo-multi-typeahead .pf-v5-c-text-input-group__text-input", "Salmiak")
+ b.wait_text("#multi-typeahead-widget .pf-v5-c-menu__item", "'Salmiak' not found")
+ b.click("#demo-multi-typeahead .pf-v5-c-menu-toggle__button")
+ b.wait_not_present("#multi-typeahead-widget")
+ b.wait_text("#multi-toggles", "10")
+ b.wait_text("#multi-changes", "7")
+
+ # Again with formatted "not found" message
+
+ b.set_checked("#notFoundIsStringMulti", val=True)
+ b.click("#demo-multi-typeahead .pf-v5-c-text-input-group__text-input")
+ b.set_input_text("#demo-multi-typeahead .pf-v5-c-text-input-group__text-input", "Salmiaki")
+ b.wait_text("#multi-typeahead-widget .pf-v5-c-menu__item", "Not found")
+ b.click("#demo-multi-typeahead .pf-v5-c-menu-toggle__button")
+ b.wait_not_present("#multi-typeahead-widget")
+ b.wait_text("#multi-toggles", "12")
+ b.wait_text("#multi-changes", "15")
+
+ # Search for existing, pixel test, select
+
+ b.click("#demo-multi-typeahead .pf-v5-c-text-input-group__text-input")
+ b.set_input_text("#demo-multi-typeahead .pf-v5-c-text-input-group__text-input", "Rum")
+ b.wait_visible("#multi-typeahead-widget")
+ b.click("#multi-typeahead-widget .pf-v5-c-menu__item")
+ b.wait_not_present("#multi-typeahead-widget")
+ b.wait_text("#multi-value", "[32,5,67]")
+ b.wait_text("#multi-toggles", "14")
+ b.wait_text("#multi-changes", "18")
+
if __name__ == '__main__':
testlib.test_main()