Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
61 commits
Select commit Hold shift + click to select a range
33b5f8b
Finally got the arrow key presses working, but broke the tabbing order
mackcheesman Sep 10, 2025
4eca05a
Bump mysql2 from 3.14.3 to 3.14.4 in /src
dependabot[bot] Sep 4, 2025
0bfa2f4
Bump pg from 8.13.3 to 8.16.3 in /src
dependabot[bot] Sep 4, 2025
03bcd38
Bump concurrently from 9.1.2 to 9.2.1 in /src
dependabot[bot] Sep 4, 2025
45e057b
Bump vite from 6.3.5 to 7.1.4 in /src
dependabot[bot] Sep 4, 2025
cc19ecf
Bump versions for release.
thekevinbrown Sep 4, 2025
bbf4e26
Bump pino from 9.9.0 to 9.9.1 in /src
dependabot[bot] Sep 4, 2025
f1daa2a
Bump @tanstack/react-form from 1.19.2 to 1.19.3 in /src
dependabot[bot] Sep 4, 2025
449ba0e
Allow many to many filters for relationship filters.
thekevinbrown Sep 4, 2025
d02ac72
Bump @floating-ui/dom from 1.7.3 to 1.7.4 in /src
dependabot[bot] Sep 4, 2025
899adb1
Bump rollup-plugin-visualizer from 5.14.0 to 6.0.3 in /src
dependabot[bot] Sep 4, 2025
f961573
Silence dotenv tips.
thekevinbrown Sep 4, 2025
4d8a0ef
Bump versions for release
thekevinbrown Sep 4, 2025
3127cb1
Add placeholder for numeric filter to match other filter inputs.
thekevinbrown Sep 4, 2025
ea5d8dc
Bump versions for release
thekevinbrown Sep 4, 2025
949e742
Bump globals from 16.0.0 to 16.3.0 in /src
dependabot[bot] Sep 4, 2025
587dded
Bump actions/setup-node from 4 to 5
dependabot[bot] Sep 4, 2025
338edec
Bump @types/react-dom from 19.1.7 to 19.1.9 in /src
dependabot[bot] Sep 4, 2025
dbe5e8d
Bump @types/semver from 7.7.0 to 7.7.1 in /src
dependabot[bot] Sep 4, 2025
0ab7b89
Bump the opentelemetry group in /src with 6 updates
dependabot[bot] Sep 5, 2025
dcf9a8a
Making the link field return a bit of placeholder text when it gets a…
mackcheesman Sep 5, 2025
24ced54
Bump the aws-sdk group in /src with 5 updates
dependabot[bot] Sep 7, 2025
46b61b2
Bump aws-cdk from 2.1028.0 to 2.1029.0 in /src in the aws-cdk group
dependabot[bot] Sep 7, 2025
8f5f61a
Bump the storybook group in /src with 4 updates
dependabot[bot] Sep 7, 2025
2d1b704
Bump the tiptap group in /src with 3 updates
dependabot[bot] Sep 7, 2025
97a5d3d
Bump yargs from 17.7.2 to 18.0.0 in /src
dependabot[bot] Sep 5, 2025
ba2a4b1
Attempting new API style
thekevinbrown Sep 5, 2025
829ee55
Try 'saner' import.
thekevinbrown Sep 5, 2025
9147ffc
Yargs upgrade feedback.
thekevinbrown Sep 8, 2025
a037bed
Bump @types/node from 24.3.0 to 24.3.1 in /src
dependabot[bot] Sep 8, 2025
26e44ee
Bump eslint-plugin-storybook from 9.1.4 to 9.1.5 in /src
dependabot[bot] Sep 8, 2025
cb22b01
Bump fastify from 5.5.0 to 5.6.0 in /src
dependabot[bot] Sep 8, 2025
a31a12f
Bump @azure/msal-browser from 4.21.1 to 4.22.0 in /src
dependabot[bot] Sep 8, 2025
d5faea5
Bump @graphql-codegen/typescript from 4.1.6 to 5.0.0 in /src
dependabot[bot] Sep 8, 2025
afbaaed
Bump eslint from 9.34.0 to 9.35.0 in /src
dependabot[bot] Sep 8, 2025
0114b85
Bump luxon from 3.7.1 to 3.7.2 in /src
dependabot[bot] Sep 8, 2025
5293fbb
Bump node-sqlite3-wasm from 0.8.47 to 0.8.48 in /src
dependabot[bot] Sep 8, 2025
6031948
Bump @graphql-codegen/typescript-operations from 4.6.1 to 5.0.0 in /src
dependabot[bot] Sep 8, 2025
aaa5251
Initial addition of count relationship behaviour.
thekevinbrown Sep 9, 2025
6bd95a3
Add relationship counting behaviour.
thekevinbrown Sep 9, 2025
b4ad5af
Added link functionality.
thekevinbrown Sep 9, 2025
bfca494
Link behaviour works.
thekevinbrown Sep 10, 2025
acd8ea8
Got links working from the table.
thekevinbrown Sep 10, 2025
d117512
Allow relationship filter to read both _in and pure PK filters when d…
thekevinbrown Sep 10, 2025
ca84a83
Bump vite from 7.1.4 to 7.1.5 in /src
dependabot[bot] Sep 8, 2025
87c16d6
Bump tiptap-markdown from 0.8.10 to 0.9.0 in /src
dependabot[bot] Sep 8, 2025
4bab759
Bump @tanstack/react-form from 1.19.3 to 1.19.4 in /src
dependabot[bot] Sep 8, 2025
ff5cbef
Bump @eslint/js from 9.34.0 to 9.35.0 in /src
dependabot[bot] Sep 8, 2025
5513685
Bump mysql2 from 3.14.4 to 3.14.5 in /src
dependabot[bot] Sep 9, 2025
f6f16df
Bump typescript-eslint from 8.42.0 to 8.43.0 in /src
dependabot[bot] Sep 9, 2025
4561d58
Bump the aws-sdk group in /src with 2 updates
dependabot[bot] Sep 9, 2025
55884c0
Bump @graphql-codegen/add from 5.0.3 to 6.0.0 in /src
dependabot[bot] Sep 9, 2025
9dd96ac
Don't call hook conditionally
thekevinbrown Sep 10, 2025
87fb97e
Break up mega function.
thekevinbrown Sep 10, 2025
eb39bfc
Add keyboard handler as well as click handler for link.
thekevinbrown Sep 10, 2025
3bfad60
Use nullish coalescing operator for simplicity instead of ternary.
thekevinbrown Sep 10, 2025
1bd0b3d
Bump versions for release
thekevinbrown Sep 10, 2025
41ffd53
Moved and repurposed deselection function, got rid of extraneous imports
mackcheesman Sep 11, 2025
ce77730
Added UI test for keyboard interaction
mackcheesman Sep 11, 2025
3d56f46
Merge branch 'main' into fix/combobox-improvements-and-tests
mackcheesman Sep 11, 2025
dca7090
Tiny bit more cleanup
mackcheesman Sep 11, 2025
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
87 changes: 36 additions & 51 deletions src/packages/admin-ui-components/src/combo-box/component.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import clsx from 'clsx';
import { useCombobox, useMultipleSelection } from 'downshift';
import { useCombobox } from 'downshift';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';

