Skip to content

Commit

Permalink
feat: add Popover component
Browse files Browse the repository at this point in the history
  • Loading branch information
leopuleo committed Jan 14, 2025
1 parent 165bdb3 commit 3bef7bd
Show file tree
Hide file tree
Showing 11 changed files with 236 additions and 132 deletions.
1 change: 1 addition & 0 deletions packages/admin-ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
"@radix-ui/react-checkbox": "^1.1.2",
"@radix-ui/react-dropdown-menu": "^2.1.2",
"@radix-ui/react-label": "^2.1.0",
"@radix-ui/react-popover": "^1.1.4",
"@radix-ui/react-radio-group": "^1.2.1",
"@radix-ui/react-select": "^2.1.2",
"@radix-ui/react-separator": "^1.1.0",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -310,7 +310,6 @@ export const WithCustomOptionRenderer: Story = {
{
label: "Fiji Time (FJT)",
value: "fjt",
separator: true,
item: {
name: "Fiji Time (FJT)",
time_difference: "+12:00",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import React, { KeyboardEvent } from "react";
import { Command } from "~/Command";
import { Popover } from "~/Popover";
import { InputPrimitiveProps } from "~/Input";
import { useAutoComplete } from "./useAutoComplete";
import { AutoCompleteInputIcons, AutoCompleteList } from "./components";
Expand Down Expand Up @@ -89,38 +90,50 @@ const AutoCompletePrimitive = (props: AutoCompletePrimitiveProps) => {
);

return (
<Command label={props.label} onKeyDown={handleKeyDown}>
<Command.Input
value={vm.inputVm.value}
onValueChange={searchOption}
placeholder={vm.inputVm.placeholder}
size={props.size}
variant={props.variant}
disabled={props.disabled}
invalid={props.invalid}
startIcon={props.startIcon}
endIcon={
<AutoCompleteInputIcons
displayResetAction={vm.inputVm.displayResetAction}
isDisabled={props.disabled}
onResetValue={resetSelectedOption}
onOpenChange={() => setListOpenState(!vm.optionsListVm.isOpen)}
/>
}
onBlur={() => setListOpenState(false)}
onFocus={() => setListOpenState(true)}
/>
<AutoCompleteList
options={vm.optionsListVm.options}
onOptionSelect={handleSelectOption}
isEmpty={vm.optionsListVm.isEmpty}
isLoading={props.isLoading}
isOpen={vm.optionsListVm.isOpen}
loadingMessage={vm.optionsListVm.loadingMessage}
emptyMessage={vm.optionsListVm.emptyMessage}
optionRenderer={props.optionRenderer}
/>
</Command>
<Popover open={vm.optionsListVm.isOpen} onOpenChange={() => setListOpenState(true)}>
<Command label={props.label} onKeyDown={handleKeyDown}>
<Popover.Trigger asChild>
<span>
<Command.Input
value={vm.inputVm.value}
onValueChange={searchOption}
placeholder={vm.inputVm.placeholder}
size={props.size}
variant={props.variant}
disabled={props.disabled}
invalid={props.invalid}
startIcon={props.startIcon}
endIcon={
<AutoCompleteInputIcons
displayResetAction={vm.inputVm.displayResetAction}
isDisabled={props.disabled}
onResetValue={resetSelectedOption}
onOpenChange={() => setListOpenState(!vm.optionsListVm.isOpen)}
/>
}
onBlur={() => setListOpenState(false)}
onFocus={() => setListOpenState(true)}
/>
</span>
</Popover.Trigger>
<Popover.Portal>
<Popover.Content
style={{ width: "var(--radix-popover-trigger-width)" }}
onOpenAutoFocus={e => e.preventDefault()}
>
<AutoCompleteList
options={vm.optionsListVm.options}
onOptionSelect={handleSelectOption}
isEmpty={vm.optionsListVm.isEmpty}
isLoading={props.isLoading}
loadingMessage={vm.optionsListVm.loadingMessage}
emptyMessage={vm.optionsListVm.emptyMessage}
optionRenderer={props.optionRenderer}
/>
</Popover.Content>
</Popover.Portal>
</Command>
</Popover>
);
};

Expand Down
Original file line number Diff line number Diff line change
@@ -1,26 +1,12 @@
import React from "react";
import { CommandOptionFormatted } from "~/Command/domain/CommandOptionFormatted";
import { Command } from "~/Command";
import { cn, cva } from "~/utils";

const autoCompleteListWrapperVariants = cva(
"animate-in fade-in-0 zoom-in-95 absolute top-xs-plus z-10 w-full outline-none",
{
variants: {
isOpen: {
true: "block",
false: "hidden"
}
}
}
);

interface AutoCompleteListProps extends React.ComponentPropsWithoutRef<typeof Command.List> {
options: CommandOptionFormatted[];
emptyMessage?: React.ReactNode;
isEmpty?: boolean;
isLoading?: boolean;
isOpen?: boolean;
loadingMessage?: React.ReactNode;
onOptionSelect: (value: string) => void;
optionRenderer?: (item: any, index: number) => React.ReactNode;
Expand All @@ -30,7 +16,6 @@ export const AutoCompleteList = ({
emptyMessage,
isEmpty,
isLoading,
isOpen,
loadingMessage,
onOptionSelect,
optionRenderer,
Expand Down Expand Up @@ -74,17 +59,13 @@ export const AutoCompleteList = ({
);

return (
<div className="relative">
<div className={cn(autoCompleteListWrapperVariants({ isOpen }))}>
<Command.List {...props}>
{isLoading ? (
<Command.Loading>{loadingMessage}</Command.Loading>
) : (
renderOptions(options)
)}
{!isLoading && <Command.Empty>{emptyMessage}</Command.Empty>}
</Command.List>
</div>
</div>
<Command.List {...props}>
{isLoading ? (
<Command.Loading>{loadingMessage}</Command.Loading>
) : (
renderOptions(options)
)}
{!isLoading && <Command.Empty>{emptyMessage}</Command.Empty>}
</Command.List>
);
};
2 changes: 1 addition & 1 deletion packages/admin-ui/src/Command/components/List.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ const List = ({ className, ...props }: ListProps) => {
<CommandPrimitive.List
className={cn(
[
"block max-h-96 min-w-56 w-full shadow-lg py-sm overflow-y-auto overflow-x-hidden rounded-sm border-sm border-neutral-muted bg-neutral-base text-neutral-strong"
"block max-h-96 w-full py-sm overflow-y-auto overflow-x-hidden bg-neutral-base text-neutral-strong"
],
className
)}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import React, { KeyboardEvent } from "react";
import { Command } from "~/Command";
import { Popover } from "~/Popover";
import { InputPrimitiveProps } from "~/Input";
import { useMultiAutoComplete } from "./useMultiAutoComplete";
import {
Expand Down Expand Up @@ -116,50 +117,58 @@ const MultiAutoCompletePrimitive = (props: MultiAutoCompletePrimitiveProps) => {
);

return (
<Command
label={props.label}
onKeyDown={handleKeyDown}
className={"h-auto overflow-visible"}
>
<MultiAutoCompleteInput
value={vm.inputVm.value}
placeholder={vm.inputVm.placeholder}
changeValue={searchOption}
closeList={() => setListOpenState(false)}
openList={() => setListOpenState(true)}
variant={props.variant}
size={props.size}
invalid={props.invalid}
removeSelectedOption={removeSelectedOption}
selectedOptionRenderer={props.selectedOptionRenderer}
selectedOptions={vm.selectedOptionsVm.options}
disabled={props.disabled}
startIcon={props.startIcon}
endIcon={
<MultiAutoCompleteInputIcons
displayResetAction={
!vm.selectedOptionsVm.isEmpty && vm.inputVm.displayResetAction
}
isDisabled={props.disabled}
onResetValue={resetSelectedOptions}
onOpenChange={() => setListOpenState(!vm.optionsListVm.isOpen)}
/>
}
/>

<MultiAutoCompleteList
emptyMessage={vm.optionsListVm.emptyMessage}
isEmpty={vm.optionsListVm.isEmpty}
isLoading={props.isLoading}
isOpen={vm.optionsListVm.isOpen}
loadingMessage={vm.optionsListVm.loadingMessage}
onOptionCreate={handleCreateOption}
onOptionSelect={handleSelectOption}
optionRenderer={props.optionRenderer}
options={vm.optionsListVm.options}
temporaryOption={vm.temporaryOptionVm.option}
/>
</Command>
<Popover open={vm.optionsListVm.isOpen} onOpenChange={() => setListOpenState(true)}>
<Command label={props.label} onKeyDown={handleKeyDown}>
<Popover.Trigger asChild>
<span>
<MultiAutoCompleteInput
value={vm.inputVm.value}
placeholder={vm.inputVm.placeholder}
changeValue={searchOption}
closeList={() => setListOpenState(false)}
openList={() => setListOpenState(true)}
variant={props.variant}
size={props.size}
invalid={props.invalid}
removeSelectedOption={removeSelectedOption}
selectedOptionRenderer={props.selectedOptionRenderer}
selectedOptions={vm.selectedOptionsVm.options}
disabled={props.disabled}
startIcon={props.startIcon}
endIcon={
<MultiAutoCompleteInputIcons
displayResetAction={
!vm.selectedOptionsVm.isEmpty &&
vm.inputVm.displayResetAction
}
isDisabled={props.disabled}
onResetValue={resetSelectedOptions}
onOpenChange={() => setListOpenState(!vm.optionsListVm.isOpen)}
/>
}
/>
</span>
</Popover.Trigger>
<Popover.Portal>
<Popover.Content
style={{ width: "var(--radix-popover-trigger-width)" }}
onOpenAutoFocus={e => e.preventDefault()}
>
<MultiAutoCompleteList
emptyMessage={vm.optionsListVm.emptyMessage}
isEmpty={vm.optionsListVm.isEmpty}
isLoading={props.isLoading}
loadingMessage={vm.optionsListVm.loadingMessage}
onOptionCreate={handleCreateOption}
onOptionSelect={handleSelectOption}
optionRenderer={props.optionRenderer}
options={vm.optionsListVm.options}
temporaryOption={vm.temporaryOptionVm.option}
/>
</Popover.Content>
</Popover.Portal>
</Command>
</Popover>
);
};

Expand Down
Original file line number Diff line number Diff line change
@@ -1,25 +1,11 @@
import React from "react";
import { CommandOptionFormatted } from "~/Command/domain/CommandOptionFormatted";
import { Command } from "~/Command";
import { cn, cva } from "~/utils";

const multiAutoCompleteListWrapperVariants = cva(
"animate-in fade-in-0 zoom-in-95 absolute top-xs-plus z-10 w-full outline-none",
{
variants: {
isOpen: {
true: "block",
false: "hidden"
}
}
}
);

interface MultiAutoCompleteListProps extends React.ComponentPropsWithoutRef<typeof Command.List> {
emptyMessage?: React.ReactNode;
isEmpty?: boolean;
isLoading?: boolean;
isOpen?: boolean;
loadingMessage?: React.ReactNode;
onOptionCreate?: (value: string) => void;
onOptionSelect: (value: string) => void;
Expand All @@ -32,7 +18,6 @@ export const MultiAutoCompleteList = ({
emptyMessage,
isEmpty,
isLoading,
isOpen,
loadingMessage,
onOptionCreate,
onOptionSelect,
Expand Down Expand Up @@ -98,17 +83,13 @@ export const MultiAutoCompleteList = ({
);

return (
<div className="relative">
<div className={cn(multiAutoCompleteListWrapperVariants({ isOpen }))}>
<Command.List {...props}>
{isLoading ? (
<Command.Loading>{loadingMessage}</Command.Loading>
) : (
renderOptions(options)
)}
{!isLoading && <Command.Empty>{emptyMessage}</Command.Empty>}
</Command.List>
</div>
</div>
<Command.List {...props}>
{isLoading ? (
<Command.Loading>{loadingMessage}</Command.Loading>
) : (
renderOptions(options)
)}
{!isLoading && <Command.Empty>{emptyMessage}</Command.Empty>}
</Command.List>
);
};
37 changes: 37 additions & 0 deletions packages/admin-ui/src/Popover/Popover.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import * as React from "react";
import * as PopoverPrimitive from "@radix-ui/react-popover";
import { cn, withStaticProps } from "~/utils";

type PopoverContentProps = PopoverPrimitive.PopoverContentProps;

const PopoverContent = ({
className,
align = "center",
sideOffset = 6,
...props
}: PopoverContentProps) => (
<PopoverPrimitive.Portal>
<PopoverPrimitive.Content
align={align}
sideOffset={sideOffset}
className={cn(
[
"z-50 rounded-sm overflow-hidden border-sm border-neutral-muted shadow-lg outline-none",
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95",
"data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2"
],
className
)}
{...props}
/>
</PopoverPrimitive.Portal>
);

const Popover = withStaticProps(PopoverPrimitive.Root, {
Anchor: PopoverPrimitive.Anchor,
Content: PopoverContent,
Portal: PopoverPrimitive.Portal,
Trigger: PopoverPrimitive.Trigger
});

export { Popover, type PopoverContent };
1 change: 1 addition & 0 deletions packages/admin-ui/src/Popover/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./Popover";
1 change: 1 addition & 0 deletions packages/admin-ui/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export * from "./Icon";
export * from "./Input";
export * from "./Label";
export * from "./MultiAutoComplete";
export * from "./Popover";
export * from "./Progress";
export * from "./Providers";
export * from "./RadioGroup";
Expand Down
Loading

0 comments on commit 3bef7bd

Please sign in to comment.