Monday, 12 November 2018

Close a dropdown when an element within it is clicked

I'm working on a Notification feature in my app (pretty much like Facebook notifications).

When I click a button in the header navigation, the dropdown opens and shows the notification list. The notification has a Link (from react-router) in it.

What I need to do is to close the dropdown whenever a Link is clicked.

Here's roughly the hierarchy I currently have:

Header > Navigation > Button > Dropdown > List > Notification > Link

Since the dropdown functionality is used more that once, I've abstracted its behavior away into a HOC that uses render prop:

export default function withDropDown(ClickableElement) {
  return class ClickableDropdown extends PureComponent {
    static propTypes = {
      children: PropTypes.func.isRequired,
      showOnInit: PropTypes.bool,
    };

    static defaultProps = {
      showOnInit: false,
    };

    state = {
      show: !!this.props.showOnInit,
    };

    domRef = createRef();

    componentDidMount() {
      document.addEventListener('mousedown', this.handleGlobalClick);
    }

    toggle = show => {
      this.setState({ show });
    };

    handleClick = () => this.toggle(true);

    handleGlobalClick = event => {
      if (this.domRef.current && !this.domRef.current.contains(event.target)) {
        this.toggle(false);
      }
    };

    render() {
      const { children, ...props } = this.props;
      return (
        <Fragment>
          <ClickableElement {...props} onClick={this.handleClick} />
          {this.state.show && children(this.domRef)}
        </Fragment>
      );
    }
  };
}

The HOC above encloses the Button component, so I have:

const ButtonWithDropdown = withDropdown(Button);

class NotificationsHeaderDropdown extends PureComponent {
  static propTypes = {
    data: PropTypes.arrayOf(notification),
    load: PropTypes.func,
  };

  static defaultProps = {
    data: [],
    load: () => {},
  };

  componentDidMount() {
    this.props.load();
  }

  renderDropdown = ref => (
    <Dropdown ref={ref}>
      {data.length > 0 && <List items={this.props.data} />}
      {data.length === 0 && <EmptyList />}
    </Dropdown>
  );

  render() {
    return (
      <ButtonWithDropdown count={this.props.data.length}>
        {this.renderDropdown}
      </ButtonWithDropdown>
    );
  }
}

List and Notification are both dumb functional components, so I'm not posting their code here. Dropdown is pretty much the same, with the difference it uses ref forwarding.

What I really need is to call that .toggle() method from ClickableDropdown created by the HOC to be called whenever I click on a Link on the list.

Is there any way of doing this without passing that .toggle() method down the Button > Dropdown > List > Notification > Link subtree?

I'm using redux, but I'm not sure this is the kind of thing I'd put on the store.

Or should I handle this imperatively using the DOM API, by changing the implementation of handleGlobalClick from ClickableDropdown?


Edit:

I'm trying with the imperative approach, so I've changed the handleGlobalClick method:

const DISMISS_KEY = 'dropdown';

function contains(current, element) {
  if (!current) {
    return false;
  }

  return current.contains(element);
}

function isDismisser(dismissKey, current, element) {
  if (!element || !contains(current, element)) {
    return false;
  }

  const shouldDismiss = element.dataset.dismiss === dismissKey;

  return shouldDismiss || isDismisser(dismissKey, current, element.parentNode);
}

// Then...
handleGlobalClick = event => {
  const containsEventTarget = contains(this.domRef.current, event.target);
  const shouldDismiss = isDismisser(
    DISMISS_KEY,
    this.domRef.current,
    event.target
  );

  if (!containsEventTarget || shouldDismiss) {
    this.toggle(false);
  }

  return true;
};

Then I changed the Link to include a data-dismiss property:

<Link
  to={url}
  data-dismiss="dropdown"
>
  ...
</Link>

Now the dropdown is closed, but I'm not redirected to the provided url anymore.

I tried to defer the execution of this.toggle(false) using requestAnimationFrame and setTimeout, but it didn't work either.



from Close a dropdown when an element within it is clicked

No comments:

Post a Comment