Skip to content

Commit

Permalink
feat: show empty text in dropdowns (#708)
Browse files Browse the repository at this point in the history
* Show empty text

* Use component as props

* Pass NoOptions component via children
  • Loading branch information
poulch committed Jan 9, 2024
1 parent 50c3883 commit a59d67c
Show file tree
Hide file tree
Showing 16 changed files with 9,152 additions and 4,286 deletions.
13,219 changes: 8,946 additions & 4,273 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

26 changes: 26 additions & 0 deletions src/components/BaseSelect/NoOptions.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { Children, ReactNode, isValidElement } from "react";
import { Text } from "~/components";

interface NoOptionsProps {
children: ReactNode;
}

export const NoOptions = ({ children }: NoOptionsProps) => {
return (
<Text as="p" padding={2} textAlign="center" fontStyle="italic">
{children}
</Text>
);
};

export const hasNoOptions = (children: ReactNode): boolean => {
let hasNoOptions = false;

Children.forEach(children, (child) => {
if (isValidElement(child) && child.type === NoOptions) {
hasNoOptions = true;
}
});

return hasNoOptions;
};
6 changes: 6 additions & 0 deletions src/components/BaseSelect/helpers.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,22 @@
export const getListDisplayMode = ({
isOpen,
hasItemsToSelect,
showEmptyState,
loading,
}: {
isOpen: boolean;
hasItemsToSelect: boolean;
showEmptyState: boolean;
loading?: boolean;
}) => {
if (isOpen && hasItemsToSelect) {
return "block";
}

if (isOpen && showEmptyState) {
return "block";
}

if (isOpen && loading) {
return "block";
}
Expand Down
1 change: 1 addition & 0 deletions src/components/BaseSelect/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ export * from "./BaseSelect.css";
export * from "./LoadingListItem";
export * from "./types";
export * from "./helpers";
export * from "./NoOptions";
14 changes: 14 additions & 0 deletions src/components/Combobox/Dynamic/DynamicCombobox.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -163,3 +163,17 @@ export const Loading = () => {
/>
);
};

export const NoOptions = () => {
return (
<DynamicCombobox
__width="200px"
value={null}
label="Pick star wars character"
onChange={() => undefined}
options={[]}
>
<DynamicCombobox.NoOptions>No items to select</DynamicCombobox.NoOptions>
</DynamicCombobox>
);
};
20 changes: 18 additions & 2 deletions src/components/Combobox/Dynamic/DynamicCombobox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,12 @@ import { HelperText, inputRecipe, InputVariants } from "../../BaseInput";
import {
getListDisplayMode,
getListTextSize,
hasNoOptions,
listItemStyle,
listStyle,
listWrapperRecipe,
LoadingListItem,
NoOptions,
Option,
SingleChangeHandler,
} from "../../BaseSelect";
Expand Down Expand Up @@ -48,6 +50,7 @@ export type DynamicComboboxProps<T> = PropsWithBox<
value: T | null;
onInputValueChange?: (value: string) => void;
loading?: boolean;
children?: ReactNode;
locale?: {
loadingText?: string;
};
Expand All @@ -73,6 +76,7 @@ const DynamicComboboxInner = <T extends Option>(
onBlur,
loading,
locale,
children,
startAdornment,
endAdornment,
onScrollEnd,
Expand Down Expand Up @@ -142,7 +146,12 @@ const DynamicComboboxInner = <T extends Option>(
<Portal asChild ref={refs.setFloating} style={floatingStyles}>
<Box
position="relative"
display={getListDisplayMode({ isOpen, hasItemsToSelect, loading })}
display={getListDisplayMode({
isOpen,
loading,
hasItemsToSelect,
showEmptyState: hasNoOptions(children),
})}
className={listWrapperRecipe({ size })}
>
<List
Expand All @@ -168,6 +177,9 @@ const DynamicComboboxInner = <T extends Option>(
{item?.endAdornment}
</List.Item>
))}

{isOpen && !loading && !hasItemsToSelect && children}

{loading && (
<LoadingListItem size={size}>
{locale?.loadingText ?? "Loading"}
Expand All @@ -191,10 +203,14 @@ const DynamicComboboxInner = <T extends Option>(
);
};

export const DynamicCombobox = forwardRef(DynamicComboboxInner) as <
const DynamicComboboxRoot = forwardRef(DynamicComboboxInner) as <
T extends Option,
>(
props: DynamicComboboxProps<T> & {
ref?: React.ForwardedRef<HTMLInputElement>;
}
) => ReturnType<typeof DynamicComboboxInner>;

export const DynamicCombobox = Object.assign(DynamicComboboxRoot, {
NoOptions,
});
19 changes: 17 additions & 2 deletions src/components/Combobox/Static/Combobox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,12 @@ import {
import { Box, List, PropsWithBox, Text } from "~/components";
import { HelperText, InputVariants, inputRecipe } from "~/components/BaseInput";
import {
NoOptions,
Option,
SingleChangeHandler,
getListDisplayMode,
getListTextSize,
hasNoOptions,
listItemStyle,
listStyle,
listWrapperRecipe,
Expand Down Expand Up @@ -40,6 +43,7 @@ export type ComboboxProps<T, V> = PropsWithBox<
startAdornment?: (inputValue: V | null) => ReactNode;
endAdornment?: (inputValue: V | null) => ReactNode;
helperText?: ReactNode;
children?: ReactNode;
options: T[];
onChange?: SingleChangeHandler<V | null>;
value: V | null;
Expand All @@ -63,6 +67,7 @@ const ComboboxInner = <T extends Option, V extends Option | string>(
onBlur,
startAdornment,
endAdornment,
children,
...props
}: ComboboxProps<T, V>,
ref: ForwardedRef<HTMLInputElement>
Expand Down Expand Up @@ -132,7 +137,11 @@ const ComboboxInner = <T extends Option, V extends Option | string>(
<Portal asChild ref={refs.setFloating} style={floatingStyles}>
<Box
position="relative"
display={isOpen && hasItemsToSelect ? "block" : "none"}
display={getListDisplayMode({
hasItemsToSelect,
isOpen,
showEmptyState: hasNoOptions(children),
})}
className={listWrapperRecipe({ size })}
>
<List
Expand All @@ -158,6 +167,8 @@ const ComboboxInner = <T extends Option, V extends Option | string>(
{item?.endAdornment}
</List.Item>
))}

{isOpen && !hasItemsToSelect && children}
</List>
</Box>
</Portal>
Expand All @@ -171,9 +182,13 @@ const ComboboxInner = <T extends Option, V extends Option | string>(
);
};

export const Combobox = forwardRef(ComboboxInner) as <
const ComboboxRoot = forwardRef(ComboboxInner) as <
T extends Option,
V extends Option | string,
>(
props: ComboboxProps<T, V> & { ref?: React.ForwardedRef<HTMLInputElement> }
) => ReturnType<typeof ComboboxInner>;

export const Combobox = Object.assign(ComboboxRoot, {
NoOptions,
});
16 changes: 16 additions & 0 deletions src/components/Combobox/Static/StaticCombobox.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -232,3 +232,19 @@ export const WithEllipsis = () => {
</Box>
);
};

export const NoOptions = () => {
return (
<Box __width="200px">
<Combobox
options={[]}
value={null}
size="large"
label="Label"
onChange={() => undefined}
>
<Combobox.NoOptions>No items to select</Combobox.NoOptions>
</Combobox>
</Box>
);
};
9 changes: 6 additions & 3 deletions src/components/Multiselect/Common/useMultiselect.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,13 +32,15 @@ const getItemsFilter = <T extends Option>(

export const useMultiselect = <T extends Option, V extends Option | string>({
selectedValues,
showEmptyState = false,
options,
onChange,
onInputValueChange,
onFocus,
onBlur,
}: {
selectedValues: V[];
showEmptyState?: boolean;
options: T[];
onChange?: MultiChangeHandler<V>;
onInputValueChange?: (value: string) => void;
Expand All @@ -59,9 +61,10 @@ export const useMultiselect = <T extends Option, V extends Option | string>({

const itemsToSelect = getItemsFilter<T>(selectedItems, inputValue, options);

const showInput = onInputValueChange
? true
: selectedItems.length !== options.length;
const showInput =
onInputValueChange || showEmptyState
? true
: selectedItems.length !== options.length;

const typed = Boolean(selectedItems.length || active);

Expand Down
20 changes: 20 additions & 0 deletions src/components/Multiselect/Dynamic/DynamicMultiselect.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -90,3 +90,23 @@ export const Default = () => {
</Box>
);
};

export const NoOptions = () => {
return (
<Box __width={300}>
<DynamicMultiselect
value={[]}
label="Pick a star wars characters"
onChange={() => undefined}
options={[]}
locale={{
placeholderText: "Add character",
}}
>
<DynamicMultiselect.NoOptions>
No items to select
</DynamicMultiselect.NoOptions>
</DynamicMultiselect>
</Box>
);
};
21 changes: 19 additions & 2 deletions src/components/Multiselect/Dynamic/DynamicMultiselect.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,13 @@ import { HelperText, InputVariants } from "~/components/BaseInput";
import {
getListDisplayMode,
getListTextSize,
hasNoOptions,
listItemStyle,
listStyle,
listWrapperRecipe,
LoadingListItem,
MultiChangeHandler,
NoOptions,
Option,
} from "~/components/BaseSelect";

Expand Down Expand Up @@ -50,6 +52,7 @@ export type DynamicMultiselectProps<T> = PropsWithBox<
renderEndAdornment?: RenderEndAdornmentType;
onInputValueChange?: (value: string) => void;
loading?: boolean;
children?: ReactNode;
locale?: {
loadingText?: string;
placeholderText?: string;
Expand Down Expand Up @@ -77,6 +80,7 @@ const DynamicMultiselectInner = <T extends Option>(
onFocus,
onBlur,
locale,
children,
onScrollEnd,
...props
}: DynamicMultiselectProps<T>,
Expand All @@ -101,6 +105,7 @@ const DynamicMultiselectInner = <T extends Option>(
showInput,
} = useMultiselect<T, T>({
selectedValues: value,
showEmptyState: hasNoOptions(children),
onInputValueChange,
options,
onChange,
Expand Down Expand Up @@ -190,7 +195,12 @@ const DynamicMultiselectInner = <T extends Option>(
<Portal asChild ref={refs.setFloating} style={floatingStyles}>
<Box
position="relative"
display={getListDisplayMode({ isOpen, hasItemsToSelect, loading })}
display={getListDisplayMode({
isOpen,
loading,
hasItemsToSelect,
showEmptyState: hasNoOptions(children),
})}
className={listWrapperRecipe({ size })}
>
<List
Expand All @@ -214,6 +224,9 @@ const DynamicMultiselectInner = <T extends Option>(
<Text size={getListTextSize(size)}>{item.label}</Text>
</List.Item>
))}

{isOpen && !loading && !hasItemsToSelect && children}

{loading && (
<LoadingListItem size={size}>
{locale?.loadingText || "Loading"}
Expand All @@ -237,10 +250,14 @@ const DynamicMultiselectInner = <T extends Option>(
);
};

export const DynamicMultiselect = forwardRef(DynamicMultiselectInner) as <
const DynamicMultiselectRoot = forwardRef(DynamicMultiselectInner) as <
T extends Option,
>(
props: DynamicMultiselectProps<T> & {
ref?: React.ForwardedRef<HTMLInputElement>;
}
) => ReturnType<typeof DynamicMultiselectInner>;

export const DynamicMultiselect = Object.assign(DynamicMultiselectRoot, {
NoOptions,
});
16 changes: 16 additions & 0 deletions src/components/Multiselect/Static/Multiselect.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -200,3 +200,19 @@ export const WithStringAsValues = () => {
export const Empty = () => {
return <Multiselect options={[]} value={[]} />;
};

export const NoOptions = () => {
return (
<Box __width={300}>
<Multiselect
label="Pick colors"
size="large"
value={[]}
onChange={() => undefined}
options={[]}
>
<Multiselect.NoOptions>No items to select</Multiselect.NoOptions>
</Multiselect>
</Box>
);
};
Loading

1 comment on commit a59d67c

@vercel
Copy link

@vercel vercel bot commented on a59d67c Jan 9, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.