2

I'm working on a pretty basic note taking app with React. Purely a front end question.

On a sidebar I have a list of all the current notes, which the user can use to swap between them. Each note represented in the list has a delete button to the right that will remove that note. On clicking the button, a div (I'm calling it "approval-box") will show up asking for confirmation. "Delete? Yes or No." Approval-box is shown based on a boolean useState.

enter image description here

Here's what I'm trying to do. I would like the approval-box to be dismissed if the user clicks anywhere else on the page. My intention is to set focus on the div and then have an onBlur event toggle the useState, which should then dismiss the div. If focus is lost, no more approval-box.

The trouble is I cannot for the life of me figure out how to actually set focus on approval-box.

Here's the code for the component I'm writing this in:

import { useRef } from 'react';
import { useAppContext } from '../../App';
import { BsX } from 'react-icons/bs';

const useFocus = () => {
  const htmlElRef = useRef(null);
  const setFocus = () => {
    htmlElRef.current && htmlElRef.current.focus();
  };

  return [htmlElRef, setFocus];
};

const DeleteButton = ({ note, showApproval, setShowApproval }) => {
  const { deleteSelectedNote } = useAppContext();
  const [approvalBoxRef, setApprovalBoxFocus] = useFocus();

  const handleClickDelete = () => {
    setShowApproval(!showApproval);
    setApprovalBoxFocus();
  };

  const handleConfirmDelete = (noteId) => {
    deleteSelectedNote(noteId);
  };

  return (
    <div className='delete-btn-container'>
      <button
        className={showApproval ? 'delete-btn btn pending' : 'delete-btn btn'}
        onClick={handleClickDelete}
      >
        <BsX size={'1em'} />
      </button>

      <div
        className={showApproval ? 'approval-box active' : 'approval-box'}
        tabIndex='1'
        onBlur={() => {
          setShowApproval(false);
          console.log('approval removed');
        }}
        ref={approvalBoxRef}
      >
        Delete?
        <span className='yes' onClick={() => handleConfirmDelete(note.id)}>
          {' '}
          Y{' '}
        </span>
        /
        <span className='no' onClick={() => setShowApproval(false)}>
          {' '}
          N
        </span>
      </div>
    </div>
  );
};
export default DeleteButton;

Right now I have tabIndex set to 1 on the approval-box, and I'm using a custom hook to set focus on it when the user clicks the initial delete button.

But when I do this the approval box doesn't receive focus. The user can still click around and interact with the rest of the app and the approval-box's onBlur event never fires. Focus is only set on approval-box once the user actually clicks on it. Then, if they click away the desired effect happens and the approval-box is dismissed.

Is there something about how focus works that I'm not understanding?

I don't know if this matters, but as you can see in the code, this is the delete button for a single note item in my list. Each note will have one of these and I'd prefer the user only be able to interact with one at a time.

Also the approval-box is always rendered, but I'm using CSS to make it visible and interactive. I've tried conditionally rendering the approval-box instead of just revealing it, but the focus issue is the same in that case.

If there's a better way to achieve the effect I'm going for, I'm definitely open to advice on alternatives.

1 Answer 1

1

You can create a useClickOutside.js hook

import { useEffect, useCallback } from 'react';

const isRefArray = r => 'length' in r;

const isTarget = (ref, event) =>
    ref && ref.current && ref.current.contains(event.target);

const trueForAny = (array, condition) =>
    array.reduce(
        (conditionAlreadyMet, value) => conditionAlreadyMet || condition(value),
        false,
    );

const useClickOutside = (ref, onclick) => {
    const handleClick = useCallback(
        click => {
            if (isRefArray(ref)) {
                if (trueForAny(ref, d => isTarget(d, click))) {
                    return;
                }
            } else if (isTarget(ref, click)) {
                return;
            }
            onclick();
        },
        [onclick, ref],
    );

    useEffect(() => {
        document.addEventListener('click', handleClick);

        return () => {
            document.removeEventListener('click', handleClick);
        };
    }, [handleClick]);

    return ref;
};

export default useClickOutside;

What the above hook does is monitor when ever you click outside the passed ref component.

In your component file, do this

 import { useState, useRef } from 'react';
 import useClickOutside from 'hooks/useClickOutside';

 const clickRef = useRef();

 useClickOutside(clickRef, () => {
    // Set the state to close the approval-box on click outside here
     setShowApproval(false);
  });


  function DeleteButton(){
   ...

   return(
     <div className='delete-btn-container'>
      <button
        ref={clickRef)
        className={showApproval ? 'delete-btn btn pending' : 'delete-btn btn'}
        onClick={handleClickDelete}
      >
        <BsX size={'1em'} />
      </button>
      ...
     </div>
   )
 }

You can get rid of the useFocus function and focus implementation.

Not the answer you're looking for? Browse other questions tagged or ask your own question.