From 88a12c8b09b512d12d11a93e6dbc7735fac84067 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Grabowski?= Date: Thu, 5 Dec 2024 14:14:15 +0100 Subject: [PATCH] added dependency arrays; break popupmenu component to smaller ones --- .../public/js/scripts/helpers/react.helper.js | 2 +- .../draggable-dialog/draggable.dialog.js | 104 ++++++++++-------- .../common/popup-menu/popup.menu.group.js | 38 +++++++ .../common/popup-menu/popup.menu.helper.js | 12 ++ .../common/popup-menu/popup.menu.item.js | 32 ++++++ .../modules/common/popup-menu/popup.menu.js | 98 +++-------------- .../common/popup-menu/popup.menu.search.js | 61 ++++++++++ 7 files changed, 216 insertions(+), 131 deletions(-) create mode 100644 src/bundle/ui-dev/src/modules/common/popup-menu/popup.menu.group.js create mode 100644 src/bundle/ui-dev/src/modules/common/popup-menu/popup.menu.helper.js create mode 100644 src/bundle/ui-dev/src/modules/common/popup-menu/popup.menu.item.js create mode 100644 src/bundle/ui-dev/src/modules/common/popup-menu/popup.menu.search.js diff --git a/src/bundle/Resources/public/js/scripts/helpers/react.helper.js b/src/bundle/Resources/public/js/scripts/helpers/react.helper.js index df9511f5e2..2399378631 100644 --- a/src/bundle/Resources/public/js/scripts/helpers/react.helper.js +++ b/src/bundle/Resources/public/js/scripts/helpers/react.helper.js @@ -1,7 +1,7 @@ import { getRootDOMElement } from './context.helper'; const createDynamicRoot = ({ contextDOMElement = getRootDOMElement(), id } = {}) => { - if (id && contextDOMElement.querySelector(`#${id}`) !== null) { + if (id && window.document.getElementById(id) !== null) { console.warn(`You're creating second root element with ID "${id}". IDs should be unique inside a document.`); } diff --git a/src/bundle/ui-dev/src/modules/common/draggable-dialog/draggable.dialog.js b/src/bundle/ui-dev/src/modules/common/draggable-dialog/draggable.dialog.js index 7aa0b4631c..7dc3583f00 100644 --- a/src/bundle/ui-dev/src/modules/common/draggable-dialog/draggable.dialog.js +++ b/src/bundle/ui-dev/src/modules/common/draggable-dialog/draggable.dialog.js @@ -1,4 +1,4 @@ -import React, { useRef, createContext, useState, useEffect } from 'react'; +import React, { useRef, createContext, useState, useEffect, useCallback } from 'react'; import PropTypes from 'prop-types'; import { getRootDOMElement } from '@ibexa-admin-ui/src/bundle/Resources/public/js/scripts/helpers/context.helper'; @@ -25,43 +25,46 @@ const DraggableDialog = ({ children, referenceElement, positionOffset }) => { left: coords.x, }, }; - const getMousePosition = (event) => ({ x: event.x, y: event.y }); - const setContainerCoords = (event) => { - const mouseCoords = getMousePosition(event); - let x = mouseCoords.x - dragOffsetPosition.current.x; - let y = mouseCoords.y - dragOffsetPosition.current.y; - let newDragOffsetX; - let newDragOffsetY; - - if (x < 0) { - x = 0; - newDragOffsetX = mouseCoords.x; - } else if (x + containerSize.current.width > window.innerWidth) { - x = window.innerWidth - containerSize.current.width; - newDragOffsetX = mouseCoords.x - x; - } - - if (y < 0) { - y = 0; - newDragOffsetY = mouseCoords.y; - } else if (y + containerSize.current.height > window.innerHeight) { - y = window.innerHeight - containerSize.current.height; - newDragOffsetY = mouseCoords.y - y; - } - - if (newDragOffsetX) { - dragOffsetPosition.current.x = newDragOffsetX; - } - - if (newDragOffsetY) { - dragOffsetPosition.current.y = newDragOffsetY; - } - - setCoords({ - x, - y, - }); - }; + const getMousePosition = useCallback((event) => ({ x: event.x, y: event.y }), []); + const setContainerCoords = useCallback( + (event) => { + const mouseCoords = getMousePosition(event); + let x = mouseCoords.x - dragOffsetPosition.current.x; + let y = mouseCoords.y - dragOffsetPosition.current.y; + let newDragOffsetX; + let newDragOffsetY; + + if (x < 0) { + x = 0; + newDragOffsetX = mouseCoords.x; + } else if (x + containerSize.current.width > window.innerWidth) { + x = window.innerWidth - containerSize.current.width; + newDragOffsetX = mouseCoords.x - x; + } + + if (y < 0) { + y = 0; + newDragOffsetY = mouseCoords.y; + } else if (y + containerSize.current.height > window.innerHeight) { + y = window.innerHeight - containerSize.current.height; + newDragOffsetY = mouseCoords.y - y; + } + + if (newDragOffsetX) { + dragOffsetPosition.current.x = newDragOffsetX; + } + + if (newDragOffsetY) { + dragOffsetPosition.current.y = newDragOffsetY; + } + + setCoords({ + x, + y, + }); + }, + [getMousePosition], + ); const startDragging = (event) => { const { x: containerX, y: containerY, width, height } = containerRef.current.getBoundingClientRect(); const mouseCoords = getMousePosition(event.nativeEvent); @@ -80,24 +83,29 @@ const DraggableDialog = ({ children, referenceElement, positionOffset }) => { setIsDragging(true); }; - const stopDragging = () => { + const stopDragging = useCallback(() => { setIsDragging(false); - }; - const handleDragging = (event) => { - setContainerCoords(event); - }; + }, []); + const handleDragging = useCallback( + (event) => { + setContainerCoords(event); + }, + [setContainerCoords], + ); useEffect(() => { - if (isDragging) { - rootDOMElement.addEventListener('mousemove', handleDragging, false); - rootDOMElement.addEventListener('mouseup', stopDragging, false); + if (!isDragging) { + return; } + rootDOMElement.addEventListener('mousemove', handleDragging, false); + rootDOMElement.addEventListener('mouseup', stopDragging, false); + return () => { rootDOMElement.removeEventListener('mousemove', handleDragging); rootDOMElement.removeEventListener('mouseup', stopDragging); }; - }, [isDragging]); + }, [isDragging, rootDOMElement, handleDragging, stopDragging]); useEffect(() => { const { top: referenceTop, left: referenceLeft } = referenceElement.getBoundingClientRect(); @@ -122,7 +130,7 @@ const DraggableDialog = ({ children, referenceElement, positionOffset }) => { x, y, }); - }, [referenceElement]); + }, [referenceElement, positionOffset]); return ( { + const isAnyItemVisible = items.some((item) => showItem(item, filterText)); + + if (!isAnyItemVisible) { + return null; + } + + return ( +
+ {items.map((item) => ( + + ))} +
+ ); +}; + +PopupMenuGroup.propTypes = { + items: PropTypes.arrayOf( + PropTypes.shape({ + value: PropTypes.string.isRequired, + }), + ), + onItemClick: PropTypes.func.isRequired, + filterText: PropTypes.string, +}; + +PopupMenuGroup.defaultProps = { + items: [], + filterText: '', +}; + +export default PopupMenuGroup; diff --git a/src/bundle/ui-dev/src/modules/common/popup-menu/popup.menu.helper.js b/src/bundle/ui-dev/src/modules/common/popup-menu/popup.menu.helper.js new file mode 100644 index 0000000000..17aef3e6cd --- /dev/null +++ b/src/bundle/ui-dev/src/modules/common/popup-menu/popup.menu.helper.js @@ -0,0 +1,12 @@ +const MIN_SEARCH_LENGTH = 3; + +export const showItem = (item, filterText) => { + if (filterText.length < MIN_SEARCH_LENGTH) { + return true; + } + + const itemLabelLowerCase = item.label.toLowerCase(); + const filterTextLowerCase = filterText.toLowerCase(); + + return itemLabelLowerCase.indexOf(filterTextLowerCase) === 0; +}; diff --git a/src/bundle/ui-dev/src/modules/common/popup-menu/popup.menu.item.js b/src/bundle/ui-dev/src/modules/common/popup-menu/popup.menu.item.js new file mode 100644 index 0000000000..ece2c65b34 --- /dev/null +++ b/src/bundle/ui-dev/src/modules/common/popup-menu/popup.menu.item.js @@ -0,0 +1,32 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +import { showItem } from './popup.menu.helper'; + +const PopupMenuItem = ({ item, filterText, onItemClick }) => { + if (!showItem(item, filterText)) { + return null; + } + + return ( +
+ +
+ ); +}; + +PopupMenuItem.propTypes = { + item: PropTypes.shape({ + label: PropTypes.string.isRequired, + }).isRequired, + onItemClick: PropTypes.func.isRequired, + filterText: PropTypes.string, +}; + +PopupMenuItem.defaultProps = { + filterText: '', +}; + +export default PopupMenuItem; diff --git a/src/bundle/ui-dev/src/modules/common/popup-menu/popup.menu.js b/src/bundle/ui-dev/src/modules/common/popup-menu/popup.menu.js index 51ddae3e47..103b28a366 100644 --- a/src/bundle/ui-dev/src/modules/common/popup-menu/popup.menu.js +++ b/src/bundle/ui-dev/src/modules/common/popup-menu/popup.menu.js @@ -1,15 +1,15 @@ -import React, { useState, useEffect, useRef, useMemo } from 'react'; +import React, { useState, useEffect, useRef, useMemo, useCallback } from 'react'; import PropTypes from 'prop-types'; -import { getTranslator, getRootDOMElement } from '@ibexa-admin-ui/src/bundle/Resources/public/js/scripts/helpers/context.helper'; +import { getRootDOMElement } from '@ibexa-admin-ui/src/bundle/Resources/public/js/scripts/helpers/context.helper'; import { createCssClassNames } from '@ibexa-admin-ui/src/bundle/ui-dev/src/modules/common/helpers/css.class.names'; -import Icon from '@ibexa-admin-ui/src/bundle/ui-dev/src/modules/common/icon/icon'; -const MIN_SEARCH_ITEMS_DEFAULT = 5; +import PopupMenuSearch from './popup.menu.search'; +import PopupMenuGroup from './popup.menu.group'; + const MIN_ITEMS_LIST_HEIGHT = 150; const PopupMenu = ({ extraClasses, footer, items, onItemClick, positionOffset, referenceElement, scrollContainer, onClose }) => { - const Translator = getTranslator(); const containerRef = useRef(); const [isRendered, setIsRendered] = useState(false); const [itemsListStyles, setItemsListStyles] = useState({ @@ -23,42 +23,7 @@ const PopupMenu = ({ extraClasses, footer, items, onItemClick, positionOffset, r 'c-popup-menu--hidden': !isRendered, [extraClasses]: true, }); - const searchPlaceholder = Translator.trans(/*@Desc("Search...")*/ 'ibexa_popup_menu.search.placeholder', {}, 'ibexa_popup_menu'); - const updateFilterValue = (event) => setFilterText(event.target.value); - const resetInputValue = () => setFilterText(''); - const showItem = (item) => { - if (filterText.length < 3) { - return true; - } - - const itemLabelLowerCase = item.label.toLowerCase(); - const filterTextLowerCase = filterText.toLowerCase(); - - return itemLabelLowerCase.indexOf(filterTextLowerCase) === 0; - }; - const renderGroup = (group) => { - const isAnyItemVisible = group.items.some(showItem); - - if (!isAnyItemVisible) { - return null; - } - - return
{group.items.map(renderItem)}
; - }; - const renderItem = (item) => { - if (!showItem(item)) { - return null; - } - - return ( -
- -
- ); - }; - const calculateAndSetItemsListStyles = () => { + const calculateAndSetItemsListStyles = useCallback(() => { const itemsStyles = {}; const { top: referenceTop, left: referenceLeft } = referenceElement.getBoundingClientRect(); const { height: containerHeight } = containerRef.current.getBoundingClientRect(); @@ -80,43 +45,7 @@ const PopupMenu = ({ extraClasses, footer, items, onItemClick, positionOffset, r } setItemsListStyles(itemsStyles); - }; - const renderSearch = () => { - if (numberOfItems < MIN_SEARCH_ITEMS_DEFAULT) { - return null; - } - - return ( -
-
- -
- - -
-
-
- ); - }; + }, [referenceElement, positionOffset]); const renderFooter = () => { if (!footer) { return null; @@ -147,12 +76,16 @@ const PopupMenu = ({ extraClasses, footer, items, onItemClick, positionOffset, r setItemsListStyles({}); }; - }, [onClose, scrollContainer]); + }, [onClose, scrollContainer, referenceElement, calculateAndSetItemsListStyles]); return (
- {renderSearch()} -
{items.map(renderGroup)}
+ +
+ {items.map((group) => ( + + ))} +
{renderFooter()}
); @@ -163,8 +96,9 @@ PopupMenu.propTypes = { extraClasses: PropTypes.string, footer: PropTypes.node, items: PropTypes.arrayOf({ + id: PropTypes.string.isRequired, items: PropTypes.shape({ - value: PropTypes.oneOf([PropTypes.string, PropTypes.number]), + id: PropTypes.oneOf([PropTypes.string, PropTypes.number]), label: PropTypes.string, }), }), diff --git a/src/bundle/ui-dev/src/modules/common/popup-menu/popup.menu.search.js b/src/bundle/ui-dev/src/modules/common/popup-menu/popup.menu.search.js new file mode 100644 index 0000000000..ba99b3f5f1 --- /dev/null +++ b/src/bundle/ui-dev/src/modules/common/popup-menu/popup.menu.search.js @@ -0,0 +1,61 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +import { getTranslator } from '@ibexa-admin-ui/src/bundle/Resources/public/js/scripts/helpers/context.helper'; +import Icon from '@ibexa-admin-ui/src/bundle/ui-dev/src/modules/common/icon/icon'; + +const MIN_SEARCH_ITEMS_DEFAULT = 5; + +const PopupMenuSearch = ({ numberOfItems, filterText, setFilterText }) => { + const Translator = getTranslator(); + const searchPlaceholder = Translator.trans(/*@Desc("Search...")*/ 'ibexa_popup_menu.search.placeholder', {}, 'ibexa_popup_menu'); + const updateFilterValue = (event) => setFilterText(event.target.value); + const resetInputValue = () => setFilterText(''); + + if (numberOfItems < MIN_SEARCH_ITEMS_DEFAULT) { + return null; + } + + return ( +
+
+ +
+ + +
+
+
+ ); +}; + +PopupMenuSearch.propTypes = { + numberOfItems: PropTypes.number.isRequired, + setFilterText: PropTypes.func.isRequired, + filterText: PropTypes.string, +}; + +PopupMenuSearch.defaultProps = { + filterText: '', +}; + +export default PopupMenuSearch;