Skip to content

Commit

Permalink
lib: Support headers with TypeaheadSelect
Browse files Browse the repository at this point in the history
  • Loading branch information
mvollmer committed Nov 11, 2024
1 parent f63bd0b commit b3c57ac
Show file tree
Hide file tree
Showing 5 changed files with 163 additions and 7 deletions.
4 changes: 4 additions & 0 deletions pkg/lib/cockpit-components-typeahead-select.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
.ct-typeahead-header .pf-v5-c-menu__item-main {
color: var(--pf-v5-global--primary-color--100);
font-size: var(--pf-v5-global--FontSize--sm);
}
48 changes: 41 additions & 7 deletions pkg/lib/cockpit-components-typeahead-select.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,21 @@ SOFTWARE.
...
]
- Allow headers.
[
...
{ header: _("Nice things") }
{ value: "icecream", content: _("Icecream") },
...
]
Note that PatternFly uses SelectGroup and MenuGroup instead of
headers, but their recursive nature makes them harder to
implement here, mostly because of how keyboard navigation is
done. And there is no visual nesting going on anyway. Keeping the
options a flat list is just all around easier.
*/

/* eslint-disable */
Expand All @@ -83,6 +98,7 @@ import {
SelectProps
} from '@patternfly/react-core';
import TimesIcon from '@patternfly/react-icons/dist/esm/icons/times-icon';
import "cockpit-components-typeahead-select.scss";

const _ = cockpit.gettext;

Expand All @@ -95,6 +111,8 @@ export interface TypeaheadSelectOption extends Omit<SelectOptionProps, 'content'
isSelected?: boolean;
/** Is this just a divider */
divider?: boolean;
/** Is this a header? */
header?: string;
}

export interface TypeaheadSelectProps extends Omit<SelectProps, 'toggle' | 'onSelect'> {
Expand Down Expand Up @@ -137,8 +155,12 @@ export interface TypeaheadSelectProps extends Omit<SelectProps, 'toggle' | 'onSe
toggleProps?: MenuToggleProps;
}

const defaultFilterFunction = (filterValue: string, options: TypeaheadSelectOption[]) =>
options.filter((o) => String(o.content).toLowerCase().includes(filterValue.toLowerCase()));
const defaultFilterFunction = (filterValue: string, options: TypeaheadSelectOption[]) => {
// Filter by search term
const filtered = options.filter((o) => o.header || String(o.content).toLowerCase().includes(filterValue.toLowerCase()))
// Remove headers that have nothing following them.
return filtered.filter((o, i) => !(o.header && (i >= filtered.length - 1 || filtered[i + 1].header)));
};

