From 1bd565bff001d09b900b37bd90d0468f05449046 Mon Sep 17 00:00:00 2001 From: Taras-Hlukhovetskyi <155433425+Taras-Hlukhovetskyi@users.noreply.github.com> Date: Mon, 18 Mar 2024 22:22:11 +0200 Subject: [PATCH] Fix [FormChipCell] issues with labels (#251) --- .../FormChipCell/FormChip/FormChip.js | 2 +- .../components/FormChipCell/FormChipCell.js | 64 ++++++++++++---- .../FormChipCell/NewChipForm/NewChipForm.js | 73 +++++++++++-------- .../FormChipCell/NewChipForm/newChipForm.scss | 8 ++ .../FormChipCell/formChipCell.util.js | 31 ++++++++ src/lib/components/FormSelect/FormSelect.js | 18 +---- src/lib/components/PopUpDialog/PopUpDialog.js | 51 ++++++++++--- src/lib/components/Tooltip/Tooltip.js | 11 +-- src/lib/elements/OptionsMenu/OptionsMenu.js | 8 +- src/lib/types.js | 4 +- 10 files changed, 191 insertions(+), 79 deletions(-) diff --git a/src/lib/components/FormChipCell/FormChip/FormChip.js b/src/lib/components/FormChipCell/FormChip/FormChip.js index 92488789..c1e4c0b5 100644 --- a/src/lib/components/FormChipCell/FormChip/FormChip.js +++ b/src/lib/components/FormChipCell/FormChip/FormChip.js @@ -58,7 +58,7 @@ const FormChip = React.forwardRef( }, [chipIndex, setChipsSizes]) return ( -
handleToEditMode(event, chipIndex)} ref={chipRef}> +
handleToEditMode(event, chipIndex, keyName)} ref={chipRef}> { - const isPrevChipIndexExists = prevState.chipIndex - 1 < 0 + const firstChipIsSelected = prevState.chipIndex === 0 - isChipNotEmpty && - isPrevChipIndexExists && - onExitEditModeCallback && - onExitEditModeCallback() + isChipNotEmpty && firstChipIsSelected && onExitEditModeCallback && onExitEditModeCallback() return { - chipIndex: isPrevChipIndexExists ? null : prevState.chipIndex - 1, - isEdit: !isPrevChipIndexExists, - isKeyFocused: isPrevChipIndexExists, - isValueFocused: !isPrevChipIndexExists, + chipIndex: firstChipIsSelected ? null : prevState.chipIndex - 1, + isEdit: !firstChipIsSelected, + isKeyFocused: false, + isValueFocused: !firstChipIsSelected, isNewChip: false } }) } checkChipsList(get(formState.values, name)) - event && event.preventDefault() + + if ( + (editConfig.chipIndex > 0 && editConfig.chipIndex < fields.value.length - 1) || + (fields.value.length > 1 && editConfig.chipIndex === 0 && nameEvent !== TAB_SHIFT) || + (fields.value.length > 1 && editConfig.chipIndex === fields.value.length - 1 && nameEvent !== TAB) + ) { + event && event.preventDefault() + } }, [ editConfig.chipIndex, @@ -220,16 +224,46 @@ const FormChipCell = ({ ) const handleToEditMode = useCallback( - (event, index) => { + (event, chipIndex, keyName) => { if (isEditable) { + const { clientX: pointerCoordinateX, clientY: pointerCoordinateY } = event + let isKeyClicked = false + const isClickedInsideInputElement = (pointerCoordinateX, pointerCoordinateY, inputElement) => { + if (inputElement) { + const { + top: topPosition, + left: leftPosition, + right: rightPosition, + bottom: bottomPosition + } = inputElement.getBoundingClientRect() + if (pointerCoordinateX > rightPosition || pointerCoordinateX < leftPosition) + return false + if (pointerCoordinateY > bottomPosition || pointerCoordinateY < topPosition) + return false + + return true + } + } event.stopPropagation() + if (event.target.nodeName !== 'INPUT') { + if (event.target.firstElementChild) { + isKeyClicked = isClickedInsideInputElement( + pointerCoordinateX, + pointerCoordinateY, + event.target.firstElementChild + ) + } + } else { + isKeyClicked = event.target.name === keyName + } + setEditConfig((preState) => ({ ...preState, - chipIndex: index, + chipIndex, isEdit: true, - isKeyFocused: true, - isValueFocused: false + isKeyFocused: isKeyClicked, + isValueFocused: !isKeyClicked })) } diff --git a/src/lib/components/FormChipCell/NewChipForm/NewChipForm.js b/src/lib/components/FormChipCell/NewChipForm/NewChipForm.js index a25a6c75..63fb4366 100644 --- a/src/lib/components/FormChipCell/NewChipForm/NewChipForm.js +++ b/src/lib/components/FormChipCell/NewChipForm/NewChipForm.js @@ -17,14 +17,15 @@ such restriction. import React, { useState, useCallback, useEffect, useLayoutEffect, useMemo } from 'react' import PropTypes from 'prop-types' import classnames from 'classnames' -import { isEmpty, get } from 'lodash' +import { isEmpty, get, isNil } from 'lodash' import NewChipInput from '../NewChipInput/NewChipInput' import OptionsMenu from '../../../elements/OptionsMenu/OptionsMenu' import ValidationTemplate from '../../../elements/ValidationTemplate/ValidationTemplate' import { CHIP_OPTIONS } from '../../../types' -import { BACKSPACE, CLICK, DELETE, TAB, TAB_SHIFT } from '../../../constants' +import { CLICK, TAB, TAB_SHIFT } from '../../../constants' +import { getTextWidth } from '../formChipCell.util' import { ReactComponent as Close } from '../../../images/close.svg' @@ -95,6 +96,11 @@ const NewChipForm = React.forwardRef( 'item_edited_invalid' ) + const closeButtonClass = classnames( + 'edit-chip__icon-close', + (editConfig.chipIndex === chipIndex || !isEditable) && 'edit-chip__icon-close_hidden' + ) + useLayoutEffect(() => { if (!chipData.keyFieldWidth && !chipData.valueFieldWidth) { const currentWidthKeyInput = refInputKey.current.scrollWidth + 1 @@ -187,33 +193,24 @@ const NewChipForm = React.forwardRef( const focusChip = useCallback( (event) => { - event.stopPropagation() - if (editConfig.chipIndex === chipIndex && isEditable) { if (!event.shiftKey && event.key === TAB && editConfig.isValueFocused) { - onChange(event, TAB) + return onChange(event, TAB) } else if (event.shiftKey && event.key === TAB && editConfig.isKeyFocused) { - onChange(event, TAB_SHIFT) - } - - if (event.key === BACKSPACE || event.key === DELETE) { - setChipData((prevState) => ({ - ...prevState, - keyFieldWidth: editConfig.isKeyFocused ? minWidthInput : prevState.keyFieldWidth, - valueFieldWidth: editConfig.isValueFocused - ? minWidthValueInput - : prevState.valueFieldWidth - })) + return onChange(event, TAB_SHIFT) } } + event.stopPropagation() }, [editConfig, onChange, chipIndex, isEditable] ) const handleOnFocus = useCallback( (event) => { + const isKeyFocused = event.target.name === keyName + if (editConfig.chipIndex === chipIndex) { - if (event.target.name === keyName) { + if (isKeyFocused) { refInputKey.current.selectionStart = refInputKey.current.selectionEnd setEditConfig((prevConfig) => ({ @@ -232,6 +229,18 @@ const NewChipForm = React.forwardRef( } event && event.stopPropagation() + } else if (isNil(editConfig.chipIndex)) { + if (isKeyFocused) { + refInputKey.current.selectionStart = refInputKey.current.selectionEnd + } else { + refInputValue.current.selectionStart = refInputValue.current.selectionEnd + } + setEditConfig({ + chipIndex, + isEdit: true, + isKeyFocused: isKeyFocused, + isValueFocused: !isKeyFocused + }) } }, [keyName, refInputKey, refInputValue, setEditConfig, editConfig.chipIndex, chipIndex] @@ -241,7 +250,7 @@ const NewChipForm = React.forwardRef( (event) => { event.preventDefault() if (event.target.name === keyName) { - const currentWidthKeyInput = refInputKey.current.scrollWidth + const currentWidthKeyInput = getTextWidth(refInputKey.current) setChipData((prevState) => ({ ...prevState, @@ -256,7 +265,7 @@ const NewChipForm = React.forwardRef( : minWidthInput })) } else { - const currentWidthValueInput = refInputValue.current.scrollWidth + const currentWidthValueInput = getTextWidth(refInputValue.current) setChipData((prevState) => ({ ...prevState, @@ -275,7 +284,7 @@ const NewChipForm = React.forwardRef( [maxWidthInput, refInputKey, refInputValue, keyName] ) - useEffect(() => { + useLayoutEffect(() => { if (editConfig.chipIndex === chipIndex) { setSelectedInput( editConfig.isKeyFocused ? 'key' : editConfig.isValueFocused ? 'value' : null @@ -325,7 +334,9 @@ const NewChipForm = React.forwardRef( > )} - {editConfig.chipIndex !== chipIndex && isEditable && ( - - )} + {(editConfig.isKeyFocused ? !isEmpty(chipData.key) : !isEmpty(chipData.value)) && editConfig.chipIndex === chipIndex && !isEmpty(get(meta, ['error', editConfig.chipIndex, selectedInput], [])) && ( - + {getValidationRules()} )} diff --git a/src/lib/components/FormChipCell/NewChipForm/newChipForm.scss b/src/lib/components/FormChipCell/NewChipForm/newChipForm.scss index 49747e0a..9f98b960 100644 --- a/src/lib/components/FormChipCell/NewChipForm/newChipForm.scss +++ b/src/lib/components/FormChipCell/NewChipForm/newChipForm.scss @@ -18,6 +18,10 @@ background-color: transparent; border: none; + &[disabled] { + pointer-events: none; + } + &.item_edited { &_invalid { color: $amaranth; @@ -58,6 +62,10 @@ align-items: center; justify-content: center; + &_hidden { + visibility: hidden; + } + svg { transform: scale(0.7); } diff --git a/src/lib/components/FormChipCell/formChipCell.util.js b/src/lib/components/FormChipCell/formChipCell.util.js index 07d6e88e..bf25db9b 100644 --- a/src/lib/components/FormChipCell/formChipCell.util.js +++ b/src/lib/components/FormChipCell/formChipCell.util.js @@ -15,3 +15,34 @@ under the Apache 2.0 license is conditioned upon your compliance with such restriction. */ export const uniquenessError = { name: 'uniqueness', label: 'Key must be unique' } + +export const getTextWidth = (elementWithText) => { + if (!elementWithText) { + return 0 + } + const hiddenElementId = 'chips-hidden-element' + let hiddenElement = document.getElementById(hiddenElementId) + + if (!hiddenElement) { + hiddenElement = document.createElement('span') + const styles = { + position: 'absolute', + left: '-10000px', + top: "auto", + visibility: 'hidden' + } + + for (const [styleName, styleValue] of Object.entries(styles)) { + hiddenElement.style[styleName ] = styleValue; + } + + hiddenElement.style.font = window.getComputedStyle(elementWithText).font + hiddenElement.id = hiddenElementId + hiddenElement.tabIndex = -1 + document.body.append(hiddenElement) + } + + hiddenElement.textContent = elementWithText.value + + return hiddenElement.offsetWidth ?? 0 +} diff --git a/src/lib/components/FormSelect/FormSelect.js b/src/lib/components/FormSelect/FormSelect.js index 21c24892..1fdc234e 100644 --- a/src/lib/components/FormSelect/FormSelect.js +++ b/src/lib/components/FormSelect/FormSelect.js @@ -14,7 +14,7 @@ illegal under applicable law, and the grant of the foregoing license under the Apache 2.0 license is conditioned upon your compliance with such restriction. */ -import React, { useState, useEffect, useCallback, useMemo, useRef, useLayoutEffect } from 'react' +import React, { useState, useEffect, useCallback, useMemo, useRef } from 'react' import PropTypes from 'prop-types' import classNames from 'classnames' import { Field, useField } from 'react-final-form' @@ -54,13 +54,12 @@ const FormSelect = ({ const [isInvalid, setIsInvalid] = useState(false) const [isConfirmDialogOpen, setConfirmDialogOpen] = useState(false) const [isOpen, setIsOpen] = useState(false) - const [position, setPosition] = useState('bottom-right') const [searchValue, setSearchValue] = useState('') const optionsListRef = useRef() const popUpRef = useRef() const selectRef = useRef() const searchRef = useRef() - const { width: selectWidth, left: selectLeft } = selectRef?.current?.getBoundingClientRect() || {} + const { width: selectWidth } = selectRef?.current?.getBoundingClientRect() || {} const selectWrapperClassNames = classNames( 'form-field__wrapper', @@ -162,16 +161,6 @@ const FormSelect = ({ [closeMenu] ) - useLayoutEffect(() => { - if (popUpRef?.current) { - const { width } = popUpRef.current.getBoundingClientRect() - - selectLeft + width > window.innerWidth - ? setPosition('bottom-left') - : setPosition('bottom-right') - } - }, [isOpen, selectLeft]) - useEffect(() => { if (isOpen) { window.addEventListener('scroll', handleScroll, true) @@ -345,7 +334,8 @@ const FormSelect = ({ ref={popUpRef} customPosition={{ element: selectRef, - position + position: 'bottom-right', + autoHorizontalPosition: true }} style={{ maxWidth: `${selectWidth < 500 ? 500 : selectWidth}px`, diff --git a/src/lib/components/PopUpDialog/PopUpDialog.js b/src/lib/components/PopUpDialog/PopUpDialog.js index b6541106..d947e380 100644 --- a/src/lib/components/PopUpDialog/PopUpDialog.js +++ b/src/lib/components/PopUpDialog/PopUpDialog.js @@ -65,26 +65,59 @@ const PopUpDialog = React.forwardRef( const [verticalPosition, horizontalPosition] = customPosition.position.split('-') const popupMargin = 15 const elementMargin = 5 + const isEnoughSpaceFromLeft = elementRect.right >= popUpRect.width + popupMargin + const isEnoughSpaceFromRight = + window.innerWidth - elementRect.left >= popUpRect.width + popupMargin + const isEnoughSpaceFromTop = + elementRect.top > popUpRect.height + popupMargin + elementMargin + const isEnoughSpaceFromBottom = + elementRect.bottom + popUpRect.height + popupMargin + elementMargin <= window.innerHeight let leftPosition = horizontalPosition === 'left' ? elementRect.right - popUpRect.width : elementRect.left let topPosition if (verticalPosition === 'top') { - topPosition = - elementRect.top > popUpRect.height + popupMargin - ? elementRect.top - popUpRect.height - elementMargin - : popupMargin + topPosition = isEnoughSpaceFromTop + ? elementRect.top - popUpRect.height - elementMargin + : popupMargin } else { - topPosition = - popUpRect.height + elementRect.bottom + popupMargin > window.innerHeight - ? window.innerHeight - popUpRect.height - popupMargin - : elementRect.bottom + elementMargin + topPosition = isEnoughSpaceFromBottom + ? elementRect.bottom + elementMargin + : window.innerHeight - popUpRect.height - popupMargin + } + + if (customPosition.autoVerticalPosition) { + if (verticalPosition === 'top') { + if (!isEnoughSpaceFromTop && isEnoughSpaceFromBottom) { + topPosition = elementRect.bottom + elementMargin + } + } else { + if (isEnoughSpaceFromTop && !isEnoughSpaceFromBottom) { + topPosition = elementRect.top - popUpRect.height - elementMargin + } + } + } + + if (customPosition.autoHorizontalPosition) { + if (verticalPosition === 'left') { + if (!isEnoughSpaceFromLeft && isEnoughSpaceFromRight) { + leftPosition = elementRect.left + } else if (!isEnoughSpaceFromLeft && !isEnoughSpaceFromRight) { + leftPosition = popupMargin + } + } else { + if (isEnoughSpaceFromLeft && !isEnoughSpaceFromRight) { + leftPosition = elementRect.right - popUpRect.width + } else if (!isEnoughSpaceFromLeft && !isEnoughSpaceFromRight) { + leftPosition = window.innerWidth - popUpRect.width - popupMargin + } + } } ref.current.style.top = `${topPosition}px` - if (style.left) { + if (style.left && !(customPosition.autoHorizontalPosition && isEnoughSpaceFromRight)) { ref.current.style.left = `calc(${leftPosition}px + ${style.left})` } else { ref.current.style.left = `${leftPosition}px` diff --git a/src/lib/components/Tooltip/Tooltip.js b/src/lib/components/Tooltip/Tooltip.js index 8aa83947..57a72920 100644 --- a/src/lib/components/Tooltip/Tooltip.js +++ b/src/lib/components/Tooltip/Tooltip.js @@ -41,14 +41,15 @@ const Tooltip = ({ children, className, hidden, id, renderChildAsHtml, template, const handleMouseLeave = useCallback((event) => { if ( - tooltipRef.current && - !tooltipRef.current.contains(event.relatedTarget) && - parentRef.current && - !parentRef.current.contains(event.relatedTarget) + (tooltipRef.current && + !tooltipRef.current.contains(event.relatedTarget) && + parentRef.current && + !parentRef.current.contains(event.relatedTarget)) || + hidden ) { setShow(false) } - }, []) + }, [hidden]) const handleMouseEnter = useCallback( (event) => { diff --git a/src/lib/elements/OptionsMenu/OptionsMenu.js b/src/lib/elements/OptionsMenu/OptionsMenu.js index abef29ea..ee313a5e 100644 --- a/src/lib/elements/OptionsMenu/OptionsMenu.js +++ b/src/lib/elements/OptionsMenu/OptionsMenu.js @@ -31,9 +31,11 @@ const OptionsMenu = React.forwardRef(({ children, show, timeout }, ref) => { className="options-menu" customPosition={{ element: ref, - position: 'bottom-right' + position: 'bottom-right', + autoVerticalPosition: true, + autoHorizontalPosition : true }} - style={{ width: `${dropdownWidth}px` }} + style={{ 'minWidth': `${dropdownWidth}px` }} >
    {children}
@@ -50,7 +52,7 @@ OptionsMenu.defaultProps = { OptionsMenu.propTypes = { children: PropTypes.arrayOf(PropTypes.element), show: PropTypes.bool.isRequired, - timout: PropTypes.number + timeout: PropTypes.number } export default OptionsMenu diff --git a/src/lib/types.js b/src/lib/types.js index 89327671..58b27f84 100644 --- a/src/lib/types.js +++ b/src/lib/types.js @@ -76,7 +76,9 @@ export const CHIPS = PropTypes.arrayOf(CHIP) export const POP_UP_CUSTOM_POSITION = PropTypes.shape({ element: PropTypes.shape({}), - position: PropTypes.oneOf(['top-left', 'top-right', 'bottom-left', 'bottom-right']) + position: PropTypes.oneOf(['top-left', 'top-right', 'bottom-left', 'bottom-right']), + autoHorizontalPosition : PropTypes.bool, + autoVerticalPosition: PropTypes.bool }) export const MODAL_SIZES = PropTypes.oneOf([MODAL_SM, MODAL_MD, MODAL_LG, MODAL_MIN, MODAL_MAX])