使用带有 React JS 的箭头键递增和递减时获取重复项

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

我又回到了这个问题,试图通过菜单 increment/decrement 并试图让我的头穿过墙壁来解决这个问题。我觉得我快到了,但缺少一些东西。我 运行 在单击按钮之前无法向右箭头,而且我单击的按钮不会删除 class。任何帮助将不胜感激。

按钮 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>
    );
  }
}

..

菜单 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>
    );
  }
}

您可以从 activeTab 中获取标签编号,而不是在按下箭头键时使用光标变量:

  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)
      });
    }
  }

然后在设置 className 的地方将 cursor === i || activeTab === label ? "active" : null 更改为 activeTab === label ? "active" : null

编辑:如果您打算更改章节标题,我想您可能会这样。

  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]
      });
    }
  }

上面有很多重复和不必要的代码。我花了一些时间重构并使其更 modular/easier 好用。

工作示例:https://codesandbox.io/s/rlwq35oz4o

变化:

  • Tabs 就像多个 Tab 组件的容器
  • Tab 是一个简单的可重用组件,需要 titlechildren
  • 所有选项卡现在都由 activeTab 状态管理,并与映射的 key 进行比较(对于唯一的 id,这很容易 changed/implemented)
  • 利用 prevProps.children.length 确定 setState 回调中的制表符长度
  • 已将 TabPanea(link 元素)更改为 b(未格式化的文本元素),因为它导致了 link 的样式问题使用相同的 role="tablist"
  • 嵌入到 TabBody
  • TabBody 中的 a 元素添加了 word-wrap: break-word; 以防止它们破坏小屏幕的 section

index.js

import React from "react";
import { render } from "react-dom";
import Tabs, { Tab } from "./components/Tabs";
import "./styles.css";

const App = () => (
  <Tabs>
    <Tab title="Section 1">
      Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam euismod,
      tortor nec pharetra ultricies, ante erat imperdiet velit, nec laoreet enim
      lacus a velit.<a href="#">Nam luctus</a>, enim in interdum condimentum,
      nisl diam iaculis lorem, vel volutpat mi leo sit amet lectus. Praesent non
      odio bibendum magna bibendum accumsan.
    </Tab>
    <Tab title="Section 2">
      Nullam at diam nec arcu suscipit auctor non a erat. Sed et magna semper,
      eleifend magna non, facilisis nisl. Proin et est et lorem dictum finibus
      ut nec turpis. Aenean nisi tortor, euismod a mauris a, mattis scelerisque
      tortor. Sed dolor risus, varius a nibh id, condimentum lacinia est. In
      lacinia cursus odio a aliquam. Curabitur tortor magna, laoreet ut rhoncus
      at, sodales consequat
    </Tab>
    <Tab title="Section 3">
      Phasellus ac tristique orci. Nulla maximus
      <a href="">justo nec dignissim consequat</a>. Sed vehicula diam sit amet
      mi efficitur vehicula in in nisl. Aliquam erat volutpat. Suspendisse lorem
      turpis, accumsan consequat consectetur gravida,
      <a href="#">pellentesque ac ante</a>. Aliquam in commodo ligula, sit amet
      mollis neque. Vestibulum at facilisis massa.
    </Tab>
    <Tab title="Section 4">
      Nam luctus, enim in interdum condimentum, nisl diam iaculis lorem, vel
      volutpat mi leo sit amet lectus. Praesent non odio bibendum magna bibendum
      accumsan. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam
      euismod, tortor nec pharetra ultricies, ante erat imperdiet velit, nec
      laoreet enim lacus a velit.
    </Tab>
  </Tabs>
);

render(<App />, document.getElementById("root"));

components/Tabs.js

import React, { Component } from "react";
import PropTypes from "prop-types";
import TabPane from "./TabPane";
import TabBody from "./TabBody";

export const Tab = ({ title, children }) => <div tab={title}>{children}</div>;

Tab.propTypes = {
  children: PropTypes.node.isRequired,
  title: PropTypes.string.isRequired
};

class Tabs extends Component {
  state = { activeTab: 0 };

  componentDidMount = () => {
    document.addEventListener("keydown", this.handleKeyPress, false);
    window.focus();
  }

