mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 09:25:06 +03:00
feat(ui): support bounded MultiSelector wrapping (#50439)
## What kind of change does this PR introduce?
UI component enhancement and Design System documentation update.
## What is the current behavior?
`MultiSelectorTrigger` can either limit the number of visible badges or
wrap every selected badge. Consumers cannot combine a numeric limit with
wrapping.
## What is the new behavior?
Adds `wrapBadges` so consumers can combine it with a numeric
`badgeLimit`. For example, `badgeLimit={3} wrapBadges` renders up to
three wrapped badges followed by the remaining `+n` count.
Existing `badgeLimit=\"wrap\"` behaviour remains supported.
## To test
1. Open the Design System Multi Select page.
2. Find the **Wrapped badge limit** example.
3. Confirm three selected fruit badges wrap within the trigger and the
remaining selections appear as `+2`.
4. Remove or add selections and confirm the visible badges and remaining
count update together.
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
- **New Features**
- Added a `wrapBadges` option to multi-select triggers, allowing
selected badges to wrap across multiple lines while retaining a numeric
badge limit.
- Numeric limits now show the specified badges and an overflow count for
additional selections.
- Updated the example with controls for adjusting the badge limit.
- **Documentation**
- Clarified multi-select badge limit and wrapping behavior.
- **Tests**
- Added coverage for badge limits, wrapping, visible selections, and
overflow counts.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
1 parent
229d04d65c
commit
7e9c483625
4 files changed
+68
-34
No files matched your search
@@ -60,16 +60,16 @@ creatable: `boolean`
|
||||
|
||||
<ComponentPreview name="multi-select-combobox-creatable" />
|
||||
|
||||
### Badge Limit
|
||||
### Badge limit
|
||||
|
||||
badgeLimit: `number` | `"wrap"`.
|
||||
`badgeLimit` prop on the `MultiSelectorTrigger` component can be used to limit the number of badges displayed.
|
||||
|
||||
<ComponentPreview name="multi-select-badge-limit" />
|
||||
|
||||
### Badge Limit="wrap"
|
||||
### Wrapped badge limit
|
||||
|
||||
`badgeLimit` prop can also be "wrap" to wrap the badges to the next line.
|
||||
Combine `badgeLimit` with `wrapBadges` to limit the number of badges and allow them to wrap onto additional lines. Use `badgeLimit="wrap"` to show and wrap every selected badge.
|
||||
|
||||
<ComponentPreview name="multi-select-badge-limit-wrap" />
|
||||
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { Minus, Plus } from 'lucide-react'
|
||||
import { useState } from 'react'
|
||||
import { Button } from 'ui'
|
||||
import {
|
||||
MultiSelector,
|
||||
MultiSelectorContent,
|
||||
@@ -15,30 +17,42 @@ export default function MultiSelectDemo() {
|
||||
'Date',
|
||||
'Elderberrie',
|
||||
])
|
||||
const [limit, setLimit] = useState(3)
|
||||
|
||||
return (
|
||||
<MultiSelector values={selectedValues} onValuesChange={setSelectedValues}>
|
||||
<MultiSelectorTrigger
|
||||
className="w-72"
|
||||
label="Select fruits"
|
||||
persistLabel
|
||||
badgeLimit="wrap"
|
||||
deletableBadge={false}
|
||||
/>
|
||||
<MultiSelectorContent>
|
||||
<MultiSelectorList>
|
||||
<MultiSelectorItem value="Apple">Apple</MultiSelectorItem>
|
||||
<MultiSelectorItem value="Banana">Banana</MultiSelectorItem>
|
||||
<MultiSelectorItem value="Cherry">Cherry</MultiSelectorItem>
|
||||
<MultiSelectorItem value="Date">Date</MultiSelectorItem>
|
||||
<MultiSelectorItem value="Elderberrie">Elderberrie</MultiSelectorItem>
|
||||
<MultiSelectorItem value="Fig">Fig</MultiSelectorItem>
|
||||
<MultiSelectorItem value="Grape">Grape</MultiSelectorItem>
|
||||
<MultiSelectorItem value="Kiwi">Kiwi</MultiSelectorItem>
|
||||
<MultiSelectorItem value="Mango">Mango</MultiSelectorItem>
|
||||
<MultiSelectorItem value="Strawberry">Strawberry</MultiSelectorItem>
|
||||
</MultiSelectorList>
|
||||
</MultiSelectorContent>
|
||||
</MultiSelector>
|
||||
<div className="flex flex-col items-center gap-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<Button size="tiny" onClick={() => setLimit((value) => value - 1)} disabled={limit < 1}>
|
||||
<Minus size={12} />
|
||||
</Button>
|
||||
<span className="text-sm font-semibold text-foreground/90">Limit: {limit}</span>
|
||||
<Button size="tiny" onClick={() => setLimit((value) => value + 1)}>
|
||||
<Plus size={12} />
|
||||
</Button>
|
||||
</div>
|
||||
<MultiSelector values={selectedValues} onValuesChange={setSelectedValues}>
|
||||
<MultiSelectorTrigger
|
||||
className="w-72"
|
||||
label="Select fruits"
|
||||
badgeLimit={limit}
|
||||
wrapBadges
|
||||
deletableBadge={false}
|
||||
/>
|
||||
<MultiSelectorContent>
|
||||
<MultiSelectorList>
|
||||
<MultiSelectorItem value="Apple">Apple</MultiSelectorItem>
|
||||
<MultiSelectorItem value="Banana">Banana</MultiSelectorItem>
|
||||
<MultiSelectorItem value="Cherry">Cherry</MultiSelectorItem>
|
||||
<MultiSelectorItem value="Date">Date</MultiSelectorItem>
|
||||
<MultiSelectorItem value="Elderberrie">Elderberrie</MultiSelectorItem>
|
||||
<MultiSelectorItem value="Fig">Fig</MultiSelectorItem>
|
||||
<MultiSelectorItem value="Grape">Grape</MultiSelectorItem>
|
||||
<MultiSelectorItem value="Kiwi">Kiwi</MultiSelectorItem>
|
||||
<MultiSelectorItem value="Mango">Mango</MultiSelectorItem>
|
||||
<MultiSelectorItem value="Strawberry">Strawberry</MultiSelectorItem>
|
||||
</MultiSelectorList>
|
||||
</MultiSelectorContent>
|
||||
</MultiSelector>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -97,6 +97,24 @@ describe('multi-select', () => {
|
||||
expect(badge).toHaveClass('normal-case', 'tracking-normal')
|
||||
})
|
||||
|
||||
it('supports wrapping badges with a numeric badge limit', () => {
|
||||
render(
|
||||
<MultiSelector
|
||||
values={['Apple', 'Banana', 'Cherry', 'Date', 'Fig']}
|
||||
onValuesChange={() => undefined}
|
||||
>
|
||||
<MultiSelectorTrigger badgeLimit={2} wrapBadges />
|
||||
</MultiSelector>
|
||||
)
|
||||
|
||||
const trigger = screen.getByRole('combobox')
|
||||
expect(trigger.firstElementChild).toHaveClass('flex-wrap')
|
||||
expect(trigger).toHaveTextContent('Apple')
|
||||
expect(trigger).toHaveTextContent('Banana')
|
||||
expect(trigger).toHaveTextContent('+3')
|
||||
expect(trigger).not.toHaveTextContent('Cherry')
|
||||
})
|
||||
|
||||
it('opens the dropdown when the MultiSelectorTrigger is clicked', () => {
|
||||
render(<MultiSelectDemo />)
|
||||
|
||||
|
||||
@@ -223,6 +223,7 @@ export interface MultiSelectorTriggerProps extends React.HTMLAttributes<HTMLButt
|
||||
persistLabel?: boolean
|
||||
className?: string
|
||||
badgeLimit?: number | 'wrap'
|
||||
wrapBadges?: boolean
|
||||
deletableBadge?: boolean
|
||||
showIcon?: boolean
|
||||
mode?: MultiSelectorMode
|
||||
@@ -335,6 +336,7 @@ const MultiSelectorTrigger = React.forwardRef<HTMLButtonElement, MultiSelectorTr
|
||||
className,
|
||||
deletableBadge = true,
|
||||
badgeLimit = 9999,
|
||||
wrapBadges = false,
|
||||
showIcon = true,
|
||||
mode = 'combobox',
|
||||
renderValue,
|
||||
@@ -357,7 +359,7 @@ const MultiSelectorTrigger = React.forwardRef<HTMLButtonElement, MultiSelectorTr
|
||||
const [extraBadgesCount, setExtraBadgesCount] = React.useState(0)
|
||||
const [isDeleteHovered, setIsDeleteHovered] = React.useState(false)
|
||||
|
||||
const IS_BADGE_LIMIT_WRAP = badgeLimit === 'wrap'
|
||||
const SHOULD_WRAP_BADGES = wrapBadges || badgeLimit === 'wrap'
|
||||
const IS_NUMERIC_LIMIT = typeof badgeLimit === 'number'
|
||||
const IS_INLINE_MODE = mode === 'inline-combobox'
|
||||
const HAS_TINY_PLACEHOLDER = size === 'tiny' && values.length === 0
|
||||
@@ -365,12 +367,12 @@ const MultiSelectorTrigger = React.forwardRef<HTMLButtonElement, MultiSelectorTr
|
||||
React.useEffect(() => {
|
||||
if (!inputRef?.current || !badgesRef.current) return
|
||||
|
||||
if (IS_BADGE_LIMIT_WRAP) {
|
||||
setVisibleBadges(values)
|
||||
setExtraBadgesCount(0)
|
||||
} else {
|
||||
if (IS_NUMERIC_LIMIT) {
|
||||
setVisibleBadges(values.slice(0, badgeLimit))
|
||||
setExtraBadgesCount(Math.max(0, values.length - badgeLimit))
|
||||
} else {
|
||||
setVisibleBadges(values)
|
||||
setExtraBadgesCount(0)
|
||||
}
|
||||
}, [values, badgeLimit])
|
||||
|
||||
@@ -427,8 +429,8 @@ const MultiSelectorTrigger = React.forwardRef<HTMLButtonElement, MultiSelectorTr
|
||||
ref={badgesRef}
|
||||
className={cn(
|
||||
MultiSelectorBadgesVariants({ size }),
|
||||
IS_BADGE_LIMIT_WRAP && 'flex-wrap',
|
||||
!IS_BADGE_LIMIT_WRAP &&
|
||||
SHOULD_WRAP_BADGES && 'flex-wrap',
|
||||
!SHOULD_WRAP_BADGES &&
|
||||
'overflow-x-auto scrollbar-thin scrollbar-track-transparent transition-colors scrollbar-thumb-muted-foreground dark:scrollbar-thumb-muted scrollbar-thumb-rounded-lg'
|
||||
)}
|
||||
>
|
||||
@@ -478,7 +480,7 @@ const MultiSelectorTrigger = React.forwardRef<HTMLButtonElement, MultiSelectorTr
|
||||
autoFocus={false}
|
||||
wrapperClassName={cn(
|
||||
MultiSelectorInlineInputWrapperVariants({ size }),
|
||||
IS_BADGE_LIMIT_WRAP && 'min-w-[85px]'
|
||||
SHOULD_WRAP_BADGES && 'min-w-[85px]'
|
||||
)}
|
||||
className={cn(
|
||||
MultiSelectorInlineInputVariants({ size }),
|
||||
|
||||
Reference in new issue
Block a user