Fix multi-select menus and selected label rendering

This commit is contained in:
Mike Cao committed 2026-07-30 23:47:34 -07:00
1 parent 0941ea06f9
commit cfe8020831
10 files changed
+142 -38

No files matched your search

+74
View File
@@ -0,0 +1,74 @@
diff --git a/dist/index.js b/dist/index.js
index 2dd4992d4f033c22be13a8846637d48ef235c57f..cf5264a8ea0b6803b6fb9a950d35e7bd1ed0f959 100644
--- a/dist/index.js
+++ b/dist/index.js
@@ -6600,6 +6600,23 @@ function SearchField({
] })
] });
}
+function getSelectItemLabel(children, selectedValue) {
+ let label;
+ react.Children.forEach(children, (child) => {
+ if (label !== void 0 || !react.isValidElement(child)) {
+ return;
+ }
+ if (child.type === ListItem) {
+ const itemValue = child.props.value ?? child.props.id ?? (typeof child.props.children === "string" ? child.props.children : "");
+ if (itemValue === selectedValue) {
+ label = child.props.children;
+ }
+ return;
+ }
+ label = getSelectItemLabel(child.props.children, selectedValue);
+ });
+ return label;
+}
function Select({
value,
defaultValue,
@@ -6666,7 +6683,7 @@ function Select({
),
children: [
/* @__PURE__ */ jsxRuntime.jsx(select.Select.Value, { placeholder, children: (selected) => {
- const defaultChildren = selected ?? placeholder;
+ const defaultChildren = selected == null ? placeholder : getSelectItemLabel(collection, selected) ?? selected;
return typeof renderValue === "function" ? renderValue({
defaultChildren,
isPlaceholder: selected == null
diff --git a/dist/index.mjs b/dist/index.mjs
index 448a673d0ef00587ba6a233d5e5f8e99efc8215e..f0c0f4e7a5a9f0e2066225e1598f944fbc033c6e 100644
--- a/dist/index.mjs
+++ b/dist/index.mjs
@@ -6575,6 +6575,23 @@ function SearchField({
] })
] });
}
+function getSelectItemLabel(children, selectedValue) {
+ let label;
+ Children.forEach(children, (child) => {
+ if (label !== void 0 || !isValidElement(child)) {
+ return;
+ }
+ if (child.type === ListItem) {
+ const itemValue = child.props.value ?? child.props.id ?? (typeof child.props.children === "string" ? child.props.children : "");
+ if (itemValue === selectedValue) {
+ label = child.props.children;
+ }
+ return;
+ }
+ label = getSelectItemLabel(child.props.children, selectedValue);
+ });
+ return label;
+}
function Select({
value,
defaultValue,
@@ -6641,7 +6658,7 @@ function Select({
),
children: [
/* @__PURE__ */ jsx(Select$1.Value, { placeholder, children: (selected) => {
- const defaultChildren = selected ?? placeholder;
+ const defaultChildren = selected == null ? placeholder : getSelectItemLabel(collection, selected) ?? selected;
return typeof renderValue === "function" ? renderValue({
defaultChildren,
isPlaceholder: selected == null
+5 -2
View File
@@ -4,6 +4,9 @@ settings:
autoInstallPeers: true
excludeLinksFromLockfile: false
patchedDependencies:
'@umami/react-zen@0.247.0': 7a13e402d0a59d203f92fa4c64c8511aa38c5cd914092f5232c6271c606c9126
importers:
.:
@@ -37,7 +40,7 @@ importers:
version: 5.101.4(react@19.2.8)
'@umami/react-zen':
specifier: ^0.247.0
version: 0.247.0(@date-fns/tz@1.5.0)(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(date-fns@4.4.0)(immer@11.1.15)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(tailwindcss@4.1.18)(use-sync-external-store@1.6.0(react@19.2.8))
version: 0.247.0(patch_hash=7a13e402d0a59d203f92fa4c64c8511aa38c5cd914092f5232c6271c606c9126)(@date-fns/tz@1.5.0)(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(date-fns@4.4.0)(immer@11.1.15)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(tailwindcss@4.1.18)(use-sync-external-store@1.6.0(react@19.2.8))
bcryptjs:
specifier: ^3.0.2
version: 3.0.3
@@ -8265,7 +8268,7 @@ snapshots:
dependencies:
'@types/yargs-parser': 21.0.3
'@umami/react-zen@0.247.0(@date-fns/tz@1.5.0)(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(date-fns@4.4.0)(immer@11.1.15)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(tailwindcss@4.1.18)(use-sync-external-store@1.6.0(react@19.2.8))':
'@umami/react-zen@0.247.0(patch_hash=7a13e402d0a59d203f92fa4c64c8511aa38c5cd914092f5232c6271c606c9126)(@date-fns/tz@1.5.0)(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(date-fns@4.4.0)(immer@11.1.15)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(tailwindcss@4.1.18)(use-sync-external-store@1.6.0(react@19.2.8))':
dependencies:
'@base-ui/react': 1.6.0(@date-fns/tz@1.5.0)(@types/react@19.2.17)(date-fns@4.4.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)
'@react-spring/web': 9.7.5(react-dom@19.2.8(react@19.2.8))(react@19.2.8)
+2
View File
@@ -12,3 +12,5 @@ allowBuilds:
sharp: false
minimumReleaseAgeExclude:
- '@umami/react-zen@0.247.0'
patchedDependencies:
'@umami/react-zen@0.247.0': patches/@umami__react-zen@0.247.0.patch
+1 -1
View File
@@ -51,7 +51,7 @@ export function TeamMemberAddForm({
<UserSelect teamId={teamId} />
</FormField>
<FormField name="role" label={t(labels.role)} rules={{ required: 'Required' }}>
<Select renderValue={value => renderRole(value as any)}>
<Select>
{roles.map(value => (
<ListItem key={value} id={value}>
{renderRole(value)}
@@ -3,7 +3,7 @@ import { Column, Grid, Label, ListItem, Loading, Select } from '@umami/react-zen
import { useState } from 'react';
import { WebsiteControls } from '@/app/(main)/websites/[websiteId]/WebsiteControls';
import { Empty } from '@/components/common/Empty';
import { MultiSelect } from '@/components/common/MultiSelect';
import { MultiSelect, MultiSelectItem } from '@/components/common/MultiSelect';
import { useDateRange, useMessages, useWebsiteValuesQuery } from '@/components/hooks';
import { Attribution } from './Attribution';
@@ -72,9 +72,9 @@ export function AttributionPage({ websiteId }: { websiteId: string }) {
allowSearch
>
{items.map(({ value }) => (
<ListItem key={value} id={value}>
<MultiSelectItem key={value} value={value}>
{value}
</ListItem>
</MultiSelectItem>
))}
</MultiSelect>
</Column>
+3 -3
View File
@@ -11,7 +11,7 @@ import {
} from '@umami/react-zen';
import { useState } from 'react';
import { Empty } from '@/components/common/Empty';
import { MultiSelect } from '@/components/common/MultiSelect';
import { MultiSelect, MultiSelectItem } from '@/components/common/MultiSelect';
import { useFilters, useFormat, useWebsiteValuesQuery } from '@/components/hooks';
import { X } from '@/components/icons';
import { isSearchOperator } from '@/lib/params';
@@ -108,9 +108,9 @@ export function FilterRecord({
allowSearch
>
{items.map(({ value }) => (
<ListItem key={value} id={value}>
<MultiSelectItem key={value} value={value}>
{formatValue(value, type)}
</ListItem>
</MultiSelectItem>
))}
</MultiSelect>
)}
+1 -1
View File
@@ -35,5 +35,5 @@ export function LinkButton({
</Link>
);
return <Button {...props} variant={variant} render={linkElement} />;
return <Button {...props} variant={variant} nativeButton={false} render={linkElement} />;
}
+42 -24
View File
@@ -1,11 +1,14 @@
import { Button, Column, Icon, List, MenuTrigger, Popover, SearchField } from '@umami/react-zen';
import type { ReactNode } from 'react';
import { Button, Icon, Menu, MenuItem, MenuTrigger, SearchField } from '@umami/react-zen';
import { Children, type ReactNode } from 'react';
import { ChevronRight } from '@/components/icons';
const listStyle = {
maxHeight: 'min(320px, calc(100dvh - 8rem))',
overflowY: 'auto' as const,
};
const menuItemProps = { closeOnClick: false };
interface MultiSelectProps {
value?: string[];
onChange?: (values: string[]) => void;
@@ -18,6 +21,14 @@ interface MultiSelectProps {
children: ReactNode;
}
export function MultiSelectItem({ value, children }: { value: string; children: ReactNode }) {
return (
<MenuItem id={value} {...menuItemProps}>
{children}
</MenuItem>
);
}
export function MultiSelect({
value = [],
onChange,
@@ -49,28 +60,35 @@ export function MultiSelect({
<ChevronRight />
</Icon>
</Button>
<Popover style={{ minWidth: 'var(--trigger-width)', maxWidth: 'var(--trigger-width)' }}>
<Column
gap="2"
padding="2"
border
borderRadius="md"
shadow="lg"
className="bg-surface-overlay"
>
{allowSearch && <SearchField value={searchValue} onSearch={onSearch} autoFocus />}
<List
selectionMode="multiple"
value={value}
onChange={onChange}
showCheckmark
style={listStyle}
renderEmptyState={renderEmptyState}
>
{children}
</List>
</Column>
</Popover>
<Menu
selectionMode="multiple"
selectedKeys={value}
onSelectionChange={keys =>
onChange?.(keys === 'all' ? [] : Array.from(keys, key => String(key)))
}
className="flex flex-col gap-2"
style={{
width: 'var(--anchor-width)',
maxWidth: 'var(--available-width)',
}}
>
{allowSearch && (
<SearchField
value={searchValue}
onSearch={onSearch}
autoFocus
onKeyDown={event => {
if (event.key !== 'Escape' && event.key !== 'Tab') {
event.stopPropagation();
}
}}
/>
)}
<div style={listStyle}>
{children}
{Children.count(children) === 0 && renderEmptyState?.()}
</div>
</Menu>
</MenuTrigger>
);
}
@@ -48,7 +48,14 @@ export function MetricsExpandedTable({
return (
<>
<Row alignItems="center" paddingBottom="3">
{allowSearch && <SearchField value={search} onSearch={setSearch} delay={300} />}
{allowSearch && (
<SearchField
value={search}
onSearch={setSearch}
delay={300}
className="w-full max-w-md"
/>
)}
<Row justifyContent="flex-end" flexGrow={1} gap>
{children}
{allowDownload && <DownloadButton filename={type} data={data} />}
@@ -20,7 +20,7 @@ import { useState } from 'react';
import { ComboBox } from '@/components/common/ComboBox';
import { DateDisplay } from '@/components/common/DateDisplay';
import { Empty } from '@/components/common/Empty';
import { MultiSelect } from '@/components/common/MultiSelect';
import { MultiSelect, MultiSelectItem } from '@/components/common/MultiSelect';
import { useMessages, usePropertyValuesQuery } from '@/components/hooks';
import type { PropertyDataSource } from '@/components/hooks/queries/usePropertyFieldsQuery';
import { X } from '@/components/icons';
@@ -222,9 +222,9 @@ export function PropertyFilterRecord({
}
>
{filteredValues.map(v => (
<ListItem key={v} id={v}>
<MultiSelectItem key={v} value={v}>
{v}
</ListItem>
</MultiSelectItem>
))}
</MultiSelect>
)}