✨(Select) add classic variant with placeholder support
- Use FieldVariant enum for variant prop - Add styled placeholder for classic mode (.c__select__placeholder) - Label rendered outside wrapper in classic mode - Compact height in classic mode - Add unit tests for mono and multi Select - Add Storybook stories Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -3,6 +3,81 @@
|
||||
.c__select {
|
||||
position: relative;
|
||||
|
||||
&__label {
|
||||
display: block;
|
||||
font-size: var(--c--components--forms-labelledbox--classic-label-font-size);
|
||||
color: var(--c--components--forms-labelledbox--label-color--small);
|
||||
margin-bottom: var(
|
||||
--c--components--forms-labelledbox--classic-label-margin-bottom
|
||||
);
|
||||
cursor: pointer;
|
||||
|
||||
&--disabled {
|
||||
color: var(
|
||||
--c--components--forms-labelledbox--label-color--small--disabled
|
||||
);
|
||||
cursor: default;
|
||||
}
|
||||
}
|
||||
|
||||
&--classic {
|
||||
.c__select__wrapper {
|
||||
align-items: center;
|
||||
height: 2.75rem;
|
||||
}
|
||||
|
||||
.c__select__inner {
|
||||
align-items: center;
|
||||
|
||||
&__actions {
|
||||
top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
// When not populated, keep flex layout so actions are vertically centered
|
||||
// alongside the placeholder text (single line).
|
||||
&.c__select--multiline:not(.c__select--populated) {
|
||||
.c__select__wrapper {
|
||||
min-height: 2.75rem;
|
||||
}
|
||||
|
||||
.c__select__inner {
|
||||
display: flex;
|
||||
|
||||
&__actions {
|
||||
float: none;
|
||||
height: auto;
|
||||
top: 0;
|
||||
order: 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// When populated, keep flex layout and use fixed padding instead of
|
||||
// centering so that the first row of pills stays at a consistent
|
||||
// vertical position regardless of how many rows wrap below.
|
||||
&.c__select--multiline.c__select--populated {
|
||||
.c__select__wrapper {
|
||||
align-items: flex-start;
|
||||
min-height: 2.75rem;
|
||||
padding-top: 9px;
|
||||
padding-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.c__select__inner {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
|
||||
&__actions {
|
||||
float: none;
|
||||
height: auto;
|
||||
top: 0;
|
||||
order: 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__wrapper {
|
||||
border-radius: var(--c--components--forms-select--border-radius);
|
||||
border-width: var(--c--components--forms-select--border-width);
|
||||
@@ -34,14 +109,16 @@
|
||||
&:hover {
|
||||
border-radius: var(--c--components--forms-select--border-radius--hover);
|
||||
border-color: var(--c--components--forms-select--border-color--hover);
|
||||
box-shadow: 0 0 0 var(--c--components--forms-select--border-width--hover) var(--c--components--forms-select--border-color--hover);
|
||||
box-shadow: 0 0 0 var(--c--components--forms-select--border-width--hover)
|
||||
var(--c--components--forms-select--border-color--hover);
|
||||
}
|
||||
|
||||
&:focus-within,
|
||||
&--focus {
|
||||
border-radius: var(--c--components--forms-select--border-radius--focus);
|
||||
border-color: var(--c--components--forms-select--border-color--focus);
|
||||
box-shadow: 0 0 0 var(--c--components--forms-select--border-width--focus) var(--c--components--forms-select--border-color--focus);
|
||||
box-shadow: 0 0 0 var(--c--components--forms-select--border-width--focus)
|
||||
var(--c--components--forms-select--border-color--focus);
|
||||
|
||||
label {
|
||||
color: var(--c--components--forms-select--label-color--focus);
|
||||
@@ -49,6 +126,11 @@
|
||||
}
|
||||
}
|
||||
|
||||
&__placeholder {
|
||||
color: var(--c--components--forms-select--placeholder-color);
|
||||
font-size: var(--c--components--forms-select--font-size);
|
||||
}
|
||||
|
||||
&__inner {
|
||||
flex-grow: 1;
|
||||
display: flex;
|
||||
@@ -103,7 +185,9 @@
|
||||
color: var(--c--contextuals--content--semantic--neutral--secondary);
|
||||
}
|
||||
&__separator {
|
||||
background-color: var(--c--contextuals--content--semantic--neutral--tertiary);
|
||||
background-color: var(
|
||||
--c--contextuals--content--semantic--neutral--tertiary
|
||||
);
|
||||
height: 24px;
|
||||
width: 1px;
|
||||
}
|
||||
@@ -210,7 +294,9 @@
|
||||
}
|
||||
|
||||
&:hover {
|
||||
border-color: var(--c--contextuals--border--semantic--neutral--tertiary-hover);
|
||||
border-color: var(
|
||||
--c--contextuals--border--semantic--neutral--tertiary-hover
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import React, { PropsWithChildren, ReactNode, RefAttributes } from "react";
|
||||
import { SelectMulti } from ":/components/Forms/Select/multi";
|
||||
import { SelectMono } from ":/components/Forms/Select/mono";
|
||||
import { FieldProps } from ":/components/Forms/Field";
|
||||
import type { FieldVariant } from ":/components/Forms/types";
|
||||
|
||||
export * from ":/components/Forms/Select/mono";
|
||||
export * from ":/components/Forms/Select/multi";
|
||||
@@ -32,6 +33,8 @@ export type SelectProps = PropsWithChildren &
|
||||
FieldProps & {
|
||||
label: string;
|
||||
hideLabel?: boolean;
|
||||
variant?: FieldVariant;
|
||||
placeholder?: string;
|
||||
options: Option[];
|
||||
searchable?: boolean;
|
||||
name?: string;
|
||||
|
||||
@@ -6,6 +6,7 @@ import { Field } from ":/components/Forms/Field";
|
||||
import { LabelledBox } from ":/components/Forms/LabelledBox";
|
||||
import { Button } from ":/components/Button";
|
||||
import { Option, SelectProps } from ":/components/Forms/Select";
|
||||
import { ClassicLabel } from ":/components/Forms/ClassicLabel";
|
||||
import { isOptionWithRender } from ":/components/Forms/Select/utils";
|
||||
import { SelectMenu } from ":/components/Forms/Select/select-menu";
|
||||
|
||||
@@ -77,6 +78,8 @@ export const SelectMonoAux = ({
|
||||
name,
|
||||
label,
|
||||
hideLabel,
|
||||
variant = "floating",
|
||||
placeholder,
|
||||
labelAsPlaceholder,
|
||||
downshiftProps,
|
||||
downshiftReturn,
|
||||
@@ -89,51 +92,19 @@ export const SelectMonoAux = ({
|
||||
const { t } = useCunningham();
|
||||
const labelProps = downshiftReturn.getLabelProps();
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
const isClassic = variant === "classic";
|
||||
const showPlaceholder =
|
||||
isClassic && !downshiftReturn.selectedItem && placeholder;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Field state={state} {...props}>
|
||||
<div
|
||||
ref={ref}
|
||||
className={classNames(
|
||||
"c__select",
|
||||
"c__select--mono",
|
||||
"c__select--" + state,
|
||||
{
|
||||
"c__select--disabled": disabled,
|
||||
},
|
||||
)}
|
||||
onBlur={() =>
|
||||
onBlur?.({ target: { value: downshiftReturn.selectedItem?.value } })
|
||||
}
|
||||
>
|
||||
{/* 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}
|
||||
hideLabel={hideLabel}
|
||||
labelAsPlaceholder={labelAsPlaceholder}
|
||||
htmlFor={labelProps.htmlFor}
|
||||
labelId={labelProps.id}
|
||||
disabled={disabled}
|
||||
>
|
||||
const selectInner = (
|
||||
<div className="c__select__inner">
|
||||
<div className="c__select__inner__value">{children}</div>
|
||||
<div className="c__select__inner__value">
|
||||
{showPlaceholder ? (
|
||||
<span className="c__select__placeholder">{placeholder}</span>
|
||||
) : (
|
||||
children
|
||||
)}
|
||||
</div>
|
||||
<div className="c__select__inner__actions">
|
||||
{clearable && !disabled && downshiftReturn.selectedItem && (
|
||||
<>
|
||||
@@ -141,9 +112,7 @@ export const SelectMonoAux = ({
|
||||
variant="tertiary"
|
||||
color="neutral"
|
||||
size="nano"
|
||||
aria-label={t(
|
||||
"components.forms.select.clear_button_aria_label",
|
||||
)}
|
||||
aria-label={t("components.forms.select.clear_button_aria_label")}
|
||||
className="c__select__inner__actions__clear"
|
||||
onClick={(e) => {
|
||||
downshiftReturn.selectItem(null);
|
||||
@@ -176,7 +145,69 @@ export const SelectMonoAux = ({
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Field state={state} {...props}>
|
||||
<div
|
||||
ref={ref}
|
||||
className={classNames(
|
||||
"c__select",
|
||||
"c__select--mono",
|
||||
"c__select--" + state,
|
||||
{
|
||||
"c__select--disabled": disabled,
|
||||
"c__select--classic": isClassic,
|
||||
},
|
||||
)}
|
||||
onBlur={() =>
|
||||
onBlur?.({ target: { value: downshiftReturn.selectedItem?.value } })
|
||||
}
|
||||
>
|
||||
{isClassic && (
|
||||
<ClassicLabel
|
||||
label={label}
|
||||
hideLabel={hideLabel}
|
||||
disabled={disabled}
|
||||
className="c__select__label"
|
||||
disabledClassName="c__select__label--disabled"
|
||||
htmlFor={labelProps.htmlFor}
|
||||
id={labelProps.id}
|
||||
/>
|
||||
)}
|
||||
{/* 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)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{isClassic ? (
|
||||
selectInner
|
||||
) : (
|
||||
<LabelledBox
|
||||
label={label}
|
||||
hideLabel={hideLabel}
|
||||
variant={variant}
|
||||
labelAsPlaceholder={labelAsPlaceholder}
|
||||
htmlFor={labelProps.htmlFor}
|
||||
labelId={labelProps.id}
|
||||
disabled={disabled}
|
||||
>
|
||||
{selectInner}
|
||||
</LabelledBox>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Field>
|
||||
|
||||
@@ -2133,4 +2133,122 @@ describe("<Select/>", () => {
|
||||
document.querySelector(".c__field.my-custom-class"),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
describe("classic variant", () => {
|
||||
it("renders with classic variant", async () => {
|
||||
render(
|
||||
<CunninghamProvider>
|
||||
<Select
|
||||
label="City"
|
||||
variant="classic"
|
||||
options={[
|
||||
{ label: "Paris", value: "paris" },
|
||||
{ label: "London", value: "london" },
|
||||
]}
|
||||
/>
|
||||
</CunninghamProvider>,
|
||||
);
|
||||
// In classic mode, label is rendered outside the wrapper with its own class
|
||||
expect(document.querySelector(".c__select__label")).toBeInTheDocument();
|
||||
expect(document.querySelector(".c__select--classic")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("shows placeholder in classic variant when no selection", async () => {
|
||||
render(
|
||||
<CunninghamProvider>
|
||||
<Select
|
||||
label="City"
|
||||
variant="classic"
|
||||
placeholder="Select a city..."
|
||||
options={[
|
||||
{ label: "Paris", value: "paris" },
|
||||
{ label: "London", value: "london" },
|
||||
]}
|
||||
/>
|
||||
</CunninghamProvider>,
|
||||
);
|
||||
expect(screen.getByText("Select a city...")).toBeInTheDocument();
|
||||
expect(
|
||||
document.querySelector(".c__select__placeholder"),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("hides placeholder after selection in classic variant", async () => {
|
||||
const user = userEvent.setup();
|
||||
render(
|
||||
<CunninghamProvider>
|
||||
<Select
|
||||
label="City"
|
||||
variant="classic"
|
||||
placeholder="Select a city..."
|
||||
options={[
|
||||
{ label: "Paris", value: "paris" },
|
||||
{ label: "London", value: "london" },
|
||||
]}
|
||||
/>
|
||||
</CunninghamProvider>,
|
||||
);
|
||||
|
||||
// Placeholder should be visible initially
|
||||
expect(screen.getByText("Select a city...")).toBeInTheDocument();
|
||||
|
||||
// Open menu and select an option
|
||||
const input = screen.getByRole("combobox", { name: "City" });
|
||||
await user.click(input);
|
||||
await user.click(screen.getByRole("option", { name: "Paris" }));
|
||||
|
||||
// Placeholder should be hidden, value should be shown
|
||||
expect(screen.queryByText("Select a city...")).not.toBeInTheDocument();
|
||||
const valueRendered = document.querySelector(".c__select__inner__value");
|
||||
expect(valueRendered).toHaveTextContent("Paris");
|
||||
});
|
||||
|
||||
it("label is always static in classic variant", async () => {
|
||||
const user = userEvent.setup();
|
||||
render(
|
||||
<CunninghamProvider>
|
||||
<Select
|
||||
label="City"
|
||||
variant="classic"
|
||||
options={[
|
||||
{ label: "Paris", value: "paris" },
|
||||
{ label: "London", value: "london" },
|
||||
]}
|
||||
/>
|
||||
</CunninghamProvider>,
|
||||
);
|
||||
|
||||
const label = screen.getByText("City");
|
||||
|
||||
// In classic variant, label is outside the wrapper with c__select__label class
|
||||
expect(label.classList.contains("c__select__label")).toBe(true);
|
||||
|
||||
// Open menu
|
||||
const input = screen.getByRole("combobox", { name: "City" });
|
||||
await user.click(input);
|
||||
|
||||
// Label should still have the same class
|
||||
expect(label.classList.contains("c__select__label")).toBe(true);
|
||||
});
|
||||
|
||||
it("defaults to floating variant (no placeholder shown)", () => {
|
||||
render(
|
||||
<CunninghamProvider>
|
||||
<Select
|
||||
label="City"
|
||||
placeholder="Select a city..."
|
||||
options={[
|
||||
{ label: "Paris", value: "paris" },
|
||||
{ label: "London", value: "london" },
|
||||
]}
|
||||
/>
|
||||
</CunninghamProvider>,
|
||||
);
|
||||
// In floating variant, placeholder prop is ignored
|
||||
expect(screen.queryByText("Select a city...")).not.toBeInTheDocument();
|
||||
expect(
|
||||
document.querySelector(".c__select--classic"),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -16,6 +16,12 @@ import { Input } from ":/components/Forms/Input";
|
||||
export default {
|
||||
title: "Components/Forms/Select/Mono",
|
||||
component: Select,
|
||||
argTypes: {
|
||||
variant: {
|
||||
control: "select",
|
||||
options: ["floating", "classic"],
|
||||
},
|
||||
},
|
||||
} as Meta<typeof Select>;
|
||||
|
||||
const Template: StoryFn<typeof Select> = (args) => (
|
||||
@@ -303,6 +309,66 @@ export const SearchableCustomRender = {
|
||||
},
|
||||
};
|
||||
|
||||
export const ClassicVariant = {
|
||||
render: Template,
|
||||
|
||||
args: {
|
||||
label: "Select a city",
|
||||
variant: "classic",
|
||||
placeholder: "Choose a city...",
|
||||
options: OPTIONS,
|
||||
},
|
||||
};
|
||||
|
||||
export const ClassicVariantFilled = {
|
||||
render: Template,
|
||||
|
||||
args: {
|
||||
label: "Select a city",
|
||||
variant: "classic",
|
||||
placeholder: "Choose a city...",
|
||||
options: OPTIONS,
|
||||
defaultValue: OPTIONS[4].value,
|
||||
},
|
||||
};
|
||||
|
||||
export const ClassicVariantSearchable = {
|
||||
render: Template,
|
||||
|
||||
args: {
|
||||
label: "Select a city",
|
||||
variant: "classic",
|
||||
placeholder: "Search for a city...",
|
||||
options: OPTIONS,
|
||||
searchable: true,
|
||||
},
|
||||
};
|
||||
|
||||
export const ClassicVariantDisabled = {
|
||||
render: Template,
|
||||
|
||||
args: {
|
||||
label: "Select a city",
|
||||
variant: "classic",
|
||||
placeholder: "Choose a city...",
|
||||
options: OPTIONS,
|
||||
disabled: true,
|
||||
},
|
||||
};
|
||||
|
||||
export const ClassicVariantError = {
|
||||
render: Template,
|
||||
|
||||
args: {
|
||||
label: "Select a city",
|
||||
variant: "classic",
|
||||
placeholder: "Choose a city...",
|
||||
options: OPTIONS,
|
||||
state: "error",
|
||||
text: "Please select a city",
|
||||
},
|
||||
};
|
||||
|
||||
export const Ref = () => {
|
||||
const ref = useRef<SelectHandle>(null);
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
getOptionsFilter,
|
||||
optionToValue,
|
||||
} from ":/components/Forms/Select/mono-common";
|
||||
import { ClassicLabel } from ":/components/Forms/ClassicLabel";
|
||||
import { SelectedItems } from ":/components/Forms/Select/multi-selected-items";
|
||||
import { SelectMultiMenu } from ":/components/Forms/Select/multi-menu";
|
||||
|
||||
@@ -64,52 +65,16 @@ export const SelectMultiAux = ({ children, ...props }: SelectMultiAuxProps) => {
|
||||
const { t } = useCunningham();
|
||||
const labelProps = props.downshiftReturn.getLabelProps();
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
const variant = props.variant ?? "floating";
|
||||
const isClassic = variant === "classic";
|
||||
const showPlaceholder =
|
||||
isClassic && props.selectedItems.length === 0 && props.placeholder;
|
||||
|
||||
// We need to remove onBlur from toggleButtonProps because it triggers a menu closing each time
|
||||
// we tick a checkbox using the monoline style.
|
||||
const { onBlur, ...toggleProps } = props.downshiftReturn.toggleButtonProps;
|
||||
return (
|
||||
<>
|
||||
<Field {...props}>
|
||||
<div
|
||||
ref={ref}
|
||||
className={classNames(
|
||||
"c__select",
|
||||
"c__select--multi",
|
||||
"c__select--" + props.state,
|
||||
"c__select--" + props.selectedItemsStyle,
|
||||
{
|
||||
"c__select--disabled": props.disabled,
|
||||
"c__select--populated": props.selectedItems.length > 0,
|
||||
"c__select--monoline": props.monoline,
|
||||
"c__select--multiline": !props.monoline,
|
||||
},
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={classNames("c__select__wrapper", {
|
||||
"c__select__wrapper--focus":
|
||||
props.downshiftReturn.isOpen && !props.disabled,
|
||||
})}
|
||||
{...props.downshiftReturn.wrapperProps}
|
||||
{...toggleProps}
|
||||
>
|
||||
{props.selectedItems.map((selectedItem, index) => (
|
||||
<input
|
||||
key={`${optionToValue(selectedItem)}${index.toString()}`}
|
||||
type="hidden"
|
||||
name={props.name}
|
||||
value={optionToValue(selectedItem)}
|
||||
/>
|
||||
))}
|
||||
<LabelledBox
|
||||
label={props.label}
|
||||
labelAsPlaceholder={props.labelAsPlaceholder}
|
||||
htmlFor={labelProps.htmlFor}
|
||||
labelId={labelProps.id}
|
||||
hideLabel={props.hideLabel}
|
||||
disabled={props.disabled}
|
||||
>
|
||||
|
||||
const selectInner = (
|
||||
<div className="c__select__inner">
|
||||
<div className="c__select__inner__actions">
|
||||
{props.clearable &&
|
||||
@@ -153,11 +118,79 @@ export const SelectMultiAux = ({ children, ...props }: SelectMultiAuxProps) => {
|
||||
/>
|
||||
</div>
|
||||
<div className="c__select__inner__value">
|
||||
{showPlaceholder ? (
|
||||
<span className="c__select__placeholder">{props.placeholder}</span>
|
||||
) : (
|
||||
<>
|
||||
<SelectedItems {...props} />
|
||||
{children}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Field {...props}>
|
||||
<div
|
||||
ref={ref}
|
||||
className={classNames(
|
||||
"c__select",
|
||||
"c__select--multi",
|
||||
"c__select--" + props.state,
|
||||
"c__select--" + props.selectedItemsStyle,
|
||||
{
|
||||
"c__select--disabled": props.disabled,
|
||||
"c__select--populated": props.selectedItems.length > 0,
|
||||
"c__select--monoline": props.monoline,
|
||||
"c__select--multiline": !props.monoline,
|
||||
"c__select--classic": isClassic,
|
||||
},
|
||||
)}
|
||||
>
|
||||
{isClassic && (
|
||||
<ClassicLabel
|
||||
label={props.label}
|
||||
hideLabel={props.hideLabel}
|
||||
disabled={props.disabled}
|
||||
className="c__select__label"
|
||||
disabledClassName="c__select__label--disabled"
|
||||
htmlFor={labelProps.htmlFor}
|
||||
id={labelProps.id}
|
||||
/>
|
||||
)}
|
||||
<div
|
||||
className={classNames("c__select__wrapper", {
|
||||
"c__select__wrapper--focus":
|
||||
props.downshiftReturn.isOpen && !props.disabled,
|
||||
})}
|
||||
{...props.downshiftReturn.wrapperProps}
|
||||
{...toggleProps}
|
||||
>
|
||||
{props.selectedItems.map((selectedItem, index) => (
|
||||
<input
|
||||
key={`${optionToValue(selectedItem)}${index.toString()}`}
|
||||
type="hidden"
|
||||
name={props.name}
|
||||
value={optionToValue(selectedItem)}
|
||||
/>
|
||||
))}
|
||||
{isClassic ? (
|
||||
selectInner
|
||||
) : (
|
||||
<LabelledBox
|
||||
label={props.label}
|
||||
variant={variant}
|
||||
labelAsPlaceholder={props.labelAsPlaceholder}
|
||||
htmlFor={labelProps.htmlFor}
|
||||
labelId={labelProps.id}
|
||||
hideLabel={props.hideLabel}
|
||||
disabled={props.disabled}
|
||||
>
|
||||
{selectInner}
|
||||
</LabelledBox>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Field>
|
||||
|
||||
@@ -1895,4 +1895,105 @@ describe("<Select multi={true} />", () => {
|
||||
document.querySelector(".c__field.my-custom-class"),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
describe("classic variant", () => {
|
||||
it("renders with classic variant", async () => {
|
||||
render(
|
||||
<CunninghamProvider>
|
||||
<Select
|
||||
label="Cities"
|
||||
variant="classic"
|
||||
multi={true}
|
||||
options={[
|
||||
{ label: "Paris", value: "paris" },
|
||||
{ label: "London", value: "london" },
|
||||
]}
|
||||
/>
|
||||
</CunninghamProvider>,
|
||||
);
|
||||
// In classic mode, label is rendered outside the wrapper with its own class
|
||||
expect(document.querySelector(".c__select__label")).toBeInTheDocument();
|
||||
expect(document.querySelector(".c__select--classic")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("shows placeholder in classic variant when no selection", async () => {
|
||||
render(
|
||||
<CunninghamProvider>
|
||||
<Select
|
||||
label="Cities"
|
||||
variant="classic"
|
||||
placeholder="Select cities..."
|
||||
multi={true}
|
||||
options={[
|
||||
{ label: "Paris", value: "paris" },
|
||||
{ label: "London", value: "london" },
|
||||
]}
|
||||
/>
|
||||
</CunninghamProvider>,
|
||||
);
|
||||
expect(screen.getByText("Select cities...")).toBeInTheDocument();
|
||||
expect(
|
||||
document.querySelector(".c__select__placeholder"),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("hides placeholder after selection in classic variant", async () => {
|
||||
const user = userEvent.setup();
|
||||
render(
|
||||
<CunninghamProvider>
|
||||
<Select
|
||||
label="Cities"
|
||||
variant="classic"
|
||||
placeholder="Select cities..."
|
||||
multi={true}
|
||||
options={[
|
||||
{ label: "Paris", value: "paris" },
|
||||
{ label: "London", value: "london" },
|
||||
]}
|
||||
/>
|
||||
</CunninghamProvider>,
|
||||
);
|
||||
|
||||
// Placeholder should be visible initially
|
||||
expect(screen.getByText("Select cities...")).toBeInTheDocument();
|
||||
|
||||
// Open menu and select an option
|
||||
const input = screen.getByRole("combobox", { name: "Cities" });
|
||||
await user.click(input);
|
||||
await user.click(screen.getByRole("option", { name: "Paris" }));
|
||||
|
||||
// Placeholder should be hidden, selection should be shown
|
||||
expect(screen.queryByText("Select cities...")).not.toBeInTheDocument();
|
||||
expectSelectedOptions(["Paris"]);
|
||||
});
|
||||
|
||||
it("label is always static in classic variant", async () => {
|
||||
const user = userEvent.setup();
|
||||
render(
|
||||
<CunninghamProvider>
|
||||
<Select
|
||||
label="Cities"
|
||||
variant="classic"
|
||||
multi={true}
|
||||
options={[
|
||||
{ label: "Paris", value: "paris" },
|
||||
{ label: "London", value: "london" },
|
||||
]}
|
||||
/>
|
||||
</CunninghamProvider>,
|
||||
);
|
||||
|
||||
const label = screen.getByText("Cities");
|
||||
|
||||
// In classic variant, label is outside the wrapper with c__select__label class
|
||||
expect(label.classList.contains("c__select__label")).toBe(true);
|
||||
|
||||
// Open menu
|
||||
const input = screen.getByRole("combobox", { name: "Cities" });
|
||||
await user.click(input);
|
||||
|
||||
// Label should still have the same class
|
||||
expect(label.classList.contains("c__select__label")).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -16,6 +16,12 @@ import { Input } from ":/components/Forms/Input";
|
||||
export default {
|
||||
title: "Components/Forms/Select/Multi",
|
||||
component: Select,
|
||||
argTypes: {
|
||||
variant: {
|
||||
control: "select",
|
||||
options: ["floating", "classic"],
|
||||
},
|
||||
},
|
||||
} as Meta<typeof Select>;
|
||||
|
||||
const Template: StoryFn<typeof Select> = (args) => {
|
||||
@@ -73,6 +79,49 @@ export const Monoline = {
|
||||
},
|
||||
};
|
||||
|
||||
export const ClassicVariant = {
|
||||
render: Template,
|
||||
args: {
|
||||
label: "Select cities",
|
||||
variant: "classic",
|
||||
placeholder: "Choose cities...",
|
||||
options: OPTIONS,
|
||||
},
|
||||
};
|
||||
|
||||
export const ClassicVariantFilled = {
|
||||
render: Template,
|
||||
args: {
|
||||
label: "Select cities",
|
||||
variant: "classic",
|
||||
placeholder: "Choose cities...",
|
||||
options: OPTIONS,
|
||||
defaultValue: [OPTIONS[4].value, OPTIONS[2].value],
|
||||
},
|
||||
};
|
||||
|
||||
export const ClassicVariantSearchable = {
|
||||
render: Template,
|
||||
args: {
|
||||
label: "Select cities",
|
||||
variant: "classic",
|
||||
placeholder: "Search for cities...",
|
||||
options: OPTIONS,
|
||||
searchable: true,
|
||||
},
|
||||
};
|
||||
|
||||
export const ClassicVariantDisabled = {
|
||||
render: Template,
|
||||
args: {
|
||||
label: "Select cities",
|
||||
variant: "classic",
|
||||
placeholder: "Choose cities...",
|
||||
options: OPTIONS,
|
||||
disabled: true,
|
||||
},
|
||||
};
|
||||
|
||||
export const WithText = {
|
||||
render: Template,
|
||||
args: {
|
||||
|
||||
@@ -33,4 +33,5 @@ export const tokens = (defaults: DefaultTokens) => ({
|
||||
"multi-pill-border-radius": "2px",
|
||||
"multi-pill-max-width": "68%",
|
||||
"multi-pill-font-size": defaults.globals.font.sizes.md,
|
||||
"placeholder-color": defaults.contextuals.content.semantic.neutral.secondary,
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user