Search code examples
javascriptreactjsincrementarrow-keys

Getting duplicates when Incrementing & Decrementing using the Arrow keys with React JS


I came back to this problem of trying to increment/decrement through a Menu and about to drive my head though wall figuring this out. I feel I'm almost there, but missing something. I am running into not being able to arrow right until I click on a button as well the button I clicked doesn't remove the class. Any help would much appreciated.

Button js:

...

class Button extends Component {
  onClick() {
    const { label, onClick } = this.props;
    onClick(label);
  }

  render() {
    const {
      onClick,
      props: { activeTab, label, tab, className }
    } = this;
    let ariaSelected = "";
    if (activeTab === label || className === "active") {
      ariaSelected += "true";
    }
    return (
      <li role="presentation">
        <a
          className={className}
          aria-selected={ariaSelected ? "true" : undefined} 
          onClick={e => this.onClick(e)}
          role="tab"
          id={"tab" + tab}
          //tabIndex="-1"
        >
          {label}
        </a>
      </li>
    );
  }
}

..

Menu Js:

class Menu extends Component {
  constructor(props) {
    super(props);
    this.state = {
      activeTab: this.props.children[0].props.label,
      cursor: 0
    };
    this.handleKeyDown = this.handleKeyDown.bind(this);
  }

  componentDidMount() {
    document.addEventListener("keydown", this.handleKeyDown, false);
  }

  componentWillUnmount() {
    document.removeEventListener("keydown", this.handleKeyDown, false);
  }

  handleKeyDown(e) {
    const { cursor } = this.state;
    const cnt = React.Children.count(this.props.children);
    if (e.keyCode === 37 && cursor > 0) {
      this.setState(prevState => ({
        cursor: prevState.cursor - 1
      }));
      console.log(cursor);
    } else if (e.keyCode === 39 && cursor < cnt - 1) {
      this.setState(prevState => ({
        cursor: prevState.cursor + 1
      }));
      console.log(cursor);
    }
  }

  onClickTabItem = tab => {
    this.setState({
      activeTab: tab
    });
  };

  render() {
    const {
      onClickTabItem,
      props: { children },
      state: { activeTab, cursor, className }
    } = this;

    return (
      <div className="tabbed">
        <ul role="tablist">
          {children.map((child, i) => {
            const { label, className } = child.props;
            return (
              <Tab
                activeTab={activeTab}
                key={label}
                label={label}
                onClick={onClickTabItem}
                tab={i}
                className={ cursor === i || activeTab === label ? "active" : null}
              />
            );
          })}
        </ul>
        <div className="tab-content">
          {children.map(child => {
            //if tab has label or active set, otherwise do nohthing
            if (child.props.label !== activeTab) return undefined;
            return child.props.children;
          })}
        </div>
      </div>
    );
  }
}

Solution

  • You could grab the label number from activeTab instead of having a cursor variable for when arrow keys are pressed:

      handleKeyDown(e) {
        const cnt = React.Children.count(this.props.children);
        const pos = ~~this.state.activeTab[8]  // get current position
        if (e.keyCode === 37 && pos > 1) {
          this.setState({
            activeTab: "Section " + (pos - 1)
          });
        } else if (e.keyCode === 39 && pos < cnt) {
          this.setState({
            activeTab: "Section " + (pos + 1)
          });
        }
      }
    

    and then where className is set change cursor === i || activeTab === label ? "active" : null to just activeTab === label ? "active" : null

    edit: This way is a little cleaner, in case you plan on changing section titles, which I imagine you might.

      handleKeyDown(e) {
        const labels = this.props.children.map((child) => {
          return child.props.label;
        });
        const cnt = labels.length;
        const pos = labels.indexOf(this.state.activeTab); // get current position
        if (e.keyCode === 37 && pos > 0) {
          this.setState({
            activeTab: labels[pos - 1]
          });
        } else if (e.keyCode === 39 && pos < cnt - 1) {
          this.setState({
            activeTab: labels[pos + 1]
          });
        }
      }