Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
97 changes: 97 additions & 0 deletions static/app/components/core/compactSelect/composite.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ Use `<CompositeSelect>` when you need a dropdown with multiple independent selec

- **`<CompositeSelect>`**: The wrapper component that manages the dropdown
- **`<CompositeSelect.Region>`**: Individual selection sections within the dropdown
- **`<CompositeSelect.ClearButton>`**: A "Clear" button for use in `menuHeaderTrailingItems` that calls your `onClick` to reset all regions. Re-exported from `<CompactSelect>`'s internal clear button for visual consistency

Each region acts like an independent `<CompactSelect>`, requiring its own `value`, `onChange`, and `options`.

Expand Down Expand Up @@ -124,6 +125,102 @@ const [day, setDay] = useState('1');
</CompositeSelect>;
```

## Clear Button

Use `<CompositeSelect.ClearButton>` in `menuHeaderTrailingItems` to add a "Clear" button to the menu header. When clicked, it calls your `onClick` handler where you reset each region's value. The menu stays open, matching the behavior of the built-in clear button in `<CompactSelect>`. The component is the same styled button used internally by `<CompactSelect>`, ensuring visual consistency.

Only render the button when there is an active selection to clear.

export function ClearButtonDemo() {
const monthOptions = [
{value: 'jan', label: 'January'},
{value: 'feb', label: 'February'},
{value: 'mar', label: 'March'},
];
const tagOptions = [
{value: 'cool', label: 'cool'},
{value: 'funny', label: 'funny'},
{value: 'awesome', label: 'awesome'},
];
const [month, setMonth] = useState('');
const [tags, setTags] = useState(tagOptions.slice(0, 0).map(o => o.value));
const hasSelection = month !== '' || tags.length > 0;
return (
<CompositeSelect
size="sm"
menuTitle="Filters"
menuHeaderTrailingItems={
hasSelection ? (
<CompositeSelect.ClearButton
onClick={() => {
setMonth('');
setTags([]);
}}
/>
) : null
}
trigger={props => (
<OverlayTrigger.Button icon={<IconSentry />} {...props}>
Filters
</OverlayTrigger.Button>
)}
>
<CompositeSelect.Region
label="Month"
value={month}
onChange={selection => setMonth(selection.value)}
options={monthOptions}
/>
<CompositeSelect.Region
label="Tags"
multiple
value={tags}
onChange={selection => setTags(selection.map(s => s.value))}
options={tagOptions}
/>
</CompositeSelect>
);
}

<Storybook.Demo minHeight="400px" align="start">
<ClearButtonDemo />
</Storybook.Demo>

```jsx
const [month, setMonth] = useState(null);
const [tags, setTags] = useState([]);
const hasSelection = month !== null || tags.length > 0;

<CompositeSelect
menuTitle="Filters"
menuHeaderTrailingItems={
hasSelection ? (
<CompositeSelect.ClearButton
onClick={() => {
setMonth(null);
setTags([]);
}}
/>
) : null
}
trigger={props => <OverlayTrigger.Button {...props}>Filters</OverlayTrigger.Button>}
>
<CompositeSelect.Region
label="Month"
value={month}
onChange={selection => setMonth(selection.value)}
options={monthOptions}
/>
<CompositeSelect.Region
label="Tags"
multiple
value={tags}
onChange={selection => setTags(selection.map(s => s.value))}
options={tagOptions}
/>
</CompositeSelect>;
```

## Multi-Select Regions

Individual regions can enable multi-select by setting the `multiple` prop. This allows mixing single and multi-select behavior within the same dropdown.
Expand Down
14 changes: 13 additions & 1 deletion static/app/components/core/compactSelect/composite.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@ import {FocusScope} from '@react-aria/focus';
import {Item} from '@react-stately/collections';
import type {DistributedOmit} from 'type-fest';

import {type ButtonProps} from '@sentry/scraps/button';

import {t} from 'sentry/locale';

import {ClearButton, Control} from './control';
import type {ControlProps} from './control';
import {Control} from './control';
import type {MultipleListProps, SingleListProps} from './list';
import {List} from './list';
import {EmptyMessage} from './styles';
Expand Down Expand Up @@ -118,6 +120,16 @@ CompositeSelect.Region = function <Value extends SelectKey>(
return null;
};

CompositeSelect.ClearButton = function CompositeSelectClearButton(
props: DistributedOmit<ButtonProps, 'priority' | 'size' | 'children'>
) {
return (
<ClearButton size="zero" priority="transparent" {...props}>
{t('Clear')}
</ClearButton>
);
};
Comment thread
nsdeschenes marked this conversation as resolved.

export {CompositeSelect};

type RegionProps<Value extends SelectKey> = CompositeSelectRegion<Value> & {
Expand Down
2 changes: 1 addition & 1 deletion static/app/components/core/compactSelect/control.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -643,7 +643,7 @@ const StyledLoadingIndicator = styled(LoadingIndicator)`
}
`;

const ClearButton = styled(Button)`
export const ClearButton = styled(Button)`
font-size: inherit; /* Inherit font size from MenuHeader */
font-weight: ${p => p.theme.font.weight.sans.regular};
color: ${p => p.theme.tokens.content.secondary};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,10 +48,18 @@ export function AggregateDropdown({traceMetric}: {traceMetric: TraceMetric}) {
}

const selectedList = [...selectedNames].filter(Boolean);
const defaultValue = DEFAULT_YAXIS_BY_TYPE[traceMetric.type];
const isDefaultSelection =
selectedList.length === 1 && selectedList[0] === defaultValue;

return (
<CompositeSelect
disabled={groups.length === 0}
menuHeaderTrailingItems={
isDefaultSelection
? undefined
: () => <CompositeSelect.ClearButton onClick={() => handleChange([])} />
}
style={{width: '100%'}}
trigger={triggerProps => (
<OverlayTrigger.Button
Comment thread
nsdeschenes marked this conversation as resolved.
Expand Down Expand Up @@ -88,22 +96,22 @@ export function AggregateDropdown({traceMetric}: {traceMetric: TraceMetric}) {
return (
<CompositeSelect.Region
key={groupKey}
label={group.label as string}
label={group.label}
multiple
options={group.options}
value={activeValues}
onChange={(opts: Array<SelectOption<string>>) => handleChange(opts)}
onChange={handleChange}
/>
);
}

return (
<CompositeSelect.Region
key={groupKey}
label={group.label as string}
label={group.label}
options={group.options}
value={activeValues[0] as string}
onChange={(opt: SelectOption<string>) => handleChange([opt])}
value={activeValues[0]}
onChange={opt => handleChange([opt])}
/>
);
})}
Expand Down
Loading