From 70e53fb0eea72156b81136fc5e6040d7e57af09a Mon Sep 17 00:00:00 2001 From: Romain Lancia Date: Fri, 2 Apr 2021 16:34:45 +0100 Subject: [PATCH 1/4] feat: add possibility nested dropdown --- .../components/dropdown/Dropdown.component.js | 159 +++++++++++------- src/lib/components/dropdown/utils.js | 72 ++++++++ stories/dropdown.stories.js | 49 ++++++ 3 files changed, 219 insertions(+), 61 deletions(-) create mode 100644 src/lib/components/dropdown/utils.js diff --git a/src/lib/components/dropdown/Dropdown.component.js b/src/lib/components/dropdown/Dropdown.component.js index 3cf614481e..1daa7c3adf 100644 --- a/src/lib/components/dropdown/Dropdown.component.js +++ b/src/lib/components/dropdown/Dropdown.component.js @@ -1,5 +1,5 @@ //@flow -import React, { useState, useCallback } from "react"; +import React, { useState, useCallback, forwardRef } from "react"; import type { Node } from "react"; import styled, { css } from "styled-components"; import { @@ -8,23 +8,45 @@ import { ButtonText } from "../button/Button.component"; import * as defaultTheme from "../../style/theme"; -import { getThemePropSelector } from "../../utils"; +import { getThemePropSelector } from '../../utils'; +import { getPositionDropdownMenu } from './utils'; export type Item = { label: string, name?: string, selected?: boolean, - onClick: any => void + onClick?: any => void, + submenuIcon?: Node, + submenuItems?: Array }; + +type DropdownTriggerContainerProps = { + isItem?: boolean, + dataIndex: number, + open?: boolean, + size?: string, + variant?: string, + title?: string, + onBlur: any => void, + onFocus: any => void, + onClick: any => void, + onMouseEnter: any => void, + onMouseLeave: any => void, + children: Node, +} + type Items = Array; type Props = { + isItem?: boolean, text?: string, size?: string, variant?: string, title?: string, items: Items, icon?: Node, - caret?: boolean + caret?: boolean, + dataIndex?: number, + onClick?: any => void, }; const DropdownStyled = styled.div` @@ -41,43 +63,19 @@ const DropdownMenuStyled = styled.ul` position: absolute; margin: 0; padding: 0; - border: 1px solid ${getThemePropSelector("primary")}; z-index: ${defaultTheme.zIndex.dropdown}; max-height: 200px; min-width: 100%; - overflow: auto; - - ${props => { - if ( - props.size && - props.triggerSize && - props.triggerSize.x + props.size.width > window.innerWidth - ) { - return css` - right: 0; - top: 100%; - `; - } else if ( - props.size && - props.triggerSize && - props.triggerSize.y + props.size.height > window.innerHeight - ) { - return css` - left: 0; - bottom: ${props.triggerSize.height + "px"}; - `; - } else { - return css` - left: 0; - top: 100%; - `; - } + + ${({ triggerSize, isItem, size, itemIndex = 0, nbItems}) => { + return getPositionDropdownMenu({ isItem, triggerSize, size, nbItems, itemIndex }) }}; `; const DropdownMenuItemStyled = styled.li` display: flex; align-items: center; + justify-content: space-between; padding: ${defaultTheme.padding.base}; white-space: nowrap; cursor: pointer; @@ -101,7 +99,43 @@ const Caret = styled.span` const TriggerStyled = ButtonStyled.withComponent("div"); +const DropdownTriggerContainer = forwardRef(({isItem, dataIndex, open, size , variant, title, onBlur, onFocus, onClick, onMouseEnter, onMouseLeave, children, ...rest}, ref) => { + return isItem ? ( + + {children} + + ) : ( + + + {children} + + + ) +}) + function Dropdown({ + isItem = false, items, text, icon, @@ -109,41 +143,45 @@ function Dropdown({ variant = "base", title, caret = true, + dataIndex = null, + onClick = null, ...rest }: Props) { const [open, setOpen] = useState(false); const [menuSize, setMenuSize] = useState(); const [triggerSize, setTriggerSize] = useState(); + const [itemIndex, setItemIndex] = useState() const refMenuCallback = useCallback(node => { if (node !== null) { setMenuSize(node.getBoundingClientRect()); } - }, []); + }, [setMenuSize]); const refTriggerCallback = useCallback(node => { if (node !== null) { setTriggerSize(node.getBoundingClientRect()); } - }, []); + }, [setTriggerSize]); return ( - - { + setItemIndex(e && e.target && e.target.getAttribute('data-index') || 0) + setOpen(true) + }} + onMouseLeave={() => setOpen(false)} variant={variant} size={size} - className="trigger" onBlur={() => setOpen(!open)} onFocus={() => setOpen(!open)} - onClick={event => event.stopPropagation()} - tabIndex="0" + onClick={onClick ? onClick : event => event.stopPropagation()} title={title} ref={refTriggerCallback} + dataIndex={dataIndex} + {...rest} > {icon && ( @@ -151,36 +189,35 @@ function Dropdown({ )} {text && {text}} - {caret && ( + {caret && items.length > 0 && ( - + )} {open && ( - {items.map(({ label, onClick, ...itemRest }) => { - return ( - - {label} - - ); + {items.map(({ label, onClick, submenuIcon = null, submenuItems = [], ...itemRest }, index) => { + return })} )} - - + ); } diff --git a/src/lib/components/dropdown/utils.js b/src/lib/components/dropdown/utils.js new file mode 100644 index 0000000000..32484e06af --- /dev/null +++ b/src/lib/components/dropdown/utils.js @@ -0,0 +1,72 @@ +import { css } from 'styled-components'; + +const getBorderCollisionDetection = (isItem, triggerSize, size) => { + let isTopHit = false + let isRightHit = false + let isBottomHit = false + let isLeftHit = false + + if (size && triggerSize && triggerSize.top - size.height <= 0) { + isTopHit = true + } + if (size && triggerSize && triggerSize.right + triggerSize.width > window.innerWidth) { + isRightHit = true + } + if (size && triggerSize && triggerSize.top + size.height >= window.innerHeight) { + isBottomHit = true + } + if (size && triggerSize && triggerSize.left - triggerSize.width <= 0) { + isLeftHit = true + } + + return { + isTopHit, + isRightHit, + isBottomHit, + isLeftHit + } +} + +export const getPositionDropdownMenu = ({ isItem, triggerSize, size, nbItems, itemIndex }) => { + const { isTopHit, isRightHit, isBottomHit, isLeftHit } = getBorderCollisionDetection(isItem, triggerSize, size) + + if (isItem) { + // Check collision for dropdown acting as an item + if (isRightHit && isBottomHit) { + return css` + right: ${size.width}px; + bottom: ${-triggerSize.height * (itemIndex - nbItems + 1)}px; + `; + } else if (isLeftHit && isBottomHit || isBottomHit) { + return css` + left: ${size.width}px; + bottom: ${-triggerSize.height * (itemIndex - nbItems + 1)}px; + `; + } else if (isTopHit && isRightHit || isRightHit) { + return css` + right: ${size.width}px; + top: ${triggerSize.height * itemIndex}px; + `; + } + else { + return css` + left: ${triggerSize.width}px; + top: ${triggerSize.height * itemIndex}px; + `; + } + } + else { + // Check collision for dropdown acting as a root button* + if (isBottomHit) { + return css` + left: 0; + bottom: ${triggerSize.height}px; + `; + } else { + return css` + left: 0; + top: 100%; + `; + } + } +} diff --git a/stories/dropdown.stories.js b/stories/dropdown.stories.js index c2bdb54ec5..105cb5e098 100644 --- a/stories/dropdown.stories.js +++ b/stories/dropdown.stories.js @@ -22,6 +22,48 @@ const items = [ }, ]; +const itemsWithNested = [ + { + label: 'Language', + submenuIcon: null, + submenuItems: [ + { + label: 'French', + onClick: action('French clicked'), + 'data-cy': 'French', + }, + { + label: 'English', + onClick: action('English clicked'), + 'data-cy': 'English', + }, + ], + 'data-cy': 'Language' + }, + { + label: 'Theme', + submenuIcon: , + submenuItems: [ + { + label: 'Light', + onClick: action('Light clicked'), + 'data-cy': 'French', + }, + { + label: 'Dark', + onClick: action('Dark clicked'), + 'data-cy': 'Dark', + }, + ], + 'data-cy': 'Theme' + }, + { + label: 'Support', + onClick: action('Support clicked'), + 'data-cy': 'Support', + }, +] + export default { title: 'Components/Dropdown', component: Dropdown, @@ -102,6 +144,13 @@ export const Default = () => { variant="danger" text="danger" /> + + Dropdown with nested dropdowns + } + items={itemsWithNested} + /> ); }; From de214c88da2c5416ff488a4c0cda91337da42c49 Mon Sep 17 00:00:00 2001 From: Romain Lancia Date: Tue, 6 Apr 2021 13:21:02 +0100 Subject: [PATCH 2/4] Dropdown: udpate tests --- src/__snapshots__/dropdown.stories.storyshot | 38 ++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/src/__snapshots__/dropdown.stories.storyshot b/src/__snapshots__/dropdown.stories.storyshot index 5b09ea18b2..cb76927b79 100644 --- a/src/__snapshots__/dropdown.stories.storyshot +++ b/src/__snapshots__/dropdown.stories.storyshot @@ -400,5 +400,43 @@ exports[`Storyshots Components/Dropdown Default 1`] = ` +

