✨ 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 {
|
&:active {
|
||||||
background-color: var(--dropdown-option-background-pressed);
|
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 Typography, { ETypo, ETypoColor } from "@Front/Components/DesignSystem/Typography";
|
||||||
import { CheckIcon } from "@heroicons/react/24/outline";
|
import { CheckIcon } from "@heroicons/react/24/outline";
|
||||||
import { useCallback } from "react";
|
import { useCallback } from "react";
|
||||||
@ -8,6 +7,7 @@ import classes from "./classes.module.scss";
|
|||||||
export type IOption = {
|
export type IOption = {
|
||||||
id: string;
|
id: string;
|
||||||
label: string | { text: string; subtext: string };
|
label: string | { text: string; subtext: string };
|
||||||
|
notSelectable?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
type IProps = {
|
type IProps = {
|
||||||
@ -22,7 +22,7 @@ export default function DropdownOption(props: IProps) {
|
|||||||
const handleOnClick = useCallback(() => onClick && onClick(option), [onClick, option]);
|
const handleOnClick = useCallback(() => onClick && onClick(option), [onClick, option]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={classes["root"]} onClick={handleOnClick}>
|
<div className={classes["root"]} data-not-selectable={!!option.notSelectable} onClick={handleOnClick}>
|
||||||
{getContent(option.label)}
|
{getContent(option.label)}
|
||||||
{isActive && <CheckIcon />}
|
{isActive && <CheckIcon />}
|
||||||
</div>
|
</div>
|
||||||
|
@ -7,6 +7,7 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
|
z-index: 3;
|
||||||
|
|
||||||
padding: var(--spacing-sm, 8px);
|
padding: var(--spacing-sm, 8px);
|
||||||
border-radius: var(--dropdown-radius, 0px);
|
border-radius: var(--dropdown-radius, 0px);
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
@import "@Themes/constants.scss";
|
@import "@Themes/constants.scss";
|
||||||
|
|
||||||
.root {
|
.root {
|
||||||
|
height: 56px;
|
||||||
|
|
||||||
display: flex;
|
display: flex;
|
||||||
padding: var(--spacing-2, 16px) var(--spacing-sm, 8px);
|
padding: var(--spacing-2, 16px) var(--spacing-sm, 8px);
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
@ -10,6 +12,10 @@
|
|||||||
border: 1px solid var(--input-main-border-default, #d7dce0);
|
border: 1px solid var(--input-main-border-default, #d7dce0);
|
||||||
background: var(--input-background, #fff);
|
background: var(--input-background, #fff);
|
||||||
|
|
||||||
|
svg {
|
||||||
|
stroke: var(--button-icon-button-default-default);
|
||||||
|
}
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
border-radius: var(--input-radius, 0px);
|
border-radius: var(--input-radius, 0px);
|
||||||
border: 1px solid var(--input-main-border-hovered, #b4bec5);
|
border: 1px solid var(--input-main-border-hovered, #b4bec5);
|
||||||
@ -28,6 +34,11 @@
|
|||||||
background: var(--input-background, #fff);
|
background: var(--input-background, #fff);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&[data-is-disabled="true"] {
|
||||||
|
opacity: var(--opacity-disabled, 0.3);
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
.input-container {
|
.input-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
@ -1,16 +1,18 @@
|
|||||||
import React, { useCallback } from "react";
|
import React, { useCallback, useEffect } from "react";
|
||||||
|
|
||||||
import classes from "./classes.module.scss";
|
import classes from "./classes.module.scss";
|
||||||
import { MagnifyingGlassIcon, XMarkIcon } from "@heroicons/react/24/outline";
|
import { MagnifyingGlassIcon, XMarkIcon } from "@heroicons/react/24/outline";
|
||||||
|
|
||||||
type IProps = {
|
type IProps = {
|
||||||
onChange?: (input: string) => void;
|
onChange?: (input: string) => void;
|
||||||
|
value?: string;
|
||||||
placeholder?: 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 [isFocused, setIsFocused] = React.useState(false);
|
||||||
const [value, setValue] = React.useState("");
|
const [value, setValue] = React.useState(propValue || "");
|
||||||
|
|
||||||
const changeValue = useCallback(
|
const changeValue = useCallback(
|
||||||
(value: string) => {
|
(value: string) => {
|
||||||
@ -25,8 +27,14 @@ export default function SearchBar({ onChange, placeholder = "Rechercher" }: IPro
|
|||||||
const handleBlur = () => setIsFocused(false);
|
const handleBlur = () => setIsFocused(false);
|
||||||
const clearValue = () => changeValue("");
|
const clearValue = () => changeValue("");
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (propValue !== undefined) {
|
||||||
|
setValue(propValue);
|
||||||
|
}
|
||||||
|
}, [propValue]);
|
||||||
|
|
||||||
return (
|
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"]}>
|
<div className={classes["input-container"]}>
|
||||||
<MagnifyingGlassIcon width="24" height="24" />
|
<MagnifyingGlassIcon width="24" height="24" />
|
||||||
<input
|
<input
|
||||||
|
@ -31,6 +31,7 @@ import {
|
|||||||
import { useCallback, useMemo, useState } from "react";
|
import { useCallback, useMemo, useState } from "react";
|
||||||
|
|
||||||
import classes from "./classes.module.scss";
|
import classes from "./classes.module.scss";
|
||||||
|
import Autocomplete from "@Front/Components/DesignSystem/Autocomplete";
|
||||||
|
|
||||||
export default function DesignSystem() {
|
export default function DesignSystem() {
|
||||||
const { isOpen, open, close } = useOpenable();
|
const { isOpen, open, close } = useOpenable();
|
||||||
@ -81,7 +82,30 @@ export default function DesignSystem() {
|
|||||||
<Typography typo={ETypo.DISPLAY_LARGE}>DesignSystem</Typography>
|
<Typography typo={ETypo.DISPLAY_LARGE}>DesignSystem</Typography>
|
||||||
<Newsletter isOpen={false} />
|
<Newsletter isOpen={false} />
|
||||||
<div className={classes["root"]}>
|
<div className={classes["root"]}>
|
||||||
|
<div />
|
||||||
<div className={classes["components"]}>
|
<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>
|
<Typography typo={ETypo.TEXT_LG_BOLD}>Dropdown</Typography>
|
||||||
<Dropdown
|
<Dropdown
|
||||||
options={[
|
options={[
|
||||||
@ -104,8 +128,6 @@ export default function DesignSystem() {
|
|||||||
]}
|
]}
|
||||||
placeholder="Placeholder"
|
placeholder="Placeholder"
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
<div className={classes["components"]}>
|
|
||||||
<Typography typo={ETypo.TEXT_LG_BOLD}>Navigation latérale</Typography>
|
<Typography typo={ETypo.TEXT_LG_BOLD}>Navigation latérale</Typography>
|
||||||
<SearchBlockList
|
<SearchBlockList
|
||||||
blocks={[
|
blocks={[
|
||||||
|
Loading…
x
Reference in New Issue
Block a user