Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(admin-ui): Loader component (CircularProgress) #4508

Open
wants to merge 12 commits into
base: feat/new-admin-ui
Choose a base branch
from
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ export const WithStartIcon: Story = {
export const WithLoading: Story = {
args: {
...Default.args,
isLoading: true
loading: true
}
};

Expand Down Expand Up @@ -181,6 +181,14 @@ export const WithCustomEmptyMessage: Story = {
}
};

export const WithCustomInitialMessage: Story = {
args: {
...Default.args,
initialMessage: "Custom initial message.",
options: []
}
};

export const WithFormattedOptions: Story = {
args: {
...Default.args,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,19 @@ type AutoCompletePrimitiveProps = Omit<InputPrimitiveProps, "endIcon"> & {
* Accessible label for the command menu. Not shown visibly.
*/
label?: string;
/**
* Message to display when there are no options loaded or selected.
* Use it to invite the user to interact with the autocomplete by typing a value.
*/
initialMessage?: React.ReactNode;
/**
* Message to display when there are no options.
*/
emptyMessage?: React.ReactNode;
/**
* Indicates if the autocomplete is loading options.
*/
isLoading?: boolean;
loading?: boolean;
/**
* Message to display while loading options.
*/
Expand Down Expand Up @@ -96,7 +101,7 @@ const AutoCompletePrimitive = (props: AutoCompletePrimitiveProps) => {
}, [setListOpenState]);

return (
<Popover open={vm.optionsListVm.isOpen} onOpenChange={() => setListOpenState(true)}>
<Popover open={vm.optionsListVm.open} onOpenChange={() => setListOpenState(true)}>
<Command label={props.label} onKeyDown={handleKeyDown}>
<Popover.Trigger asChild>
<span>
Expand All @@ -111,10 +116,12 @@ const AutoCompletePrimitive = (props: AutoCompletePrimitiveProps) => {
startIcon={props.startIcon}
endIcon={
<AutoCompleteInputIcons
inputSize={props.size}
displayResetAction={vm.inputVm.displayResetAction}
isDisabled={props.disabled}
disabled={props.disabled}
loading={props.loading}
onResetValue={resetSelectedOption}
onOpenChange={() => setListOpenState(!vm.optionsListVm.isOpen)}
onOpenChange={() => setListOpenState(!vm.optionsListVm.open)}
/>
}
onBlur={handleOnBlur}
Expand All @@ -129,8 +136,8 @@ const AutoCompletePrimitive = (props: AutoCompletePrimitiveProps) => {
<AutoCompleteList
options={vm.optionsListVm.options}
onOptionSelect={handleSelectOption}
isEmpty={vm.optionsListVm.isEmpty}
isLoading={props.isLoading}
empty={vm.optionsListVm.empty}
loading={props.loading}
loadingMessage={vm.optionsListVm.loadingMessage}
emptyMessage={vm.optionsListVm.emptyMessage}
optionRenderer={props.optionRenderer}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,31 +3,35 @@ import { ReactComponent as Close } from "@material-design-icons/svg/outlined/clo
import { ReactComponent as ChevronDown } from "@material-design-icons/svg/outlined/keyboard_arrow_down.svg";
import { IconButton } from "~/Button";
import { Icon } from "~/Icon";
import { Loader } from "~/Loader";

interface AutoCompleteInputIconsProps {
displayResetAction: boolean;
isDisabled?: boolean;
inputSize?: "md" | "lg" | "xl" | null;
loading?: boolean;
disabled?: boolean;
onOpenChange: (open: boolean) => void;
onResetValue: () => void;
}

export const AutoCompleteInputIcons = (props: AutoCompleteInputIconsProps) => {
return (
<div className={"wby-flex wby-items-center wby-gap-sm"}>
{props.loading && <Loader size={props.inputSize === "xl" ? "sm" : "xs"} />}
{props.displayResetAction && (
<IconButton
size={"xs"}
size={props.inputSize === "xl" ? "sm" : "xs"} // Map button size based on the input size.
variant={"secondary"}
icon={<Icon icon={<Close />} label={"Reset"} />}
disabled={props.isDisabled}
disabled={props.disabled}
onClick={event => {
event.stopPropagation();
props.onResetValue();
}}
/>
)}
<Icon
size={"sm"}
size={props.inputSize === "xl" ? "lg" : "sm"} // Map icon size based on the input size.
icon={<ChevronDown />}
label={"Open list"}
onClick={event => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,17 @@ import { Command } from "~/Command";
interface AutoCompleteListProps extends React.ComponentPropsWithoutRef<typeof Command.List> {
options: CommandOptionFormatted[];
emptyMessage?: React.ReactNode;
isEmpty?: boolean;
isLoading?: boolean;
empty?: boolean;
loading?: boolean;
loadingMessage?: React.ReactNode;
onOptionSelect: (value: string) => void;
optionRenderer?: (item: any, index: number) => React.ReactNode;
}

export const AutoCompleteList = ({
emptyMessage,
isEmpty,
isLoading,
empty,
loading,
loadingMessage,
onOptionSelect,
optionRenderer,
Expand All @@ -24,7 +24,7 @@ export const AutoCompleteList = ({
}: AutoCompleteListProps) => {
const renderOptions = React.useCallback(
(items: CommandOptionFormatted[]) => {
if (isEmpty) {
if (empty) {
return null;
}

Expand Down Expand Up @@ -57,17 +57,13 @@ export const AutoCompleteList = ({
return acc;
}, elements);
},
[onOptionSelect, optionRenderer, isEmpty]
[onOptionSelect, optionRenderer, empty]
);

return (
<Command.List {...props}>
{isLoading ? (
<Command.Loading>{loadingMessage}</Command.Loading>
) : (
renderOptions(options)
)}
{!isLoading && <Command.Empty>{emptyMessage}</Command.Empty>}
{loading ? <Command.Loading>{loadingMessage}</Command.Loading> : renderOptions(options)}
{!loading && <Command.Empty>{emptyMessage}</Command.Empty>}
</Command.List>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -8,27 +8,31 @@ export interface IAutoCompleteListOptionsPresenterParams {
options?: CommandOption[];
emptyMessage?: any;
loadingMessage?: any;
initialMessage?: any;
}

export interface IAutoCompleteListOptionsPresenter {
vm: {
options: CommandOptionFormatted[];
emptyMessage: string;
loadingMessage: string;
isOpen: boolean;
isEmpty: boolean;
emptyMessage: any;
loadingMessage: any;
open: boolean;
empty: boolean;
};
init: (params: IAutoCompleteListOptionsPresenterParams) => void;
setListOpenState: (open: boolean) => void;
setLoadedOptions: (loaded: boolean) => void;
setSelectedOption: (value: string) => void;
removeSelectedOption: (value: string) => void;
resetSelectedOption: () => void;
}

export class AutoCompleteListOptionsPresenter implements IAutoCompleteListOptionsPresenter {
private isOpen = false;
private open = false;
private loadedOptions = false;
private emptyMessage = "No results.";
private loadingMessage = "Loading...";
private initialMessage = "Start typing to find an option.";
private options = new ListCache<CommandOption>();

constructor() {
Expand All @@ -40,20 +44,25 @@ export class AutoCompleteListOptionsPresenter implements IAutoCompleteListOption
params.options && this.options.addItems(params.options);
this.emptyMessage = params.emptyMessage || this.emptyMessage;
this.loadingMessage = params.loadingMessage || this.loadingMessage;
this.initialMessage = params.initialMessage || this.initialMessage;
}

get vm() {
return {
options: this.options.getItems().map(option => CommandOptionFormatter.format(option)),
emptyMessage: this.emptyMessage,
emptyMessage: this.loadedOptions ? this.emptyMessage : this.initialMessage,
loadingMessage: this.loadingMessage,
isOpen: this.isOpen,
isEmpty: !this.options.hasItems()
open: this.open,
empty: !this.options.hasItems()
};
}

setListOpenState = (open: boolean) => {
this.isOpen = open;
this.open = open;
};

setLoadedOptions = (loaded: boolean) => {
this.loadedOptions = loaded;
};

setSelectedOption = (value: string) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ describe("AutoCompletePresenter", () => {
item: null
}
]);
expect(presenter.vm.optionsListVm.isEmpty).toEqual(false);
expect(presenter.vm.optionsListVm.empty).toEqual(false);
}

// with `options` as formatted options
Expand Down Expand Up @@ -129,7 +129,7 @@ describe("AutoCompletePresenter", () => {
}
}
]);
expect(presenter.vm.optionsListVm.isEmpty).toEqual(false);
expect(presenter.vm.optionsListVm.empty).toEqual(false);
}

// with `options` and `value`
Expand All @@ -153,7 +153,7 @@ describe("AutoCompletePresenter", () => {
item: null
}
]);
expect(presenter.vm.optionsListVm.isEmpty).toEqual(false);
expect(presenter.vm.optionsListVm.empty).toEqual(false);
}

{
Expand All @@ -162,8 +162,8 @@ describe("AutoCompletePresenter", () => {
expect(presenter.vm.optionsListVm.options).toEqual([]);
expect(presenter.vm.optionsListVm.emptyMessage).toEqual("No results.");
expect(presenter.vm.optionsListVm.loadingMessage).toEqual("Loading...");
expect(presenter.vm.optionsListVm.isOpen).toEqual(false);
expect(presenter.vm.optionsListVm.isEmpty).toEqual(true);
expect(presenter.vm.optionsListVm.open).toEqual(false);
expect(presenter.vm.optionsListVm.empty).toEqual(true);
}
});

Expand Down Expand Up @@ -362,12 +362,12 @@ describe("AutoCompletePresenter", () => {
// let's open it
presenter.init({ onValueChange, onOpenChange });
presenter.setListOpenState(true);
expect(presenter.vm.optionsListVm.isOpen).toBe(true);
expect(presenter.vm.optionsListVm.open).toBe(true);
expect(onOpenChange).toHaveBeenCalledWith(true);

// let's close it
presenter.setListOpenState(false);
expect(presenter.vm.optionsListVm.isOpen).toBe(false);
expect(presenter.vm.optionsListVm.open).toBe(false);
expect(onOpenChange).toHaveBeenCalledWith(false);
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { AutoCompleteOption } from "../domains";
interface AutoCompletePresenterParams {
emptyMessage?: any;
loadingMessage?: any;
initialMessage?: any;
onOpenChange?: (open: boolean) => void;
onValueChange: (value: string) => void;
onValueReset?: () => void;
Expand Down Expand Up @@ -53,7 +54,8 @@ class AutoCompletePresenter implements IAutoCompletePresenterParams {
this.optionsListPresenter.init({
options: listOptions,
emptyMessage: params.emptyMessage,
loadingMessage: params.loadingMessage
loadingMessage: params.loadingMessage,
initialMessage: params.initialMessage
});
}

Expand All @@ -80,6 +82,7 @@ class AutoCompletePresenter implements IAutoCompletePresenterParams {

public searchOption = (value: string) => {
this.inputPresenter.setValue(value);
this.optionsListPresenter.setLoadedOptions(true);
this.params?.onValueSearch?.(value);
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export const useAutoComplete = (props: AutoCompletePrimitiveProps) => {
value: props.value,
emptyMessage: props.emptyMessage,
loadingMessage: props.loadingMessage,
initialMessage: props.initialMessage,
onOpenChange: props.onOpenChange,
onValueChange: props.onValueChange,
onValueReset: props.onValueReset,
Expand All @@ -26,6 +27,7 @@ export const useAutoComplete = (props: AutoCompletePrimitiveProps) => {
props.value,
props.emptyMessage,
props.loadingMessage,
props.initialMessage,
props.onOpenChange,
props.onValueChange,
props.onValueReset,
Expand Down
4 changes: 2 additions & 2 deletions packages/admin-ui/src/Input/InputPrimitive.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import * as React from "react";
import { cva, type VariantProps } from "class-variance-authority";
import { Icon as BaseIcon } from "~/Icon";
import { cn } from "~/utils";
import { cn, cva, type VariantProps } from "~/utils";

/**
* Icon
Expand Down Expand Up @@ -276,6 +275,7 @@ export {
InputPrimitive,
getIconPosition,
inputVariants,
inputIconVariants,
type InputIconProps,
type InputPrimitiveProps
};
Loading
Loading