(null);
+ const [isOpen, setIsOpen] = useState(isOpenProp);
+ const placement = mapPositionTypeToPlacement(position);
+ const { attributes, forceUpdate, styles: popperStyles } = usePopper(
+ referenceElement,
+ popperElement,
+ {
+ placement,
+ modifiers: isFullWidth ? [sameWidth] : undefined,
+ },
+ );
+ const classNames = cn(styles['Dropdown'], className);
+ const containerTestId = testId ? `${testId}-container` : testId;
- this.bindEventListeners();
- }
+ useEffect(() => {
+ setIsOpen(isOpenProp);
+ }, [isOpenProp]);
- bindEventListeners = () => {
- if (this.state.isOpen) {
- document.addEventListener('keydown', this.handleEscapeKey, true);
- window.addEventListener('resize', this.setAnchorDimensions, true);
- document.addEventListener('scroll', this.setAnchorDimensions, true);
- } else {
- document.removeEventListener('keydown', this.handleEscapeKey, true);
- window.removeEventListener('resize', this.setAnchorDimensions, true);
- document.removeEventListener('scroll', this.setAnchorDimensions, true);
+ useEffect(() => {
+ if (forceUpdate) {
+ forceUpdate();
}
- };
+ }, [children]);
- componentWillUnmount() {
- if (!isBrowser) {
- return;
+ const openSubmenu = (isOpen: boolean) => {
+ if (submenuToggleLabel) {
+ setIsOpen(isOpen);
}
-
- document.removeEventListener('keydown', this.handleEscapeKey, true);
- window.removeEventListener('resize', this.setAnchorDimensions, true);
- document.removeEventListener('scroll', this.setAnchorDimensions, true);
- }
-
- openMenu = (isOpen: boolean) => {
- this.setState({ isOpen });
};
- handleEscapeKey = (event: KeyboardEvent) => {
- const ESCAPE_KEYCODE = 27;
-
- if (event.keyCode === ESCAPE_KEYCODE) {
- event.stopPropagation();
+ const close = useCallback(() => {
+ setIsOpen(false);
- this.setState({
- isOpen: false,
- });
-
- if (this.props.onClose) {
- this.props.onClose();
- }
+ if (onClose) {
+ onClose();
}
- };
+ }, [onClose, setIsOpen]);
- openSubmenu = (isOpen: boolean) => {
- if (this.props.submenuToggleLabel) {
- this.openMenu(isOpen);
- }
- };
+ const handleEscapeKey = useCallback(
+ (event: KeyboardEvent) => {
+ if (event.code === 'Escape') {
+ event.stopPropagation();
- render() {
- const {
- className,
- toggleElement,
- testId,
- submenuToggleLabel,
- getContainerRef,
- dropdownContainerClassName,
- children,
- isOpen,
- isAutoalignmentEnabled,
- isFullWidth,
- ...otherProps
- } = this.props;
-
- const classNames = cn(styles['Dropdown'], className);
- const width = isFullWidth && this.state.anchorDimensionsAndPositon.width;
- const containerTestId = testId ? `${testId}-container` : testId;
-
- return submenuToggleLabel ? (
- this.openMenu(true)}
- onLeave={() => this.openMenu(false)}
- {...otherProps}
- >
- {toggleElement &&
- React.cloneElement(toggleElement, {
- 'aria-haspopup': 'menu',
- 'aria-expanded': this.state.isOpen,
- })}
- {this.state.isOpen && (
-
- {this.props.children}
-
- )}
-
- ) : (
- {
- if (!submenuToggleLabel) {
- this.dropdownAnchor = ref;
- }
- }}
- {...otherProps}
- >
- {toggleElement &&
- React.cloneElement(toggleElement, {
- 'aria-haspopup': 'menu',
- 'aria-expanded': this.state.isOpen,
- })}
- {this.state.isOpen && (
-
- {this.props.children}
-
- )}
-
- );
- }
+ close();
+ }
+ },
+ [close],
+ );
+
+ useEffect(() => {
+ document.addEventListener('keydown', handleEscapeKey, true);
+
+ return () => {
+ document.removeEventListener('keydown', handleEscapeKey, true);
+ };
+ }, []);
+
+ return submenuToggleLabel ? (
+ setIsOpen(true)}
+ onLeave={() => setIsOpen(false)}
+ ref={setReferenceElement}
+ {...otherProps}
+ >
+ {toggleElement &&
+ React.cloneElement(toggleElement, {
+ 'aria-haspopup': 'menu',
+ 'aria-expanded': isOpen,
+ })}
+ {isOpen && (
+
+ {children}
+
+ )}
+
+ ) : (
+
+ {toggleElement &&
+ React.cloneElement(toggleElement, {
+ 'aria-haspopup': 'menu',
+ 'aria-expanded': isOpen,
+ })}
+
+ {isOpen && (
+
+ {children}
+
+ )}
+
+ );
}
+Dropdown.defaultProps = {
+ testId: 'cf-ui-dropdown',
+ position: 'bottom-left',
+ isOpen: false,
+ getContainerRef: () => {},
+};
+
export default Dropdown;
diff --git a/packages/forma-36-react-components/src/components/Dropdown/DropdownContainer/DropdownContainer.css b/packages/forma-36-react-components/src/components/Dropdown/DropdownContainer/DropdownContainer.css
index c7d158924b..c27a2e977d 100644
--- a/packages/forma-36-react-components/src/components/Dropdown/DropdownContainer/DropdownContainer.css
+++ b/packages/forma-36-react-components/src/components/Dropdown/DropdownContainer/DropdownContainer.css
@@ -13,26 +13,10 @@
list-style: none;
padding: 0;
margin: 0;
- position: fixed;
+
z-index: var(--z-index-dropdown);
}
.DropdownContainer > div {
width: 100%;
}
-
-.DropdownContainer__submenu {
- position: absolute;
-}
-
-.DropdownContainer__container-position--right {
- margin-top: 0;
- top: 0;
- left: 100%;
-}
-
-.DropdownContainer__container-position--left {
- margin-top: 0;
- top: 0;
- right: 100%;
-}
diff --git a/packages/forma-36-react-components/src/components/Dropdown/DropdownContainer/DropdownContainer.test.tsx b/packages/forma-36-react-components/src/components/Dropdown/DropdownContainer/DropdownContainer.test.tsx
index 7e7267d875..ec1958a0a1 100644
--- a/packages/forma-36-react-components/src/components/Dropdown/DropdownContainer/DropdownContainer.test.tsx
+++ b/packages/forma-36-react-components/src/components/Dropdown/DropdownContainer/DropdownContainer.test.tsx
@@ -4,21 +4,14 @@ import DropdownContainer from './DropdownContainer';
it('renders the component', () => {
const output = shallow(
-
- DropdownContainer
- ,
+ DropdownContainer ,
);
expect(output).toMatchSnapshot();
});
it('renders the component with an additional class name', () => {
const output = shallow(
-
+
DropdownContainer
,
);
@@ -27,11 +20,7 @@ it('renders the component with an additional class name', () => {
it('renders the component as a submenu', () => {
const output = shallow(
-
+
DropdownContainer
,
);
@@ -40,11 +29,7 @@ it('renders the component as a submenu', () => {
it('has no a11y issues', async () => {
const output = shallow(
-
- DropdownContainer
- ,
+ DropdownContainer ,
);
expect(output).toMatchSnapshot();
});
diff --git a/packages/forma-36-react-components/src/components/Dropdown/DropdownContainer/DropdownContainer.tsx b/packages/forma-36-react-components/src/components/Dropdown/DropdownContainer/DropdownContainer.tsx
index ce375f96a9..ba73ff0848 100644
--- a/packages/forma-36-react-components/src/components/Dropdown/DropdownContainer/DropdownContainer.tsx
+++ b/packages/forma-36-react-components/src/components/Dropdown/DropdownContainer/DropdownContainer.tsx
@@ -1,252 +1,130 @@
-import React, { Component } from 'react';
+import React, { forwardRef, useCallback, useEffect, useRef } from 'react';
import cn from 'classnames';
import ReactDOM from 'react-dom';
-import InViewport from '../../InViewport';
-import { positionType, AnchorDimensionsAndPositonType } from '../Dropdown';
+import { positionType } from '../Dropdown';
import styles from './DropdownContainer.css';
-export interface DropdownContainerProps {
- onClose?: Function;
- dropdownAnchor?: HTMLElement | null;
- className?: string;
+export interface DropdownContainerProps
+ extends React.HTMLAttributes {
children?: React.ReactNode;
- testId?: string;
- openSubmenu?: (value: boolean) => void;
- anchorDimensionsAndPositon?: AnchorDimensionsAndPositonType;
- position: positionType;
+ className?: string;
getRef?: (ref: HTMLElement | null) => void;
+ isOpen: boolean;
+ onClose?: Function;
+ openSubmenu?: (value: boolean) => void;
+ position?: positionType;
submenu?: boolean;
- width?: number | false;
- isAutoalignmentEnabled?: boolean;
-}
-
-export interface DropdownState {
- dropdownDimensions: {
- width: number;
- height: number;
- };
- position: positionType;
+ testId?: string;
}
-const defaultProps: Partial = {
- testId: 'cf-ui-dropdown-portal',
- position: 'bottom-left',
- submenu: false,
- isAutoalignmentEnabled: true,
-};
-
-class DropdownContainer extends Component<
- DropdownContainerProps,
- DropdownState
-> {
- static defaultProps = defaultProps;
-
- portalTarget = document.createElement('div');
- dropdown: HTMLElement | null = null;
- lastOverflowAt: string | null = null;
-
- state = {
- position: this.props.position,
- dropdownDimensions: {
- width: 0,
- height: 0,
+export const DropdownContainer = forwardRef<
+ HTMLElement,
+ DropdownContainerProps
+>(
+ (
+ {
+ children,
+ className,
+ getRef,
+ isOpen,
+ onClose,
+ openSubmenu,
+ position,
+ style,
+ submenu,
+ testId,
+ ...props
},
- };
-
- componentDidMount() {
- document.body.appendChild(this.portalTarget);
- if (this.dropdown) {
- const dropdownRect = this.dropdown.getBoundingClientRect();
- this.setState({
- dropdownDimensions: {
- width: dropdownRect.width,
- height: dropdownRect.height,
- },
- });
- }
- document.addEventListener('mousedown', this.trackOutsideClick, true);
- this.props.getRef?.(this.dropdown);
- }
-
- componentDidUpdate(
- prevProps: DropdownContainerProps,
- prevState: DropdownState,
- ) {
- if (!this.dropdown) {
- return;
- }
-
- const dropdownRect = this.dropdown.getBoundingClientRect();
-
- if (
- dropdownRect.width !== prevState.dropdownDimensions.width ||
- dropdownRect.height !== prevState.dropdownDimensions.height
- ) {
- this.setState({
- dropdownDimensions: {
- width: dropdownRect.width,
- height: dropdownRect.height,
- },
- });
- }
- }
-
- componentWillUnmount() {
- document.body.removeChild(this.portalTarget);
- document.removeEventListener('mousedown', this.trackOutsideClick, true);
- }
-
- trackOutsideClick = (e: MouseEvent) => {
- if (
- this.dropdown &&
- !this.dropdown.contains(e.target as Node) &&
- this.props.dropdownAnchor &&
- !this.props.dropdownAnchor.contains(e.target as Node)
- ) {
- if (this.props.onClose) {
- this.props.onClose();
- }
- }
- };
-
- handleOverflow = (overflowAt: string) => {
- if (!this.props.isAutoalignmentEnabled) {
- return;
- }
- if (overflowAt === this.lastOverflowAt) {
- return;
- }
-
- const resolutions = {
- right: {
- 'bottom-left': 'bottom-right',
- 'top-left': 'top-right',
- right: 'left',
- },
- left: {
- 'bottom-right': 'bottom-left',
- 'top-right': 'top-left',
- left: 'right',
- },
- top: {
- 'top-left': 'bottom-left',
- 'top-right': 'bottom-right',
+ refCallback,
+ ) => {
+ // We're not dealing with React RefObjects but with useState (because we
+ // want to re-render on all changes)
+ const setReference = refCallback as React.Dispatch<
+ React.SetStateAction
+ >;
+ const dropdown = useRef(null);
+ const portalTarget = useRef(document.createElement('div'));
+ const classNames = cn(className, styles['DropdownContainer']);
+
+ const trackOutsideClick = useCallback(
+ (event: MouseEvent) => {
+ event.preventDefault();
+
+ if (
+ onClose &&
+ dropdown.current &&
+ !dropdown.current.contains(event.target as Node)
+ ) {
+ onClose();
+ }
},
- bottom: {
- 'bottom-left': 'top-left',
- 'bottom-right': 'top-right',
- },
- };
- const currentPosition = this.state.position;
- const resolution = resolutions[overflowAt][currentPosition];
- if (resolution) {
- this.setState(
- {
- position: resolution,
- },
- () => {
- this.lastOverflowAt = overflowAt;
- },
- );
- }
- };
-
- calculatePosition = () => {
- const { anchorDimensionsAndPositon } = this.props;
- const { dropdownDimensions, position } = this.state;
-
- if (!anchorDimensionsAndPositon || !dropdownDimensions) {
- return false;
- }
-
- switch (position) {
- case 'bottom-left':
- return {
- top:
- anchorDimensionsAndPositon.top + anchorDimensionsAndPositon.height,
- left: anchorDimensionsAndPositon.left,
- };
- case 'top-left':
- return {
- bottom: window.innerHeight - anchorDimensionsAndPositon.top,
- left: anchorDimensionsAndPositon.left,
- };
- case 'bottom-right':
- return {
- top:
- anchorDimensionsAndPositon.top + anchorDimensionsAndPositon.height,
- left:
- anchorDimensionsAndPositon.left -
- (dropdownDimensions.width - anchorDimensionsAndPositon.width),
- };
- case 'top-right':
- return {
- bottom: window.innerHeight - anchorDimensionsAndPositon.top,
- left:
- anchorDimensionsAndPositon.left -
- (dropdownDimensions.width - anchorDimensionsAndPositon.width),
- };
- }
- };
-
- getSubmenuClassNames = () =>
- cn(
- styles['DropdownContainer__submenu'],
- styles[`DropdownContainer__container-position--${this.state.position}`],
+ [onClose, dropdown.current],
);
- render() {
- const { submenu, className, width, testId } = this.props;
+ useEffect(() => {
+ if (isOpen) {
+ document.body.appendChild(portalTarget.current);
+ document.addEventListener('mousedown', trackOutsideClick, true);
+ } else {
+ document.body.removeChild(portalTarget.current);
+ document.removeEventListener('mousedown', trackOutsideClick, true);
+ }
+
+ return () => {
+ document.body.removeChild(portalTarget.current);
+ document.removeEventListener('mousedown', trackOutsideClick, true);
+ };
+ }, [isOpen]);
- const classNames = cn(
- className,
- styles['DropdownContainer'],
- submenu ? this.getSubmenuClassNames() : '',
- );
+ useEffect(() => {
+ if (getRef && dropdown.current) {
+ getRef(dropdown.current);
+ }
+ }, [dropdown.current]);
- const dropdown = (
+ const dropdownComponent = (
{
- this.dropdown = ref;
- }}
- data-test-id={testId}
- style={{
- ...(width ? { width: `${width}px` } : {}),
- ...(!submenu && this.calculatePosition()),
- }}
+ {...props}
className={classNames}
+ data-test-id={testId}
onMouseEnter={() => {
- if (this.props.openSubmenu) {
- this.props.openSubmenu(true);
+ if (openSubmenu) {
+ openSubmenu(true);
}
}}
onFocus={() => {
- if (this.props.openSubmenu) {
- this.props.openSubmenu(true);
+ if (openSubmenu) {
+ openSubmenu(true);
}
}}
onMouseLeave={() => {
- if (this.props.openSubmenu) {
- this.props.openSubmenu(false);
+ if (openSubmenu) {
+ openSubmenu(false);
}
}}
+ ref={(node) => {
+ setReference(node);
+ dropdown.current = node;
+ }}
+ style={style}
>
- this.handleOverflow('left')}
- onOverflowRight={() => this.handleOverflow('right')}
- onOverflowTop={() => this.handleOverflow('top')}
- onOverflowBottom={() => this.handleOverflow('bottom')}
- >
- {this.props.children}
-
+ {children}
);
return submenu
- ? dropdown
- : ReactDOM.createPortal(dropdown, this.portalTarget);
- }
-}
+ ? dropdownComponent
+ : ReactDOM.createPortal(dropdownComponent, portalTarget.current);
+ },
+);
+
+DropdownContainer.displayName = 'DropdownContainer';
+
+DropdownContainer.defaultProps = {
+ testId: 'cf-ui-dropdown-portal',
+ position: 'bottom-left' as positionType,
+ submenu: false,
+};
export default DropdownContainer;
diff --git a/packages/forma-36-react-components/src/components/Dropdown/DropdownContainer/__snapshots__/DropdownContainer.test.tsx.snap b/packages/forma-36-react-components/src/components/Dropdown/DropdownContainer/__snapshots__/DropdownContainer.test.tsx.snap
index a2fd56bc72..b7c042d83f 100644
--- a/packages/forma-36-react-components/src/components/Dropdown/DropdownContainer/__snapshots__/DropdownContainer.test.tsx.snap
+++ b/packages/forma-36-react-components/src/components/Dropdown/DropdownContainer/__snapshots__/DropdownContainer.test.tsx.snap
@@ -10,23 +10,8 @@ exports[`has no a11y issues 1`] = `
onFocus={[Function]}
onMouseEnter={[Function]}
onMouseLeave={[Function]}
- style={
- Object {
- "left": 0,
- "top": 0,
- }
- }
>
-
- DropdownContainer
-
+ DropdownContainer
`;
@@ -41,46 +26,21 @@ exports[`renders the component 1`] = `
onFocus={[Function]}
onMouseEnter={[Function]}
onMouseLeave={[Function]}
- style={
- Object {
- "left": 0,
- "top": 0,
- }
- }
>
-
- DropdownContainer
-
+ DropdownContainer
`;
exports[`renders the component as a submenu 1`] = `
-
- DropdownContainer
-
+ DropdownContainer
`;
@@ -94,23 +54,8 @@ exports[`renders the component with an additional class name 1`] = `
onFocus={[Function]}
onMouseEnter={[Function]}
onMouseLeave={[Function]}
- style={
- Object {
- "left": 0,
- "top": 0,
- }
- }
>
-
- DropdownContainer
-
+ DropdownContainer
`;
diff --git a/packages/forma-36-react-components/src/components/Dropdown/DropdownList/DropdownList.tsx b/packages/forma-36-react-components/src/components/Dropdown/DropdownList/DropdownList.tsx
index 25ad7bee4a..3207b7cd90 100644
--- a/packages/forma-36-react-components/src/components/Dropdown/DropdownList/DropdownList.tsx
+++ b/packages/forma-36-react-components/src/components/Dropdown/DropdownList/DropdownList.tsx
@@ -1,8 +1,9 @@
-import React, { Component } from 'react';
+import React from 'react';
import cn from 'classnames';
-import cssStyles from './DropdownList.css';
import * as CSS from 'csstype';
+import cssStyles from './DropdownList.css';
+
export interface DropdownListProps {
children: React.ReactNode;
listRef?: React.RefObject;
@@ -13,46 +14,40 @@ export interface DropdownListProps {
styles?: object;
}
-const defaultProps: Partial = {
- testId: 'cf-ui-dropdown-list',
-};
-
-export class DropdownList extends Component {
- static defaultProps = defaultProps;
+export const DropdownList = ({
+ className,
+ border,
+ maxHeight,
+ testId,
+ children,
+ listRef,
+ styles,
+ ...otherProps
+}: DropdownListProps) => {
+ const classNames = cn(cssStyles['DropdownList'], className, {
+ [cssStyles[`DropdownList--border-${border}`]]: border,
+ });
- render() {
- const {
- className,
- border,
- maxHeight,
- testId,
- children,
- listRef,
- styles,
- ...otherProps
- } = this.props;
-
- const classNames = cn(cssStyles['DropdownList'], className, {
- [cssStyles[`DropdownList--border-${border}`]]: border,
- });
+ return (
+
+ );
+};
- return (
-
- );
- }
-}
+DropdownList.defaultProps = {
+ testId: 'cf-ui-dropdown-list',
+};
export default DropdownList;
diff --git a/packages/forma-36-react-components/src/components/Dropdown/DropdownList/__snapshots__/DropdownList.test.tsx.snap b/packages/forma-36-react-components/src/components/Dropdown/DropdownList/__snapshots__/DropdownList.test.tsx.snap
index 46140e49a4..f9f3e222a6 100644
--- a/packages/forma-36-react-components/src/components/Dropdown/DropdownList/__snapshots__/DropdownList.test.tsx.snap
+++ b/packages/forma-36-react-components/src/components/Dropdown/DropdownList/__snapshots__/DropdownList.test.tsx.snap
@@ -12,14 +12,14 @@ exports[`renders the component 1`] = `
}
}
>
-
List Item
-
+
`;
@@ -35,14 +35,14 @@ exports[`renders the component with a border attribute 1`] = `
}
}
>
-
List Item
-
+
`;
@@ -58,13 +58,13 @@ exports[`renders the component with an additional class name 1`] = `
}
}
>
-
List Item
-
+
`;
diff --git a/packages/forma-36-react-components/src/components/Dropdown/DropdownListItem/DropdownListItem.tsx b/packages/forma-36-react-components/src/components/Dropdown/DropdownListItem/DropdownListItem.tsx
index 6470eb29b1..59175967b8 100644
--- a/packages/forma-36-react-components/src/components/Dropdown/DropdownListItem/DropdownListItem.tsx
+++ b/packages/forma-36-react-components/src/components/Dropdown/DropdownListItem/DropdownListItem.tsx
@@ -1,16 +1,19 @@
import React, {
- Component,
+ forwardRef,
MouseEventHandler,
FocusEventHandler,
MouseEvent as ReactMouseEvent,
+ useCallback,
} from 'react';
import cn from 'classnames';
+
import TabFocusTrap from '../../TabFocusTrap/TabFocusTrap';
import styles from './DropdownListItem.css';
-export interface DropdownListItemProps {
+export interface DropdownListItemProps
+ extends React.HTMLAttributes {
isDisabled?: boolean;
- listItemRef?: React.RefObject;
+ listItemRef?: React.MutableRefObject;
isActive?: boolean;
isTitle?: boolean;
children: React.ReactNode;
@@ -25,126 +28,30 @@ export interface DropdownListItemProps {
testId?: string;
}
-const defaultProps: Partial = {
- testId: 'cf-ui-dropdown-list-item',
- isDisabled: false,
- isActive: false,
- isTitle: false,
-};
-
-export class DropdownListItem extends Component {
- static defaultProps = defaultProps;
-
- renderSubmenuToggle = () => {
- const {
- onClick,
- onEnter,
- onLeave,
- onFocus,
+export const DropdownListItem = forwardRef(
+ (
+ {
children,
- submenuToggleLabel,
- testId,
- isDisabled,
isActive,
- isTitle,
- ...otherProps
- } = this.props;
-
- return (
-
-
-
- {submenuToggleLabel}
-
-
- {children}
-
- );
- };
-
- renderListItem = () => {
- const {
- onClick,
- onMouseDown,
- href,
isDisabled,
- children,
isTitle,
- isActive,
- testId,
- listItemRef,
- ...otherProps
- } = this.props;
-
- const isClickable = onClick || onMouseDown || href;
-
- if (isClickable) {
- const Element = href ? 'a' : 'button';
-
- const buttonProps = {
- disabled: isDisabled,
- 'aria-disabled': isDisabled,
- };
-
- const linkProps = {
- href,
- };
-
- return (
- {
- if (!isDisabled && onClick) {
- onClick(e);
- }
- }}
- onMouseDown={(e: ReactMouseEvent) => {
- if (!isDisabled && onMouseDown) {
- onMouseDown(e);
- }
- }}
- {...(href ? linkProps : buttonProps)}
- {...otherProps}
- data-test-id="cf-ui-dropdown-list-item-button"
- className={styles['DropdownListItem__button']}
- >
-
- {children}
-
-
- );
- }
-
- return {children} ;
- };
-
- render() {
- const {
- className,
- isDisabled,
- testId,
- listItemRef,
- isActive,
onClick,
- onMouseDown,
- href,
+ onEnter,
+ onFocus,
+ onLeave,
+ style,
submenuToggleLabel,
- isTitle,
- } = this.props;
-
+ testId,
+ ...props
+ },
+ refCallback,
+ ) => {
+ const { className, href, listItemRef, onMouseDown, ...otherProps } = props;
+ // We're not dealing with React RefObjects but with useState (because we
+ // want to re-render on all changes)
+ const setReference = refCallback as React.Dispatch<
+ React.SetStateAction
+ >;
const classNames = cn(styles['DropdownListItem'], className, {
[styles['DropdownListItem__submenu-toggle']]:
submenuToggleLabel || onClick || onMouseDown || href,
@@ -153,19 +60,102 @@ export class DropdownListItem extends Component {
[styles['DropdownListItem--title']]: isTitle,
});
+ const renderListItem = useCallback(() => {
+ const { onMouseDown, href, listItemRef, ...otherProps } = props;
+
+ const isClickable = onClick || onMouseDown || href;
+
+ if (isClickable) {
+ const Element = href ? 'a' : 'button';
+
+ const buttonProps = {
+ disabled: isDisabled,
+ 'aria-disabled': isDisabled,
+ };
+
+ const linkProps = {
+ href,
+ };
+
+ return (
+ {
+ if (!isDisabled && onClick) {
+ onClick(e);
+ }
+ }}
+ onMouseDown={(e: ReactMouseEvent) => {
+ if (!isDisabled && onMouseDown) {
+ onMouseDown(e);
+ }
+ }}
+ type="button"
+ {...(href ? linkProps : buttonProps)}
+ {...otherProps}
+ >
+
+ {children}
+
+
+ );
+ }
+
+ return {children} ;
+ }, [children, isActive, isDisabled, isTitle, onClick, props]);
+
return (
{
+ if (setReference) {
+ setReference(node);
+ }
+
+ if (listItemRef) {
+ listItemRef.current = node;
+ }
+ }}
+ style={style}
>
- {submenuToggleLabel
- ? this.renderSubmenuToggle()
- : this.renderListItem()}
+ {submenuToggleLabel ? (
+
+
+
+ {submenuToggleLabel}
+
+
+ {children}
+
+ ) : (
+ renderListItem()
+ )}
);
- }
-}
+ },
+);
+
+DropdownListItem.defaultProps = {
+ testId: 'cf-ui-dropdown-list-item',
+ isDisabled: false,
+ isActive: false,
+ isTitle: false,
+};
export default DropdownListItem;
diff --git a/packages/forma-36-react-components/src/components/Dropdown/README.mdx b/packages/forma-36-react-components/src/components/Dropdown/README.mdx
new file mode 100644
index 0000000000..358a5cb3b1
--- /dev/null
+++ b/packages/forma-36-react-components/src/components/Dropdown/README.mdx
@@ -0,0 +1,243 @@
+Dropdowns allow users to access a list of multiple actions. A common use-case for Dropdowns is to build context menus.
+
+A Dropdown should contain at least one DropdownList, and multiple DropdownListItem components to represent each action.
+
+## Examples of usage
+
+```jsx
+import {
+ Button,
+ Dropdown,
+ DropdownList,
+ DropdownListItem,
+} from '@contentful/forma-36-react-components';
+
+function DropdownExample() {
+ const [isOpen, setOpen] = React.useState(false);
+ return (
+ setOpen(false)}
+ toggleElement={
+ setOpen(!isOpen)}
+ >
+ Trigger Dropdown
+
+ }
+ >
+
+
+ Dropdown list item 1
+
+
+ Dropdown list item 2
+
+
+
+ );
+}
+```
+
+Dropdown with a title – Use a title to clarify the purpose of a dropdown.
+
+```jsx
+import {
+ Button,
+ Dropdown,
+ DropdownList,
+ DropdownListItem,
+} from '@contentful/forma-36-react-components';
+
+function DropdownExample() {
+ const [isOpen, setOpen] = React.useState(false);
+ return (
+ setOpen(false)}
+ toggleElement={
+ setOpen(!isOpen)}
+ >
+ Trigger Dropdown with a title
+
+ }
+ >
+
+ Dropdown title
+
+ Dropdown list item 1
+
+
+ Dropdown list item 2
+
+
+
+ );
+}
+```
+
+Dropdown with multiple DropdownLists – Use multiple DropdownLists to group actions together. A DropdownList can contain a border to visually separating these lists.
+
+```jsx
+import {
+ Button,
+ Dropdown,
+ DropdownList,
+ DropdownListItem,
+} from '@contentful/forma-36-react-components';
+
+function DropdownExample() {
+ const [isOpen, setOpen] = React.useState(false);
+ return (
+ setOpen(false)}
+ toggleElement={
+ setOpen(!isOpen)}
+ >
+ Trigger Dropdown with multiple DropdownLists
+
+ }
+ >
+
+
+ Dropdown list item 1
+
+
+ Dropdown list item 2
+
+
+
+
+ Dropdown list item 1
+
+
+ Dropdown list item 2
+
+
+
+ );
+}
+```
+
+Dropdown with links – DropdownListItems can also contain TextLinks. These are often used to highlight related actions to the content of the DropdownList.
+
+```jsx
+import {
+ Button,
+ Dropdown,
+ DropdownList,
+ DropdownListItem,
+} from '@contentful/forma-36-react-components';
+
+function DropdownExample() {
+ const [isOpen, setOpen] = React.useState(false);
+ return (
+ setOpen(false)}
+ key={Date.now()} // Force Reinit
+ toggleElement={
+ setOpen(!isOpen)}
+ >
+ Trigger Dropdown with links
+
+ }
+ >
+
+ Kitten
+ Puppy
+ Piglet
+
+
+
+ Add a cute animal
+
+
+
+ );
+}
+```
+
+Dropdown with max height – DropdownLists can have a max height to limit the height of the dropdown and introduce scrolling. This should be used when dropdowns need to contain a lot of data.
+
+```jsx
+import {
+ Button,
+ Dropdown,
+ DropdownList,
+ DropdownListItem,
+} from '@contentful/forma-36-react-components';
+
+function DropdownExample() {
+ const [isOpen, setOpen] = React.useState(false);
+ return (
+ setOpen(false)}
+ key={Date.now()} // Force Reinit
+ toggleElement={
+ setOpen(!isOpen)}
+ >
+ Trigger Dropdown with max height
+
+ }
+ >
+
+ Item 1
+ Item 2
+ Item 3
+ Item 4
+ Item 5
+ Item 6
+ Item 7
+ Item 8
+ Item 9
+ Item 10
+ Item 11
+ Item 12
+ Item 13
+ Item 14
+ Item 15
+ Item 16
+ Item 17
+ Item 18
+
+
+ );
+}
+```
+
+## Best practices
+
+- If there is a single action to display, use TextLink or Button instead
+- Use lists and titles to group similar actions
+- Use a single TextLink at the bottom of the list for an action that is considerably different than the rest
+
+## Content recommendations:
+
+- To make Dropdown action-oriented, use a verb. For example, "Add field", not "New field"
+- Use clear and succinct copy
+
+## Accessibility
+
+- Missing
diff --git a/packages/forma-36-react-components/src/components/Dropdown/__snapshots__/Dropdown.test.tsx.snap b/packages/forma-36-react-components/src/components/Dropdown/__snapshots__/Dropdown.test.tsx.snap
index 2bd8faadf8..26253e2454 100644
--- a/packages/forma-36-react-components/src/components/Dropdown/__snapshots__/Dropdown.test.tsx.snap
+++ b/packages/forma-36-react-components/src/components/Dropdown/__snapshots__/Dropdown.test.tsx.snap
@@ -4,7 +4,6 @@ exports[`renders the component 1`] = `
-
entry
-
+
@@ -80,7 +75,6 @@ exports[`renders the component with a submenu 1`] = `
-
entry
-
+
-
entry
-
+
,
@@ -151,7 +141,6 @@ exports[`renders the component with an additional class name 1`] = `
-
Actions
-
-
+
Edit
-
-
+
Download
-
-
+
Remove
-
+