♻️(react) use react aria for select menu
The way the menu of the select was made was causing it to be cropped inside modals, it was due to the fact the menu was nested inside a position relative parent. Now we use react aria to position in full absolute the menu, making it to be correctly displayed inside modals.
This commit is contained in:
5
.changeset/spotty-bulldogs-tell.md
Normal file
5
.changeset/spotty-bulldogs-tell.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"@openfun/cunningham-react": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
♻️(react) use react aria for select menu
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { HTMLAttributes } from "react";
|
import React, { HTMLAttributes, useRef } from "react";
|
||||||
import { UseSelectReturnValue } from "downshift";
|
import { UseSelectReturnValue } from "downshift";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import { useCunningham } from ":/components/Provider";
|
import { useCunningham } from ":/components/Provider";
|
||||||
@@ -7,6 +7,7 @@ import { LabelledBox } from ":/components/Forms/LabelledBox";
|
|||||||
import { Button } from ":/components/Button";
|
import { Button } from ":/components/Button";
|
||||||
import { Option, SelectProps } from ":/components/Forms/Select";
|
import { Option, SelectProps } from ":/components/Forms/Select";
|
||||||
import { isOptionWithRender } from ":/components/Forms/Select/utils";
|
import { isOptionWithRender } from ":/components/Forms/Select/utils";
|
||||||
|
import { SelectMenu } from ":/components/Forms/Select/select-menu";
|
||||||
|
|
||||||
export function getOptionsFilter(inputValue?: string) {
|
export function getOptionsFilter(inputValue?: string) {
|
||||||
return (option: Option) => {
|
return (option: Option) => {
|
||||||
@@ -87,9 +88,13 @@ export const SelectMonoAux = ({
|
|||||||
}: SelectAuxProps) => {
|
}: SelectAuxProps) => {
|
||||||
const { t } = useCunningham();
|
const { t } = useCunningham();
|
||||||
const labelProps = downshiftReturn.getLabelProps();
|
const labelProps = downshiftReturn.getLabelProps();
|
||||||
|
const ref = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<>
|
||||||
<Field state={state} {...props}>
|
<Field state={state} {...props}>
|
||||||
<div
|
<div
|
||||||
|
ref={ref}
|
||||||
className={classNames(
|
className={classNames(
|
||||||
"c__select",
|
"c__select",
|
||||||
"c__select--mono",
|
"c__select--mono",
|
||||||
@@ -99,8 +104,7 @@ export const SelectMonoAux = ({
|
|||||||
},
|
},
|
||||||
)}
|
)}
|
||||||
onBlur={() =>
|
onBlur={() =>
|
||||||
onBlur &&
|
onBlur?.({ target: { value: downshiftReturn.selectedItem?.value } })
|
||||||
onBlur({ target: { value: downshiftReturn.selectedItem?.value } })
|
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{/* We disabled linting for this specific line because we consider that the onClick props is only used for */}
|
{/* We disabled linting for this specific line because we consider that the onClick props is only used for */}
|
||||||
@@ -172,15 +176,14 @@ export const SelectMonoAux = ({
|
|||||||
</div>
|
</div>
|
||||||
</LabelledBox>
|
</LabelledBox>
|
||||||
</div>
|
</div>
|
||||||
<div
|
</div>
|
||||||
className={classNames("c__select__menu", {
|
</Field>
|
||||||
"c__select__menu--opened": downshiftReturn.isOpen || false,
|
<SelectMenu
|
||||||
})}
|
isOpen={downshiftReturn.isOpen}
|
||||||
{...downshiftReturn.getMenuProps()}
|
selectRef={ref}
|
||||||
|
downshiftReturn={downshiftReturn}
|
||||||
>
|
>
|
||||||
<ul>
|
<ul>
|
||||||
{(downshiftReturn.isOpen || false) && (
|
|
||||||
<>
|
|
||||||
{options.map((item, index) => {
|
{options.map((item, index) => {
|
||||||
const isActive = index === downshiftReturn.highlightedIndex;
|
const isActive = index === downshiftReturn.highlightedIndex;
|
||||||
return (
|
return (
|
||||||
@@ -207,11 +210,8 @@ export const SelectMonoAux = ({
|
|||||||
{t("components.forms.select.menu_empty_placeholder")}
|
{t("components.forms.select.menu_empty_placeholder")}
|
||||||
</li>
|
</li>
|
||||||
)}
|
)}
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</SelectMenu>
|
||||||
</div>
|
</>
|
||||||
</Field>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Meta, StoryFn } from "@storybook/react";
|
import { Meta, StoryFn } from "@storybook/react";
|
||||||
import React, { useRef, useState } from "react";
|
import React, { useRef, useState } from "react";
|
||||||
import { useForm, FormProvider } from "react-hook-form";
|
import { FormProvider, useForm } from "react-hook-form";
|
||||||
import * as Yup from "yup";
|
import * as Yup from "yup";
|
||||||
import { yupResolver } from "@hookform/resolvers/yup";
|
import { yupResolver } from "@hookform/resolvers/yup";
|
||||||
import { onSubmit } from ":/components/Forms/Examples/ReactHookForm/reactHookFormUtils";
|
import { onSubmit } from ":/components/Forms/Examples/ReactHookForm/reactHookFormUtils";
|
||||||
@@ -10,6 +10,8 @@ import {
|
|||||||
getCountryOption,
|
getCountryOption,
|
||||||
RhfSelect,
|
RhfSelect,
|
||||||
} from ":/components/Forms/Select/stories-utils";
|
} from ":/components/Forms/Select/stories-utils";
|
||||||
|
import { Modal, ModalSize, useModal } from ":/components/Modal";
|
||||||
|
import { Input } from ":/components/Forms/Input";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
title: "Components/Forms/Select/Mono",
|
title: "Components/Forms/Select/Mono",
|
||||||
@@ -17,7 +19,7 @@ export default {
|
|||||||
} as Meta<typeof Select>;
|
} as Meta<typeof Select>;
|
||||||
|
|
||||||
const Template: StoryFn<typeof Select> = (args) => (
|
const Template: StoryFn<typeof Select> = (args) => (
|
||||||
<div style={{ paddingBottom: "200px" }}>
|
<div style={{ paddingBottom: "200px", position: "relative" }}>
|
||||||
<Select {...args} />
|
<Select {...args} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -461,6 +463,74 @@ export const FormExample = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const SelectInModal = () => {
|
||||||
|
const modal = useModal({ isOpenDefault: true });
|
||||||
|
return (
|
||||||
|
<Modal size={ModalSize.MEDIUM} {...modal} title="Example">
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
gap: "1rem",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Select
|
||||||
|
label="Context"
|
||||||
|
options={[
|
||||||
|
{
|
||||||
|
label: "Ask a document",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Download files",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Ask for help",
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
fullWidth={true}
|
||||||
|
clearable={true}
|
||||||
|
/>
|
||||||
|
<div style={{ display: "flex", gap: "1rem" }}>
|
||||||
|
<Input label="First name" />
|
||||||
|
<Input label="Last name" />
|
||||||
|
</div>
|
||||||
|
<Input
|
||||||
|
label="Email address"
|
||||||
|
fullWidth={true}
|
||||||
|
text="Only @acme.com domain is authorized"
|
||||||
|
/>
|
||||||
|
<div style={{ display: "flex", gap: "1rem" }}>
|
||||||
|
<div style={{ width: "25%" }}>
|
||||||
|
<Input label="ZIP" fullWidth={true} />
|
||||||
|
</div>
|
||||||
|
<Input label="City" fullWidth={true} />
|
||||||
|
</div>
|
||||||
|
<Select
|
||||||
|
label="Skills"
|
||||||
|
options={[
|
||||||
|
{
|
||||||
|
label: "Communication",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Teamwork",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Problem solving",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Leadership",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Work ethic",
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
fullWidth={true}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export const ReactHookForm = () => {
|
export const ReactHookForm = () => {
|
||||||
enum CitiesOptionEnum {
|
enum CitiesOptionEnum {
|
||||||
NONE = "",
|
NONE = "",
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { HTMLAttributes } from "react";
|
import React, { HTMLAttributes, useRef } from "react";
|
||||||
import { useMultipleSelection } from "downshift";
|
import { useMultipleSelection } from "downshift";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import { Field } from ":/components/Forms/Field";
|
import { Field } from ":/components/Forms/Field";
|
||||||
@@ -63,13 +63,16 @@ export interface SelectMultiAuxProps extends SubProps {
|
|||||||
export const SelectMultiAux = ({ children, ...props }: SelectMultiAuxProps) => {
|
export const SelectMultiAux = ({ children, ...props }: SelectMultiAuxProps) => {
|
||||||
const { t } = useCunningham();
|
const { t } = useCunningham();
|
||||||
const labelProps = props.downshiftReturn.getLabelProps();
|
const labelProps = props.downshiftReturn.getLabelProps();
|
||||||
|
const ref = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
// We need to remove onBlur from toggleButtonProps because it triggers a menu closing each time
|
// We need to remove onBlur from toggleButtonProps because it triggers a menu closing each time
|
||||||
// we tick a checkbox using the monoline style.
|
// we tick a checkbox using the monoline style.
|
||||||
const { onBlur, ...toggleProps } = props.downshiftReturn.toggleButtonProps;
|
const { onBlur, ...toggleProps } = props.downshiftReturn.toggleButtonProps;
|
||||||
return (
|
return (
|
||||||
|
<>
|
||||||
<Field {...props}>
|
<Field {...props}>
|
||||||
<div
|
<div
|
||||||
|
ref={ref}
|
||||||
className={classNames(
|
className={classNames(
|
||||||
"c__select",
|
"c__select",
|
||||||
"c__select--multi",
|
"c__select--multi",
|
||||||
@@ -154,8 +157,9 @@ export const SelectMultiAux = ({ children, ...props }: SelectMultiAuxProps) => {
|
|||||||
</div>
|
</div>
|
||||||
</LabelledBox>
|
</LabelledBox>
|
||||||
</div>
|
</div>
|
||||||
<SelectMultiMenu {...props} />
|
|
||||||
</div>
|
</div>
|
||||||
</Field>
|
</Field>
|
||||||
|
<SelectMultiMenu {...props} selectRef={ref} />
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -8,19 +8,20 @@ import {
|
|||||||
import { useCunningham } from ":/components/Provider";
|
import { useCunningham } from ":/components/Provider";
|
||||||
import { Checkbox } from ":/components/Forms/Checkbox";
|
import { Checkbox } from ":/components/Forms/Checkbox";
|
||||||
import { Option } from ":/components/Forms/Select/index";
|
import { Option } from ":/components/Forms/Select/index";
|
||||||
|
import { SelectMenu } from ":/components/Forms/Select/select-menu";
|
||||||
|
|
||||||
export const SelectMultiMenu = (props: SelectMultiAuxProps) => {
|
export const SelectMultiMenu = (
|
||||||
|
props: SelectMultiAuxProps & {
|
||||||
|
selectRef: React.RefObject<HTMLDivElement>;
|
||||||
|
},
|
||||||
|
) => {
|
||||||
const { t } = useCunningham();
|
const { t } = useCunningham();
|
||||||
return (
|
return (
|
||||||
<div
|
<SelectMenu
|
||||||
className={classNames(
|
isOpen={props.downshiftReturn.isOpen}
|
||||||
"c__select__menu",
|
selectRef={props.selectRef}
|
||||||
"c__select__menu--" + props.menuOptionsStyle,
|
downshiftReturn={props.downshiftReturn}
|
||||||
{
|
menuOptionsStyle={props.menuOptionsStyle}
|
||||||
"c__select__menu--opened": props.downshiftReturn.isOpen,
|
|
||||||
},
|
|
||||||
)}
|
|
||||||
{...props.downshiftReturn.getMenuProps()}
|
|
||||||
>
|
>
|
||||||
<ul>
|
<ul>
|
||||||
{props.downshiftReturn.isOpen && (
|
{props.downshiftReturn.isOpen && (
|
||||||
@@ -41,7 +42,7 @@ export const SelectMultiMenu = (props: SelectMultiAuxProps) => {
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</SelectMenu>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ import {
|
|||||||
getCountryOption,
|
getCountryOption,
|
||||||
RhfSelect,
|
RhfSelect,
|
||||||
} from ":/components/Forms/Select/stories-utils";
|
} from ":/components/Forms/Select/stories-utils";
|
||||||
|
import { Modal, ModalSize, useModal } from ":/components/Modal";
|
||||||
|
import { Input } from ":/components/Forms/Input";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
title: "Components/Forms/Select/Multi",
|
title: "Components/Forms/Select/Multi",
|
||||||
@@ -418,6 +420,76 @@ export const FormExample = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const SelectInModal = () => {
|
||||||
|
const modal = useModal({ isOpenDefault: true });
|
||||||
|
return (
|
||||||
|
<Modal size={ModalSize.MEDIUM} {...modal} title="Example">
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
gap: "1rem",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Select
|
||||||
|
label="Context"
|
||||||
|
options={[
|
||||||
|
{
|
||||||
|
label: "Ask a document",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Download files",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Ask for help",
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
multi={true}
|
||||||
|
fullWidth={true}
|
||||||
|
clearable={true}
|
||||||
|
/>
|
||||||
|
<div style={{ display: "flex", gap: "1rem" }}>
|
||||||
|
<Input label="First name" />
|
||||||
|
<Input label="Last name" />
|
||||||
|
</div>
|
||||||
|
<Input
|
||||||
|
label="Email address"
|
||||||
|
fullWidth={true}
|
||||||
|
text="Only @acme.com domain is authorized"
|
||||||
|
/>
|
||||||
|
<div style={{ display: "flex", gap: "1rem" }}>
|
||||||
|
<div style={{ width: "25%" }}>
|
||||||
|
<Input label="ZIP" fullWidth={true} />
|
||||||
|
</div>
|
||||||
|
<Input label="City" fullWidth={true} />
|
||||||
|
</div>
|
||||||
|
<Select
|
||||||
|
label="Skills"
|
||||||
|
options={[
|
||||||
|
{
|
||||||
|
label: "Communication",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Teamwork",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Problem solving",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Leadership",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Work ethic",
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
multi={true}
|
||||||
|
fullWidth={true}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export const ReactHookForm = () => {
|
export const ReactHookForm = () => {
|
||||||
enum CitiesOptionEnum {
|
enum CitiesOptionEnum {
|
||||||
NONE = "",
|
NONE = "",
|
||||||
|
|||||||
57
packages/react/src/components/Forms/Select/select-menu.tsx
Normal file
57
packages/react/src/components/Forms/Select/select-menu.tsx
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
import React, { PropsWithChildren } from "react";
|
||||||
|
import classNames from "classnames";
|
||||||
|
import { useOverlayPosition } from "react-aria";
|
||||||
|
import { SelectProps } from ":/components/Forms/Select/index";
|
||||||
|
import { SelectAuxProps } from ":/components/Forms/Select/mono-common";
|
||||||
|
import { SelectMultiAuxProps } from ":/components/Forms/Select/multi-common";
|
||||||
|
|
||||||
|
export interface SelectDropdownProps extends PropsWithChildren {
|
||||||
|
isOpen: boolean;
|
||||||
|
selectRef: React.RefObject<HTMLDivElement>;
|
||||||
|
menuOptionsStyle?: SelectProps["menuOptionsStyle"];
|
||||||
|
downshiftReturn:
|
||||||
|
| SelectAuxProps["downshiftReturn"]
|
||||||
|
| SelectMultiAuxProps["downshiftReturn"];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SelectMenu = ({
|
||||||
|
isOpen,
|
||||||
|
selectRef,
|
||||||
|
downshiftReturn,
|
||||||
|
menuOptionsStyle,
|
||||||
|
children,
|
||||||
|
}: SelectDropdownProps) => {
|
||||||
|
const menuRef = React.useRef<HTMLElement | null>(null);
|
||||||
|
const overlayPosition = useOverlayPosition({
|
||||||
|
targetRef: selectRef,
|
||||||
|
overlayRef: menuRef,
|
||||||
|
placement: "bottom",
|
||||||
|
isOpen,
|
||||||
|
maxHeight: 160,
|
||||||
|
shouldUpdatePosition: true,
|
||||||
|
});
|
||||||
|
const menuProps = downshiftReturn.getMenuProps({
|
||||||
|
ref: menuRef,
|
||||||
|
});
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={classNames(
|
||||||
|
"c__select__menu",
|
||||||
|
menuOptionsStyle ? "c__select__menu--" + menuOptionsStyle : "",
|
||||||
|
{
|
||||||
|
"c__select__menu--opened": isOpen,
|
||||||
|
},
|
||||||
|
)}
|
||||||
|
{...menuProps}
|
||||||
|
style={{
|
||||||
|
marginLeft: "-4px",
|
||||||
|
width: selectRef.current
|
||||||
|
? selectRef.current.getBoundingClientRect().width - 4
|
||||||
|
: 0,
|
||||||
|
...overlayPosition.overlayProps.style,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{isOpen && children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user