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: 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);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
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 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>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -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>;
|
||||||
|
|
@ -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}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue