Skip to content

Commit

Permalink
added dependency arrays; break popupmenu component to smaller ones
Browse files Browse the repository at this point in the history
  • Loading branch information
GrabowskiM committed Dec 5, 2024
1 parent 08da0bc commit 829825d
Show file tree
Hide file tree
Showing 7 changed files with 216 additions and 131 deletions.
Original file line number Diff line number Diff line change
@@ -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.`);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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);
Expand All @@ -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();
Expand All @@ -122,7 +130,7 @@ const DraggableDialog = ({ children, referenceElement, positionOffset }) => {
x,
y,
});
}, [referenceElement]);
}, [referenceElement, positionOffset]);

return (
<DraggableContext.Provider
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import React from 'react';
import PropTypes from 'prop-types';

import PopupMenuItem from './popup.menu.item';
import { showItem } from './popup.menu.helper';

const PopupMenuGroup = ({ items, filterText, onItemClick }) => {
const isAnyItemVisible = items.some((item) => showItem(item, filterText));

if (!isAnyItemVisible) {
return null;
}

return (
<div className="c-popup-menu__group">
{items.map((item) => (
<PopupMenuItem key={item.value} item={item} filterText={filterText} onItemClick={onItemClick} />
))}
</div>
);
};

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;
Original file line number Diff line number Diff line change
@@ -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;
};
32 changes: 32 additions & 0 deletions src/bundle/ui-dev/src/modules/common/popup-menu/popup.menu.item.js
Original file line number Diff line number Diff line change
@@ -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 (
<div className="c-popup-menu__item">
<button type="button" className="c-popup-menu__item-content" onClick={() => onItemClick(item)}>
<span className="c-popup-menu__item-label">{item.label}</span>
</button>
</div>
);
};

PopupMenuItem.propTypes = {
item: PropTypes.shape({
label: PropTypes.string.isRequired,
}).isRequired,
onItemClick: PropTypes.func.isRequired,
filterText: PropTypes.string,
};

PopupMenuItem.defaultProps = {
filterText: '',
};

export default PopupMenuItem;
98 changes: 16 additions & 82 deletions src/bundle/ui-dev/src/modules/common/popup-menu/popup.menu.js
Original file line number Diff line number Diff line change
@@ -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({
Expand All @@ -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 <div className="c-popup-menu__group">{group.items.map(renderItem)}</div>;
};
const renderItem = (item) => {
if (!showItem(item)) {
return null;
}

return (
<div className="c-popup-menu__item" key={item.value}>
<button type="button" className="c-popup-menu__item-content" onClick={() => onItemClick(item)}>
<span className="c-popup-menu__item-label">{item.label}</span>
</button>
</div>
);
};
const calculateAndSetItemsListStyles = () => {
const calculateAndSetItemsListStyles = useCallback(() => {
const itemsStyles = {};
const { top: referenceTop, left: referenceLeft } = referenceElement.getBoundingClientRect();
const { height: containerHeight } = containerRef.current.getBoundingClientRect();
Expand All @@ -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 (
<div className="c-popup-menu__search">
<div className="ibexa-input-text-wrapper">
<input
type="text"
placeholder={searchPlaceholder}
className="c-popup-menu__search-input ibexa-input ibexa-input--small ibexa-input--text form-control"
onChange={updateFilterValue}
value={filterText}
/>
<div className="ibexa-input-text-wrapper__actions">
<button
type="button"
className="btn ibexa-input-text-wrapper__action-btn ibexa-input-text-wrapper__action-btn--clear"
tabIndex="-1"
onClick={resetInputValue}
>
<Icon name="discard" extraClasses="ibexa-icon--tiny-small" />
</button>
<button
type="button"
className="btn ibexa-input-text-wrapper__action-btn ibexa-input-text-wrapper__action-btn--search"
tabIndex="-1"
>
<Icon name="search" extraClasses="ibexa-icon--small" />
</button>
</div>
</div>
</div>
);
};
}, [referenceElement, positionOffset]);
const renderFooter = () => {
if (!footer) {
return null;
Expand Down Expand Up @@ -147,12 +76,16 @@ const PopupMenu = ({ extraClasses, footer, items, onItemClick, positionOffset, r

setItemsListStyles({});
};
}, [onClose, scrollContainer]);
}, [onClose, scrollContainer, referenceElement, calculateAndSetItemsListStyles]);

return (
<div className={popupMenuClassName} style={itemsListStyles} ref={containerRef}>
{renderSearch()}
<div className="c-popup-menu__groups">{items.map(renderGroup)}</div>
<PopupMenuSearch numberOfItems={numberOfItems} filterText={filterText} setFilterText={setFilterText} />
<div className="c-popup-menu__groups">
{items.map((group) => (
<PopupMenuGroup key={group.key} items={group.items} filterText={filterText} onItemClick={onItemClick} />
))}
</div>
{renderFooter()}
</div>
);
Expand All @@ -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,
}),
}),
Expand Down
Loading

0 comments on commit 829825d

Please sign in to comment.