+ Dropdown with nested dropdowns +

+
+
+ + + + + Help + + + + +
+
`; From abc113b02437785b6dd7844ec133bcce043f1cb2 Mon Sep 17 00:00:00 2001 From: Romain Lancia Date: Thu, 8 Apr 2021 16:43:14 +0100 Subject: [PATCH 3/4] resolve comments --- .../components/dropdown/Dropdown.component.js | 306 +++++++++++------- .../utils/getPositionDropdownMenu.test.js | 153 +++++++++ src/lib/components/dropdown/utils.js | 116 ++++--- stories/dropdown.stories.js | 10 +- 4 files changed, 421 insertions(+), 164 deletions(-) create mode 100644 src/lib/components/dropdown/__tests__/utils/getPositionDropdownMenu.test.js diff --git a/src/lib/components/dropdown/Dropdown.component.js b/src/lib/components/dropdown/Dropdown.component.js index 1daa7c3adf..ff79ad8a43 100644 --- a/src/lib/components/dropdown/Dropdown.component.js +++ b/src/lib/components/dropdown/Dropdown.component.js @@ -1,13 +1,13 @@ //@flow -import React, { useState, useCallback, forwardRef } from "react"; -import type { Node } from "react"; -import styled, { css } from "styled-components"; +import React, { useState, useCallback } from 'react'; +import type { Node } from 'react'; +import styled, { css } from 'styled-components'; import { ButtonStyled, ButtonIcon, - ButtonText -} from "../button/Button.component"; -import * as defaultTheme from "../../style/theme"; + ButtonText, +} from '../button/Button.component'; +import * as defaultTheme from '../../style/theme'; import { getThemePropSelector } from '../../utils'; import { getPositionDropdownMenu } from './utils'; @@ -15,26 +15,12 @@ export type Item = { label: string, name?: string, selected?: boolean, - onClick?: any => void, + onClick?: (SyntheticMouseEvent) => void, + iconExternal?: Node, submenuIcon?: Node, - submenuItems?: Array + submenuItems?: Array, }; -type DropdownTriggerContainerProps = { - isItem?: boolean, - dataIndex: number, - open?: boolean, - size?: string, - variant?: string, - title?: string, - onBlur: any => void, - onFocus: any => void, - onClick: any => void, - onMouseEnter: any => void, - onMouseLeave: any => void, - children: Node, -} - type Items = Array; type Props = { isItem?: boolean, @@ -46,7 +32,8 @@ type Props = { icon?: Node, caret?: boolean, dataIndex?: number, - onClick?: any => void, + iconExternal?: Node, + onClick?: (SyntheticMouseEvent) => void, }; const DropdownStyled = styled.div` @@ -59,128 +46,213 @@ const DropdownStyled = styled.div` } `; -const DropdownMenuStyled = styled.ul` - position: absolute; - margin: 0; - padding: 0; - z-index: ${defaultTheme.zIndex.dropdown}; - max-height: 200px; - min-width: 100%; - - ${({ triggerSize, isItem, size, itemIndex = 0, nbItems}) => { - return getPositionDropdownMenu({ isItem, triggerSize, size, nbItems, itemIndex }) - }}; -`; - const DropdownMenuItemStyled = styled.li` display: flex; align-items: center; justify-content: space-between; padding: ${defaultTheme.padding.base}; + box-sizing: border-box; + border-bottom: 1px solid ${getThemePropSelector('border')}; white-space: nowrap; cursor: pointer; font-size: ${defaultTheme.fontSize.base}; + &:last-child { + border-bottom: 0px; + } + ${css` - background-color: ${getThemePropSelector("primary")}; - color: ${getThemePropSelector("textPrimary")}; + background-color: ${getThemePropSelector('primary')}; + color: ${getThemePropSelector('textPrimary')}; &:hover { - background-color: ${getThemePropSelector("primaryDark2")}; + background-color: ${getThemePropSelector('primaryDark2')}; } &:active { - background-color: ${getThemePropSelector("primaryDark2")}; + background-color: ${getThemePropSelector('primaryDark2')}; } `}; `; +const DropdownMenuStyled = styled.ul` + position: absolute; + margin: 0; + padding: 0; + z-index: ${defaultTheme.zIndex.dropdown}; + max-height: 200px; + min-width: 100%; + ${({ nbItems }) => + nbItems > 0 + ? css` + border: 1px solid ${getThemePropSelector('border')}; + ` + : css``}; + ${({ triggerSize, isItem = false, size, itemIndex = 0, nbItems }) => { + return css` + ${getPositionDropdownMenu({ + isItem, + triggerSize, + size, + nbItems, + itemIndex, + })} + `; + }}; +`; + const Caret = styled.span` margin-left: ${defaultTheme.padding.base}; `; -const TriggerStyled = ButtonStyled.withComponent("div"); +const TriggerStyled = ButtonStyled.withComponent('div'); + +const DropdownAsAnItem = ({ + items = [], + text, + icon = null, + size = 'base', + caret = true, + iconExternal = null, + dataIndex = null, + onClick = null, +}: Props) => { + const [open, setOpen] = useState(false); + const [menuSize, setMenuSize] = useState(); + const [triggerSize, setTriggerSize] = useState(); + const [itemIndex, setItemIndex] = useState(); + + const refMenuCallback = useCallback( + (node) => { + if (node !== null) { + setMenuSize(node.getBoundingClientRect()); + } + }, + [setMenuSize], + ); + + const refTriggerCallback = useCallback( + (node) => { + if (node !== null) { + setTriggerSize(node.getBoundingClientRect()); + } + }, + [setTriggerSize], + ); -const DropdownTriggerContainer = forwardRef(({isItem, dataIndex, open, size , variant, title, onBlur, onFocus, onClick, onMouseEnter, onMouseLeave, children, ...rest}, ref) => { - return isItem ? ( + return ( { + setItemIndex( + e && e.currentTarget && e.currentTarget.getAttribute('data-index'), + ); + setOpen(true); + }} + onMouseLeave={() => setOpen(false)} data-index={dataIndex} onClick={onClick} - ref={ref} + ref={refTriggerCallback} > - {children} + {icon && ( + + {icon} + + )} + {text && {text}} + {caret && items.length > 0 && ( + + + + )} + {iconExternal && {iconExternal}} + {open && ( + + {items.map( + ( + { + label, + onClick, + submenuIcon = null, + submenuItems = [], + iconExternal = null, + }, + index, + ) => { + return ( + + ); + }, + )} + + )} - ) : ( - - - {children} - - - ) -}) + ); +}; function Dropdown({ - isItem = false, items, text, icon, - size = "base", - variant = "base", + size = 'base', + variant = 'base', title, caret = true, - dataIndex = null, onClick = null, ...rest }: Props) { const [open, setOpen] = useState(false); const [menuSize, setMenuSize] = useState(); const [triggerSize, setTriggerSize] = useState(); - const [itemIndex, setItemIndex] = useState() - const refMenuCallback = useCallback(node => { - if (node !== null) { - setMenuSize(node.getBoundingClientRect()); - } - }, [setMenuSize]); + const refMenuCallback = useCallback( + (node) => { + if (node !== null) { + setMenuSize(node.getBoundingClientRect()); + } + }, + [setMenuSize], + ); - const refTriggerCallback = useCallback(node => { - if (node !== null) { - setTriggerSize(node.getBoundingClientRect()); - } - }, [setTriggerSize]); + const refTriggerCallback = useCallback( + (node) => { + if (node !== null) { + setTriggerSize(node.getBoundingClientRect()); + } + }, + [setTriggerSize], + ); return ( - { - setItemIndex(e && e.target && e.target.getAttribute('data-index') || 0) - setOpen(true) - }} - onMouseLeave={() => setOpen(false)} + + setOpen(!open)} onFocus={() => setOpen(!open)} - onClick={onClick ? onClick : event => event.stopPropagation()} + onClick={(event) => event.stopPropagation()} + tabIndex="0" title={title} ref={refTriggerCallback} - dataIndex={dataIndex} {...rest} > {icon && ( @@ -191,7 +263,7 @@ function Dropdown({ {text && {text}} {caret && items.length > 0 && ( - + )} {open && ( @@ -200,24 +272,36 @@ function Dropdown({ ref={refMenuCallback} size={menuSize} triggerSize={triggerSize} - itemIndex={itemIndex} nbItems={items.length} - isItem={isItem} > - {items.map(({ label, onClick, submenuIcon = null, submenuItems = [], ...itemRest }, index) => { - return - })} + {items.map( + ( + { + label, + onClick, + submenuIcon = null, + submenuItems = [], + iconExternal = null, + }, + index, + ) => { + return ( + + ); + }, + )} )} - + + ); } diff --git a/src/lib/components/dropdown/__tests__/utils/getPositionDropdownMenu.test.js b/src/lib/components/dropdown/__tests__/utils/getPositionDropdownMenu.test.js new file mode 100644 index 0000000000..34d1aa25dc --- /dev/null +++ b/src/lib/components/dropdown/__tests__/utils/getPositionDropdownMenu.test.js @@ -0,0 +1,153 @@ +import { getPositionDropdownMenu } from '../../utils'; + +describe('Testing Utils', () => { + afterEach(() => { + jest.clearAllMocks(); + }); + + describe('getPositionDropdownMenu', () => { + it('should return an object with left and bottom keys if menu hit left and bottom frame', () => { + const res = getPositionDropdownMenu({ + isItem: true, + triggerSize: { + height: 50, + left: 60, + right: 180, + top: 795, + width: 120, + }, + size: { + height: 100, + left: 180, + right: 300, + top: 610, + width: 120, + }, + nbItems: 3, + itemIndex: 1, + }); + + expect(Object.keys(res)).toEqual(['left', 'bottom']); + }); + + it('should return an object with left and top keys if menu hit left and top frame', () => { + const res = getPositionDropdownMenu({ + isItem: true, + triggerSize: { + height: 50, + left: 60, + right: 180, + top: 80, + width: 120, + }, + size: { + height: 100, + left: 180, + right: 300, + top: 80, + width: 120, + }, + nbItems: 3, + itemIndex: 1, + }); + + expect(Object.keys(res)).toEqual(['left', 'top']); + }); + + it('should return an object with right and top keys if menu hit right and top frame', () => { + const res = getPositionDropdownMenu({ + isItem: true, + triggerSize: { + height: 50, + left: 890, + right: 1010, + top: 80, + width: 120, + }, + size: { + height: 100, + left: 170, + right: 880, + top: 80, + width: 120, + }, + nbItems: 3, + itemIndex: 1, + }); + + expect(Object.keys(res)).toEqual(['right', 'top']); + }); + + it('should return an object with right and bottom keys if menu hit bottom and right frame', () => { + const res = getPositionDropdownMenu({ + isItem: true, + triggerSize: { + height: 50, + left: 870, + right: 990, + top: 610, + width: 120, + }, + size: { + height: 200, + left: 990, + right: 1110, + top: 610, + width: 120, + }, + nbItems: 4, + itemIndex: 2, + }); + + expect(Object.keys(res)).toEqual(['right', 'bottom']); + }); + + it('should return an object with left and top keys if menu hit left frame', () => { + const res = getPositionDropdownMenu({ + isItem: true, + triggerSize: { + height: 50, + left: 180, + right: 300, + top: 470, + width: 120, + }, + size: { + height: 100, + left: 300, + right: 420, + top: 470, + width: 120, + }, + nbItems: 3, + itemIndex: 1, + }); + + expect(Object.keys(res)).toEqual(['left', 'top']); + }); + + it('should return an object with right and top keys if menu hit right frame', () => { + const res = getPositionDropdownMenu({ + isItem: true, + triggerSize: { + height: 50, + left: 930, + right: 1050, + top: 520, + width: 120, + }, + size: { + height: 100, + left: 1050, + right: 1170, + top: 520, + width: 120, + }, + nbItems: 3, + itemIndex: 1, + }); + + expect(Object.keys(res)).toEqual(['right', 'top']); + }); + }); +}); diff --git a/src/lib/components/dropdown/utils.js b/src/lib/components/dropdown/utils.js index 32484e06af..8f15e27ad1 100644 --- a/src/lib/components/dropdown/utils.js +++ b/src/lib/components/dropdown/utils.js @@ -1,72 +1,86 @@ -import { css } from 'styled-components'; - -const getBorderCollisionDetection = (isItem, triggerSize, size) => { - let isTopHit = false - let isRightHit = false - let isBottomHit = false - let isLeftHit = false +export const getBorderCollisionDetection = (isItem, triggerSize, size) => { + let isTopHit = false; + let isRightHit = false; + let isBottomHit = false; + let isLeftHit = false; if (size && triggerSize && triggerSize.top - size.height <= 0) { - isTopHit = true + isTopHit = true; } - if (size && triggerSize && triggerSize.right + triggerSize.width > window.innerWidth) { - isRightHit = true + if ( + size && + triggerSize && + triggerSize.right + triggerSize.width > window.innerWidth + ) { + isRightHit = true; } - if (size && triggerSize && triggerSize.top + size.height >= window.innerHeight) { - isBottomHit = true + if ( + size && + triggerSize && + triggerSize.top + size.height >= window.innerHeight + ) { + isBottomHit = true; } if (size && triggerSize && triggerSize.left - triggerSize.width <= 0) { - isLeftHit = true + isLeftHit = true; } return { isTopHit, isRightHit, isBottomHit, - isLeftHit - } -} - -export const getPositionDropdownMenu = ({ isItem, triggerSize, size, nbItems, itemIndex }) => { - const { isTopHit, isRightHit, isBottomHit, isLeftHit } = getBorderCollisionDetection(isItem, triggerSize, size) + isLeftHit, + }; +}; +export const getPositionDropdownMenu = ({ + isItem, + triggerSize, + size, + nbItems, + itemIndex, +}) => { + const { + isTopHit, + isRightHit, + isBottomHit, + isLeftHit, + } = getBorderCollisionDetection(isItem, triggerSize, size); if (isItem) { // Check collision for dropdown acting as an item if (isRightHit && isBottomHit) { - return css` - right: ${size.width}px; - bottom: ${-triggerSize.height * (itemIndex - nbItems + 1)}px; - `; - } else if (isLeftHit && isBottomHit || isBottomHit) { - return css` - left: ${size.width}px; - bottom: ${-triggerSize.height * (itemIndex - nbItems + 1)}px; - `; - } else if (isTopHit && isRightHit || isRightHit) { - return css` - right: ${size.width}px; - top: ${triggerSize.height * itemIndex}px; - `; - } - else { - return css` - left: ${triggerSize.width}px; - top: ${triggerSize.height * itemIndex}px; - `; + return { + right: `${size.width}px`, + bottom: `${-triggerSize.height * (itemIndex - nbItems + 1)}px`, + }; + } else if ((isLeftHit && isBottomHit) || isBottomHit) { + return { + left: `${size.width}px`, + bottom: `${-triggerSize.height * (itemIndex - nbItems + 1)}px`, + }; + } else if ((isTopHit && isRightHit) || isRightHit) { + return { + right: `${size.width}px`, + top: `${triggerSize.height * itemIndex}px`, + }; + } else { + return { + left: `${triggerSize.width}px`, + top: `${triggerSize.height * itemIndex}px`, + }; } - } - else { + } else { // Check collision for dropdown acting as a root button* - if (isBottomHit) { - return css` - left: 0; - bottom: ${triggerSize.height}px; - `; + if (isBottomHit) { + return { + left: 0, + bottom: `${triggerSize.height}px`, + }; } else { - return css` - left: 0; - top: 100%; - `; + return { + left: 0, + top: '100%', + }; } } -} +}; diff --git a/stories/dropdown.stories.js b/stories/dropdown.stories.js index 105cb5e098..2fb0d87cd8 100644 --- a/stories/dropdown.stories.js +++ b/stories/dropdown.stories.js @@ -3,6 +3,7 @@ import React from 'react'; import Dropdown from '../src/lib/components/dropdown/Dropdown.component'; import { action } from '@storybook/addon-actions'; import { Wrapper, Title } from './common'; +import type { Node } from 'react'; const items = [ { @@ -25,7 +26,6 @@ const items = [ const itemsWithNested = [ { label: 'Language', - submenuIcon: null, submenuItems: [ { label: 'French', @@ -47,13 +47,19 @@ const itemsWithNested = [ { label: 'Light', onClick: action('Light clicked'), - 'data-cy': 'French', + 'data-cy': 'Light', }, { label: 'Dark', onClick: action('Dark clicked'), 'data-cy': 'Dark', }, + { + label: 'External', + onClick: action('External clicked'), + iconExternal: , + 'data-cy': 'External', + }, ], 'data-cy': 'Theme' }, From e091c5400415889479034ecf93b985c67d085ace Mon Sep 17 00:00:00 2001 From: Romain Lancia Date: Mon, 19 Apr 2021 18:03:15 +0100 Subject: [PATCH 4/4] add plugins to babel for injecting optional and coalescing syntax in jest + simplify dropdown and fix menu position --- .babelrc | 4 ++ package-lock.json | 16 +++--- package.json | 2 + .../components/dropdown/Dropdown.component.js | 53 ++++++------------- src/lib/components/dropdown/utils.js | 18 +++---- 5 files changed, 37 insertions(+), 56 deletions(-) diff --git a/.babelrc b/.babelrc index 7f1940bc20..f8ee68ae08 100644 --- a/.babelrc +++ b/.babelrc @@ -3,5 +3,9 @@ "@babel/preset-env", "@babel/preset-react", "@babel/preset-flow" + ], + "plugins": [ + "@babel/plugin-proposal-optional-chaining", + "@babel/plugin-proposal-nullish-coalescing-operator" ] } diff --git a/package-lock.json b/package-lock.json index a42b8a99d8..ca0c449547 100644 --- a/package-lock.json +++ b/package-lock.json @@ -870,13 +870,13 @@ } }, "@babel/plugin-proposal-nullish-coalescing-operator": { - "version": "7.13.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-nullish-coalescing-operator/-/plugin-proposal-nullish-coalescing-operator-7.13.0.tgz", - "integrity": "sha512-UkAvFA/9+lBBL015gjA68NvKiCReNxqFLm3SdNKaM3XXoDisA7tMAIX4PmIwatFoFqMxxT3WyG9sK3MO0Kting==", + "version": "7.13.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-nullish-coalescing-operator/-/plugin-proposal-nullish-coalescing-operator-7.13.8.tgz", + "integrity": "sha512-iePlDPBn//UhxExyS9KyeYU7RM9WScAG+D3Hhno0PLJebAEpDZMocbDe64eqynhNAnwz/vZoL/q/QB2T1OH39A==", "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.13.0", - "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.0" + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3" }, "dependencies": { "@babel/helper-plugin-utils": { @@ -926,14 +926,14 @@ } }, "@babel/plugin-proposal-optional-chaining": { - "version": "7.13.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.13.0.tgz", - "integrity": "sha512-OVRQOZEBP2luZrvEbNSX5FfWDousthhdEoAOpej+Tpe58HFLvqRClT89RauIvBuCDFEip7GW1eT86/5lMy2RNA==", + "version": "7.13.12", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.13.12.tgz", + "integrity": "sha512-fcEdKOkIB7Tf4IxrgEVeFC4zeJSTr78no9wTdBuZZbqF64kzllU0ybo2zrzm7gUQfxGhBgq4E39oRs8Zx/RMYQ==", "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.13.0", "@babel/helper-skip-transparent-expression-wrappers": "^7.12.1", - "@babel/plugin-syntax-optional-chaining": "^7.8.0" + "@babel/plugin-syntax-optional-chaining": "^7.8.3" }, "dependencies": { "@babel/helper-plugin-utils": { diff --git a/package.json b/package.json index 3f3efe4a2a..d5e3d0d500 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,8 @@ "devDependencies": { "@babel/cli": "7.6.0", "@babel/core": "7.6.0", + "@babel/plugin-proposal-nullish-coalescing-operator": "^7.13.8", + "@babel/plugin-proposal-optional-chaining": "^7.13.12", "@babel/preset-env": "7.6.0", "@babel/preset-flow": "7.0.0", "@babel/preset-react": "7.0.0", diff --git a/src/lib/components/dropdown/Dropdown.component.js b/src/lib/components/dropdown/Dropdown.component.js index ff79ad8a43..57bef6d7ad 100644 --- a/src/lib/components/dropdown/Dropdown.component.js +++ b/src/lib/components/dropdown/Dropdown.component.js @@ -31,7 +31,6 @@ type Props = { items: Items, icon?: Node, caret?: boolean, - dataIndex?: number, iconExternal?: Node, onClick?: (SyntheticMouseEvent) => void, }; @@ -47,6 +46,7 @@ const DropdownStyled = styled.div` `; const DropdownMenuItemStyled = styled.li` + position: relative; display: flex; align-items: center; justify-content: space-between; @@ -86,14 +86,12 @@ const DropdownMenuStyled = styled.ul` border: 1px solid ${getThemePropSelector('border')}; ` : css``}; - ${({ triggerSize, isItem = false, size, itemIndex = 0, nbItems }) => { + ${({ triggerSize, isItem = false, size }) => { return css` ${getPositionDropdownMenu({ isItem, triggerSize, size, - nbItems, - itemIndex, })} `; }}; @@ -112,13 +110,11 @@ const DropdownAsAnItem = ({ size = 'base', caret = true, iconExternal = null, - dataIndex = null, onClick = null, }: Props) => { const [open, setOpen] = useState(false); const [menuSize, setMenuSize] = useState(); const [triggerSize, setTriggerSize] = useState(); - const [itemIndex, setItemIndex] = useState(); const refMenuCallback = useCallback( (node) => { @@ -140,14 +136,8 @@ const DropdownAsAnItem = ({ return ( { - setItemIndex( - e && e.currentTarget && e.currentTarget.getAttribute('data-index'), - ); - setOpen(true); - }} + onMouseEnter={() => setOpen(true)} onMouseLeave={() => setOpen(false)} - data-index={dataIndex} onClick={onClick} ref={refTriggerCallback} > @@ -169,28 +159,23 @@ const DropdownAsAnItem = ({ ref={refMenuCallback} size={menuSize} triggerSize={triggerSize} - itemIndex={itemIndex} nbItems={items.length} isItem > {items.map( - ( - { - label, - onClick, - submenuIcon = null, - submenuItems = [], - iconExternal = null, - }, - index, - ) => { + ({ + label, + onClick, + submenuIcon = null, + submenuItems = [], + iconExternal = null, + }) => { return ( @@ -275,23 +260,19 @@ function Dropdown({ nbItems={items.length} > {items.map( - ( - { - label, - onClick, - submenuIcon = null, - submenuItems = [], - iconExternal = null, - }, - index, - ) => { + ({ + label, + onClick, + submenuIcon = null, + submenuItems = [], + iconExternal = null, + }) => { return ( diff --git a/src/lib/components/dropdown/utils.js b/src/lib/components/dropdown/utils.js index 8f15e27ad1..40c3ed7d9b 100644 --- a/src/lib/components/dropdown/utils.js +++ b/src/lib/components/dropdown/utils.js @@ -33,13 +33,7 @@ export const getBorderCollisionDetection = (isItem, triggerSize, size) => { }; }; -export const getPositionDropdownMenu = ({ - isItem, - triggerSize, - size, - nbItems, - itemIndex, -}) => { +export const getPositionDropdownMenu = ({ isItem, triggerSize, size }) => { const { isTopHit, isRightHit, @@ -51,22 +45,22 @@ export const getPositionDropdownMenu = ({ if (isRightHit && isBottomHit) { return { right: `${size.width}px`, - bottom: `${-triggerSize.height * (itemIndex - nbItems + 1)}px`, + bottom: 0, }; } else if ((isLeftHit && isBottomHit) || isBottomHit) { return { left: `${size.width}px`, - bottom: `${-triggerSize.height * (itemIndex - nbItems + 1)}px`, + bottom: 0, }; } else if ((isTopHit && isRightHit) || isRightHit) { return { right: `${size.width}px`, - top: `${triggerSize.height * itemIndex}px`, + top: 0, }; } else { return { - left: `${triggerSize.width}px`, - top: `${triggerSize.height * itemIndex}px`, + left: `${size?.width ?? triggerSize.width}px`, + top: 0, }; } } else {