Skip to content

New components for DataVis NITRO#144

Open
tvenable-mie wants to merge 4 commits intomainfrom
components-for-datavis-nitro
Open

New components for DataVis NITRO#144
tvenable-mie wants to merge 4 commits intomainfrom
components-for-datavis-nitro

Conversation

@tvenable-mie
Copy link

@tvenable-mie tvenable-mie commented Mar 23, 2026

I'm adding some UI widgets that are needed to support the parts of DataVis NITRO that aren't yet using mieweb/ui components — such as searchable dropdowns.

CleanShot 20260323 at 162444

Copilot AI review requested due to automatic review settings March 23, 2026 15:24
@cloudflare-workers-and-pages
Copy link

cloudflare-workers-and-pages bot commented Mar 23, 2026

Deploying ui with  Cloudflare Pages  Cloudflare Pages

Latest commit: 7f956f0
Status: ✅  Deploy successful!
Preview URL: https://0f331210.ui-6d0.pages.dev
Branch Preview URL: https://components-for-datavis-nitro.ui-6d0.pages.dev

View logs

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds new Dropdown capabilities to support DataVis NITRO UI needs (notably searchable and multi-select dropdown behaviors), along with supporting tests/stories and improved jsdom test environment setup.

Changes:

  • Extend Dropdown with searchable filtering UI and multi-select (checkbox + select-all) behavior.
  • Add Vitest coverage for searchable and multi-select dropdown behaviors.
  • Add a localStorage mock + per-test cleanup in jsdom test setup.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 4 comments.

File Description
src/test/setup.ts Adds localStorage mocking logic and clears storage between tests.
src/components/Dropdown/Dropdown.tsx Implements searchable dropdown rendering/filtering and multi-select + select-all support.
src/components/Dropdown/Dropdown.test.tsx Adds unit tests covering search filtering, empty state, multi-select, and select-all behaviors.
src/components/Dropdown/Dropdown.stories.tsx Adds Storybook examples for searchable and multi-select dropdown variants.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +7 to +33
if (
typeof window.localStorage === 'undefined' ||
typeof window.localStorage.getItem !== 'function'
) {
const storage = new Map<string, string>();
const localStorageMock: Storage = {
get length() {
return storage.size;
},
clear: () => {
storage.clear();
},
getItem: (key: string) => storage.get(key) ?? null,
key: (index: number) => Array.from(storage.keys())[index] ?? null,
removeItem: (key: string) => {
storage.delete(key);
},
setItem: (key: string, value: string) => {
storage.set(key, String(value));
},
};

Object.defineProperty(window, 'localStorage', {
configurable: true,
value: localStorageMock,
});
}
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Accessing window.localStorage can throw a SecurityError in jsdom when the origin is opaque (default about:blank). The current typeof window.localStorage... check will still evaluate window.localStorage and can fail before the mock is installed, and the unconditional window.localStorage.clear() in afterEach can throw for the same reason. Wrap the availability check in a try/catch (e.g., attempt a getItem), install the mock when access fails, and guard/try-catch the clear() in afterEach so the test setup is robust across environments.