  componentWillUnmount = () =>
    document.removeEventListener("keydown", this.handleKeyPress, false);

  handleClickTabItem = ({ target: { id } }) =>
    this.setState({ activeTab: ~~id });

  handleKeyPress = ({ keyCode }) => {
    if (keyCode === 37 || keyCode === 39) {
      this.setState((prevState, prevProps) => {
        const nextTab = keyCode === 37 
          ? prevState.activeTab - 1 
          : prevState.activeTab + 1;

        return nextTab >= 0 && nextTab < prevProps.children.length
          ? { activeTab: nextTab }
          : null;
      });
    }
  };

  render = () => {
    const { activeTab } = this.state;
    const { children } = this.props;
    return (
      <div className="tabbed">
        <ul role="tablist">
          {children.map(({ props }, key) => (
            <TabPane
              key={key}
              activeTab={activeTab}
              onClickTabItem={this.handleClickTabItem}
              id={key}
              {...props}
            />
          ))}
          <div className="tab-content">
            {children.map(({ props }, key) =>
              key === activeTab ? (
                <TabBody key={key} id={key} {...props} />
              ) : null
            )}
          </div>
        </ul>
      </div>
    );
  };
}

Tabs.propTypes = {
  children: PropTypes.node.isRequired
};

export default Tabs;

components/TabPane.js

import React from "react";
import PropTypes from "prop-types";

const TabPane = ({ activeTab, id, onClickTabItem, title, ...rest }) => (
  <li role="presentation">
    <b
      id={id}
      aria-selected={activeTab === id ? "true" : null}
      onClick={onClickTabItem}
      role="tab"
      {...rest}
    >
      {title}
    </b>
  </li>
);

TabPane.propTypes = {
  activeTab: PropTypes.number.isRequired,
  id: PropTypes.number.isRequired,
  onClickTabItem: PropTypes.func.isRequired,
  title: PropTypes.string.isRequired
};

export default TabPane;

components/TabBody.js

import React from "react";
import PropTypes from "prop-types";

const TabBody = ({ title, id, children }) => (
  <section id={id} role="tabpanel" tabIndex="-1" aria-labelledby={id}>
    <h2>{title}</h2>
    <div>{children}</div>
  </section>
);

TabBody.propTypes = {
  children: PropTypes.node.isRequired,
  id: PropTypes.number.isRequired,
  title: PropTypes.string.isRequired
};

export default TabBody;

styles.css

body {
  max-width: 40rem;
  padding: 0 1rem;
  font-size: 125%;
  line-height: 1.5;
  margin: 1.5rem auto;
  font-family: "Lato", Arial, sans-serif;
  font-size: 16px;
}

* {
  color: inherit;
  margin: 0;
}

[role="tablist"] {
  padding: 0;
}

[role="tablist"] li,
[role="tablist"] b {
  display: inline-block;
}

[role="tablist"] b {
  text-decoration: none;
  padding: 0.5rem 1em;
  cursor: pointer;
}

[role="tablist"] a {
  text-decoration: none;
  padding-left: 0.2rem;
  word-wrap: break-word;
}

[role="tablist"] [aria-selected] {
  border: 2px solid;
  background: #fff;
  border-bottom: 0;
  position: relative;
  top: 2px;
}

[role="tabpanel"] {
  border: 2px solid;
  padding: 1.5rem;
}

[role="tabpanel"] * + * {
  margin-top: 0.75rem;
}

*:focus {
  outline: none;
  box-shadow: inset 0 0 0 4px lightBlue;
}

@media (max-width: 550px) {
  [role="tablist"] li,
  [role="tablist"] b {
    display: block;
    position: static;
  }

  [role="tablist"] b {
    border: 2px solid #222 !important;
  }

  [role="tablist"] li + li b {
    border-top: 0 !important;
  }

  [role="tablist"] [aria-selected] {
    position: static;
  }

  [role="tablist"] [aria-selected]::after {
    content: "[=14=]20⬅";
  }

  [role="tabpanel"] {
    border-top: 0;
  }
}

section a {
  color: rgb(66, 133, 244);
}