Compare commits
2 Commits
4d6b3fde89
...
246c555684
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
246c555684 | ||
|
|
8b86cbd534 |
|
|
@ -1,17 +1,35 @@
|
||||||
import { Combobox } from "@headlessui/react";
|
import { Combobox } from "@headlessui/react";
|
||||||
|
|
||||||
import { DropDownItem } from "@shared/nej-react-components/Parts/DropDown";
|
import { DropDownItem } from "@shared/nej-react-components/Parts/DropDown";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
|
|
||||||
import tw, { styled } from "twin.macro";
|
import tw, { styled } from "twin.macro";
|
||||||
import "styled-components/macro";
|
import "styled-components/macro";
|
||||||
import { useField } from "@shared/nej-react-components/Parts/Input";
|
import { useField } from "@shared/nej-react-components/Parts/Input";
|
||||||
|
|
||||||
export function AutocompleteSelect({ title, titleProps = null, data, nullable = false, ...props }) {
|
/**
|
||||||
|
* @param {object} props
|
||||||
|
* @param {((query: string) => Promise<Array>) | Array} props.data
|
||||||
|
* @param {(val: any, state: { active: boolean, selected: boolean }) => React.ReactNode} [props.renderOption] - Custom renderer for each option. Receives the raw item and combobox state.
|
||||||
|
* @param {() => void} [props.onScrollEnd] - Called when the options list is scrolled to the bottom (for infinite scroll).
|
||||||
|
* @param {(query: string) => void} [props.onQueryChange] - Called when the search input changes. Use this with renderOption to drive server-side filtering.
|
||||||
|
*/
|
||||||
|
export function AutocompleteSelect({ title, titleProps = null, data, nullable = false, renderOption, onScrollEnd, onQueryChange, ...props }) {
|
||||||
const [field, meta, helpers] = useField(props);
|
const [field, meta, helpers] = useField(props);
|
||||||
|
|
||||||
const [query, setQuery] = useState(field.value ?? "");
|
const [query, setQuery] = useState(field.value ?? "");
|
||||||
const [internalData, setInternalData] = useState([]);
|
const [internalData, setInternalData] = useState([]);
|
||||||
|
const sentinelRef = useRef(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!onScrollEnd || !sentinelRef.current) return;
|
||||||
|
const observer = new IntersectionObserver(
|
||||||
|
(entries) => { if (entries[0].isIntersecting) onScrollEnd(); },
|
||||||
|
{ threshold: 0.1 }
|
||||||
|
);
|
||||||
|
observer.observe(sentinelRef.current);
|
||||||
|
return () => observer.disconnect();
|
||||||
|
}, [onScrollEnd]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
(async () => {
|
(async () => {
|
||||||
|
|
@ -19,6 +37,10 @@ export function AutocompleteSelect({ title, titleProps = null, data, nullable =
|
||||||
//if data is async function await it
|
//if data is async function await it
|
||||||
setInternalData(await data(query));
|
setInternalData(await data(query));
|
||||||
} else if (Array.isArray(data)) {
|
} else if (Array.isArray(data)) {
|
||||||
|
// When renderOption is provided the caller manages filtering externally, just show data as-is
|
||||||
|
if (renderOption) {
|
||||||
|
setInternalData(data);
|
||||||
|
} else {
|
||||||
setInternalData(
|
setInternalData(
|
||||||
query === ""
|
query === ""
|
||||||
? data
|
? data
|
||||||
|
|
@ -29,6 +51,7 @@ export function AutocompleteSelect({ title, titleProps = null, data, nullable =
|
||||||
return item.label.toLowerCase().includes(query.toLowerCase());
|
return item.label.toLowerCase().includes(query.toLowerCase());
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
}
|
||||||
} else if(typeof data === "object")
|
} else if(typeof data === "object")
|
||||||
{
|
{
|
||||||
setInternalData(Object.values(data).map((value) => {
|
setInternalData(Object.values(data).map((value) => {
|
||||||
|
|
@ -60,8 +83,9 @@ export function AutocompleteSelect({ title, titleProps = null, data, nullable =
|
||||||
: null
|
: null
|
||||||
}
|
}
|
||||||
onChange={(value) => {
|
onChange={(value) => {
|
||||||
console.log(value);
|
// If value is an object (from internalData lookup), store only the primitive .value
|
||||||
field.onChange({ target: { value: value, name: field.name } });
|
const stored = value != null && typeof value === "object" ? value.value : value;
|
||||||
|
field.onChange({ target: { value: stored, name: field.name } });
|
||||||
}}
|
}}
|
||||||
nullable={nullable}
|
nullable={nullable}
|
||||||
>
|
>
|
||||||
|
|
@ -80,7 +104,7 @@ export function AutocompleteSelect({ title, titleProps = null, data, nullable =
|
||||||
<div tw="relative">
|
<div tw="relative">
|
||||||
<Combobox.Input
|
<Combobox.Input
|
||||||
tw="bg-primary cursor-pointer appearance-none border-2 border-secondary rounded w-full py-2 px-4 text-primary leading-tight focus:outline-none focus:bg-secondary focus:border-accent transition duration-150 "
|
tw="bg-primary cursor-pointer appearance-none border-2 border-secondary rounded w-full py-2 px-4 text-primary leading-tight focus:outline-none focus:bg-secondary focus:border-accent transition duration-150 "
|
||||||
onChange={(event) => setQuery(event.target.value)}
|
onChange={(event) => { setQuery(event.target.value); onQueryChange?.(event.target.value); }}
|
||||||
displayValue={(val) => (typeof val === "string" ? val : val?.label)}
|
displayValue={(val) => (typeof val === "string" ? val : val?.label)}
|
||||||
/>
|
/>
|
||||||
<Combobox.Button tw="absolute top-0 right-0 left-0 bottom-0 " />
|
<Combobox.Button tw="absolute top-0 right-0 left-0 bottom-0 " />
|
||||||
|
|
@ -92,11 +116,13 @@ export function AutocompleteSelect({ title, titleProps = null, data, nullable =
|
||||||
key={typeof val === "string" ? val : val.value}
|
key={typeof val === "string" ? val : val.value}
|
||||||
value={typeof val === "string" ? val : val.value}
|
value={typeof val === "string" ? val : val.value}
|
||||||
>
|
>
|
||||||
<DropDownItem>
|
{renderOption
|
||||||
{typeof val === "string" ? val : val.label}
|
? (state) => renderOption(val, state)
|
||||||
</DropDownItem>
|
: <DropDownItem>{typeof val === "string" ? val : val.label}</DropDownItem>
|
||||||
|
}
|
||||||
</Combobox.Option>
|
</Combobox.Option>
|
||||||
))}
|
))}
|
||||||
|
{onScrollEnd && <div ref={sentinelRef} />}
|
||||||
</Combobox.Options>
|
</Combobox.Options>
|
||||||
</div>
|
</div>
|
||||||
</Combobox>
|
</Combobox>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user