diff --git a/CHANGELOG.md b/CHANGELOG.md index 26097ad1d..b79d90655 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ * Change `Repeatable field` focus behaviour. Refs STCOM-1341. * Fix `` bug with option list closing when scrollbar is used. Refs STCOM-1371. * `` - fix bug handling empty string options/values. Refs STCOM-1373. +* `` - switch to MutationObserver to resolve focus-management issues. Refs STCOM-1372. ## [12.2.0](https://github.com/folio-org/stripes-components/tree/v12.2.0) (2024-10-11) [Full Changelog](https://github.com/folio-org/stripes-components/compare/v12.1.0...v12.2.0) diff --git a/lib/RepeatableField/RepeatableField.js b/lib/RepeatableField/RepeatableField.js index c3ace1b0b..d44e17af8 100644 --- a/lib/RepeatableField/RepeatableField.js +++ b/lib/RepeatableField/RepeatableField.js @@ -1,4 +1,4 @@ -import React, { useRef } from 'react'; +import React, { useRef, useEffect, useState } from 'react'; import classnames from 'classnames'; import PropTypes from 'prop-types'; import { FormattedMessage } from 'react-intl'; @@ -8,11 +8,9 @@ import Button from '../Button'; import Headline from '../Headline'; import EmptyMessage from '../EmptyMessage'; import IconButton from '../IconButton'; -import { RepeatableFieldContent } from "./RepeatableFieldContent"; +import { RepeatableFieldContent } from './RepeatableFieldContent'; import css from './RepeatableField.css'; -import { useFocusedIndex } from "./hooks/useFocusedIndex"; -import { useIsElementFocused } from "./hooks/useIsElementFocused"; - +import { getFirstFocusable } from '../../util/getFocusableElements'; const RepeatableField = ({ canAdd = true, @@ -33,9 +31,51 @@ const RepeatableField = ({ const rootRef = useRef(null); const showDeleteBtn = typeof onRemove === 'function'; const fieldsLength = fields.length; - const isSomeChildElementFocused = useIsElementFocused(rootRef); - const focusedIndex = useFocusedIndex(fieldsLength); - const hasToBeFocused = (index) => isSomeChildElementFocused && focusedIndex === index; + const [hasBeenFocused, setHasBeenFocused] = useState(false); + + // use mutation observers to handle focus-management since we only have internal state. + useEffect(() => { + const observer = new MutationObserver(mutations => { + mutations.forEach(mutation => { + if (mutation.type !== 'childList') return; + + const addedNode = mutation.addedNodes?.[0]; + const removedNode = mutation.removedNodes?.[0]; + let rowElem; + // Handle added node + // we check if the user has interacted with the component before handling this, otherwise focus could be + // unwantedly moved when a form is initialized. + if (hasBeenFocused) { + if (addedNode && + addedNode.nodeType === 1 && // looking for nodeType: element only... not attribute (2) or text (3) + addedNode.matches(`.${css.repeatableFieldItem}`)) { // only apply to repeatable field item addition removal + rowElem = getFirstFocusable(addedNode); + rowElem?.focus(); + } + } + + // Handle removed node + if (removedNode && + mutation.previousSibling && + mutation.previousSibling.matches(`.${css.repeatableFieldItem}`)) { + rowElem = getFirstFocusable(mutation.previousSibling); + rowElem?.focus(); + } + }); + }); + + if (rootRef.current) { + // observe for item additions/removals from list. + observer.observe(rootRef.current, { + childList: true, + subtree: true, + }); + } + + return () => { + observer.disconnect(); + }; + }, [hasBeenFocused]) return (
{ setHasBeenFocused(true) }} > {legend && ( {renderField(field, index, fields)} diff --git a/lib/RepeatableField/RepeatableFieldContent.js b/lib/RepeatableField/RepeatableFieldContent.js index 854cdb341..edbeae103 100644 --- a/lib/RepeatableField/RepeatableFieldContent.js +++ b/lib/RepeatableField/RepeatableFieldContent.js @@ -1,31 +1,17 @@ -import React, { useCallback } from "react"; -import PropTypes from "prop-types"; +import React from 'react'; +import PropTypes from 'prop-types'; -import { getFirstFocusable } from "../../util/getFocusableElements"; +import css from './RepeatableField.css'; -import css from "./RepeatableField.css"; - -export const RepeatableFieldContent = ({ children, isFocused }) => { - - const callbackRef = useCallback((node) => { - if (node && isFocused) { - const elem = getFirstFocusable(node, true, true); - - elem?.focus(); - } - }, [isFocused]) - - return ( -
- {children} -
- ); -} +export const RepeatableFieldContent = ({ children }) => ( +
+ {children} +
+); RepeatableFieldContent.propTypes = { children: PropTypes.oneOfType([ PropTypes.node, PropTypes.func, ]).isRequired, - isFocused: PropTypes.bool.isRequired, }