【问题标题】:How to create a custom radio button?如何创建自定义单选按钮?
【发布时间】:2020-05-20 11:58:56
【问题描述】:

我一直在网上寻找创建自定义复选框和单选按钮的方法。我设法创建了一个复选框,但我遇到的单选框问题是单击它不会激活或触发我输入的 onChange 调用。我目前有这个:

const customButtons = props => {
 const [isChecked, setChecked] = React.useState(false);

 const toggleCheck = e => {
  setChecked(e.target.checked || !isChecked)
 }

 return (
  <>
   <span className={container}>
    <input type={props.type} checked={isChecked} onChange={e => toggleCheck(e)} id={props.id} />
    <span></span>
   </span>
  </>
 ) 
}

我已经使用 css 来获取跨度以覆盖单选按钮并制作原始单选按钮 display: none; 但是当我点击跨度圆圈时它不会触发点击。我在跨度中添加了一个 onClick:&lt;span onClick={toggleCheck}&gt; 但这会导致单击两次时未选中单选按钮。在保持原始行为的同时实现自定义单选按钮的更好方法是什么?

如果这很重要,我也在使用 scss。

【问题讨论】:

    标签: css reactjs sass radio-button


    【解决方案1】:

    如果输入未设置为display: none,您的方法适用于radiocheckboxes,当然就像正常输入一样。但是,如果您将它们设置为 display: none,您实际上是在将它们隐藏在 UI 事件中,因此它们根本不会触发任何点击。


    TLDR:更好的方法是,在输入上设置opacity: 0,使用带有htmlFor 的标签来触发更改。然后设置标签伪元素的样式,使其看起来像收音机。

    这里是Live Code Sandbox 的链接


    由于您没有提供样式,因此很难判断您是如何在视觉上布置自定义输入的。以我的方式,

    • 大多数 UI 使用 radios 时只需要一个选项进行选择,checkboxes 用于多个选择。也就是说,很容易将状态从单个单选选项提升到父单选组组件,然后传递单选状态,同时让复选框控制其各自的状态,因为它们是相互独立的。

    • 另一个观察结果是您的收音机缺少name 属性(Reason why you were seeing multiple clicks with just fewer or no change at all) 使它们彼此脱节。要将它们放在一个组中,它们需要共享一个共同的name attr,这样您就只能针对每个无线电的选项值。

    • 一旦选择了所有没有公共组(无名称属性)的单选选项,您就无法在 UI 上取消选择它们,因此它们不会触发任何进一步的 onChange 事件。出于这个原因,如果它们不是强制性的,也建议添加一个重置选项以清除这些选项。

    这是每个 Radio Input 组件的代码。

    const RadioInput = ({ name, label, value, isChecked, handleChange }) => {
      const handleRadioChange = e => {
        const { id } = e.currentTarget;
        handleChange(id); // Send back id to radio group for comparison
      };
    
      return (
        <div>
          {/* Target this input: opacity 0 */}
          <input
            type="radio"
            className="custom-radio"
            name={name}
            id={value} // htlmlFor targets this id.
            checked={isChecked}
            onChange={handleRadioChange}
          />
          <label htmlFor={value}>
            <span>{label}</span>
          </label>
        </div>
      );
    };
    

    请参阅,通常在编写自定义输入以覆盖本机输入时,如果您以label 元素为目标并利用其for 又名htmlFor 属性来选择输入,则更容易。从以前的喧嚣中,很难用自定义元素取悦所有屏幕阅读器,尤其是当您覆盖的本机 input 设置为不显示时。

    在我看来,最好将其绝对定位,将其不透明度设置为零并让标签触发它发生变化。

    链接到Sandbox here


    组件的完整代码

    App.js

    
    
    import React, { useState } from "react";
    import "./styles.scss";
    
    /*
    Let Checkbox the controls its own state.
    Styling 'custom-radio', but only make the borders square in .scss file.
    */
    const CheckboxInput = ({ name, label }) => {
      const [isChecked, setIsChecked] = useState(false);
    
      const toggleCheck = e => {
        setIsChecked(() => !isChecked);
      };
    
      return (
        <div>
          <input
            type="checkbox"
            className="custom-radio"
            name={name}
            id={name}
            checked={isChecked}
            onChange={toggleCheck}
          />
          <label htmlFor={name}>
            <span>{label}</span>
          </label>
        </div>
      );
    };
    
    /*
    The custom radio input, uses the same styles like the checkbox, and relies on the 
    radio group parent for its state.
    */
    const RadioInput = ({ name, label, value, isChecked, handleChange }) => {
      const handleRadioChange = e => {
        const { id } = e.currentTarget;
        handleChange(id);
      };
    
      return (
        <div>
          <input
            type="radio"
            className="custom-radio"
            name={name}
            id={value}
            checked={isChecked}
            onChange={handleRadioChange}
          />
          <label htmlFor={value}>
            <span>{label}</span>
          </label>
        </div>
      );
    };
    
    /*
    This is what control the radio options. Each radio input has the same name attribute
    that way you can have multiple groups on the form.
    */
    const RadioGropupInput = () => {
      const [selectedInput, setSelectedInput] = useState("");
    
      const handleChange = inputValue => {
        setSelectedInput(inputValue);
      };
    
      return (
        <>
          <div>
            {/*
              You could map these values instead from an array of options
              And an option to clear the selections if they are not mandatory.
              PS: Add aria attributes for accessibility
            */}
            <RadioInput
              name="option"
              value="option-1"
              label="First Choice"
              isChecked={selectedInput === "option-1"}
              handleChange={handleChange}
            />
            <RadioInput
              name="option"
              value="option-2"
              label="Second Choice"
              isChecked={selectedInput === "option-2"}
              handleChange={handleChange}
            />
            <RadioInput
              name="option"
              value="option-3"
              label="Third Choice"
              isChecked={selectedInput === "option-3"}
              handleChange={handleChange}
            />
          </div>
        </>
      );
    };
    
    export default () => (
      <div className="App">
        <RadioGropupInput />
        <hr />
        <CheckboxInput name="remember-me" label="Remember Me" />
        <CheckboxInput name="subscribe" label="Subscribe" />
      </div>
    );
    
    

    风格

    
    
    .custom-radio {
      /* Hide the input element and target the next label that comes after it in the DOM */
      position: absolute;
      display: inline-block;
      opacity: 0;
    
      & + label {
        cursor: pointer;
        display: inline-block;
        position: relative;
        white-space: nowrap;
        line-height: 1rem;
        margin: 0 0 1.5rem 0;
        padding: 0 0 0 1rem;
        transition: all 0.5s ease-in-out;
    
        span {
          margin-left: 0.5rem;
        }
    
        /* Styles these pseudo elements to look like radio inputs. */
        &::before,
        &::after {
          content: '';
          position: absolute;
          color: #f5f5f5;
          text-align: center;
          border-radius: 0;
          top: 0;
          left: 0;
          width: 1rem;
          height: 1rem;
          transition: all 0.5s ease-in-out;
        }
    
        &::before {
          text-rendering: auto;
          -webkit-font-smoothing: antialiased;
          -moz-osx-font-smoothing: grayscale;
          line-height: 1rem;
          border-radius: 0;
          background-color: #ffffff;
          color: #ffffff;
          box-shadow: inset 0 0 0 1px #666565, inset 0 0 0 1rem #ffffff,
            inset 0 0 0 1rem #6b0707;
        }
    
        &:hover,
        &:focus,
        &:active {
          color: red;
          font-weight: bolder;
          transition: all 0.3s ease;
          outline: none;
    
          &::before {
            color: #ffffff;
            animation-duration: 0.5s;
            animation-name: changeSizeAnim;
            animation-iteration-count: infinite;
            animation-direction: alternate;
            box-shadow: inset 0 0 0 1px #6b0707, inset 0 0 0 16px #ffffff,
              inset 0 0 0 16px #6b0707;
          }
        }
      }
    
      &:focus,
      &:hover,
      &:checked {
        & + label {
          color: #220000 !important;
        }
    
        & + label::before {
          animation-duration: 0.3s;
          animation-name: selectCheckboxAnim;
          animation-iteration-count: 1;
          animation-direction: alternate;
          border: solid 1px rgba(255, 0, 0, 0.5);
          box-shadow: inset 0 0 0 1px #bc88d4, inset 0 0 0 0 #ffffff,
            inset 0 0 1px 2px #6d1717;
        }
      }
    
      &:checked {
        & + label::before {
          content: '✔'; /* Swap out this emoji checkmark with maybe an icon font of base svg*/
          background-color: #d43333;
          color: #ffffff;
          border: solid 1px rgba(202, 50, 230, 0.5);
          box-shadow: inset 0 0 0 1px #bc88d4, inset 0 0 0 0 #ffffff,
            inset 0 0 0 16px #d43333;
        }
      }
    
      & + label {
        &::before {
          border-radius: 50%;
        }
      }
    
      &[type=checkbox] {
        & + label {
          &::before {
            /* Remove the border radius on the checkboxes for a square effect */
            border-radius: 0;
          }
        }
      }
    
    
      @keyframes changeSizeAnim {
        from {
          box-shadow: 0 0 0 0 #d43333,
            inset 0 0 0 1px #d43333,
            inset 0 0 0 16px #FFFFFF,
            inset 0 0 0 16px #d43333;
        }
    
        to {
          box-shadow: 0 0 0 1px #d43333,
            inset 0 0 0 1px #d43333,
            inset 0 0 0 16px #FFFFFF,
            inset 0 0 0 16px #d43333;
        }
      }
    
      /* Add some animations like a boss, cause why would you hustle to build
      a custom component when you can't touch this!
      */
      @keyframes selectCheckboxAnim {
        0% {
          box-shadow: 0 0 0 0 #bc88d4,
            inset 0 0 0 2px #FFFFFF,
            inset 0 0 0 3px #d43333,
            inset 0 0 0 16px #FFFFFF,
            inset 0 0 0 16px #d43333;
        }
    
        100% {
          box-shadow: 0 0 20px 8px #eeddee,
            inset 0 0 0 0 white,
            inset 0 0 0 1px #bc88d4,
            inset 0 0 0 0 #FFFFFF,
            inset 0 0 0 16px #d43333;
        }
      }
    }
    
    /* Styles used to test out and reproduce out your approach */
    .container.control-experiment {
      background: #fee;
    
    
      span,
      input {
        display: flex;
    
        border: solid 1px red;
        width: 2rem;
        height: 2rem;
        line-height: 2rem;
        display: inline-block;
      }
    
      input {    
        position: absolute;
        margin: 0;
        padding: 0;
      }
    
      input[type='radio'] {
        // display: none; /* Uncommenting this out makes all your inputs unsable.*/
      }
    }
    

    我重复强调,不要忘记为自定义输入添加 aria 属性。 你可以再次测试live Sandbox

    【讨论】:

    • 感谢您的回复!很抱歉没有包含 css,我主要是按照我找到的教程进行操作,但不确定是什么原因造成的。在大多数情况下,它将跨度创建为一个圆圈并隐藏本机输入。然后它使用transform: (translate()) 来创建活动外观。我不知道 htmlFor 现在很高兴知道。谢谢!
    • 很高兴它有帮助。编码愉快。
    【解决方案2】:

    我尝试了您的示例并在 toggleCheck 中使用了登录,它被单选和复选框触发。

    自定义按钮组件

    import React from "react";
    
    const CustomButtons = props => {
        const [isChecked, setChecked] = React.useState(false);
    
        const toggleCheck = e => {
            console.log(e);
            setChecked(e.target.checked || !isChecked)
        };
    
        return (
            <>
               <span>
                <input type={props.type} checked={isChecked} onChange={e => toggleCheck(e)} id={props.id}/>
                <span>{props.text}</span>
               </span>
            </>
        )
    };
    
    export default CustomButtons
    

    如何在 App.js 中使用自定义按钮

    <CustomButtons type={"radio"} text={"One"}/>
    <CustomButtons type={"radio"} text={"Two"}/>
    
    <CustomButtons type={"checkbox"} text={"One"}/>
    <CustomButtons type={"checkbox"} text={"Two"}/>
    

    【讨论】:

    • 感谢您的回复。您是否编辑了 css 以使原始单选显示:无并且跨度覆盖原始单选按钮?当按钮存在时它可以工作,但问题是我想创建一个带有样式的自定义单选按钮,所以我不得不用跨度覆盖原始按钮
    • @Nukodi,您好,不客气,希望您已经找到答案
    猜你喜欢
    • 2021-12-26
    • 2013-10-07
    • 2020-05-18
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2020-07-21
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多