Skip to content

Commit

Permalink
use mutationobserver for focusmanagement in repeatable field
Browse files Browse the repository at this point in the history
  • Loading branch information
JohnC-80 committed Oct 25, 2024
1 parent 4b06eed commit 90ca131
Show file tree
Hide file tree
Showing 2 changed files with 52 additions and 21 deletions.
49 changes: 47 additions & 2 deletions lib/RepeatableField/RepeatableField.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useRef } from 'react';
import React, { useRef, useEffect } from 'react';
import classnames from 'classnames';
import PropTypes from 'prop-types';
import { FormattedMessage } from 'react-intl';
Expand All @@ -12,7 +12,7 @@ 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,
Expand All @@ -35,6 +35,51 @@ const RepeatableField = ({
const fieldsLength = fields.length;
const isSomeChildElementFocused = useIsElementFocused(rootRef);
const focusedIndex = useFocusedIndex(fieldsLength);

// use mutation observers to handle focus-management since we only have internal state.
useEffect(() => {
const observer = new MutationObserver(mutations => {
let rowElem;
// field additions - we only manage focus when focus is
// within the container...
if (rootRef.current.matches(':focus-within')) {
mutations.forEach((m) => {
if (m.type === 'childList') {
if (m.addedNodes?.length === 1) {
rowElem = m.addedNodes[0];
if (rowElem) {
getFirstFocusable(rowElem)?.focus();
}
}
}
});
}
// removals may or may not keep focus within the field list...
mutations.forEach((m) => {
if (m.type === 'childList') {
if (m.removedNodes.length === 1) {
rowElem = m.previousSibling;
if (rowElem) {
getFirstFocusable(rowElem)?.focus();
}
}
}
});
});

if (rootRef.current) {
// observe for item additions/removals from list.
observer.observe(rootRef.current, {
childList: true,
subtree: true,
});
}

return () => {
observer.disconnect();
};
}, [])

const hasToBeFocused = (index) => isSomeChildElementFocused && focusedIndex === index;

return (
Expand Down
24 changes: 5 additions & 19 deletions lib/RepeatableField/RepeatableFieldContent.js
Original file line number Diff line number Diff line change
@@ -1,31 +1,17 @@
import React, { useCallback } from "react";
import PropTypes from "prop-types";

import { getFirstFocusable } from "../../util/getFocusableElements";

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 (
<div className={css.repeatableFieldItemContent} ref={callbackRef}>
{children}
</div>
);
}
export const RepeatableFieldContent = ({ children }) => (
<div className={css.repeatableFieldItemContent}>
{children}
</div>
);

RepeatableFieldContent.propTypes = {
children: PropTypes.oneOfType([
PropTypes.node,
PropTypes.func,
]).isRequired,
isFocused: PropTypes.bool.isRequired,
}

0 comments on commit 90ca131

Please sign in to comment.