✨ Autocomplete
This commit is contained in:
parent
b39662f395
commit
1ee2b977f6
@ -0,0 +1,41 @@
|
||||
@import "@Themes/constants.scss";
|
||||
|
||||
.root {
|
||||
cursor: pointer;
|
||||
height: 56px;
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
padding: var(--spacing-2, 16px) var(--spacing-sm, 8px);
|
||||
gap: var(--spacing-2, 16px);
|
||||
|
||||
border-radius: var(--input-radius, 0px);
|
||||
border: 1px solid var(--dropdown-input-border-default, #d7dce0);
|
||||
background: var(--dropdown-input-background, #fff);
|
||||
|
||||
.content {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
padding: 0px var(--spacing-2, 16px);
|
||||
align-items: center;
|
||||
flex: 1 0 0;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
border-color: var(--dropdown-input-border-hovered);
|
||||
}
|
||||
|
||||
&.active {
|
||||
border-color: var(--dropdown-input-border-filled);
|
||||
}
|
||||
|
||||
&.open {
|
||||
border-color: var(--dropdown-input-border-expanded);
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
opacity: var(--opacity-disabled, 0.3);
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
65
src/front/Components/DesignSystem/Autocomplete/index.tsx
Normal file
65
src/front/Components/DesignSystem/Autocomplete/index.tsx
Normal file
@ -0,0 +1,65 @@
|
||||
import useOpenable from "@Front/Hooks/useOpenable";
|
||||
import { useState, useEffect, useCallback } from "react";
|
||||
import DropdownMenu from "../Dropdown/DropdownMenu";
|
||||
import { IOption } from "../Dropdown/DropdownMenu/DropdownOption";
|
||||
import SearchBar from "../SearchBar";
|
||||
|
||||
type IProps = {
|
||||
options: IOption[];
|
||||
placeholder: string;
|
||||
disabled?: boolean;
|
||||
};
|
||||
|
||||
export default function Autocomplete(props: IProps) {
|
||||
const { options, placeholder, disabled } = props;
|
||||
const [selectedOption, setSelectedOption] = useState<IOption | null>(null);
|
||||
const [searchValue, setSearchValue] = useState("");
|
||||
const [filteredOptions, setFilteredOptions] = useState<IOption[]>(options);
|
||||
const openable = useOpenable({ defaultOpen: false });
|
||||
|
||||
useEffect(() => {
|
||||
if (searchValue) {
|
||||
const filteredOptions = options.filter((option) => getLabel(option)?.toLowerCase().includes(searchValue.toLowerCase()));
|
||||
console.log(filteredOptions);
|
||||
if (filteredOptions.length === 0)
|
||||
return setFilteredOptions([{ id: "no-results", label: "Aucun résulats", notSelectable: true }]);
|
||||
return setFilteredOptions(filteredOptions);
|
||||
}
|
||||
return setFilteredOptions(options);
|
||||
}, [searchValue, options]);
|
||||
|
||||
const handleSearchChange = useCallback(
|
||||
(value: string) => {
|
||||
setSearchValue(value);
|
||||
if (value) {
|
||||
openable.open();
|
||||
} else {
|
||||
openable.close();
|
||||
}
|
||||
},
|
||||
[openable],
|
||||
);
|
||||
|
||||
const handleSelectOption = useCallback(
|
||||
(option: IOption) => {
|
||||
setSelectedOption(option);
|
||||
setSearchValue(getLabel(option) || "");
|
||||
openable.close();
|
||||
},
|
||||
[openable],
|
||||
);
|
||||
|
||||
function getLabel(option: IOption | null): string | null {
|
||||
if (!option) return null;
|
||||
if (typeof option.label === "string") {
|
||||
return option.label;
|
||||
}
|
||||
return `${option.label.text} ${option.label.subtext}`;
|
||||
}
|
||||
|
||||
return (
|
||||
<DropdownMenu options={filteredOptions} openable={openable} onSelect={handleSelectOption} selectedOption={selectedOption}>
|
||||
<SearchBar placeholder={placeholder} disabled={disabled} onChange={handleSearchChange} value={searchValue} />
|
||||
</DropdownMenu>
|
||||
);
|
||||
}
|
@ -30,4 +30,9 @@
|
||||
&:active {
|
||||
background-color: var(--dropdown-option-background-pressed);
|
||||
}
|
||||
|
||||
&[data-not-selectable="true"] {
|
||||
pointer-events: none;
|
||||
user-select: none;
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,3 @@
|
||||
import IconButton from "@Front/Components/DesignSystem/IconButton";
|
||||
import Typography, { ETypo, ETypoColor } from "@Front/Components/DesignSystem/Typography";
|
||||
import { CheckIcon } from "@heroicons/react/24/outline";
|
||||
import { useCallback } from "react";
|
||||
@ -8,6 +7,7 @@ import classes from "./classes.module.scss";
|
||||
export type IOption = {
|
||||
id: string;
|
||||
label: string | { text: string; subtext: string };
|
||||
notSelectable?: boolean;
|
||||
};
|
||||
|
||||
type IProps = {
|
||||
@ -22,7 +22,7 @@ export default function DropdownOption(props: IProps) {
|
||||
const handleOnClick = useCallback(() => onClick && onClick(option), [onClick, option]);
|
||||
|
||||
return (
|
||||
<div className={classes["root"]} onClick={handleOnClick}>
|
||||
<div className={classes["root"]} data-not-selectable={!!option.notSelectable} onClick={handleOnClick}>
|
||||
{getContent(option.label)}
|
||||
{isActive && <CheckIcon />}
|
||||
</div>
|
||||
|
@ -7,6 +7,7 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
z-index: 3;
|
||||
|
||||
padding: var(--spacing-sm, 8px);
|
||||
border-radius: var(--dropdown-radius, 0px);
|
||||
|
@ -1,6 +1,8 @@
|
||||
@import "@Themes/constants.scss";
|
||||
|
||||
.root {
|
||||
height: 56px;
|
||||
|
||||
display: flex;
|
||||
padding: var(--spacing-2, 16px) var(--spacing-sm, 8px);
|
||||
align-items: flex-start;
|
||||
@ -10,6 +12,10 @@
|
||||
border: 1px solid var(--input-main-border-default, #d7dce0);
|
||||
background: var(--input-background, #fff);
|
||||
|
||||
svg {
|
||||
stroke: var(--button-icon-button-default-default);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
border-radius: var(--input-radius, 0px);
|
||||
border: 1px solid var(--input-main-border-hovered, #b4bec5);
|
||||
@ -28,6 +34,11 @@
|
||||
background: var(--input-background, #fff);
|
||||
}
|
||||
|
||||
&[data-is-disabled="true"] {
|
||||
opacity: var(--opacity-disabled, 0.3);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.input-container {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
|
@ -1,16 +1,18 @@
|
||||
import React, { useCallback } from "react";
|
||||
import React, { useCallback, useEffect } from "react";
|
||||
|
||||
import classes from "./classes.module.scss";
|
||||
import { MagnifyingGlassIcon, XMarkIcon } from "@heroicons/react/24/outline";
|
||||
|
||||
type IProps = {
|
||||
onChange?: (input: string) => void;
|
||||
value?: string;
|
||||
placeholder?: string;
|
||||
disabled?: boolean;
|
||||
};
|
||||
|
||||
export default function SearchBar({ onChange, placeholder = "Rechercher" }: IProps) {
|
||||
export default function SearchBar({ onChange, value: propValue, placeholder = "Rechercher", disabled = false }: IProps) {
|
||||
const [isFocused, setIsFocused] = React.useState(false);
|
||||
const [value, setValue] = React.useState("");
|
||||
const [value, setValue] = React.useState(propValue || "");
|
||||
|
||||
const changeValue = useCallback(
|
||||
(value: string) => {
|
||||
@ -25,8 +27,14 @@ export default function SearchBar({ onChange, placeholder = "Rechercher" }: IPro
|
||||
const handleBlur = () => setIsFocused(false);
|
||||
const clearValue = () => changeValue("");
|
||||
|
||||
useEffect(() => {
|
||||
if (propValue !== undefined) {
|
||||
setValue(propValue);
|
||||
}
|
||||
}, [propValue]);
|
||||
|
||||
return (
|
||||
<label className={classes["root"]} data-is-focused={isFocused} data-has-value={value !== ""}>
|
||||
<label className={classes["root"]} data-is-focused={isFocused} data-has-value={value !== ""} data-is-disabled={disabled}>
|
||||
<div className={classes["input-container"]}>
|
||||
<MagnifyingGlassIcon width="24" height="24" />
|
||||
<input
|
||||
|
@ -31,6 +31,7 @@ import {
|
||||
import { useCallback, useMemo, useState } from "react";
|
||||
|
||||
import classes from "./classes.module.scss";
|
||||
import Autocomplete from "@Front/Components/DesignSystem/Autocomplete";
|
||||
|
||||
export default function DesignSystem() {
|
||||
const { isOpen, open, close } = useOpenable();
|
||||
@ -81,7 +82,30 @@ export default function DesignSystem() {
|
||||
<Typography typo={ETypo.DISPLAY_LARGE}>DesignSystem</Typography>
|
||||
<Newsletter isOpen={false} />
|
||||
<div className={classes["root"]}>
|
||||
<div />
|
||||
<div className={classes["components"]}>
|
||||
<Typography typo={ETypo.TEXT_LG_BOLD}>Autocomplete</Typography>
|
||||
<Autocomplete
|
||||
options={[
|
||||
{
|
||||
id: "1",
|
||||
label: "Option 1",
|
||||
},
|
||||
{
|
||||
id: "2",
|
||||
label: "Option 2",
|
||||
},
|
||||
{
|
||||
id: "3",
|
||||
label: "Option 3",
|
||||
},
|
||||
{
|
||||
id: "4",
|
||||
label: { text: "Option 4", subtext: "Subtext" },
|
||||
},
|
||||
]}
|
||||
placeholder="Placeholder"
|
||||
/>
|
||||
<Typography typo={ETypo.TEXT_LG_BOLD}>Dropdown</Typography>
|
||||
<Dropdown
|
||||
options={[
|
||||
@ -104,8 +128,6 @@ export default function DesignSystem() {
|
||||
]}
|
||||
placeholder="Placeholder"
|
||||
/>
|
||||
</div>
|
||||
<div className={classes["components"]}>
|
||||
<Typography typo={ETypo.TEXT_LG_BOLD}>Navigation latérale</Typography>
|
||||
<SearchBlockList
|
||||
blocks={[
|
||||
|
Loading…
x
Reference in New Issue
Block a user