From 3da47517b8f76e395e4d1b54ed5c1eac7b78ba4f Mon Sep 17 00:00:00 2001 From: Linda Paiste Date: Mon, 14 Aug 2023 11:59:59 -0500 Subject: [PATCH 1/9] Common dropdown component for table actions. --- client/components/Dropdown.jsx | 3 +- client/components/Dropdown/DropdownMenu.jsx | 83 ++++++++ client/components/Dropdown/MenuItem.jsx | 35 ++++ client/components/Dropdown/TableDropdown.jsx | 20 ++ client/modules/IDE/components/AssetList.jsx | 161 +++++---------- .../CollectionList/CollectionListRow.jsx | 119 ++--------- client/modules/IDE/components/SketchList.jsx | 193 ++++-------------- .../SketchList.unit.test.jsx.snap | 40 ++-- client/styles/components/_asset-list.scss | 21 -- client/styles/components/_sketch-list.scss | 17 -- 10 files changed, 265 insertions(+), 427 deletions(-) create mode 100644 client/components/Dropdown/DropdownMenu.jsx create mode 100644 client/components/Dropdown/MenuItem.jsx create mode 100644 client/components/Dropdown/TableDropdown.jsx diff --git a/client/components/Dropdown.jsx b/client/components/Dropdown.jsx index 9a91d54cd2..369bef51b0 100644 --- a/client/components/Dropdown.jsx +++ b/client/components/Dropdown.jsx @@ -4,7 +4,7 @@ import styled from 'styled-components'; import { remSize, prop } from '../theme'; import IconButton from './mobile/IconButton'; -const DropdownWrapper = styled.ul` +export const DropdownWrapper = styled.ul` background-color: ${prop('Modal.background')}; border: 1px solid ${prop('Modal.border')}; box-shadow: 0 0 18px 0 ${prop('shadowColor')}; @@ -52,6 +52,7 @@ const DropdownWrapper = styled.ul` & button span, & a { padding: ${remSize(8)} ${remSize(16)}; + font-size: ${remSize(12)}; } * { diff --git a/client/components/Dropdown/DropdownMenu.jsx b/client/components/Dropdown/DropdownMenu.jsx new file mode 100644 index 0000000000..ce7e03837e --- /dev/null +++ b/client/components/Dropdown/DropdownMenu.jsx @@ -0,0 +1,83 @@ +import PropTypes from 'prop-types'; +import React, { forwardRef, useCallback, useRef, useState } from 'react'; +import useModalClose from '../../common/useModalClose'; +import DownArrowIcon from '../../images/down-filled-triangle.svg'; +import { DropdownWrapper } from '../Dropdown'; + +// TODO: enable arrow keys to navigate options from list + +const DropdownMenu = forwardRef( + ({ children, 'aria-label': ariaLabel, align, className, classes }, ref) => { + // Note: need to use a ref instead of a state to avoid stale closures. + const focusedRef = useRef(false); + + const [isOpen, setIsOpen] = useState(false); + + const close = useCallback(() => setIsOpen(false), [setIsOpen]); + + const anchorRef = useModalClose(close, ref); + + const toggle = useCallback(() => { + setIsOpen((prevState) => !prevState); + }, [setIsOpen]); + + const handleFocus = () => { + focusedRef.current = true; + }; + + const handleBlur = () => { + focusedRef.current = false; + setTimeout(() => { + if (!focusedRef.current) { + close(); + } + }, 200); + }; + + return ( +
+ + {isOpen && ( + + {children} + + )} +
+ ); + } +); + +DropdownMenu.propTypes = { + children: PropTypes.node, + 'aria-label': PropTypes.string.isRequired, + align: PropTypes.oneOf(['left', 'right']), + className: PropTypes.string, + classes: PropTypes.shape({ + button: PropTypes.string, + list: PropTypes.string + }) +}; + +DropdownMenu.defaultProps = { + children: null, + align: 'right', + className: '', + classes: {} +}; + +export default DropdownMenu; diff --git a/client/components/Dropdown/MenuItem.jsx b/client/components/Dropdown/MenuItem.jsx new file mode 100644 index 0000000000..8b6f6d7247 --- /dev/null +++ b/client/components/Dropdown/MenuItem.jsx @@ -0,0 +1,35 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import ButtonOrLink from '../../common/ButtonOrLink'; + +// TODO: combine with NavMenuItem + +function MenuItem({ hideIf, ...rest }) { + if (hideIf) { + return null; + } + + return ( +
  • + +
  • + ); +} + +MenuItem.propTypes = { + ...ButtonOrLink.propTypes, + onClick: PropTypes.func, + value: PropTypes.string, + /** + * Provides a way to deal with optional items. + */ + hideIf: PropTypes.bool +}; + +MenuItem.defaultProps = { + onClick: null, + value: null, + hideIf: false +}; + +export default MenuItem; diff --git a/client/components/Dropdown/TableDropdown.jsx b/client/components/Dropdown/TableDropdown.jsx new file mode 100644 index 0000000000..da7b6d7342 --- /dev/null +++ b/client/components/Dropdown/TableDropdown.jsx @@ -0,0 +1,20 @@ +import styled from 'styled-components'; +import { prop, remSize } from '../../theme'; +import DropdownMenu from './DropdownMenu'; + +const TableDropdown = styled(DropdownMenu).attrs({ align: 'right' })` + & > button { + width: ${remSize(25)}; + height: ${remSize(25)}; + & polygon, + & path { + fill: ${prop('inactiveTextColor')}; + } + } + & ul { + top: 63%; + right: calc(100% - 26px); + } +`; + +export default TableDropdown; diff --git a/client/modules/IDE/components/AssetList.jsx b/client/modules/IDE/components/AssetList.jsx index 559f60c580..7da5c0e865 100644 --- a/client/modules/IDE/components/AssetList.jsx +++ b/client/modules/IDE/components/AssetList.jsx @@ -1,129 +1,68 @@ import PropTypes from 'prop-types'; import React from 'react'; -import { connect } from 'react-redux'; +import { connect, useDispatch } from 'react-redux'; import { bindActionCreators } from 'redux'; import { Link } from 'react-router-dom'; import { Helmet } from 'react-helmet'; import prettyBytes from 'pretty-bytes'; -import { withTranslation } from 'react-i18next'; +import { useTranslation, withTranslation } from 'react-i18next'; +import MenuItem from '../../../components/Dropdown/MenuItem'; +import TableDropdown from '../../../components/Dropdown/TableDropdown'; import Loader from '../../App/components/loader'; +import { deleteAssetRequest } from '../actions/assets'; import * as AssetActions from '../actions/assets'; -import DownFilledTriangleIcon from '../../../images/down-filled-triangle.svg'; -class AssetListRowBase extends React.Component { - constructor(props) { - super(props); - this.state = { - isFocused: false, - optionsOpen: false - }; - } - - onFocusComponent = () => { - this.setState({ isFocused: true }); - }; - - onBlurComponent = () => { - this.setState({ isFocused: false }); - setTimeout(() => { - if (!this.state.isFocused) { - this.closeOptions(); - } - }, 200); - }; - - openOptions = () => { - this.setState({ - optionsOpen: true - }); - }; +const AssetMenu = ({ item: asset }) => { + const { t } = useTranslation(); - closeOptions = () => { - this.setState({ - optionsOpen: false - }); - }; + const dispatch = useDispatch(); - toggleOptions = () => { - if (this.state.optionsOpen) { - this.closeOptions(); - } else { - this.openOptions(); + const handleAssetDelete = () => { + const { key, name } = asset; + if (window.confirm(t('Common.DeleteConfirmation', { name }))) { + dispatch(deleteAssetRequest(key)); } }; - handleDropdownOpen = () => { - this.closeOptions(); - this.openOptions(); - }; + return ( + + {t('AssetList.Delete')} + + {t('AssetList.OpenNewTab')} + + + ); +}; - handleAssetDelete = () => { - const { key, name } = this.props.asset; - this.closeOptions(); - if (window.confirm(this.props.t('Common.DeleteConfirmation', { name }))) { - this.props.deleteAssetRequest(key); - } - }; +AssetMenu.propTypes = { + item: PropTypes.shape({ + key: PropTypes.string.isRequired, + url: PropTypes.string.isRequired, + name: PropTypes.string.isRequired + }).isRequired +}; - render() { - const { asset, username, t } = this.props; - const { optionsOpen } = this.state; - return ( - - - - {asset.name} - - - {prettyBytes(asset.size)} - - {asset.sketchId && ( - - {asset.sketchName} - - )} - - - - {optionsOpen && ( - - )} - - - ); - } -} +const AssetListRowBase = ({ asset, username }) => ( + + + + {asset.name} + + + {prettyBytes(asset.size)} + + {asset.sketchId && ( + + {asset.sketchName} + + )} + + + + + +); AssetListRowBase.propTypes = { asset: PropTypes.shape({ @@ -134,9 +73,7 @@ AssetListRowBase.propTypes = { name: PropTypes.string.isRequired, size: PropTypes.number.isRequired }).isRequired, - deleteAssetRequest: PropTypes.func.isRequired, - username: PropTypes.string.isRequired, - t: PropTypes.func.isRequired + username: PropTypes.string.isRequired }; function mapStateToPropsAssetListRow(state) { diff --git a/client/modules/IDE/components/CollectionList/CollectionListRow.jsx b/client/modules/IDE/components/CollectionList/CollectionListRow.jsx index ed109141d7..ffb2d3c3d2 100644 --- a/client/modules/IDE/components/CollectionList/CollectionListRow.jsx +++ b/client/modules/IDE/components/CollectionList/CollectionListRow.jsx @@ -4,84 +4,35 @@ import { connect } from 'react-redux'; import { Link } from 'react-router-dom'; import { bindActionCreators } from 'redux'; import { withTranslation } from 'react-i18next'; +import MenuItem from '../../../../components/Dropdown/MenuItem'; +import TableDropdown from '../../../../components/Dropdown/TableDropdown'; import * as ProjectActions from '../../actions/project'; import * as CollectionsActions from '../../actions/collections'; import * as IdeActions from '../../actions/ide'; import * as ToastActions from '../../actions/toast'; import dates from '../../../../utils/formatDate'; -import DownFilledTriangleIcon from '../../../../images/down-filled-triangle.svg'; - class CollectionListRowBase extends React.Component { - static projectInCollection(project, collection) { - return ( - collection.items.find((item) => item.project.id === project.id) != null - ); - } - constructor(props) { super(props); this.state = { - optionsOpen: false, - isFocused: false, renameOpen: false, renameValue: '' }; this.renameInput = React.createRef(); } - onFocusComponent = () => { - this.setState({ isFocused: true }); - }; - - onBlurComponent = () => { - this.setState({ isFocused: false }); - setTimeout(() => { - if (!this.state.isFocused) { - this.closeAll(); - } - }, 200); - }; - - openOptions = () => { - this.setState({ - optionsOpen: true - }); - }; - - closeOptions = () => { - this.setState({ - optionsOpen: false - }); - }; - - toggleOptions = () => { - if (this.state.optionsOpen) { - this.closeOptions(); - } else { - this.openOptions(); - } - }; - closeAll = () => { this.setState({ - optionsOpen: false, renameOpen: false }); }; handleAddSketches = () => { - this.closeAll(); this.props.onAddSketches(); }; - handleDropdownOpen = () => { - this.closeAll(); - this.openOptions(); - }; - handleCollectionDelete = () => { - this.closeAll(); if ( window.confirm( this.props.t('Common.DeleteConfirmation', { @@ -94,7 +45,6 @@ class CollectionListRowBase extends React.Component { }; handleRenameOpen = () => { - this.closeAll(); this.setState( { renameOpen: true, @@ -132,61 +82,24 @@ class CollectionListRowBase extends React.Component { }; renderActions = () => { - const { optionsOpen } = this.state; const userIsOwner = this.props.user.username === this.props.username; return ( - - - {optionsOpen && ( -
      -
    • - -
    • - {userIsOwner && ( -
    • - -
    • - )} - {userIsOwner && ( -
    • - -
    • - )} -
    + + > + + {this.props.t('CollectionListRow.AddSketch')} + + + {this.props.t('CollectionListRow.Delete')} + + + {this.props.t('CollectionListRow.Rename')} + + ); }; diff --git a/client/modules/IDE/components/SketchList.jsx b/client/modules/IDE/components/SketchList.jsx index af03453e0d..108e00755e 100644 --- a/client/modules/IDE/components/SketchList.jsx +++ b/client/modules/IDE/components/SketchList.jsx @@ -7,6 +7,8 @@ import { Link } from 'react-router-dom'; import { bindActionCreators } from 'redux'; import classNames from 'classnames'; import slugify from 'slugify'; +import MenuItem from '../../../components/Dropdown/MenuItem'; +import TableDropdown from '../../../components/Dropdown/TableDropdown'; import dates from '../../../utils/formatDate'; import * as ProjectActions from '../actions/project'; import * as ProjectsActions from '../actions/projects'; @@ -22,7 +24,6 @@ import getConfig from '../../../utils/getConfig'; import ArrowUpIcon from '../../../images/sort-arrow-up.svg'; import ArrowDownIcon from '../../../images/sort-arrow-down.svg'; -import DownFilledTriangleIcon from '../../../images/down-filled-triangle.svg'; const ROOT_URL = getConfig('API_URL'); @@ -33,47 +34,12 @@ class SketchListRowBase extends React.Component { constructor(props) { super(props); this.state = { - optionsOpen: false, renameOpen: false, - renameValue: props.sketch.name, - isFocused: false + renameValue: props.sketch.name }; this.renameInput = React.createRef(); } - onFocusComponent = () => { - this.setState({ isFocused: true }); - }; - - onBlurComponent = () => { - this.setState({ isFocused: false }); - setTimeout(() => { - if (!this.state.isFocused) { - this.closeAll(); - } - }, 200); - }; - - openOptions = () => { - this.setState({ - optionsOpen: true - }); - }; - - closeOptions = () => { - this.setState({ - optionsOpen: false - }); - }; - - toggleOptions = () => { - if (this.state.optionsOpen) { - this.closeOptions(); - } else { - this.openOptions(); - } - }; - openRename = () => { this.setState( { @@ -90,13 +56,6 @@ class SketchListRowBase extends React.Component { }); }; - closeAll = () => { - this.setState({ - renameOpen: false, - optionsOpen: false - }); - }; - handleRenameChange = (e) => { this.setState({ renameValue: e.target.value @@ -106,13 +65,13 @@ class SketchListRowBase extends React.Component { handleRenameEnter = (e) => { if (e.key === 'Enter') { this.updateName(); - this.closeAll(); + this.closeRename(); } }; handleRenameBlur = () => { this.updateName(); - this.closeAll(); + this.closeRename(); }; updateName = () => { @@ -125,23 +84,6 @@ class SketchListRowBase extends React.Component { } }; - resetSketchName = () => { - this.setState({ - renameValue: this.props.sketch.name, - renameOpen: false - }); - }; - - handleDropdownOpen = () => { - this.closeAll(); - this.openOptions(); - }; - - handleRenameOpen = () => { - this.closeAll(); - this.openRename(); - }; - handleSketchDownload = () => { const { sketch } = this.props; const downloadLink = document.createElement('a'); @@ -153,12 +95,10 @@ class SketchListRowBase extends React.Component { }; handleSketchDuplicate = () => { - this.closeAll(); this.props.cloneProject(this.props.sketch); }; handleSketchShare = () => { - this.closeAll(); this.props.showShareModal( this.props.sketch.id, this.props.sketch.name, @@ -167,7 +107,6 @@ class SketchListRowBase extends React.Component { }; handleSketchDelete = () => { - this.closeAll(); if ( window.confirm( this.props.t('Common.DeleteConfirmation', { @@ -179,102 +118,42 @@ class SketchListRowBase extends React.Component { } }; - renderViewButton = (sketchURL) => ( - - {this.props.t('SketchList.View')} - - ); - renderDropdown = () => { - const { optionsOpen } = this.state; const userIsOwner = this.props.user.username === this.props.username; return ( - - {optionsOpen && ( -
      - {userIsOwner && ( -
    • - -
    • - )} -
    • - -
    • - {this.props.user.authenticated && ( -
    • - -
    • - )} - {this.props.user.authenticated && ( -
    • - -
    • - )} - {/*
    • - -
    • */} - {userIsOwner && ( -
    • - -
    • - )} -
    - )} + + + {this.props.t('SketchList.DropdownRename')} + + + {this.props.t('SketchList.DropdownDownload')} + + + {this.props.t('SketchList.DropdownDuplicate')} + + { + this.props.onAddToCollection(); + }} + > + {this.props.t('SketchList.DropdownAddToCollection')} + + + {/* + + Share + + */} + + {this.props.t('SketchList.DropdownDelete')} + + ); }; diff --git a/client/modules/IDE/components/__snapshots__/SketchList.unit.test.jsx.snap b/client/modules/IDE/components/__snapshots__/SketchList.unit.test.jsx.snap index bd7475ebf9..63a4542d36 100644 --- a/client/modules/IDE/components/__snapshots__/SketchList.unit.test.jsx.snap +++ b/client/modules/IDE/components/__snapshots__/SketchList.unit.test.jsx.snap @@ -85,15 +85,19 @@ exports[` snapshot testing 1`] = ` - + + snapshot testing 1`] = ` - + + diff --git a/client/styles/components/_asset-list.scss b/client/styles/components/_asset-list.scss index 6f7c035993..7d8f6065e8 100644 --- a/client/styles/components/_asset-list.scss +++ b/client/styles/components/_asset-list.scss @@ -76,24 +76,3 @@ background-color: getThemifyVariable('background-color'); } } - -.asset-table__dropdown-button { - width:#{25 / $base-font-size}rem; - height:#{25 / $base-font-size}rem; - - @include themify() { - & polygon, & path { - fill: getThemifyVariable('inactive-text-color'); - } - } -} - -.asset-table__action-dialogue { - @extend %dropdown-open-right; - top: 63%; - right: calc(100% - 26px); -} - -.asset-table__action-option { - font-size: #{12 / $base-font-size}rem; -} diff --git a/client/styles/components/_sketch-list.scss b/client/styles/components/_sketch-list.scss index b03381b091..58e17f109b 100644 --- a/client/styles/components/_sketch-list.scss +++ b/client/styles/components/_sketch-list.scss @@ -100,17 +100,6 @@ font-weight: normal; } - -.sketch-list__dropdown-button { - width:#{25 / $base-font-size}rem; - height:#{25 / $base-font-size}rem; - @include themify() { - & polygon, & path { - fill: getThemifyVariable('inactive-text-color'); - } - } -} - .sketches-table__name { display: flex; align-items: center; @@ -120,12 +109,6 @@ width: #{35 / $base-font-size}rem; } -.sketch-list__action-dialogue { - @extend %dropdown-open-right; - top: 63%; - right: calc(100% - 26px); -} - .sketches-table__empty { text-align: center; font-size: #{16 / $base-font-size}rem; From 3980be0037015dd287452fc7ac3e61a24aaed1d1 Mon Sep 17 00:00:00 2001 From: Linda Paiste Date: Wed, 16 Aug 2023 12:30:27 -0500 Subject: [PATCH 2/9] add setTimeout to close --- client/components/Dropdown/DropdownMenu.jsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/client/components/Dropdown/DropdownMenu.jsx b/client/components/Dropdown/DropdownMenu.jsx index ce7e03837e..0e18207f26 100644 --- a/client/components/Dropdown/DropdownMenu.jsx +++ b/client/components/Dropdown/DropdownMenu.jsx @@ -50,7 +50,9 @@ const DropdownMenu = forwardRef( { + setTimeout(close, 0); + }} onBlur={handleBlur} onFocus={handleFocus} > From c7bf38ee56acf93b405babd68859a3eab224ffc6 Mon Sep 17 00:00:00 2001 From: Linda Paiste Date: Mon, 14 Aug 2023 11:59:59 -0500 Subject: [PATCH 3/9] Common dropdown component for table actions. --- client/components/Dropdown.jsx | 3 +- client/components/Dropdown/DropdownMenu.jsx | 83 ++++++++ client/components/Dropdown/MenuItem.jsx | 35 ++++ client/components/Dropdown/TableDropdown.jsx | 20 ++ client/modules/IDE/components/AssetList.jsx | 161 +++++--------- .../CollectionList/CollectionListRow.jsx | 124 ++--------- client/modules/IDE/components/SketchList.jsx | 198 ++++-------------- .../SketchList.unit.test.jsx.snap | 40 ++-- client/styles/components/_asset-list.scss | 21 -- client/styles/components/_sketch-list.scss | 17 -- 10 files changed, 265 insertions(+), 437 deletions(-) create mode 100644 client/components/Dropdown/DropdownMenu.jsx create mode 100644 client/components/Dropdown/MenuItem.jsx create mode 100644 client/components/Dropdown/TableDropdown.jsx diff --git a/client/components/Dropdown.jsx b/client/components/Dropdown.jsx index 9a91d54cd2..369bef51b0 100644 --- a/client/components/Dropdown.jsx +++ b/client/components/Dropdown.jsx @@ -4,7 +4,7 @@ import styled from 'styled-components'; import { remSize, prop } from '../theme'; import IconButton from './mobile/IconButton'; -const DropdownWrapper = styled.ul` +export const DropdownWrapper = styled.ul` background-color: ${prop('Modal.background')}; border: 1px solid ${prop('Modal.border')}; box-shadow: 0 0 18px 0 ${prop('shadowColor')}; @@ -52,6 +52,7 @@ const DropdownWrapper = styled.ul` & button span, & a { padding: ${remSize(8)} ${remSize(16)}; + font-size: ${remSize(12)}; } * { diff --git a/client/components/Dropdown/DropdownMenu.jsx b/client/components/Dropdown/DropdownMenu.jsx new file mode 100644 index 0000000000..ce7e03837e --- /dev/null +++ b/client/components/Dropdown/DropdownMenu.jsx @@ -0,0 +1,83 @@ +import PropTypes from 'prop-types'; +import React, { forwardRef, useCallback, useRef, useState } from 'react'; +import useModalClose from '../../common/useModalClose'; +import DownArrowIcon from '../../images/down-filled-triangle.svg'; +import { DropdownWrapper } from '../Dropdown'; + +// TODO: enable arrow keys to navigate options from list + +const DropdownMenu = forwardRef( + ({ children, 'aria-label': ariaLabel, align, className, classes }, ref) => { + // Note: need to use a ref instead of a state to avoid stale closures. + const focusedRef = useRef(false); + + const [isOpen, setIsOpen] = useState(false); + + const close = useCallback(() => setIsOpen(false), [setIsOpen]); + + const anchorRef = useModalClose(close, ref); + + const toggle = useCallback(() => { + setIsOpen((prevState) => !prevState); + }, [setIsOpen]); + + const handleFocus = () => { + focusedRef.current = true; + }; + + const handleBlur = () => { + focusedRef.current = false; + setTimeout(() => { + if (!focusedRef.current) { + close(); + } + }, 200); + }; + + return ( +
    + + {isOpen && ( + + {children} + + )} +
    + ); + } +); + +DropdownMenu.propTypes = { + children: PropTypes.node, + 'aria-label': PropTypes.string.isRequired, + align: PropTypes.oneOf(['left', 'right']), + className: PropTypes.string, + classes: PropTypes.shape({ + button: PropTypes.string, + list: PropTypes.string + }) +}; + +DropdownMenu.defaultProps = { + children: null, + align: 'right', + className: '', + classes: {} +}; + +export default DropdownMenu; diff --git a/client/components/Dropdown/MenuItem.jsx b/client/components/Dropdown/MenuItem.jsx new file mode 100644 index 0000000000..8b6f6d7247 --- /dev/null +++ b/client/components/Dropdown/MenuItem.jsx @@ -0,0 +1,35 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import ButtonOrLink from '../../common/ButtonOrLink'; + +// TODO: combine with NavMenuItem + +function MenuItem({ hideIf, ...rest }) { + if (hideIf) { + return null; + } + + return ( +
  • + +
  • + ); +} + +MenuItem.propTypes = { + ...ButtonOrLink.propTypes, + onClick: PropTypes.func, + value: PropTypes.string, + /** + * Provides a way to deal with optional items. + */ + hideIf: PropTypes.bool +}; + +MenuItem.defaultProps = { + onClick: null, + value: null, + hideIf: false +}; + +export default MenuItem; diff --git a/client/components/Dropdown/TableDropdown.jsx b/client/components/Dropdown/TableDropdown.jsx new file mode 100644 index 0000000000..da7b6d7342 --- /dev/null +++ b/client/components/Dropdown/TableDropdown.jsx @@ -0,0 +1,20 @@ +import styled from 'styled-components'; +import { prop, remSize } from '../../theme'; +import DropdownMenu from './DropdownMenu'; + +const TableDropdown = styled(DropdownMenu).attrs({ align: 'right' })` + & > button { + width: ${remSize(25)}; + height: ${remSize(25)}; + & polygon, + & path { + fill: ${prop('inactiveTextColor')}; + } + } + & ul { + top: 63%; + right: calc(100% - 26px); + } +`; + +export default TableDropdown; diff --git a/client/modules/IDE/components/AssetList.jsx b/client/modules/IDE/components/AssetList.jsx index 559f60c580..7da5c0e865 100644 --- a/client/modules/IDE/components/AssetList.jsx +++ b/client/modules/IDE/components/AssetList.jsx @@ -1,129 +1,68 @@ import PropTypes from 'prop-types'; import React from 'react'; -import { connect } from 'react-redux'; +import { connect, useDispatch } from 'react-redux'; import { bindActionCreators } from 'redux'; import { Link } from 'react-router-dom'; import { Helmet } from 'react-helmet'; import prettyBytes from 'pretty-bytes'; -import { withTranslation } from 'react-i18next'; +import { useTranslation, withTranslation } from 'react-i18next'; +import MenuItem from '../../../components/Dropdown/MenuItem'; +import TableDropdown from '../../../components/Dropdown/TableDropdown'; import Loader from '../../App/components/loader'; +import { deleteAssetRequest } from '../actions/assets'; import * as AssetActions from '../actions/assets'; -import DownFilledTriangleIcon from '../../../images/down-filled-triangle.svg'; -class AssetListRowBase extends React.Component { - constructor(props) { - super(props); - this.state = { - isFocused: false, - optionsOpen: false - }; - } - - onFocusComponent = () => { - this.setState({ isFocused: true }); - }; - - onBlurComponent = () => { - this.setState({ isFocused: false }); - setTimeout(() => { - if (!this.state.isFocused) { - this.closeOptions(); - } - }, 200); - }; - - openOptions = () => { - this.setState({ - optionsOpen: true - }); - }; +const AssetMenu = ({ item: asset }) => { + const { t } = useTranslation(); - closeOptions = () => { - this.setState({ - optionsOpen: false - }); - }; + const dispatch = useDispatch(); - toggleOptions = () => { - if (this.state.optionsOpen) { - this.closeOptions(); - } else { - this.openOptions(); + const handleAssetDelete = () => { + const { key, name } = asset; + if (window.confirm(t('Common.DeleteConfirmation', { name }))) { + dispatch(deleteAssetRequest(key)); } }; - handleDropdownOpen = () => { - this.closeOptions(); - this.openOptions(); - }; + return ( + + {t('AssetList.Delete')} + + {t('AssetList.OpenNewTab')} + + + ); +}; - handleAssetDelete = () => { - const { key, name } = this.props.asset; - this.closeOptions(); - if (window.confirm(this.props.t('Common.DeleteConfirmation', { name }))) { - this.props.deleteAssetRequest(key); - } - }; +AssetMenu.propTypes = { + item: PropTypes.shape({ + key: PropTypes.string.isRequired, + url: PropTypes.string.isRequired, + name: PropTypes.string.isRequired + }).isRequired +}; - render() { - const { asset, username, t } = this.props; - const { optionsOpen } = this.state; - return ( - - - - {asset.name} - - - {prettyBytes(asset.size)} - - {asset.sketchId && ( - - {asset.sketchName} - - )} - - - - {optionsOpen && ( -
      -
    • - -
    • -
    • - - {t('AssetList.OpenNewTab')} - -
    • -
    - )} - - - ); - } -} +const AssetListRowBase = ({ asset, username }) => ( + + + + {asset.name} + + + {prettyBytes(asset.size)} + + {asset.sketchId && ( + + {asset.sketchName} + + )} + + + + + +); AssetListRowBase.propTypes = { asset: PropTypes.shape({ @@ -134,9 +73,7 @@ AssetListRowBase.propTypes = { name: PropTypes.string.isRequired, size: PropTypes.number.isRequired }).isRequired, - deleteAssetRequest: PropTypes.func.isRequired, - username: PropTypes.string.isRequired, - t: PropTypes.func.isRequired + username: PropTypes.string.isRequired }; function mapStateToPropsAssetListRow(state) { diff --git a/client/modules/IDE/components/CollectionList/CollectionListRow.jsx b/client/modules/IDE/components/CollectionList/CollectionListRow.jsx index 16421f7a6b..1d52163230 100644 --- a/client/modules/IDE/components/CollectionList/CollectionListRow.jsx +++ b/client/modules/IDE/components/CollectionList/CollectionListRow.jsx @@ -4,88 +4,38 @@ import { connect } from 'react-redux'; import { Link } from 'react-router-dom'; import { bindActionCreators } from 'redux'; import { withTranslation } from 'react-i18next'; +import MenuItem from '../../../../components/Dropdown/MenuItem'; +import TableDropdown from '../../../../components/Dropdown/TableDropdown'; import * as ProjectActions from '../../actions/project'; import * as CollectionsActions from '../../actions/collections'; import * as IdeActions from '../../actions/ide'; import * as ToastActions from '../../actions/toast'; import dates from '../../../../utils/formatDate'; -import DownFilledTriangleIcon from '../../../../images/down-filled-triangle.svg'; -import MoreIconSvg from '../../../../images/more.svg'; - const formatDateCell = (date, mobile = false) => dates.format(date, { showTime: !mobile }); class CollectionListRowBase extends React.Component { - static projectInCollection(project, collection) { - return ( - collection.items.find((item) => item.project.id === project.id) != null - ); - } - constructor(props) { super(props); this.state = { - optionsOpen: false, - isFocused: false, renameOpen: false, renameValue: '' }; this.renameInput = React.createRef(); } - onFocusComponent = () => { - this.setState({ isFocused: true }); - }; - - onBlurComponent = () => { - this.setState({ isFocused: false }); - setTimeout(() => { - if (!this.state.isFocused) { - this.closeAll(); - } - }, 200); - }; - - openOptions = () => { - this.setState({ - optionsOpen: true - }); - }; - - closeOptions = () => { - this.setState({ - optionsOpen: false - }); - }; - - toggleOptions = () => { - if (this.state.optionsOpen) { - this.closeOptions(); - } else { - this.openOptions(); - } - }; - closeAll = () => { this.setState({ - optionsOpen: false, renameOpen: false }); }; handleAddSketches = () => { - this.closeAll(); this.props.onAddSketches(); }; - handleDropdownOpen = () => { - this.closeAll(); - this.openOptions(); - }; - handleCollectionDelete = () => { - this.closeAll(); if ( window.confirm( this.props.t('Common.DeleteConfirmation', { @@ -98,7 +48,6 @@ class CollectionListRowBase extends React.Component { }; handleRenameOpen = () => { - this.closeAll(); this.setState( { renameOpen: true, @@ -136,65 +85,24 @@ class CollectionListRowBase extends React.Component { }; renderActions = () => { - const { optionsOpen } = this.state; const userIsOwner = this.props.user.username === this.props.username; return ( - - - {optionsOpen && ( -
      -
    • - -
    • - {userIsOwner && ( -
    • - -
    • - )} - {userIsOwner && ( -
    • - -
    • - )} -
    + + > + + {this.props.t('CollectionListRow.AddSketch')} + + + {this.props.t('CollectionListRow.Delete')} + + + {this.props.t('CollectionListRow.Rename')} + + ); }; diff --git a/client/modules/IDE/components/SketchList.jsx b/client/modules/IDE/components/SketchList.jsx index bce30da1dc..85d3030d61 100644 --- a/client/modules/IDE/components/SketchList.jsx +++ b/client/modules/IDE/components/SketchList.jsx @@ -7,6 +7,8 @@ import { Link } from 'react-router-dom'; import { bindActionCreators } from 'redux'; import classNames from 'classnames'; import slugify from 'slugify'; +import MenuItem from '../../../components/Dropdown/MenuItem'; +import TableDropdown from '../../../components/Dropdown/TableDropdown'; import dates from '../../../utils/formatDate'; import * as ProjectActions from '../actions/project'; import * as ProjectsActions from '../actions/projects'; @@ -22,8 +24,6 @@ import getConfig from '../../../utils/getConfig'; import ArrowUpIcon from '../../../images/sort-arrow-up.svg'; import ArrowDownIcon from '../../../images/sort-arrow-down.svg'; -import DownFilledTriangleIcon from '../../../images/down-filled-triangle.svg'; -import MoreIconSvg from '../../../images/more.svg'; const ROOT_URL = getConfig('API_URL'); @@ -34,47 +34,12 @@ class SketchListRowBase extends React.Component { constructor(props) { super(props); this.state = { - optionsOpen: false, renameOpen: false, - renameValue: props.sketch.name, - isFocused: false + renameValue: props.sketch.name }; this.renameInput = React.createRef(); } - onFocusComponent = () => { - this.setState({ isFocused: true }); - }; - - onBlurComponent = () => { - this.setState({ isFocused: false }); - setTimeout(() => { - if (!this.state.isFocused) { - this.closeAll(); - } - }, 200); - }; - - openOptions = () => { - this.setState({ - optionsOpen: true - }); - }; - - closeOptions = () => { - this.setState({ - optionsOpen: false - }); - }; - - toggleOptions = () => { - if (this.state.optionsOpen) { - this.closeOptions(); - } else { - this.openOptions(); - } - }; - openRename = () => { this.setState( { @@ -91,13 +56,6 @@ class SketchListRowBase extends React.Component { }); }; - closeAll = () => { - this.setState({ - renameOpen: false, - optionsOpen: false - }); - }; - handleRenameChange = (e) => { this.setState({ renameValue: e.target.value @@ -107,13 +65,13 @@ class SketchListRowBase extends React.Component { handleRenameEnter = (e) => { if (e.key === 'Enter') { this.updateName(); - this.closeAll(); + this.closeRename(); } }; handleRenameBlur = () => { this.updateName(); - this.closeAll(); + this.closeRename(); }; updateName = () => { @@ -126,23 +84,6 @@ class SketchListRowBase extends React.Component { } }; - resetSketchName = () => { - this.setState({ - renameValue: this.props.sketch.name, - renameOpen: false - }); - }; - - handleDropdownOpen = () => { - this.closeAll(); - this.openOptions(); - }; - - handleRenameOpen = () => { - this.closeAll(); - this.openRename(); - }; - handleSketchDownload = () => { const { sketch } = this.props; const downloadLink = document.createElement('a'); @@ -154,12 +95,10 @@ class SketchListRowBase extends React.Component { }; handleSketchDuplicate = () => { - this.closeAll(); this.props.cloneProject(this.props.sketch); }; handleSketchShare = () => { - this.closeAll(); this.props.showShareModal( this.props.sketch.id, this.props.sketch.name, @@ -168,7 +107,6 @@ class SketchListRowBase extends React.Component { }; handleSketchDelete = () => { - this.closeAll(); if ( window.confirm( this.props.t('Common.DeleteConfirmation', { @@ -180,106 +118,42 @@ class SketchListRowBase extends React.Component { } }; - renderViewButton = (sketchURL) => ( - - {this.props.t('SketchList.View')} - - ); - renderDropdown = () => { - const { optionsOpen } = this.state; const userIsOwner = this.props.user.username === this.props.username; return ( - - {optionsOpen && ( -
      - {userIsOwner && ( -
    • - -
    • - )} -
    • - -
    • - {this.props.user.authenticated && ( -
    • - -
    • - )} - {this.props.user.authenticated && ( -
    • - -
    • - )} - {/*
    • - -
    • */} - {userIsOwner && ( -
    • - -
    • - )} -
    - )} + + + {this.props.t('SketchList.DropdownRename')} + + + {this.props.t('SketchList.DropdownDownload')} + + + {this.props.t('SketchList.DropdownDuplicate')} + + { + this.props.onAddToCollection(); + }} + > + {this.props.t('SketchList.DropdownAddToCollection')} + + + {/* + + Share + + */} + + {this.props.t('SketchList.DropdownDelete')} + + ); }; diff --git a/client/modules/IDE/components/__snapshots__/SketchList.unit.test.jsx.snap b/client/modules/IDE/components/__snapshots__/SketchList.unit.test.jsx.snap index bd7475ebf9..63a4542d36 100644 --- a/client/modules/IDE/components/__snapshots__/SketchList.unit.test.jsx.snap +++ b/client/modules/IDE/components/__snapshots__/SketchList.unit.test.jsx.snap @@ -85,15 +85,19 @@ exports[` snapshot testing 1`] = ` - + + snapshot testing 1`] = ` - + + diff --git a/client/styles/components/_asset-list.scss b/client/styles/components/_asset-list.scss index 6f7c035993..7d8f6065e8 100644 --- a/client/styles/components/_asset-list.scss +++ b/client/styles/components/_asset-list.scss @@ -76,24 +76,3 @@ background-color: getThemifyVariable('background-color'); } } - -.asset-table__dropdown-button { - width:#{25 / $base-font-size}rem; - height:#{25 / $base-font-size}rem; - - @include themify() { - & polygon, & path { - fill: getThemifyVariable('inactive-text-color'); - } - } -} - -.asset-table__action-dialogue { - @extend %dropdown-open-right; - top: 63%; - right: calc(100% - 26px); -} - -.asset-table__action-option { - font-size: #{12 / $base-font-size}rem; -} diff --git a/client/styles/components/_sketch-list.scss b/client/styles/components/_sketch-list.scss index 69d41bdca4..315b10712f 100644 --- a/client/styles/components/_sketch-list.scss +++ b/client/styles/components/_sketch-list.scss @@ -169,17 +169,6 @@ font-weight: normal; } -.sketch-list__dropdown-button { - width: #{25 / $base-font-size}rem; - height: #{25 / $base-font-size}rem; - @include themify() { - & polygon, - & path { - fill: getThemifyVariable("inactive-text-color"); - } - } -} - .sketches-table__name { display: flex; align-items: center; @@ -189,12 +178,6 @@ width: #{35 / $base-font-size}rem; } -.sketch-list__action-dialogue { - @extend %dropdown-open-right; - top: 63%; - right: calc(100% - 26px); -} - .sketches-table__empty { text-align: center; font-size: #{16 / $base-font-size}rem; From 4ffbb42af973c902638a0bc50eb756fe6489eadf Mon Sep 17 00:00:00 2001 From: Linda Paiste Date: Wed, 16 Aug 2023 12:30:27 -0500 Subject: [PATCH 4/9] add setTimeout to close --- client/components/Dropdown/DropdownMenu.jsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/client/components/Dropdown/DropdownMenu.jsx b/client/components/Dropdown/DropdownMenu.jsx index ce7e03837e..0e18207f26 100644 --- a/client/components/Dropdown/DropdownMenu.jsx +++ b/client/components/Dropdown/DropdownMenu.jsx @@ -50,7 +50,9 @@ const DropdownMenu = forwardRef( { + setTimeout(close, 0); + }} onBlur={handleBlur} onFocus={handleFocus} > From c9cca4a0182647fd27b58c2ba0b1cc8180043dc7 Mon Sep 17 00:00:00 2001 From: Linda Paiste Date: Sun, 17 Sep 2023 16:58:59 -0500 Subject: [PATCH 5/9] Apply new responsive styles --- client/components/Dropdown/DropdownMenu.jsx | 19 +++++++++--- client/components/Dropdown/TableDropdown.jsx | 30 ++++++++++++++++++- .../SketchList.unit.test.jsx.snap | 27 +++++++++++++++-- client/styles/components/_sketch-list.scss | 7 +---- 4 files changed, 69 insertions(+), 14 deletions(-) diff --git a/client/components/Dropdown/DropdownMenu.jsx b/client/components/Dropdown/DropdownMenu.jsx index 0e18207f26..da41b30101 100644 --- a/client/components/Dropdown/DropdownMenu.jsx +++ b/client/components/Dropdown/DropdownMenu.jsx @@ -7,7 +7,10 @@ import { DropdownWrapper } from '../Dropdown'; // TODO: enable arrow keys to navigate options from list const DropdownMenu = forwardRef( - ({ children, 'aria-label': ariaLabel, align, className, classes }, ref) => { + ( + { children, anchor, 'aria-label': ariaLabel, align, className, classes }, + ref + ) => { // Note: need to use a ref instead of a state to avoid stale closures. const focusedRef = useRef(false); @@ -44,7 +47,7 @@ const DropdownMenu = forwardRef( onBlur={handleBlur} onFocus={handleFocus} > -