add select input with multiple values

This commit is contained in:
Alexander Navarro 2023-10-07 18:52:43 -03:00
parent c8b808bb14
commit e6df894c85
8 changed files with 1265 additions and 1057 deletions

2076
pnpm-lock.yaml generated

File diff suppressed because it is too large Load diff

View file

@ -20,4 +20,13 @@
--prj-primary: var(--ctp-macchiato-teal); --prj-primary: var(--ctp-macchiato-teal);
--prj-primary-text: var(--ctp-macchiato-base); --prj-primary-text: var(--ctp-macchiato-base);
--prj-danger: var(--ctp-macchiato-red);
--prj-danger-text: var(--ctp-macchiato-base);
--prj-disabled: var(--ctp-macchiato-red);
--prj-disabled-text: rgba(var(--ctp-macchiato-base-raw), 0.5);
--prj-input: var(--ctp-macchiato-text);
--prj-input-text: var(--ctp-macchiato-base);
} }

View file

@ -0,0 +1,69 @@
.wrapper {
--bg-color: var(--prj-input);
--text-color: var(--prj-input-text);
position: relative;
padding: var(--prj-spacing-1);
background-color: var(--bg-color);
color: var(--text-color);
}
.input {
width: 100%;
display: flex;
gap: var(--prj-spacing-1);
font-size: 0.8em;
}
.realInput {
}
.selectedItem {
background-color: var(--prj-surface-3);
color: var(--prj-text);
font-size: 0.9em;
}
.selectedItem > * {
padding: var(--prj-spacing-1);
}
.deleteItem:hover {
background-color: var(--prj-danger);
}
.optionList {
position: absolute;
left: 0;
top: 120%;
width: 100%;
padding: var(--prj-spacing-1);
text-align: start;
background-color: var(--bg-color);
color: var(--text-color);
}
.optionItem {
display: block;
width: 100%;
border: none;
background-color: transparent;
text-align: start;
padding: var(--prj-spacing-1);
}
.optionItem:disabled {
color: var(--prj-disabled-text);
}
.optionItem:not(:first-child) {
margin-top: var(--prj-spacing-1);
}
.optionItem:not(:disabled):hover {
background-color: var(--prj-accent-bg);
}

View file

@ -0,0 +1,130 @@
import React, {
useState,
type ChangeEventHandler,
useRef,
useEffect,
} from 'react';
import styles from './SelectInput.module.css';
interface Props {
onChange: (value: string | string[] | null) => void;
options: [{ label: string; value: any }];
keyData: string;
isMultiple?: boolean;
}
export default function SelectInput({
options,
isMultiple = false,
onChange,
}: Props): JSX.Element {
const [selected, setSelected] = useState<string[]>([]);
const [filteredOptions, setFilteredOptions] = useState<Props['options']>([]);
const [isOptionsOpen, setIsOptionsOpen] = useState<boolean>(false);
const inputRef = useRef<HTMLInputElement>(null);
useEffect(() => {
setFilteredOptions(options);
}, [options]);
useEffect(() => {
if (selected.length === 0) {
onChange(null);
return;
}
onChange(isMultiple ? selected : selected.at(0));
}, [selected]);
const handleFocusInput = (): void => {
setIsOptionsOpen(true);
inputRef.current.focus();
};
const handleAddElement = (item: any): void => {
setSelected((prev) => {
if (isMultiple) {
return [...prev, item];
}
return [item];
});
setIsOptionsOpen(false);
inputRef.current.value = '';
};
const handleRemoveElement = (idx: number): void => {
setIsOptionsOpen(false);
setSelected((prev) => {
prev.splice(idx, 1);
return [...prev];
});
};
const handleLooseFocus = (e: React.FocusEvent<HTMLDivElement>): void => {
if (!e.currentTarget.contains(e.relatedTarget)) {
// Not triggered when swapping focus between children
setIsOptionsOpen(false);
}
};
const handleFilterOptions = ({
target,
}: React.ChangeEvent<HTMLInputElement>): void => {
if (target.value === '') {
setFilteredOptions(options);
return;
}
const newOptions = options.filter(
(item) =>
item.label.toLowerCase().search(target.value.toLowerCase()) !== -1,
);
setFilteredOptions(newOptions);
};
return (
<div className={styles.wrapper} onBlur={handleLooseFocus}>
<div
className={styles.input}
onClick={() => {
handleFocusInput();
}}
>
{selected.map((item, idx) => (
<div className={styles.selectedItem + ' hstack'} key={idx}>
<div>{item}</div>
<div
className={styles.deleteItem}
onClick={() => {
handleRemoveElement(idx);
}}
>
{'X'}
</div>
</div>
))}
<input
ref={inputRef}
className={styles.realInput}
type="text"
onChange={handleFilterOptions}
/>
</div>
<div className={styles.optionList} hidden={!isOptionsOpen}>
{filteredOptions.map((item, idx) => (
<button
className={styles.optionItem}
key={idx}
disabled={selected.includes(item.value)}
onClick={() => {
handleAddElement(item.value);
}}
>
{item.label}
</button>
))}
</div>
</div>
);
}

