diff --git a/config/eslint.config.js b/config/eslint.config.js index 03da9695e..2c7016a2b 100644 --- a/config/eslint.config.js +++ b/config/eslint.config.js @@ -80,6 +80,13 @@ module.exports = { "@typescript-eslint/no-useless-constructor": "error", "@typescript-eslint/consistent-type-assertions": "warn", + + "import/no-extraneous-dependencies": [ + "error", + { + devDependencies: [".storybook/**", "**/*.stories.tsx"], + }, + ], }, settings: { react: { diff --git a/packages/css/src/Form/InputSelect/InputSelect.agent.scss b/packages/css/src/Form/InputSelect/InputSelect.agent.scss new file mode 100644 index 000000000..94c892b22 --- /dev/null +++ b/packages/css/src/Form/InputSelect/InputSelect.agent.scss @@ -0,0 +1,112 @@ +@use "../../common/common.agent.scss" as common; +@use "../core/FormCore.agent.scss"; +@use "../../common/grid.scss"; +@use "../../common/reboot.scss"; + +@mixin _set-message-type($color) { + .af-form__input-text { + border: 1px solid $color; + color: $color; + } + + .af-form__select-container { + border: 1px solid $color; + color: $color; + } +} + +.af-form { + &__select { + position: relative; + + &-container { + position: relative; + display: inline-block; + border: 1px solid common.$color-silver; + background: common.$white; + + .glyphicon-menu-down { + position: absolute; + top: 50%; + right: 1em; + font-size: 0.7em; + transform: translateY(-50%); + } + } + + &--success, + &--valid { + .af-form__select-container { + margin-right: 1rem; + } + + &::after { + font-family: common.$font-family-icon; + color: common.$color-malachite; + content: "\EABA"; + } + + > .af-btn--circle, + > .af-form__message { + display: none; + } + } + + &--valid { + &::after { + display: none; + } + + .glyphicon-ok { + position: absolute; + top: 50%; + right: -25px; + width: 17px; + margin-left: 2px; + transform: translate(0, -50%); + fill: common.$color-btn-success; + } + } + + &--disabled { + .af-form__select-container { + background: common.$color-mercury; + cursor: not-allowed; + } + } + + &--error { + @include _set-message-type(common.$color-red-axa); + + select { + color: common.$color-red-axa; + } + } + + &--warning { + @include _set-message-type(common.$color-orange-dark); + } + } + + &__input-select { + position: relative; + z-index: 1; + padding: 0.5em 2.7em 0.5em 1em; + border: 0; + font-size: 1em; + background: transparent; + appearance: none; + + &::-ms-expand { + display: none; + } + + &:focus { + border-color: common.$color-axa; + } + + &--hasinfobulle { + margin-right: 1rem; + } + } +} diff --git a/packages/css/src/Form/InputSelect/InputSelect.agent.stories.ts b/packages/css/src/Form/InputSelect/InputSelect.agent.stories.ts new file mode 100644 index 000000000..f7bebdf40 --- /dev/null +++ b/packages/css/src/Form/InputSelect/InputSelect.agent.stories.ts @@ -0,0 +1,40 @@ +import type { Meta, StoryObj } from "@storybook/html"; +import "../../common/icons.scss"; +import "./InputSelect.agent.scss"; + +const meta: Meta = { + title: "Select", +}; + +export default meta; + +export const Default: StoryObj = { + render: () => { + const btn = document.createElement("div"); + btn.innerHTML = `
+ +
+
+
+
+ + +
Aide à la saisie +
+
`; + + btn.className = "af-form__group row"; + + return btn; + }, + args: { + label: "Select", + }, + argTypes: {}, +}; diff --git a/packages/react/src/Form/InputSelect/Select.mdx b/packages/react/src/Form/InputSelect/Select.mdx new file mode 100644 index 000000000..63905de61 --- /dev/null +++ b/packages/react/src/Form/InputSelect/Select.mdx @@ -0,0 +1,44 @@ +import * as SelectInputStories from "./SelectInput.stories.tsx"; +import * as SelectStories from "./Select.stories.tsx"; +import { ArgsTable, Canvas, Meta } from "@storybook/addon-docs"; + + + +# Select + +The select component comes in two variants: [**with layout** `InputSelect`](#inputselect--with-label) and [**without** label `Select`](#select-without-label). +`SelectBase` also exists, but is just the `Select` component in `mode="base"`, and will be deprecated in the future. + +In most cases you will want to use the version with the label. However, in some cases, the default layout will not work for you. +In that case, you can use the version without the label. + +## `InputSelect` — With label + +This is the fully-fledged component, with label, description, and error message. + + + + + +### Required + +The component can be required. In that case, the label will be followed by a red asterisk. In order to make the component required, you need to add to the `classModifier` the value `required`. + +### Status messages + +The component can be in one of 4 states: the default one which will display the help message, `success`, `error`, and `warning`. +In order to display the message and color the component, you need to pass the `message`, `messageType` props and set `forceDisplayMessage` to `true`. + + + +### Placeholders + +The component comes with 2 modes : `base` and `default`. The only difference is that the `base` mode does not display the placeholder. + +## `Select` Without label + +The component without the label is a bare-bones version of the component. It is useful when you need to customize the layout of the component. + + + + diff --git a/packages/react/src/Form/InputSelect/Select.stories.tsx b/packages/react/src/Form/InputSelect/Select.stories.tsx new file mode 100644 index 000000000..9a6af10fd --- /dev/null +++ b/packages/react/src/Form/InputSelect/Select.stories.tsx @@ -0,0 +1,43 @@ +import { Meta, StoryObj } from "@storybook/react"; +import { ComponentProps } from "react"; +import { Select } from "./Select"; + +const options = [ + { value: "fun", label: "For fun" }, + { value: "work", label: "For work" }, + { value: "drink", label: "For drink" }, +]; + +const meta: Meta = { + component: Select, + title: "Components/Form/Input/Select", + argTypes: { onChange: { action: "onChange" } }, +}; + +export default meta; + +type StoryProps = ComponentProps; +type Story = StoryObj; + +export const SelectStory: Story = { + name: "Select", + tags: ["Form", "Input"], + render: ({ onChange, ...args }) => + {options.map(({ label, ...opt }) => ( + + ))} + + + + ); + }, +); + +SelectBase.displayName = "SelectBase"; + +export { SelectBase }; diff --git a/packages/react/src/Form/InputSelect/SelectDefault.tsx b/packages/react/src/Form/InputSelect/SelectDefault.tsx new file mode 100644 index 000000000..a063525e3 --- /dev/null +++ b/packages/react/src/Form/InputSelect/SelectDefault.tsx @@ -0,0 +1,41 @@ +import { ComponentPropsWithRef, useId, useState } from "react"; +import { SelectBase } from "./SelectBase"; + +type Props = ComponentPropsWithRef & { + forceDisplayPlaceholder?: boolean; + placeholder?: string; +}; + +const SelectDefault = ({ + onChange, + forceDisplayPlaceholder = false, + value, + placeholder = "- Select -", + options, + id, + ...otherProps +}: Props) => { + const [hasHandleChangeOnce, setHasHandleChangeOnce] = useState(false); + const generatedId = useId(); + const inputId = id ?? generatedId; + const newOptions = hasHandleChangeOnce + ? options + : [{ value: "", label: placeholder }, ...options]; + + return ( + { + if (onChange) { + onChange(e); + } + setHasHandleChangeOnce(!forceDisplayPlaceholder); + }} + /> + ); +}; + +export { SelectDefault }; diff --git a/packages/react/src/Form/InputSelect/SelectInput.stories.tsx b/packages/react/src/Form/InputSelect/SelectInput.stories.tsx new file mode 100644 index 000000000..932f843cb --- /dev/null +++ b/packages/react/src/Form/InputSelect/SelectInput.stories.tsx @@ -0,0 +1,125 @@ +import { Meta, StoryObj } from "@storybook/react"; +import { ComponentProps } from "react"; +import { MessageTypes } from "../core"; +import { SelectInput } from "./SelectInput"; + +const meta: Meta = { + component: SelectInput, + title: "Components/Form/Input/Select", + argTypes: { onChange: { action: "onChange" } }, +}; + +export default meta; + +type StoryProps = ComponentProps; +type Story = StoryObj; + +const options = [ + { value: "fun", label: "For fun" }, + { value: "work", label: "For work" }, + { value: "drink", label: "For drink" }, +]; + +export const SelectInputStory: Story = { + name: "Select with label", + tags: ["Form", "Input"], + render: ({ onChange, ...args }) => ( + + ), + args: { + label: "Place type", + mode: "default", + helpMessage: "Enter the place type", + disabled: false, + isVisible: true, + classModifier: "", + className: "", + placeholder: "- Select -", + message: "", + messageType: undefined, + forceDisplayMessage: false, + classNameContainerLabel: "col-md-2", + classNameContainerInput: "col-md-10", + forceDisplayPlaceholder: false, + name: "placeName", + options, + }, + argTypes: { + messageType: { + control: { + type: "select", + options: Object.keys(MessageTypes), + }, + }, + classModifier: { + control: "inline-check", + options: ["required"], + }, + name: { table: { disable: true } }, + id: { table: { disable: true } }, + setStateMemoryFn: { table: { disable: true } }, + onChangeByStateFn: { table: { disable: true } }, + initialState: { table: { disable: true } }, + setStateOnFocusFn: { table: { disable: true } }, + setStateOnBlurFn: { table: { disable: true } }, + roleContainer: { table: { disable: true } }, + ariaLabelContainer: { table: { disable: true } }, + isLabelContainerLinkedToInput: { table: { disable: true } }, + }, +}; + +export const SelectWithStatus: StoryObj> = { + name: "Select with statuses", + tags: ["Form", "Input"], + render: ({ onChange }) => ( + <> + + + + + + + ), + argTypes: { + onChange: { action: "onChange" }, + }, +}; diff --git a/packages/react/src/Form/InputSelect/SelectInput.tsx b/packages/react/src/Form/InputSelect/SelectInput.tsx new file mode 100644 index 000000000..131339725 --- /dev/null +++ b/packages/react/src/Form/InputSelect/SelectInput.tsx @@ -0,0 +1,64 @@ +import { ComponentProps, PropsWithChildren, ReactNode, useId } from "react"; + +import { Field, FieldInput, HelpMessage, useInputClassModifier } from "../core"; +import { Select } from "./Select"; + +type Props = ComponentProps & + ComponentProps & { + helpMessage?: ReactNode; + }; + +const SelectInput = ({ + classModifier = "", + message, + children, + helpMessage, + id, + disabled = false, + label, + classNameContainerLabel, + classNameContainerInput, + messageType, + isVisible, + forceDisplayMessage, + className, + ...otherSelectProps +}: PropsWithChildren) => { + const generatedId = useId(); + const inputId = id ?? generatedId; + const { inputClassModifier, inputFieldClassModifier } = useInputClassModifier( + classModifier, + disabled, + Boolean(children), + ); + return ( + + +