export const TypeaheadSelectBase: React.FunctionComponent<TypeaheadSelectProps> = ({
innerRef,
Expand Down Expand Up @@ -320,7 +342,7 @@ export const TypeaheadSelectBase: React.FunctionComponent<TypeaheadSelectProps>

openMenu();

if (filteredSelections.every((option) => option.isDisabled)) {
if (filteredSelections.every((option) => option.isDisabled || option.header)) {
return;
}

Expand All @@ -332,8 +354,8 @@ export const TypeaheadSelectBase: React.FunctionComponent<TypeaheadSelectProps>
indexToFocus = focusedItemIndex - 1;
}

// Skip disabled options
while (filteredSelections[indexToFocus].isDisabled) {
// Skip disabled options and headers
while (filteredSelections[indexToFocus].isDisabled || filteredSelections[indexToFocus].header) {
indexToFocus--;
if (indexToFocus === -1) {
indexToFocus = filteredSelections.length - 1;
Expand All @@ -349,8 +371,8 @@ export const TypeaheadSelectBase: React.FunctionComponent<TypeaheadSelectProps>
indexToFocus = focusedItemIndex + 1;
}

// Skip disabled options
while (filteredSelections[indexToFocus].isDisabled) {
// Skip disabled options and headers
while (filteredSelections[indexToFocus].isDisabled || filteredSelections[indexToFocus].header) {
indexToFocus++;
if (indexToFocus === filteredSelections.length) {
indexToFocus = 0;
Expand Down Expand Up @@ -459,6 +481,18 @@ export const TypeaheadSelectBase: React.FunctionComponent<TypeaheadSelectProps>
if (option.divider)
return <Divider key={option.key || index} component="li" />;

if (option.header) {
const { header, content, ...props } = option;
return (
<SelectOption key={option.key || index}
isDisabled
className="ct-typeahead-header"
{...props}>
{option.header}
</SelectOption>
);
}

const { content, value, ...props } = option;
return (
<SelectOption key={value} value={value} isFocused={focusedItemIndex === index} {...props}>
Expand Down
109 changes: 109 additions & 0 deletions pkg/playground/react-demo-typeahead.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
/*
* This file is part of Cockpit.
*
* Copyright (C) 2017 Red Hat, Inc.
*
* Cockpit is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation; either version 2.1 of the License, or
* (at your option) any later version.
*
* Cockpit is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Cockpit; If not, see <https://www.gnu.org/licenses/>.
*/

import React, { useState } from "react";
import { createRoot } from 'react-dom/client';

import { TypeaheadSelect } from "cockpit-components-typeahead-select";

const TypeaheadDemo = ({ options }) => {
const [value, setValue ] = useState(null);

return (
<div>
<TypeaheadSelect id='typeahead-widget'
selectOptions={options}
selected={value}
onSelect={(_, value) => {
setValue(value);
}}
/>
<span>Selected: {value || "-"}</span>
</div>
);
};

export function showTypeaheadDemo(rootElement) {
const states = {
"AL": "Alabama",
"AK": "Alaska",
"AZ": "Arizona",
"AR": "Arkansas",
"CA": "California",
"CO": "Colorado",
"CT": "Connecticut",
"DE": "Delaware",
"FL": "Florida",
"GA": "Georgia",
"HI": "Hawaii",
"ID": "Idaho",
"IL": "Illinois",
"IN": "Indiana",
"IA": "Iowa",
"KS": "Kansas",
"KY": "Kentucky",
"LA": "Louisiana",
"ME": "Maine",
"MD": "Maryland",
"MA": "Massachusetts",
"MI": "Michigan",
"MN": "Minnesota",
"MS": "Mississippi",
"MO": "Missouri",
"MT": "Montana",
"NE": "Nebraska",
"NV": "Nevada",
"NH": "New Hampshire",
"NJ": "New Jersey",
"NM": "New Mexico",
"NY": "New York",
"NC": "North Carolina",
"ND": "North Dakota",
"OH": "Ohio",
"OK": "Oklahoma",
"OR": "Oregon",
"PA": "Pennsylvania",
"RI": "Rhode Island",
"SC": "South Carolina",
"SD": "South Dakota",
"TN": "Tennessee",
"TX": "Texas",
"UT": "Utah",
"VT": "Vermont",
"VA": "Virginia",
"WA": "Washington",
"WV": "West Virginia",
"WI": "Wisconsin",
"WY": "Wyoming"
};

const options = [];
let last = "";

for (const st of Object.keys(states).sort()) {
if (st[0] != last) {
options.push({ key: "_header-" + st[0], header: "Starting with " + st[0]});
last = st[0];
}
options.push({ value: st, content: states[st] });
}

const root = createRoot(rootElement);
root.render(<TypeaheadDemo options={options} />);
}
5 changes: 5 additions & 0 deletions pkg/playground/react-patterns.html
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,11 @@ <h3>Select file</h3>
<div id="demo-file-ac-preselected"></div>
</section>

<section class="pf-v5-c-page__main-section pf-m-light">
<h3>Typeahead</h3>
<div id="demo-typeahead"></div>
</section>

<section class="pf-v5-c-page__main-section pf-m-light">
<h3>Dialogs</h3>
<button id="demo-show-dialog" class="pf-v5-c-button pf-m-secondary">Show Dialog</button>
Expand Down
4 changes: 4 additions & 0 deletions pkg/playground/react-patterns.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import { PatternDialogBody } from "./react-demo-dialog.jsx";
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";

/* -----------------------------------------------------------------------------
Modal Dialog
Expand Down Expand Up @@ -125,6 +126,9 @@ document.addEventListener("DOMContentLoaded", function() {
showFileAcDemo(document.getElementById('demo-file-ac'));
showFileAcDemoPreselected(document.getElementById('demo-file-ac-preselected'));

// Plain typeahead select with headers and dividers
showTypeaheadDemo(document.getElementById('demo-typeahead'));

// Cards
showCardsDemo(document.getElementById('demo-cards'));

Expand Down

0 comments on commit b3c57ac

Please sign in to comment.