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 autoInstallPeers: true
excludeLinksFromLockfile: false excludeLinksFromLockfile: false
patchedDependencies:
'@umami/react-zen@0.247.0': 7a13e402d0a59d203f92fa4c64c8511aa38c5cd914092f5232c6271c606c9126
importers: importers:
.: .:
@@ -37,7 +40,7 @@ importers:
version: 5.101.4(react@19.2.8) version: 5.101.4(react@19.2.8)
'@umami/react-zen': '@umami/react-zen':
specifier: ^0.247.0 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: bcryptjs:
specifier: ^3.0.2 specifier: ^3.0.2
version: 3.0.3 version: 3.0.3
@@ -8265,7 +8268,7 @@ snapshots:
dependencies: dependencies:
'@types/yargs-parser': 21.0.3 '@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: 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) '@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) '@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 sharp: false
minimumReleaseAgeExclude: minimumReleaseAgeExclude:
- '@umami/react-zen@0.247.0' - '@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} /> <UserSelect teamId={teamId} />
</FormField> </FormField>
<FormField name="role" label={t(labels.role)} rules={{ required: 'Required' }}> <FormField name="role" label={t(labels.role)} rules={{ required: 'Required' }}>
<Select renderValue={value => renderRole(value as any)}> <Select>
{roles.map(value => ( {roles.map(value => (
<ListItem key={value} id={value}> <ListItem key={value} id={value}>
{renderRole(value)} {renderRole(value)}
@@ -3,7 +3,7 @@ import { Column, Grid, Label, ListItem, Loading, Select } from '@umami/react-zen
import { useState } from 'react'; import { useState } from 'react';
import { WebsiteControls } from '@/app/(main)/websites/[websiteId]/WebsiteControls'; import { WebsiteControls } from '@/app/(main)/websites/[websiteId]/WebsiteControls';
import { Empty } from '@/components/common/Empty'; 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 { useDateRange, useMessages, useWebsiteValuesQuery } from '@/components/hooks';
import { Attribution } from './Attribution'; import { Attribution } from './Attribution';
@@ -72,9 +72,9 @@ export function AttributionPage({ websiteId }: { websiteId: string }) {
allowSearch allowSearch
> >
{items.map(({ value }) => ( {items.map(({ value }) => (
<ListItem key={value} id={value}> <MultiSelectItem key={value} value={value}>
{value} {value}
</ListItem> </MultiSelectItem>
))} ))}
</MultiSelect> </MultiSelect>
</Column> </Column>
+3 -3
View File
@@ -11,7 +11,7 @@ import {
} from '@umami/react-zen'; } from '@umami/react-zen';
import { useState } from 'react'; import { useState } from 'react';
import { Empty } from '@/components/common/Empty'; 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 { useFilters, useFormat, useWebsiteValuesQuery } from '@/components/hooks';
import { X } from '@/components/icons'; import { X } from '@/components/icons';
import { isSearchOperator } from '@/lib/params'; import { isSearchOperator } from '@/lib/params';
@@ -108,9 +108,9 @@ export function FilterRecord({
allowSearch allowSearch
> >
{items.map(({ value }) => ( {items.map(({ value }) => (
<ListItem key={value} id={value}> <MultiSelectItem key={value} value={value}>
{formatValue(value, type)} {formatValue(value, type)}
</ListItem> </MultiSelectItem>
))} ))}
</MultiSelect> </MultiSelect>
)} )}
+1 -1
View File
@@ -35,5 +35,5 @@ export function LinkButton({
</Link> </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 { Button, Icon, Menu, MenuItem, MenuTrigger, SearchField } from '@umami/react-zen';
import type { ReactNode } from 'react'; import { Children, type ReactNode } from 'react';
import { ChevronRight } from '@/components/icons'; import { ChevronRight } from '@/components/icons';
const listStyle = { const listStyle = {
maxHeight: 'min(320px, calc(100dvh - 8rem))', maxHeight: 'min(320px, calc(100dvh - 8rem))',
overflowY: 'auto' as const,
}; };
const menuItemProps = { closeOnClick: false };
interface MultiSelectProps { interface MultiSelectProps {
value?: string[]; value?: string[];
onChange?: (values: string[]) => void; onChange?: (values: string[]) => void;
@@ -18,6 +21,14 @@ interface MultiSelectProps {
children: ReactNode; children: ReactNode;
} }
export function MultiSelectItem({ value, children }: { value: string; children: ReactNode }) {
return (
<MenuItem id={value} {...menuItemProps}>
{children}
</MenuItem>
);
}
export function MultiSelect({ export function MultiSelect({
value = [], value = [],
onChange, onChange,
@@ -49,28 +60,35 @@ export function MultiSelect({
<ChevronRight /> <ChevronRight />
</Icon> </Icon>
</Button> </Button>
<Popover style={{ minWidth: 'var(--trigger-width)', maxWidth: 'var(--trigger-width)' }}> <Menu
<Column selectionMode="multiple"
gap="2" selectedKeys={value}
padding="2" onSelectionChange={keys =>
border onChange?.(keys === 'all' ? [] : Array.from(keys, key => String(key)))
borderRadius="md" }
shadow="lg" className="flex flex-col gap-2"
className="bg-surface-overlay" style={{
> width: 'var(--anchor-width)',
{allowSearch && <SearchField value={searchValue} onSearch={onSearch} autoFocus />} maxWidth: 'var(--available-width)',
<List }}
selectionMode="multiple" >
value={value} {allowSearch && (
onChange={onChange} <SearchField
showCheckmark value={searchValue}
style={listStyle} onSearch={onSearch}
renderEmptyState={renderEmptyState} autoFocus
> onKeyDown={event => {
{children} if (event.key !== 'Escape' && event.key !== 'Tab') {
</List> event.stopPropagation();
</Column> }
</Popover> }}
/>
)}
<div style={listStyle}>
{children}
{Children.count(children) === 0 && renderEmptyState?.()}
</div>
</Menu>
</MenuTrigger> </MenuTrigger>
); );
} }
@@ -48,7 +48,14 @@ export function MetricsExpandedTable({
return ( return (
<> <>
<Row alignItems="center" paddingBottom="3"> <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> <Row justifyContent="flex-end" flexGrow={1} gap>
{children} {children}
{allowDownload && <DownloadButton filename={type} data={data} />} {allowDownload && <DownloadButton filename={type} data={data} />}
@@ -20,7 +20,7 @@ import { useState } from 'react';
import { ComboBox } from '@/components/common/ComboBox'; import { ComboBox } from '@/components/common/ComboBox';
import { DateDisplay } from '@/components/common/DateDisplay'; import { DateDisplay } from '@/components/common/DateDisplay';
import { Empty } from '@/components/common/Empty'; 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 { useMessages, usePropertyValuesQuery } from '@/components/hooks';
import type { PropertyDataSource } from '@/components/hooks/queries/usePropertyFieldsQuery'; import type { PropertyDataSource } from '@/components/hooks/queries/usePropertyFieldsQuery';
import { X } from '@/components/icons'; import { X } from '@/components/icons';
@@ -222,9 +222,9 @@ export function PropertyFilterRecord({
} }
> >
{filteredValues.map(v => ( {filteredValues.map(v => (
<ListItem key={v} id={v}> <MultiSelectItem key={v} value={v}>
{v} {v}
</ListItem> </MultiSelectItem>
))} ))}
</MultiSelect> </MultiSelect>
)} )}