Autocomplete

This commit is contained in:
Max S 2024-07-26 12:22:33 +02:00
parent b39662f395
commit 1ee2b977f6
8 changed files with 161 additions and 8 deletions

View File

@ -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;
}
}

View 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>
);
}

View File

@ -30,4 +30,9 @@
&:active {
background-color: var(--dropdown-option-background-pressed);
}
&[data-not-selectable="true"] {
pointer-events: none;
user-select: none;
}
}

View File

@ -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>

View File

@ -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);

View File

@ -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;

View File

@ -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

View File

@ -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={[