Copilot uses AI. Check for mistakes.
Comment on lines 116 to 119
// Cleanup after each test case (e.g. clearing jsdom)
afterEach(() => {
window.localStorage.clear();
cleanup();
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

window.localStorage.clear() is called unconditionally. If localStorage is unavailable or throws in this jsdom configuration, this will fail the test run. After making the localStorage setup robust (e.g., via a try/catch mock), also guard this call (or wrap it in try/catch) so cleanup cannot throw.

Copilot uses AI. Check for mistakes.
Comment on lines +709 to +716
(isMultiSelectItem
? dropdownContext.selectedValues.includes(itemValue)
: false);

const handleClick = React.useCallback(
(event: React.MouseEvent<HTMLButtonElement>) => {
if (isMultiSelectItem && itemValue && !disabled) {
dropdownContext.toggleSelectedValue(itemValue);
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

dropdownContext is nullable (DropdownContextValue | null), but dropdownContext.selectedValues and dropdownContext.toggleSelectedValue are accessed unconditionally. With strict TS this is a compile-time error, and at runtime it will crash if DropdownItem is ever rendered outside a DropdownContext.Provider (e.g., if someone uses DropdownItem standalone or in tests). Fix by guarding on dropdownContext before reading it (or throwing a clear error), and only treating the item as multi-select when dropdownContext?.multiSelect is true. Also ensure itemValue is a string before passing it to includes() to satisfy strict typing.

Suggested change
(isMultiSelectItem
? dropdownContext.selectedValues.includes(itemValue)
: false);
const handleClick = React.useCallback(
(event: React.MouseEvent<HTMLButtonElement>) => {
if (isMultiSelectItem && itemValue && !disabled) {
dropdownContext.toggleSelectedValue(itemValue);
(isMultiSelectItem && dropdownContext && itemValue !== undefined
? dropdownContext.selectedValues.includes(itemValue)
: false);
const handleClick = React.useCallback(
(event: React.MouseEvent<HTMLButtonElement>) => {
if (isMultiSelectItem && itemValue && !disabled) {
if (dropdownContext) {
dropdownContext.toggleSelectedValue(itemValue);
}

Copilot uses AI. Check for mistakes.
Comment on lines +467 to +495
<div
id={menuId}
role="menu"
style={widthStyle}
className={cn(
'absolute z-50 min-w-[12rem]',
'rounded-xl border border-neutral-200 bg-white shadow-lg',
'dark:border-neutral-700 dark:bg-neutral-800',
'animate-in fade-in zoom-in-95 duration-100',
placementStyles[placement],
className
)}
>
{searchable && (
<div className="border-b border-neutral-200 p-2 dark:border-neutral-700">
<input
ref={searchInputRef}
type="search"
value={searchQuery}
onChange={(event) => setSearchQuery(event.target.value)}
placeholder={searchPlaceholder}
aria-label={searchAriaLabel}
className={cn(
inputVariants({ size: 'sm' }),
'text-sm',
'dark:border-neutral-600 dark:bg-neutral-700 dark:text-neutral-100'
)}
/>
</div>
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When searchable is enabled, an <input type="search"> is rendered inside the element with role="menu". Per ARIA menu patterns, a menu should generally contain only menuitem / menuitemcheckbox / menuitemradio (and separators), and embedding a textbox can confuse screen readers and keyboard expectations. Consider switching the popup role to a more appropriate pattern when searchable (e.g., role="listbox"/combobox, or role="dialog" with an internal list), or otherwise keep the search input outside the role="menu" container and ensure keyboard navigation remains accessible.

Copilot uses AI. Check for mistakes.
Copilot AI review requested due to automatic review settings March 23, 2026 19:52
@tvenable-mie tvenable-mie force-pushed the components-for-datavis-nitro branch from 9f3aab1 to 18b4fc3 Compare March 23, 2026 19:52
@tvenable-mie tvenable-mie force-pushed the components-for-datavis-nitro branch from 18b4fc3 to 7f956f0 Compare March 23, 2026 19:55
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 6 out of 6 changed files in this pull request and generated 3 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +716 to +723
if (
dropdownContext &&
isMultiSelectItem &&
itemValue !== undefined &&
!disabled
) {
dropdownContext.toggleSelectedValue(itemValue);
onCheckedChange?.(!isChecked);
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

onCheckedChange is only fired when a DropdownContext exists and the item has a string value. If a consumer uses checked/indeterminate + onCheckedChange (which the props API now allows) without providing a value or without multiSelect on the parent, clicks will never call onCheckedChange, so the item can’t be toggled. Consider firing onCheckedChange whenever isMultiSelectItem and not disabled, and separately toggling context selection only when dropdownContext + itemValue are present.

Suggested change
if (
dropdownContext &&
isMultiSelectItem &&
itemValue !== undefined &&
!disabled
) {
dropdownContext.toggleSelectedValue(itemValue);
onCheckedChange?.(!isChecked);
if (disabled) {
return;
}
const nextChecked = !isChecked;
if (isMultiSelectItem) {
onCheckedChange?.(nextChecked);
}
if (dropdownContext && itemValue !== undefined && isMultiSelectItem) {
dropdownContext.toggleSelectedValue(itemValue);

Copilot uses AI. Check for mistakes.
Comment on lines +742 to +747
type="button"
role={isMultiSelectItem ? 'menuitemcheckbox' : 'menuitem'}
aria-checked={
isMultiSelectItem ? (indeterminate ? 'mixed' : isChecked) : undefined
}
disabled={disabled}
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because {...props} is spread after type, role, and aria-checked, a consumer can accidentally override these accessibility-critical attributes via props. To ensure consistent semantics, spread props earlier (before type/role/aria-*) or omit/lock down those attributes in DropdownItemProps (e.g., Omit role/type/onClick) so they can’t be overridden.

Copilot uses AI. Check for mistakes.
Comment on lines +467 to +470
<div
id={menuId}
role="menu"
style={widthStyle}
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When searchable is enabled the menu renders an interactive <input type="search"> inside an element with role="menu". Per ARIA menu patterns, role="menu" is expected to contain only menuitem* descendants; mixing in a searchbox can lead to confusing screen reader/keyboard behavior. Consider switching to a combobox/listbox pattern for searchable dropdowns (or adjust roles/structure so the search input is not inside the menu role container).

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants