Skip to content

Commit

Permalink
feat: add multiple select (#44)
Browse files Browse the repository at this point in the history
* feat: add multiple select

Add `MultipleSelect` component to `ThemeSpec` and split `Select` into `SingleSelectView` and `MultiSelectView` components.

* **packages/filter/src/views/components.tsx**
  - Export `SingleSelectView` and `MultiSelectView` components.
  - Remove `SelectView` component.
  - Ensure correct types for `SingleSelectView` and `MultiSelectView`.

* **packages/filter/src/theme/preset.ts**
  - Use `SingleSelectView` for `Select` component in `ThemeSpec`.
  - Use `MultiSelectView` for `MultipleSelect` component in `ThemeSpec`.

* **packages/filter/src/theme/types.ts**
  - Define `SingleSelectProps` and `MultiSelectProps` types.
  - Update `ThemeSpec` to include `MultipleSelect` component.

* **packages/filter/src/views/data-input-views.tsx**
  - Use `MultipleSelectView` for `literal array` data input view.

---

For more details, open the [Copilot Workspace session](https://copilot-workspace.githubnext.com/lawvs/fn-sphere?shareId=XXXX-XXXX-XXXX-XXXX).

* chore: add changeset
  • Loading branch information
lawvs authored Aug 16, 2024
1 parent 87acc5e commit 03624b8
Show file tree
Hide file tree
Showing 5 changed files with 51 additions and 47 deletions.
5 changes: 5 additions & 0 deletions .changeset/plenty-bottles-sell.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@fn-sphere/filter": patch
---

Add multiple select
10 changes: 8 additions & 2 deletions packages/filter/src/theme/preset.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
import { ButtonView, InputView, SelectView } from "../views/components.js";
import {
ButtonView,
InputView,
SingleSelectView,
MultiSelectView,
} from "../views/components.js";
import { presetDataInputSpecs } from "../views/data-input-views.js";
import { FieldSelect } from "../views/field-select.js";
import { FilterDataInput } from "../views/filter-data-input.js";
Expand All @@ -16,7 +21,8 @@ export const presetTheme: ThemeSpec = {
components: {
Button: ButtonView,
Input: InputView,
Select: SelectView,
Select: SingleSelectView,
MultipleSelect: MultiSelectView,
},
templates: {
SingleFilter: SingleFilterView,
Expand Down
9 changes: 6 additions & 3 deletions packages/filter/src/theme/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,10 @@ import type {
RefAttributes,
} from "react";
import type { z } from "zod";
import type { SelectProps } from "../views/components.js";
import type {
SingleSelectProps,
MultiSelectProps,
} from "../views/components.js";
import type { FieldSelectProps } from "../views/field-select.js";
import type { DataInputProps } from "../views/filter-data-input.js";
import type { FilterGroupContainerProps } from "../views/filter-group-container.js";
Expand Down Expand Up @@ -63,10 +66,10 @@ export type ThemeSpec = {
>;
// Select: ComponentType<SelectProps<unknown> & RefAttributes<HTMLElement>>;
Select: <T>(
props: SelectProps<T> & { ref?: Ref<HTMLSelectElement> },
props: SingleSelectProps<T> & { ref?: Ref<HTMLSelectElement> },
) => ReactNode;
MultipleSelect: <T>(
props: SelectProps<T> & { ref?: Ref<HTMLSelectElement> },
props: MultiSelectProps<T> & { ref?: Ref<HTMLSelectElement> },
) => ReactNode;
};
templates: {
Expand Down
69 changes: 30 additions & 39 deletions packages/filter/src/views/components.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,99 +33,90 @@ export const InputView = forwardRef<
return <InputPrimitive ref={ref} onChange={handleChange} {...props} />;
});

export type SelectProps<T> = SingleSelectProps<T> | MultiSelectProps<T>;

type SingleSelectProps<T> = Omit<
export type SingleSelectProps<T> = Omit<
SelectHTMLAttributes<HTMLSelectElement>,
"value" | "onChange" | "children"
"value" | "onChange" | "children" | "multiple"
> & {
multiple?: false | undefined;
value?: T | undefined;
options?: { value: T; label: string }[] | undefined;
onChange?: (value: T) => void;
};

type MultiSelectProps<T> = Omit<
export type MultiSelectProps<T> = Omit<
SelectHTMLAttributes<HTMLSelectElement>,
"value" | "onChange" | "children"
"value" | "onChange" | "children" | "multiple"
> & {
multiple: true;
value?: T[] | undefined;
options?: { value: T; label: string }[] | undefined;
onChange?: (value: T[]) => void;
};

const MultiSelectView = forwardRef<
export const SingleSelectView = forwardRef<
HTMLSelectElement,
MultiSelectProps<unknown>
>(({ options = [], value = [], onChange, ...props }, ref) => {
SingleSelectProps<unknown>
>(({ options = [], value, onChange, ...props }, ref) => {
const SelectPrimitive = usePrimitives("select");
const OptionPrimitive = usePrimitives("option");
const selectedIndices = value.map((val) =>
String(options.findIndex((option) => option.value === val)),
);
const selectedIdx = options.findIndex((option) => option.value === value);
const handleChange = useCallback(
(e: ChangeEvent<HTMLSelectElement>) => {
const selectedOptions = Array.from(
e.target.selectedOptions,
(option) => options[Number(option.value)].value,
);
onChange?.(selectedOptions);
const index = Number(e.target.value);
onChange?.(options[index].value);
},
[options, onChange],
);
return (
<SelectPrimitive
ref={ref}
value={selectedIndices}
value={selectedIdx}
onChange={handleChange}
{...props}
>
<OptionPrimitive key={-1} value={-1} disabled></OptionPrimitive>
{options.map(({ label }, index) => (
<OptionPrimitive key={label} value={index}>
{label}
</OptionPrimitive>
))}
</SelectPrimitive>
);
});
}) as <T>(
p: SingleSelectProps<T> & { ref?: Ref<HTMLSelectElement> },
) => ReactNode;

const SingleSelectView = forwardRef<
export const MultiSelectView = forwardRef<
HTMLSelectElement,
SingleSelectProps<unknown>
>(({ options = [], value, onChange, ...props }, ref) => {
MultiSelectProps<unknown>
>(({ options = [], value = [], onChange, ...props }, ref) => {
const SelectPrimitive = usePrimitives("select");
const OptionPrimitive = usePrimitives("option");
const selectedIdx = options.findIndex((option) => option.value === value);
const selectedIndices = value.map((val) =>
String(options.findIndex((option) => option.value === val)),
);
const handleChange = useCallback(
(e: ChangeEvent<HTMLSelectElement>) => {
const index = Number(e.target.value);
onChange?.(options[index].value);
const selectedOptions = Array.from(
e.target.selectedOptions,
(option) => options[Number(option.value)].value,
);
onChange?.(selectedOptions);
},
[options, onChange],
);
return (
<SelectPrimitive
ref={ref}
value={selectedIdx}
value={selectedIndices}
onChange={handleChange}
{...props}
>
<OptionPrimitive key={-1} value={-1} disabled></OptionPrimitive>
{options.map(({ label }, index) => (
<OptionPrimitive key={label} value={index}>
{label}
</OptionPrimitive>
))}
</SelectPrimitive>
);
});

export const SelectView = forwardRef<HTMLSelectElement, SelectProps<unknown>>(
(props, ref) => {
if (props.multiple) {
return <MultiSelectView ref={ref} {...props} />;
}
return <SingleSelectView ref={ref} {...props} />;
},
) as <T>(p: SelectProps<T> & { ref?: Ref<HTMLSelectElement> }) => ReactNode;
}) as <T>(
p: MultiSelectProps<T> & { ref?: Ref<HTMLSelectElement> },
) => ReactNode;
5 changes: 2 additions & 3 deletions packages/filter/src/views/data-input-views.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@ export const presetDataInputSpecs: DataInputViewSpec[] = [
);
},
view: forwardRef(({ requiredDataSchema, rule, updateInput }) => {
const { Select: SelectView } = useView("components");
const { MultipleSelect: MultipleSelectView } = useView("components");
const { getLocaleText } = useRootRule();
const arraySchema = requiredDataSchema[0] as z.ZodArray<
z.ZodUnion<[z.ZodLiteral<z.Primitive>]>
Expand All @@ -158,8 +158,7 @@ export const presetDataInputSpecs: DataInputViewSpec[] = [
}));
const value = (rule.args?.[0] ?? []) as z.Primitive[];
return (
<SelectView<z.Primitive>
multiple
<MultipleSelectView<z.Primitive>
value={value}
options={options}
onChange={(newValue) => {
Expand Down

0 comments on commit 03624b8

Please sign in to comment.