feat: LemonInputSelect - new variant and select/clear all btns (#31695)

Co-authored-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Michael Matloka <michael@matloka.com>
This commit is contained in:
Pawel Cebula
2025-05-06 16:26:23 +02:00
committed by GitHub
parent a10f623c14
commit 5dee3a3c51
20 changed files with 141 additions and 6 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 494 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 505 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 494 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 505 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 494 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 505 B

View File

@@ -448,6 +448,18 @@ WithTooltip.args = {
),
}
export const WithTooltipPlacementAndArrowOffset: Story = BasicTemplate.bind({})
WithTooltipPlacementAndArrowOffset.args = {
...Default.args,
tooltip: (
<>
This is example with a link: <Link to="https://posthog.com">Go home</Link>
</>
),
tooltipPlacement: 'top-start',
tooltipArrowOffset: 30,
}
export const More_ = (): JSX.Element => {
return (
<More

View File

@@ -73,6 +73,8 @@ export interface LemonButtonPropsBase
truncate?: boolean
/** Wrap the main button element with a container element */
buttonWrapper?: (button: JSX.Element) => JSX.Element
/** Static offset (px) to adjust tooltip arrow position. Should only be used with fixed tooltipPlacement */
tooltipArrowOffset?: number
}
export type SideAction = Pick<
@@ -134,6 +136,7 @@ export const LemonButton: React.FunctionComponent<LemonButtonProps & React.RefAt
size,
tooltip,
tooltipPlacement,
tooltipArrowOffset,
htmlType = 'button',
noPadding,
to,
@@ -251,7 +254,12 @@ export const LemonButton: React.FunctionComponent<LemonButtonProps & React.RefAt
if (tooltipContent || tooltipDocLink) {
workingButton = (
<Tooltip title={tooltipContent} placement={tooltipPlacement} docLink={tooltipDocLink}>
<Tooltip
title={tooltipContent}
placement={tooltipPlacement}
arrowOffset={tooltipArrowOffset}
docLink={tooltipDocLink}
>
{workingButton}
</Tooltip>
)

View File

@@ -144,3 +144,44 @@ PrefilledManyValues.args = {
allowCustomValues: true,
value: names.map((_, i) => `user-${i}`),
}
export const WithSelectAllAndClear: Story = Template.bind({})
WithSelectAllAndClear.args = {
mode: 'multiple',
bulkActions: 'select-and-clear-all',
}
export const WithClearOnly: Story = Template.bind({})
WithClearOnly.args = {
mode: 'multiple',
bulkActions: 'clear-all',
}
export const CountModeAllSelected: Story = Template.bind({})
CountModeAllSelected.args = {
mode: 'multiple',
displayMode: 'count',
value: names.map((_, i) => `user-${i}`),
}
export const CountModePartiallySelected: Story = Template.bind({})
CountModePartiallySelected.args = {
mode: 'multiple',
displayMode: 'count',
value: names.slice(0, 10).map((_, i) => `user-${i}`),
}
export const CountModeNoneSelected: Story = Template.bind({})
CountModeNoneSelected.args = {
mode: 'multiple',
displayMode: 'count',
value: [],
}
export const CountModeWithSelectClear: Story = Template.bind({})
CountModeWithSelectClear.args = {
mode: 'multiple',
displayMode: 'count',
bulkActions: 'select-and-clear-all',
value: names.slice(0, 5).map((_, i) => `user-${i}`),
}

View File

@@ -49,6 +49,8 @@ export type LemonInputSelectProps = Pick<
popoverClassName?: string
size?: 'xsmall' | 'small' | 'medium' | 'large'
transparentBackground?: boolean
displayMode?: 'snacks' | 'count'
bulkActions?: 'clear-all' | 'select-and-clear-all'
}
export function LemonInputSelect({
@@ -74,6 +76,8 @@ export function LemonInputSelect({
transparentBackground,
autoWidth = true,
fullWidth = false,
displayMode = 'snacks',
bulkActions,
}: LemonInputSelectProps): JSX.Element {
const [showPopover, setShowPopover] = useState(false)
const [inputValue, _setInputValue] = useState('')
@@ -306,9 +310,10 @@ export function LemonInputSelect({
}
const valuesPrefix = useMemo(() => {
if (mode !== 'multiple' || values.length === 0) {
if (mode !== 'multiple' || values.length === 0 || displayMode !== 'snacks') {
return null
}
const preInputValues = itemBeingEditedIndex !== null ? values.slice(0, itemBeingEditedIndex) : values
// TRICKY: We don't want the popover to affect the snack buttons
@@ -327,7 +332,9 @@ export function LemonInputSelect({
const valuesAndEditButtonSuffix = useMemo(() => {
// The edit button only applies to single-select mode with custom values allowed, when in no-input state
const isEditButtonVisible = mode !== 'multiple' && allowCustomValues && values.length && !inputValue
const postInputValues = itemBeingEditedIndex !== null ? values.slice(itemBeingEditedIndex) : []
const postInputValues =
displayMode === 'snacks' && itemBeingEditedIndex !== null ? values.slice(itemBeingEditedIndex) : []
if (!isEditButtonVisible && postInputValues.length === 0) {
return null
@@ -359,6 +366,22 @@ export function LemonInputSelect({
)
}, [mode, values, allowCustomValues, itemBeingEditedIndex, inputValue])
// Positioned like a placeholder but rendered via the suffix since the actual placeholder has to be a string
const countPlaceholder = useMemo(() => {
if (displayMode !== 'count' || mode !== 'multiple' || inputValue) {
return null
}
return values.length === 0 ? (
<span className="-ml-2 text-muted">None selected</span>
) : (
<span className="-ml-2">
{values.length === options.length
? `All ${options.length} selected`
: `${values.length}/${options.length} selected`}
</span>
)
}, [displayMode, mode, inputValue, values.length, options.length])
return (
<LemonDropdown
matchWidth
@@ -380,6 +403,50 @@ export function LemonInputSelect({
overlay={
<div className="deprecated-space-y-px overflow-y-auto">
{title && <h5 className="mx-2 my-1">{title}</h5>}
{bulkActions && mode === 'multiple' && (
<div className="flex items-center mb-0.5" onMouseEnter={() => setSelectedIndex(-1)}>
{bulkActions === 'select-and-clear-all' && (
<LemonButton
size="small"
className="flex-1"
disabledReason={
values.length === allOptionsMap.size
? 'All options are already selected'
: undefined
}
tooltipPlacement="top-start"
tooltipArrowOffset={50}
onClick={() => onChange?.(Array.from(allOptionsMap.keys()))}
icon={
<LemonCheckbox
checked={
values.length === allOptionsMap.size
? true
: values.length
? 'indeterminate'
: false
}
className="pointer-events-none"
/>
}
>
Select all
</LemonButton>
)}
<LemonButton
size="small"
className={clsx({ 'flex-1': bulkActions === 'clear-all' })}
tooltipPlacement={bulkActions === 'select-and-clear-all' ? 'top-end' : 'top-start'}
tooltipArrowOffset={bulkActions === 'clear-all' ? 30 : undefined}
disabledReason={values.length === 0 ? 'No options are selected' : undefined}
onClick={() => onChange?.([])}
>
Clear all
</LemonButton>
</div>
)}
{visibleOptions.length > 0 ? (
visibleOptions.map((option, index) => {
const isFocused = index === selectedIndex
@@ -400,7 +467,7 @@ export function LemonInputSelect({
) : undefined
}
sideAction={
!option.__isInput
!option.__isInput && allowCustomValues
? {
// To reduce visual clutter we only show the icon on focus or hover,
// but we do want it present to make sure the layout is stable
@@ -458,7 +525,9 @@ export function LemonInputSelect({
<LemonInput
inputRef={inputRef}
placeholder={
values.length === 0
displayMode === 'count'
? undefined
: values.length === 0
? placeholder
: mode === 'single'
? allOptionsMap.get(values[0])?.label ?? values[0]
@@ -469,7 +538,12 @@ export function LemonInputSelect({
autoWidth={autoWidth}
fullWidth={fullWidth}
prefix={valuesPrefix}
suffix={valuesAndEditButtonSuffix}
suffix={
<>
{countPlaceholder}
{valuesAndEditButtonSuffix}
</>
}
onFocus={_onFocus}
onBlur={_onBlur}
value={inputValue}