View file

@ -1,5 +1,6 @@
import React, { useMemo, type ChangeEventHandler } from 'react'; import React, { useMemo, type ChangeEventHandler } from 'react';
import type { DataItem } from '.'; import type { DataItem } from '.';
import SelectInput from '@components/Inputs/SelectInput';
interface Props { interface Props {
onChange: (value: string | string[] | null) => void; onChange: (value: string | string[] | null) => void;
@ -8,7 +9,7 @@ interface Props {
isMultiple?: boolean; isMultiple?: boolean;
} }
export default function SelectInput({ export default function SelectFilter({
data, data,
keyData, keyData,
isMultiple = false, isMultiple = false,
@ -25,17 +26,7 @@ export default function SelectInput({
options = [...new Set(options)]; options = [...new Set(options)];
options = options.map((item, idx) => ( options = options.map((item) => ({ label: item, value: item }));
<option key={idx} value={item}>
{item}
</option>
));
options.unshift(
<option key={-1} value="">
Select...
</option>,
);
return options; return options;
}, [data, keyData]); }, [data, keyData]);
@ -63,13 +54,10 @@ export default function SelectInput({
}; };
return ( return (
<select <SelectInput
name="fooe" options={options}
id="foo" isMultiple={isMultiple}
multiple={isMultiple} onChange={onChange}
onChange={onSelectChange} />
>
{options}
</select>
); );
} }

View file

@ -1,6 +1,6 @@
import { HeaderType } from '../Table.tsx'; import { HeaderType } from '../Table.tsx';
export { default as SelectInput } from './SelectInput.tsx'; export { default as SelectFilter } from './SelectFilter.tsx';
export { default as NumberInput } from './NumberInput.tsx'; export { default as NumberInput } from './NumberInput.tsx';
export type DataItem = Record<string, any>; export type DataItem = Record<string, any>;

View file

@ -1,11 +1,11 @@
import React, { useMemo, useState } from 'react'; import React, { useMemo, useState } from 'react';
import usePagination, { Offset } from 'src/hooks/usePagination'; import usePagination, { Offset } from 'src/hooks/usePagination';
import { import {
SelectInput, SelectFilter,
NumberInput, NumberInput,
resolveFilterByType, resolveFilterByType,
type Filter, type Filter,
} from './Inputs'; } from './Filters';
export type DataItem = Record<string, any>; export type DataItem = Record<string, any>;
@ -110,7 +110,7 @@ export default function Table({ data, headers }: Props): JSX.Element {
); );
case HeaderType.Select: case HeaderType.Select:
return ( return (
<SelectInput <SelectFilter
data={data} data={data}
keyData={header.key} keyData={header.key}
onChange={(value) => { onChange={(value) => {
@ -120,7 +120,7 @@ export default function Table({ data, headers }: Props): JSX.Element {
); );
case HeaderType.Multiple: case HeaderType.Multiple:
return ( return (
<SelectInput <SelectFilter
isMultiple isMultiple
data={data} data={data}
keyData={header.key} keyData={header.key}