import { ChevronDownIcon } from '../assets';
Expand Down Expand Up @@ -94,10 +94,17 @@ export const ComboBox = ({
// Store the selected ids in a set for easy lookup - this is our source of truth for selection
const selectedIds = useMemo(() => new Set(valueArray.map((item) => item.value)), [valueArray]);

const { getSelectedItemProps, removeSelectedItem } =
useMultipleSelection({
initialSelectedItems: valueArray
});
const sortOptionsBySelectedFirst = (opt1: SelectOption, opt2: SelectOption) => {
return (selectedIds.has(opt2.value) ? 1 : 0) - (selectedIds.has(opt1.value) ? 1 : 0)
}

// Handle individual item deselection
const handleItemDeselect = useCallback(
(itemToRemove: SelectOption) => {
onChange(valueArray.filter((item) => item.value !== itemToRemove.value));
},
[onChange, valueArray]
);

const {
isOpen,
Expand Down Expand Up @@ -133,7 +140,7 @@ export const ComboBox = ({
if (mode === SelectMode.MULTI) {
if (change.selectedItem) {
if (selectedIds.has(change.selectedItem.value)) {
onChange(valueArray.filter((item) => item.value !== change.selectedItem?.value));
handleItemDeselect(change.selectedItem)
} else {
onChange([...valueArray, change.selectedItem]);
}
Expand Down Expand Up @@ -264,15 +271,6 @@ export const ComboBox = ({
if (isOpen) onOpen?.();
}, [isOpen]);

// Handle individual item deselection
const handleItemDeselect = useCallback(
(itemToRemove: SelectOption) => {
removeSelectedItem(itemToRemove)
onChange(valueArray.filter((item) => item.value !== itemToRemove.value));
},
[onChange, valueArray]
);

const handleOnPillKeyDown = useCallback(
(e: React.KeyboardEvent<HTMLDivElement>) => {
if (e.key === 'Backspace' || e.key === 'Delete') {
Expand Down Expand Up @@ -321,7 +319,7 @@ export const ComboBox = ({
</div>
)}
<div className={styles.inputWrapper}>
{/* This input needs to render always. This lets the browser handle some default behaviour around key presses. */}
{/* This input needs to render always. Keyboard navigation will break without it. */}
<input
readOnly={!allowFreeTyping}
className={styles.selectInput}
Expand All @@ -330,7 +328,7 @@ export const ComboBox = ({
ref: inputRef,
onBlur: handleBlur,
onFocus: openMenu,
placeholder: valueArray.length === 0 ? placeholder : undefined,
placeholder: valueArray.length === 0 ? placeholder : undefined
})}
/>
</div>
Expand Down Expand Up @@ -362,46 +360,33 @@ export const ComboBox = ({
<Spinner />
) : (
<>
{/* Show selected items at the top */}
{valueArray.map((selectedItem, index) => (
<li
key={selectedItem.value}
className={clsx(
styles.option,
styles.selectedOption
)}
data-testid={`selected-option-${selectedItem.label}`}
aria-label={`Remove ${selectedItem.label ?? 'Unknown'}`}
{...getSelectedItemProps({ selectedItem, index })}
onClick={() => handleItemDeselect(selectedItem)}
role='option'
aria-selected={true}
>
<span>{selectedItem.label ?? 'Unknown'}</span>
<span>
&times;
</span>
</li>
))}

{/* Show separator if there are selected items and regular options */}
{valueArray.length > 0 && options.length > 0 && <li className={styles.separator} />}

{/* Show non-selected options */}
{options.map((item, index) => {
// Skip selected items
if (selectedIds.has(item.value)) return null;

{
// Bump selected options to the top of the list.
options.sort(sortOptionsBySelectedFirst).map((item, index) => {
const isSelected = selectedIds.has(item.value);
const testId = `${isSelected ? 'selected' : 'combo'}-option-${item.label}`
return (
<li
className={clsx(styles.option, {
[styles.highlighted]: highlightedIndex === index,
})}
className={clsx(
styles.option,
{
[styles.selectedOption]: isSelected,
[styles.highlighted]: highlightedIndex === index,
[styles.selectedOptionHighlighted]: isSelected && highlightedIndex === index,
}
)}
key={String(item.value)}
aria-label={item.label}
{...getItemProps({ item, index })}
data-testid={`combo-option-${item.label}`}
data-testid={testId}

>
<span>{item.label}</span>
{isSelected && (
<span>
&times;
</span>
)}
</li>
);
})}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -253,7 +253,7 @@
transition: background-color 0.2s ease;
}

.selectedOption:hover {
.selectedOptionHighlighted {
background-color: var(--color-red);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,32 @@ test('Detail Panel - should allow deselecting of an entity in a one to many rela
);

await page.getByTestId('detail-panel-field-invoiceLines').click();
await page.getByRole('option', { name: '2' }).click();
await page.getByRole('option', { name: '2', exact: true }).click();

await expect(page.getByTestId('detail-panel-field-invoiceLines')).toContainText('invoiceLines1×');
});

test('Filter - should be able to interact with and select options using the keyboard', async ({
page,
}) => {
await page.goto(config.adminUiUrl);
await page.getByTestId('Album-entity-link').click();

// First arrow press opens the list
await page.getByTestId('artist-filter-input').press('ArrowDown');
// Go down to the third option
await page.getByTestId('artist-filter-input').press('ArrowDown');
await page.getByTestId('artist-filter-input').press('ArrowDown');
await page.getByTestId('artist-filter-input').press('ArrowDown');
await page.getByTestId('artist-filter-input').press('Enter');
await expect(page.getByTestId('artist-filter-box')).toContainText('Aaron Goldberg');

// Re-open the list, and then the selected option should now be the first in the list
await page.getByTestId('artist-filter-input').press('ArrowDown');
await expect(page.getByRole('option').first()).toContainText('Aaron Goldberg');

// Deselect
await page.getByTestId('artist-filter-input').press('ArrowDown');
await page.getByTestId('artist-filter-input').press('Enter');
await expect(page.getByTestId('artist-filter-box')).not.toContainText('Aaron Goldberg');
});
Loading