add select input with multiple values
This commit is contained in:
parent
c8b808bb14
commit
e6df894c85
8 changed files with 1265 additions and 1057 deletions
2076
pnpm-lock.yaml
generated
2076
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load diff
|
|
@ -20,4 +20,13 @@
|
|||
|
||||
--prj-primary: var(--ctp-macchiato-teal);
|
||||
--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);
|
||||
}
|
||||
|
|
|
|||
69
src/components/Inputs/SelectInput.module.css
Normal file
69
src/components/Inputs/SelectInput.module.css
Normal 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);
|
||||
}
|
||||
130
src/components/Inputs/SelectInput.tsx
Normal file
130
src/components/Inputs/SelectInput.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
import React, { useMemo, type ChangeEventHandler } from 'react';
|
||||
import type { DataItem } from '.';
|
||||
import SelectInput from '@components/Inputs/SelectInput';
|
||||
|
||||
interface Props {
|
||||
onChange: (value: string | string[] | null) => void;
|
||||
|
|
@ -8,7 +9,7 @@ interface Props {
|
|||
isMultiple?: boolean;
|
||||
}
|
||||
|
||||
export default function SelectInput({
|
||||
export default function SelectFilter({
|
||||
data,
|
||||
keyData,
|
||||
isMultiple = false,
|
||||
|
|
@ -25,17 +26,7 @@ export default function SelectInput({
|
|||
|
||||
options = [...new Set(options)];
|
||||
|
||||
options = options.map((item, idx) => (
|
||||
<option key={idx} value={item}>
|
||||
{item}
|
||||
</option>
|
||||
));
|
||||
|
||||
options.unshift(
|
||||
<option key={-1} value="">
|
||||
Select...
|
||||
</option>,
|
||||
);
|
||||
options = options.map((item) => ({ label: item, value: item }));
|
||||
|
||||
return options;
|
||||
}, [data, keyData]);
|
||||
|
|
@ -63,13 +54,10 @@ export default function SelectInput({
|
|||
};
|
||||
|
||||
return (
|
||||
<select
|
||||
name="fooe"
|
||||
id="foo"
|
||||
multiple={isMultiple}
|
||||
onChange={onSelectChange}
|
||||
>
|
||||
{options}
|
||||
</select>
|
||||
<SelectInput
|
||||
options={options}
|
||||
isMultiple={isMultiple}
|
||||
onChange={onChange}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
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 type DataItem = Record<string, any>;
|
||||
|
|
@ -1,11 +1,11 @@
|
|||
import React, { useMemo, useState } from 'react';
|
||||
import usePagination, { Offset } from 'src/hooks/usePagination';
|
||||
import {
|
||||
SelectInput,
|
||||
SelectFilter,
|
||||
NumberInput,
|
||||
resolveFilterByType,
|
||||
type Filter,
|
||||
} from './Inputs';
|
||||
} from './Filters';
|
||||
|
||||
export type DataItem = Record<string, any>;
|
||||
|
||||
|
|
@ -110,7 +110,7 @@ export default function Table({ data, headers }: Props): JSX.Element {
|
|||
);
|
||||
case HeaderType.Select:
|
||||
return (
|
||||
<SelectInput
|
||||
<SelectFilter
|
||||
data={data}
|
||||
keyData={header.key}
|
||||
onChange={(value) => {
|
||||
|
|
@ -120,7 +120,7 @@ export default function Table({ data, headers }: Props): JSX.Element {
|
|||
);
|
||||
case HeaderType.Multiple:
|
||||
return (
|
||||
<SelectInput
|
||||
<SelectFilter
|
||||
isMultiple
|
||||
data={data}
|
||||
keyData={header.key}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue