✨(react) add Select component
Finally our powerful Select component is available to make great forms!
This commit is contained in:
183
packages/react/src/components/Forms/Select/index.scss
Normal file
183
packages/react/src/components/Forms/Select/index.scss
Normal file
@@ -0,0 +1,183 @@
|
||||
.c__select {
|
||||
position: relative;
|
||||
|
||||
&__wrapper {
|
||||
border-radius: var(--c--components--forms-select--border-radius);
|
||||
border-width: var(--c--components--forms-select--border-width);
|
||||
border-color: var(--c--components--forms-select--border-color);
|
||||
border-style: var(--c--components--forms-select--border-style);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
transition: border var(--c--theme--transitions--duration) var(--c--theme--transitions--ease-out);
|
||||
padding: 0 0.75rem;
|
||||
gap: 1rem;
|
||||
color: var(--c--components--forms-select--color);
|
||||
box-sizing: border-box;
|
||||
height: var(--c--components--forms-select--height);
|
||||
cursor: pointer;
|
||||
background-color: var(--c--components--forms-select--background-color);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
|
||||
label {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
border-radius: var(--c--components--forms-select--border-radius--hover);
|
||||
border-color: var(--c--components--forms-select--border-color--hover);
|
||||
}
|
||||
|
||||
&:focus-within {
|
||||
border-radius: var(--c--components--forms-select--border-radius--focus);
|
||||
border-color: var(--c--components--forms-select--border-color--focus);
|
||||
}
|
||||
}
|
||||
|
||||
&__inner {
|
||||
flex-grow: 1;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
user-select: none;
|
||||
min-width: 0;
|
||||
|
||||
&__value {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
flex-grow: 1;
|
||||
font-size: var(--c--components--forms-select--font-size);
|
||||
|
||||
input {
|
||||
outline: 0;
|
||||
border: 0;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
color: var(--c--components--forms-select--color);
|
||||
font-size: var(--c--components--forms-select--font-size);
|
||||
}
|
||||
}
|
||||
|
||||
&__actions {
|
||||
position: relative;
|
||||
top: -14px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
span {
|
||||
font-size: 1.25rem;
|
||||
transition: all var(--c--theme--transitions--duration) var(--c--theme--transitions--ease-out);
|
||||
&.opened {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
}
|
||||
|
||||
&__clear {
|
||||
color: var(--c--theme--colors--greyscale-500);
|
||||
}
|
||||
&__separator {
|
||||
background-color: var(--c--theme--colors--greyscale-400);
|
||||
height: 24px;
|
||||
width: 1px;
|
||||
}
|
||||
&__open {
|
||||
color: var(--c--theme--colors--greyscale-900);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__menu {
|
||||
position: absolute;
|
||||
overflow: auto;
|
||||
width: calc(100% - 4px);
|
||||
max-height: 10rem;
|
||||
box-shadow: 0px 1px 4px rgba(0, 0, 0, 0.3);
|
||||
background-color: var(--c--components--forms-select--menu-background-color);
|
||||
transform: translate(2px, 0);
|
||||
display: none;
|
||||
z-index: 1;
|
||||
|
||||
&--opened {
|
||||
display: block;
|
||||
}
|
||||
|
||||
ul {
|
||||
list-style-type: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
padding-top: 3px;
|
||||
}
|
||||
|
||||
&__item {
|
||||
padding: 0.75rem;
|
||||
font-size: var(--c--components--forms-select--item-font-size);
|
||||
color: var(--c--components--forms-select--item-color);
|
||||
cursor: pointer;
|
||||
|
||||
&--highlight {
|
||||
background-color: var(--c--components--forms-select--item-background-color--hover);
|
||||
}
|
||||
|
||||
&--selected {
|
||||
background-color: var(--c--components--forms-select--item-background-color--selected);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Modifiers */
|
||||
|
||||
&--disabled {
|
||||
|
||||
.c__select__wrapper {
|
||||
color: var(--c--theme--colors--greyscale-600);
|
||||
border-color: var(--c--theme--colors--greyscale-200);
|
||||
cursor: default;
|
||||
|
||||
label {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
input {
|
||||
color: var(--c--theme--colors--greyscale-600);
|
||||
background-color: white;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.c__input__inner {
|
||||
.c__input, label {
|
||||
color: var(--c--theme--colors--greyscale-600);
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
border-color: var(--c--theme--colors--greyscale-200);
|
||||
}
|
||||
}
|
||||
|
||||
&--error {
|
||||
|
||||
.c__select__wrapper {
|
||||
border-color: var(--c--theme--colors--danger-600);
|
||||
}
|
||||
|
||||
&:not(.c__select__wrapper--disabled) {
|
||||
.c__select__wrapper:hover {
|
||||
border-color: var(--c--theme--colors--danger-200);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&--success {
|
||||
|
||||
.c__select__wrapper {
|
||||
border-color: var(--c--theme--colors--success-600);
|
||||
}
|
||||
|
||||
&:not(.c__select__wrapper--disabled) {
|
||||
.c__select__wrapper:hover {
|
||||
border-color: var(--c--theme--colors--success-400);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
1032
packages/react/src/components/Forms/Select/index.spec.tsx
Normal file
1032
packages/react/src/components/Forms/Select/index.spec.tsx
Normal file
File diff suppressed because it is too large
Load Diff
134
packages/react/src/components/Forms/Select/index.stories.mdx
Normal file
134
packages/react/src/components/Forms/Select/index.stories.mdx
Normal file
@@ -0,0 +1,134 @@
|
||||
import { Canvas, Meta, Story, Source, ArgsTable } from '@storybook/addon-docs';
|
||||
import { Select } from "./index";
|
||||
|
||||
<Meta title="Components/Forms/Select/Doc" component={Select}/>
|
||||
|
||||
export const Template = (args) => <Input {...args} />;
|
||||
|
||||
# Select
|
||||
|
||||
Cunningham provides a versatile Select component that you can use in your forms. This component follows the [ARIA design pattern](https://www.w3.org/WAI/ARIA/apg/patterns/combobox/examples/combobox-autocomplete-list/)
|
||||
using [Downshift](https://www.downshift-js.com/), so that mean there is no `select` wrapped inside it.
|
||||
|
||||
> For now it is only available for mono selection, multiple selection will be available soon.
|
||||
|
||||
<Canvas>
|
||||
<Story id="components-forms-select-mono--uncontrolled"/>
|
||||
</Canvas>
|
||||
|
||||
## Options
|
||||
|
||||
The available options must be given via the `options` props. It is an array of objects with the following shape:
|
||||
|
||||
<Source
|
||||
language='ts'
|
||||
dark
|
||||
format={false}
|
||||
code={`{
|
||||
label: string
|
||||
value?: string
|
||||
}`}
|
||||
/>
|
||||
|
||||
As you can see the `value` is optional, if not provided, the `label` will be used as the value.
|
||||
|
||||
## Searchable
|
||||
|
||||
You can enable the text live filtering simply by using the `searchable` props.
|
||||
|
||||
<Canvas withSource="open">
|
||||
<Story id="components-forms-select-mono--searchable-uncontrolled"/>
|
||||
</Canvas>
|
||||
|
||||
## States
|
||||
|
||||
You can use the following props to change the state of the Select component by using the `state` props.
|
||||
|
||||
<Canvas withSource="open">
|
||||
<Story id="components-forms-select-mono--success"/>
|
||||
</Canvas>
|
||||
|
||||
<Canvas withSource="open">
|
||||
<Story id="components-forms-select-mono--error"/>
|
||||
</Canvas>
|
||||
|
||||
## Disabled
|
||||
|
||||
As a regular select, you can disable it by using the `disabled` props.
|
||||
|
||||
<Canvas withSource="open">
|
||||
<Story id="components-forms-select-mono--disabled"/>
|
||||
</Canvas>
|
||||
|
||||
## Texts
|
||||
|
||||
As the component uses [Field](?path=/story/components-forms-field-doc--page), you can use the `text` props to provide a description of the checkbox.
|
||||
|
||||
<Canvas withSource="open">
|
||||
<Story id="components-forms-select-mono--with-text"/>
|
||||
</Canvas>
|
||||
|
||||
## Width
|
||||
|
||||
By default, the select has a default width, like all inputs. But you can force it to take the full width of its container by using the `fullWidth` props.
|
||||
|
||||
<Canvas withSource="open">
|
||||
<Story id="components-forms-select-mono--full-width"/>
|
||||
</Canvas>
|
||||
|
||||
## Controlled / Non Controlled
|
||||
|
||||
Like a native select, you can use the Select component in a controlled or non controlled way. You can see the example below
|
||||
using the component in a controlled way.
|
||||
|
||||
<Canvas withSource="open">
|
||||
<Story id="components-forms-select-mono--controlled"/>
|
||||
</Canvas>
|
||||
|
||||
## Props
|
||||
|
||||
The props of this component are as close as possible to the native select component. You can see the list of props below.
|
||||
|
||||
<ArgsTable of={Select} />
|
||||
|
||||
## Design tokens
|
||||
|
||||
Here are the custom design tokens defined by the select.
|
||||
|
||||
| Token | Description |
|
||||
|--------------- |----------------------------- |
|
||||
| background-color | Background color of the select |
|
||||
| border-color | Border color of the select |
|
||||
| border-color--hover | Border color of the select on mouse hover |
|
||||
| border-color--focus | Border color of the select when focus |
|
||||
| border-radius | Border radius of the select |
|
||||
| border-radius--hover | Border radius of the select on mouse hover |
|
||||
| border-radius--focus | Border radius of the select when focused |
|
||||
| color | Value color |
|
||||
| font-size | Value font size |
|
||||
| height | Height of the combo box |
|
||||
| item-background-color--hover | Background color of the item on mouse hover |
|
||||
| item-background-color--selected | Background color of the selected item |
|
||||
| item-color | Color of the item |
|
||||
| item-font-size | Font size of the item |
|
||||
| menu-background-color | Background color of the menu |
|
||||
|
||||
See also [Field](?path=/story/components-forms-field-doc--page)
|
||||
|
||||
## Form Example
|
||||
|
||||
<Canvas>
|
||||
<Story id="components-forms-select-mono--form-example"/>
|
||||
</Canvas>
|
||||
|
||||
##
|
||||
|
||||
<img src="components/Forms/Select/resources/dd_1.svg"/>
|
||||
|
||||
##
|
||||
|
||||
<img src="components/Forms/Select/resources/dd_2.svg"/>
|
||||
|
||||
##
|
||||
|
||||
<img src="components/Forms/Select/resources/dd_3.svg"/>
|
||||
352
packages/react/src/components/Forms/Select/index.tsx
Normal file
352
packages/react/src/components/Forms/Select/index.tsx
Normal file
@@ -0,0 +1,352 @@
|
||||
import React, {
|
||||
HTMLAttributes,
|
||||
PropsWithChildren,
|
||||
useEffect,
|
||||
useRef,
|
||||
useState,
|
||||
} from "react";
|
||||
import {
|
||||
useCombobox,
|
||||
useSelect,
|
||||
UseSelectReturnValue,
|
||||
UseSelectStateChange,
|
||||
} from "downshift";
|
||||
import classNames from "classnames";
|
||||
import { useCunningham } from ":/components/Provider";
|
||||
import { Field, FieldProps } from ":/components/Forms/Field";
|
||||
import { LabelledBox } from ":/components/Forms/LabelledBox";
|
||||
import { Button } from ":/components/Button";
|
||||
|
||||
interface Option {
|
||||
value?: string;
|
||||
label: string;
|
||||
}
|
||||
|
||||
type Props = PropsWithChildren &
|
||||
FieldProps & {
|
||||
label: string;
|
||||
options: Option[];
|
||||
searchable?: boolean;
|
||||
name?: string;
|
||||
defaultValue?: string | number;
|
||||
value?: string | number;
|
||||
onChange?: (event: {
|
||||
target: { value: string | number | undefined };
|
||||
}) => void;
|
||||
disabled?: boolean;
|
||||
};
|
||||
|
||||
function getOptionsFilter(inputValue?: string) {
|
||||
return (option: Option) => {
|
||||
return (
|
||||
!inputValue ||
|
||||
option.label.toLowerCase().includes(inputValue.toLowerCase()) ||
|
||||
option.value?.toLowerCase().includes(inputValue.toLowerCase())
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
const optionToString = (option: Option | null) => {
|
||||
return option ? option.label : "";
|
||||
};
|
||||
|
||||
const optionToValue = (option: Option) => {
|
||||
return option.value ?? option.label;
|
||||
};
|
||||
|
||||
interface SubProps extends Props {
|
||||
defaultSelectedItem?: Option;
|
||||
downshiftProps: {
|
||||
initialSelectedItem?: Option;
|
||||
onSelectedItemChange?: any;
|
||||
};
|
||||
}
|
||||
|
||||
interface SelectAuxProps extends SubProps {
|
||||
options: Option[];
|
||||
labelAsPlaceholder: boolean;
|
||||
downshiftReturn: {
|
||||
isOpen: boolean;
|
||||
wrapperProps?: HTMLAttributes<HTMLDivElement>;
|
||||
selectedItem?: Option | null;
|
||||
getLabelProps: any;
|
||||
toggleButtonProps: any;
|
||||
getMenuProps: any;
|
||||
getItemProps: any;
|
||||
highlightedIndex: number;
|
||||
selectItem: UseSelectReturnValue<Option>["selectItem"];
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* This component is used by searchable and non-searchable select components.
|
||||
* It contains the common logic between the two.
|
||||
*/
|
||||
const SelectAux = ({
|
||||
children,
|
||||
state = "default",
|
||||
text,
|
||||
rightText,
|
||||
fullWidth,
|
||||
options,
|
||||
name,
|
||||
label,
|
||||
labelAsPlaceholder,
|
||||
downshiftProps,
|
||||
downshiftReturn,
|
||||
value,
|
||||
disabled,
|
||||
}: SelectAuxProps) => {
|
||||
const { t } = useCunningham();
|
||||
const labelProps = downshiftReturn.getLabelProps();
|
||||
|
||||
// When component is controlled, this useEffect will update the local selected item.
|
||||
useEffect(() => {
|
||||
if (downshiftProps.initialSelectedItem !== undefined) {
|
||||
return;
|
||||
}
|
||||
const optionToSelect = options.find(
|
||||
(option) => optionToValue(option) === value
|
||||
);
|
||||
downshiftReturn.selectItem(optionToSelect ?? null);
|
||||
}, [value]);
|
||||
|
||||
return (
|
||||
<Field
|
||||
state={state}
|
||||
text={text}
|
||||
rightText={rightText}
|
||||
fullWidth={fullWidth}
|
||||
>
|
||||
<div
|
||||
className={classNames("c__select", "c__select--" + state, {
|
||||
"c__select--disabled": disabled,
|
||||
})}
|
||||
>
|
||||
{/* We disabled linting for this specific line because we consider that the onClick props is only used for */}
|
||||
{/* mouse users, so this do not engender any issue for accessibility. */}
|
||||
{/* eslint-disable-next-line jsx-a11y/no-static-element-interactions */}
|
||||
<div
|
||||
className={classNames("c__select__wrapper", {
|
||||
"c__select__wrapper--focus": downshiftReturn.isOpen && !disabled,
|
||||
})}
|
||||
{...downshiftReturn.wrapperProps}
|
||||
>
|
||||
{downshiftReturn.selectedItem && (
|
||||
<input
|
||||
type="hidden"
|
||||
name={name}
|
||||
value={optionToValue(downshiftReturn.selectedItem)}
|
||||
/>
|
||||
)}
|
||||
<LabelledBox
|
||||
label={label}
|
||||
labelAsPlaceholder={labelAsPlaceholder}
|
||||
htmlFor={labelProps.htmlFor}
|
||||
labelId={labelProps.id}
|
||||
>
|
||||
<div className="c__select__inner">
|
||||
<div className="c__select__inner__value">{children}</div>
|
||||
<div className="c__select__inner__actions">
|
||||
{!disabled && downshiftReturn.selectedItem && (
|
||||
<>
|
||||
<Button
|
||||
color="tertiary"
|
||||
size="small"
|
||||
aria-label={t(
|
||||
"components.forms.select.clear_button_aria_label"
|
||||
)}
|
||||
className="c__select__inner__actions__clear"
|
||||
onClick={(e) => {
|
||||
downshiftReturn.selectItem(null);
|
||||
e.stopPropagation();
|
||||
}}
|
||||
icon={<span className="material-icons">close</span>}
|
||||
/>
|
||||
<div className="c__select__inner__actions__separator" />
|
||||
</>
|
||||
)}
|
||||
|
||||
<Button
|
||||
color="tertiary"
|
||||
size="small"
|
||||
className="c__select__inner__actions__open"
|
||||
icon={
|
||||
<span
|
||||
className={classNames("material-icons", {
|
||||
opened: downshiftReturn.isOpen,
|
||||
})}
|
||||
>
|
||||
arrow_drop_down
|
||||
</span>
|
||||
}
|
||||
disabled={disabled}
|
||||
{...downshiftReturn.toggleButtonProps}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</LabelledBox>
|
||||
</div>
|
||||
<div
|
||||
className={classNames("c__select__menu", {
|
||||
"c__select__menu--opened": downshiftReturn.isOpen,
|
||||
})}
|
||||
{...downshiftReturn.getMenuProps()}
|
||||
>
|
||||
<ul>
|
||||
{downshiftReturn.isOpen &&
|
||||
options.map((item, index) => (
|
||||
<li
|
||||
className={classNames("c__select__menu__item", {
|
||||
"c__select__menu__item--highlight":
|
||||
downshiftReturn.highlightedIndex === index,
|
||||
"c__select__menu__item--selected":
|
||||
downshiftReturn.selectedItem === item,
|
||||
})}
|
||||
key={`${item.value}${index}`}
|
||||
{...downshiftReturn.getItemProps({ item, index })}
|
||||
>
|
||||
<span>{item.label}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</Field>
|
||||
);
|
||||
};
|
||||
|
||||
const SelectSimple = (props: SubProps) => {
|
||||
const downshiftReturn = useSelect({
|
||||
...props.downshiftProps,
|
||||
items: props.options,
|
||||
itemToString: optionToString,
|
||||
});
|
||||
const [labelAsPlaceholder, setLabelAsPlaceholder] = useState(
|
||||
!downshiftReturn.selectedItem
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setLabelAsPlaceholder(!downshiftReturn.selectedItem);
|
||||
}, [downshiftReturn.selectedItem]);
|
||||
|
||||
return (
|
||||
<SelectAux
|
||||
{...props}
|
||||
downshiftReturn={{
|
||||
...downshiftReturn,
|
||||
wrapperProps: downshiftReturn.getToggleButtonProps({
|
||||
disabled: props.disabled,
|
||||
}),
|
||||
toggleButtonProps: {},
|
||||
}}
|
||||
labelAsPlaceholder={labelAsPlaceholder}
|
||||
>
|
||||
{downshiftReturn.selectedItem && (
|
||||
<span>{optionToString(downshiftReturn.selectedItem)}</span>
|
||||
)}
|
||||
</SelectAux>
|
||||
);
|
||||
};
|
||||
|
||||
const SelectSearchable = (props: SubProps) => {
|
||||
const { t } = useCunningham();
|
||||
const [optionsToDisplay, setOptionsToDisplay] = useState(props.options);
|
||||
const [hasInputFocused, setHasInputFocused] = useState(false);
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
const downshiftReturn = useCombobox({
|
||||
...props.downshiftProps,
|
||||
items: optionsToDisplay,
|
||||
itemToString: optionToString,
|
||||
onInputValueChange: (e) => {
|
||||
setOptionsToDisplay(props.options.filter(getOptionsFilter(e.inputValue)));
|
||||
if (!e.inputValue) {
|
||||
downshiftReturn.selectItem(null);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const [labelAsPlaceholder, setLabelAsPlaceholder] = useState(
|
||||
!downshiftReturn.selectedItem
|
||||
);
|
||||
useEffect(() => {
|
||||
if (hasInputFocused || downshiftReturn.inputValue) {
|
||||
setLabelAsPlaceholder(false);
|
||||
return;
|
||||
}
|
||||
setLabelAsPlaceholder(!downshiftReturn.selectedItem);
|
||||
}, [
|
||||
downshiftReturn.selectedItem,
|
||||
hasInputFocused,
|
||||
downshiftReturn.inputValue,
|
||||
]);
|
||||
|
||||
const inputProps = downshiftReturn.getInputProps({
|
||||
ref: inputRef,
|
||||
disabled: props.disabled,
|
||||
});
|
||||
|
||||
return (
|
||||
<SelectAux
|
||||
{...props}
|
||||
downshiftReturn={{
|
||||
...downshiftReturn,
|
||||
wrapperProps: {
|
||||
onClick: () => {
|
||||
inputRef.current?.focus();
|
||||
},
|
||||
},
|
||||
toggleButtonProps: downshiftReturn.getToggleButtonProps({
|
||||
disabled: props.disabled,
|
||||
"aria-label": t("components.forms.select.toggle_button_aria_label"),
|
||||
}),
|
||||
}}
|
||||
labelAsPlaceholder={labelAsPlaceholder}
|
||||
options={optionsToDisplay}
|
||||
>
|
||||
<input
|
||||
className="w-full p-1.5"
|
||||
{...inputProps}
|
||||
onFocus={(e) => {
|
||||
inputProps.onFocus(e);
|
||||
setHasInputFocused(true);
|
||||
}}
|
||||
onBlur={(e) => {
|
||||
inputProps.onBlur(e);
|
||||
setHasInputFocused(false);
|
||||
}}
|
||||
/>
|
||||
</SelectAux>
|
||||
);
|
||||
};
|
||||
|
||||
export const Select = (props: Props) => {
|
||||
if (props.defaultValue && props.value) {
|
||||
throw new Error(
|
||||
"You cannot use both defaultValue and value props on Select component"
|
||||
);
|
||||
}
|
||||
|
||||
const defaultSelectedItem = props.defaultValue
|
||||
? props.options.find(
|
||||
(option) => optionToValue(option) === props.defaultValue
|
||||
)
|
||||
: undefined;
|
||||
|
||||
const commonDownshiftProps: SubProps["downshiftProps"] = {
|
||||
initialSelectedItem: defaultSelectedItem,
|
||||
onSelectedItemChange: (e: UseSelectStateChange<Option>) => {
|
||||
props.onChange?.({
|
||||
target: {
|
||||
value: e.selectedItem ? optionToValue(e.selectedItem) : undefined,
|
||||
},
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
return props.searchable ? (
|
||||
<SelectSearchable {...props} downshiftProps={commonDownshiftProps} />
|
||||
) : (
|
||||
<SelectSimple {...props} downshiftProps={commonDownshiftProps} />
|
||||
);
|
||||
};
|
||||
258
packages/react/src/components/Forms/Select/mono.stories.tsx
Normal file
258
packages/react/src/components/Forms/Select/mono.stories.tsx
Normal file
@@ -0,0 +1,258 @@
|
||||
import { ComponentMeta, ComponentStory } from "@storybook/react";
|
||||
import React, { useState } from "react";
|
||||
import { faker } from "@faker-js/faker";
|
||||
import { Select } from ":/components/Forms/Select";
|
||||
import { Button } from ":/components/Button";
|
||||
import { CunninghamProvider } from ":/components/Provider";
|
||||
|
||||
export default {
|
||||
title: "Components/Forms/Select/Mono",
|
||||
component: Select,
|
||||
} as ComponentMeta<typeof Select>;
|
||||
|
||||
const Template: ComponentStory<typeof Select> = (args) => (
|
||||
<div style={{ paddingBottom: "200px" }}>
|
||||
<CunninghamProvider>
|
||||
<Select {...args} />
|
||||
</CunninghamProvider>
|
||||
</div>
|
||||
);
|
||||
|
||||
const CITIES = Array.from({ length: 10 }).map(() => faker.address.city());
|
||||
const OPTIONS = CITIES.map((city) => ({
|
||||
label: city,
|
||||
value: city.toLowerCase(),
|
||||
}));
|
||||
|
||||
export const Uncontrolled = Template.bind({});
|
||||
Uncontrolled.args = {
|
||||
label: "Select a city",
|
||||
options: OPTIONS,
|
||||
defaultValue: OPTIONS[4].value,
|
||||
};
|
||||
|
||||
export const Disabled = Template.bind({});
|
||||
Disabled.args = {
|
||||
label: "Select a city",
|
||||
options: OPTIONS,
|
||||
defaultValue: OPTIONS[4].value,
|
||||
disabled: true,
|
||||
};
|
||||
|
||||
export const WithText = Template.bind({});
|
||||
WithText.args = {
|
||||
label: "Select a city",
|
||||
options: OPTIONS,
|
||||
defaultValue: OPTIONS[4].value,
|
||||
text: "This is a text, you can display anything you want here like warnings, information or errors.",
|
||||
};
|
||||
export const Controlled = () => {
|
||||
const [value, setValue] = useState(OPTIONS[8].value);
|
||||
return (
|
||||
<CunninghamProvider>
|
||||
<div>
|
||||
<div>
|
||||
Value: <span>{value}</span>
|
||||
</div>
|
||||
<Select
|
||||
label="Select a city"
|
||||
options={OPTIONS}
|
||||
value={value}
|
||||
onChange={(e) => setValue(e.target.value as string)}
|
||||
/>
|
||||
<Button onClick={() => setValue("")}>Reset</Button>
|
||||
</div>
|
||||
</CunninghamProvider>
|
||||
);
|
||||
};
|
||||
|
||||
export const Overflow = Template.bind({});
|
||||
Overflow.args = {
|
||||
label: "Select a city",
|
||||
options: [
|
||||
{
|
||||
value: "1",
|
||||
label: "Very long long long long long long long city name",
|
||||
},
|
||||
],
|
||||
defaultValue: "1",
|
||||
};
|
||||
|
||||
export const SearchableEmpty = Template.bind({});
|
||||
SearchableEmpty.args = {
|
||||
label: "Select a city",
|
||||
options: OPTIONS,
|
||||
searchable: true,
|
||||
};
|
||||
|
||||
export const SearchableUncontrolled = Template.bind({});
|
||||
SearchableUncontrolled.args = {
|
||||
label: "Select a city",
|
||||
options: OPTIONS,
|
||||
defaultValue: OPTIONS[4].value,
|
||||
searchable: true,
|
||||
};
|
||||
|
||||
export const SearchableDisabled = Template.bind({});
|
||||
SearchableDisabled.args = {
|
||||
label: "Select a city",
|
||||
options: OPTIONS,
|
||||
defaultValue: OPTIONS[4].value,
|
||||
searchable: true,
|
||||
disabled: true,
|
||||
};
|
||||
|
||||
export const SearchableControlled = () => {
|
||||
const [value, setValue] = useState(OPTIONS[8].value);
|
||||
return (
|
||||
<CunninghamProvider>
|
||||
<div>
|
||||
<div>
|
||||
Value: <span>{value}</span>
|
||||
</div>
|
||||
<Select
|
||||
label="Select a city"
|
||||
options={OPTIONS}
|
||||
searchable={true}
|
||||
value={value}
|
||||
onChange={(e) => setValue(e.target.value as string)}
|
||||
/>
|
||||
<Button onClick={() => setValue("")}>Reset</Button>
|
||||
</div>
|
||||
</CunninghamProvider>
|
||||
);
|
||||
};
|
||||
|
||||
export const FullWidth = Template.bind({});
|
||||
FullWidth.args = {
|
||||
label: "Select a city",
|
||||
options: OPTIONS,
|
||||
fullWidth: true,
|
||||
};
|
||||
|
||||
export const Success = Template.bind({});
|
||||
Success.args = {
|
||||
label: "Select a city",
|
||||
options: OPTIONS,
|
||||
state: "success",
|
||||
text: "Well done",
|
||||
};
|
||||
|
||||
export const Error = Template.bind({});
|
||||
Error.args = {
|
||||
label: "Select a city",
|
||||
options: OPTIONS,
|
||||
state: "error",
|
||||
text: "Something went wrong",
|
||||
};
|
||||
|
||||
export const FormExample = () => {
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
const data = new FormData(e.target as any);
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(data.getAll("city"));
|
||||
};
|
||||
|
||||
return (
|
||||
<CunninghamProvider>
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div className="mb-s">
|
||||
<Select
|
||||
label="Your city"
|
||||
name="city"
|
||||
options={OPTIONS}
|
||||
defaultValue={OPTIONS[2].value}
|
||||
text="The city you were born in"
|
||||
state="success"
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-s">
|
||||
<Select
|
||||
label="Your gender"
|
||||
name="gender"
|
||||
options={[
|
||||
{
|
||||
label: "Male",
|
||||
},
|
||||
{
|
||||
label: "Female",
|
||||
},
|
||||
{
|
||||
label: "Other",
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-s">
|
||||
<Select
|
||||
label="Your department"
|
||||
name="department"
|
||||
searchable={true}
|
||||
options={[
|
||||
{
|
||||
label: "Legal",
|
||||
},
|
||||
{
|
||||
label: "Tech",
|
||||
},
|
||||
{
|
||||
label: "AI",
|
||||
},
|
||||
{
|
||||
label: "Accounting",
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-s">
|
||||
<Select
|
||||
label="Your grade"
|
||||
text="Any error you want"
|
||||
name="grade"
|
||||
searchable={true}
|
||||
options={[
|
||||
{
|
||||
label: "Level 1",
|
||||
},
|
||||
{
|
||||
label: "Level 2",
|
||||
},
|
||||
{
|
||||
label: "Level 3",
|
||||
},
|
||||
{
|
||||
label: "Emperor",
|
||||
},
|
||||
]}
|
||||
state="error"
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-s">
|
||||
<Select
|
||||
label="Your plan"
|
||||
text="This field is disabled"
|
||||
name="grade"
|
||||
disabled={true}
|
||||
options={[
|
||||
{
|
||||
label: "Bronze",
|
||||
},
|
||||
{
|
||||
label: "Silver",
|
||||
},
|
||||
{
|
||||
label: "Gold",
|
||||
},
|
||||
{
|
||||
label: "Platinum",
|
||||
},
|
||||
]}
|
||||
defaultValue="Platinum"
|
||||
/>
|
||||
</div>
|
||||
<Button>Submit</Button>
|
||||
</form>
|
||||
</CunninghamProvider>
|
||||
);
|
||||
};
|
||||
127
packages/react/src/components/Forms/Select/resources/dd_1.svg
Normal file
127
packages/react/src/components/Forms/Select/resources/dd_1.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 266 KiB |
128
packages/react/src/components/Forms/Select/resources/dd_2.svg
Normal file
128
packages/react/src/components/Forms/Select/resources/dd_2.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 364 KiB |
135
packages/react/src/components/Forms/Select/resources/dd_3.svg
Normal file
135
packages/react/src/components/Forms/Select/resources/dd_3.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 268 KiB |
21
packages/react/src/components/Forms/Select/tokens.ts
Normal file
21
packages/react/src/components/Forms/Select/tokens.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { DefaultTokens } from "@openfun/cunningham-tokens";
|
||||
|
||||
export const tokens = (defaults: DefaultTokens) => ({
|
||||
"border-color": defaults.theme.colors["greyscale-300"],
|
||||
"border-color--focus": defaults.theme.colors["primary-600"],
|
||||
"border-color--hover": defaults.theme.colors["greyscale-500"],
|
||||
"border-radius": "8px",
|
||||
"border-radius--focus": "2px",
|
||||
"border-radius--hover": "2px",
|
||||
"border-style": "solid",
|
||||
"border-width": "2px",
|
||||
color: defaults.theme.colors["greyscale-800"],
|
||||
"font-size": defaults.theme.font.sizes.l,
|
||||
height: "3.5rem",
|
||||
"item-background-color--hover": defaults.theme.colors["greyscale-200"],
|
||||
"item-background-color--selected": defaults.theme.colors["primary-100"],
|
||||
"item-color": defaults.theme.colors["greyscale-800"],
|
||||
"item-font-size": defaults.theme.font.sizes.l,
|
||||
"background-color": "white",
|
||||
"menu-background-color": "white",
|
||||
});
|
||||
Reference in New Issue
Block a user