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` }}
>
@@ -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])