Ref issue using react-hook-form - reactjs

I'm trying to create a form validation with react-hook-form in my current project. I've already tried different approaches but always I got errors because of the ref attribute. If I change the <FormField> to input, it starts to work.
Any idea how to solve this?
Contact
import React from 'react';
import { useForm } from "react-hook-form";
import FormField from '../../components/FormField';
import Button from '../../components/Button';
const Contact = () => {
const { handleSubmit, register, errors } = useForm();
const onSubmit = values => console.log(values);
return (
<form onSubmit={handleSubmit(onSubmit)}>
<FormField
name="email"
onChange={() => { console.log("changed!") }}
ref={register({
required: "Required",
pattern: {
value: /^[A-Z0-9._%+-]+#[A-Z0-9.-]+\.[A-Z]{2,}$/i,
message: "invalid email address"
}
})}
/>
<p style={{ color: "red" }}>
{errors.email && errors.email.message}
</p>
<Button>Submit</Button>
</form>
);
};
export default Contact;
FormField
import React from "react";
import PropTypes from "prop-types";
import styled, { css } from "styled-components";
const FormFieldWrapper = styled.div`
position: relative;
textarea {
min-height: 150px;
}
input[type="color"] {
padding-left: 67px;
}
`;
const Label = styled.label``;
Label.Text = styled.span`
color: #e5e5e5;
height: 57px;
position: absolute;
top: 0;
left: 16px;
display: flex;
align-items: center;
transform-origin: 0% 0%;
font-size: 18px;
font-style: normal;
font-weight: 300;
transition: 0.1s ease-in-out;
`;
const Input = styled.input`
background: #53585d;
color: #f5f5f5;
display: block;
width: 100%;
height: 57px;
font-size: 18px;
outline: 0;
border: 0;
border-top: 4px solid transparent;
border-bottom: 4px solid #53585d;
padding: 16px 16px;
margin-bottom: 45px;
resize: none;
border-radius: 4px;
transition: border-color 0.3s;
&:focus {
border-bottom-color: var(--primary);
}
&:focus:not([type="color"]) + ${Label.Text} {
transform: scale(0.6) translateY(-10px);
}
${({ value }) => {
const hasValue = value.length > 0;
return (
hasValue &&
css`
&:not([type="color"]) + ${Label.Text} {
transform: scale(0.6) translateY(-10px);
}
`
);
}}
`;
function FormField({ label, type, name, value, onChange, ref }) {
const isTypeTextArea = type === "textarea";
const tag = isTypeTextArea ? "textarea" : "input";
return (
<FormFieldWrapper>
<Label>
<Input
as={tag}
type={type}
value={value}
name={name}
onChange={onChange}
ref={ref}
/>
<Label.Text>{label}:</Label.Text>
</Label>
</FormFieldWrapper>
);
}
FormField.defaultProps = {
type: "text",
value: "",
};
FormField.propTypes = {
label: PropTypes.string,
name: PropTypes.string.isRequired,
type: PropTypes.string,
value: PropTypes.string,
onChange: PropTypes.func,
ref: PropTypes.func
};
export default FormField;
Errors:

Referring to the docs the register should be used as below so we won't get refs issues and also the register will change the value inside the input so we don't need to pass a value prop :
Contact :
import React from "react";
import { useForm } from "react-hook-form";
import FormField from "../../components/FormField";
import Button from "../../components/Button";
const Contact = () => {
const { handleSubmit, register, errors } = useForm();
const onSubmit = (values) => console.log("values", values);
return (
<form onSubmit={handleSubmit(onSubmit)}>
<FormField
name="email"
onChange={() => {
console.log("changed!");
}}
register={register}
/>
<p style={{ color: "red" }}>{errors.email && errors.email.message}</p>
<Button>Submit</Button>
</form>
);
};
export default Contact;
FormField :
import React from "react";
import PropTypes from "prop-types";
import styled, { css } from "styled-components";
const FormFieldWrapper = styled.div`
position: relative;
textarea {
min-height: 150px;
}
input[type="color"] {
padding-left: 67px;
}
`;
const Label = styled.label``;
Label.Text = styled.span`
color: #e5e5e5;
height: 57px;
position: absolute;
top: 0;
left: 16px;
display: flex;
align-items: center;
transform-origin: 0% 0%;
font-size: 18px;
font-style: normal;
font-weight: 300;
transition: 0.1s ease-in-out;
`;
const Input = styled.input`
background: #53585d;
color: #f5f5f5;
display: block;
width: 100%;
height: 57px;
font-size: 18px;
outline: 0;
border: 0;
border-top: 4px solid transparent;
border-bottom: 4px solid #53585d;
padding: 16px 16px;
margin-bottom: 45px;
resize: none;
border-radius: 4px;
transition: border-color 0.3s;
&:focus {
border-bottom-color: var(--primary);
}
&:focus:not([type="color"]) + ${Label.Text} {
transform: scale(0.6) translateY(-10px);
}
${({ value = {} }) => { // here you should find an other approch because there is no value props
const hasValue = value.length > 0;
return (
hasValue &&
css`
&:not([type="color"]) + ${Label.Text} {
transform: scale(0.6) translateY(-10px);
}
`
);
}}
`;
const FormField = ({ label, type, name, onChange, register }) => {
const isTypeTextArea = type === "textarea";
const tag = isTypeTextArea ? "textarea" : "input";
return (
<FormFieldWrapper>
<Label>
<Input
as={tag}
type={type}
// value={value} it's not a controlled input! so the register'ill provide the value
name={name}
onChange={onChange}
ref={register({
required: "Required",
pattern: {
value: /^[A-Z0-9._%+-]+#[A-Z0-9.-]+\.[A-Z]{2,}$/i,
message: "invalid email address",
},
})}
/>
<Label.Text>{label}:</Label.Text>
</Label>
</FormFieldWrapper>
);
};
FormField.defaultProps = {
type: "text",
value: "",
};
FormField.propTypes = {
label: PropTypes.string,
name: PropTypes.string.isRequired,
type: PropTypes.string,
value: PropTypes.string,
onChange: PropTypes.func,
ref: PropTypes.func,
};
export default FormField;

Related

Implementing animation when removing Toast

I have a working ToastList that enables me to click a button multiple times and generate a toast each time. On entry, I have an animation, but when I remove the toast, I do not get an animation. I am using Typescript and functional components.
My component is as follows:
import React, { useCallback, useEffect, useState } from 'react';
import * as Styled from './Toast.styled';
export interface ToastItem {
id: number;
title: string;
description: string;
backgroundColor: string;
}
export interface ToastProps {
toastList: ToastItem[];
setList: React.Dispatch<React.SetStateAction<ToastItem[]>>;
}
export default function Toast(props: ToastProps) {
const deleteToast = useCallback(
(id: number) => {
const toastListItem = props.toastList.filter((e) => e.id !== id);
props.setList(toastListItem);
},
[props.toastList, props.setList]
);
useEffect(() => {
const interval = setInterval(() => {
if (props.toastList.length) {
deleteToast(props.toastList[0].id);
}
}, 2000);
return () => {
clearInterval(interval);
};
}, [props.toastList, deleteToast]);
return (
<Styled.BottomRight>
{props.toastList.map((toast, i) => (
<Styled.Notification
key={i}
style={{ backgroundColor: toast.backgroundColor }}
>
<button onClick={() => deleteToast(toast.id)}>X</button>
<div>
<Styled.Title>{toast.title}</Styled.Title>
<Styled.Description>{toast.description}</Styled.Description>
</div>
</Styled.Notification>
))}
</Styled.BottomRight>
);
}
And my styling is done using styled-components and is as follows:
import styled, { keyframes } from 'styled-components';
export const Container = styled.div`
font-size: 14px;
position: fixed;
z-index: 10;
& button {
float: right;
background: none;
border: none;
color: #fff;
opacity: 0.8;
cursor: pointer;
}
`;
const toastEnter = keyframes`
from {
transform: translateX(100%);
}
to {
transform: translateX(0%);
}
}
`;
export const BottomRight = styled(Container)`
bottom: 2rem;
right: 1rem;
`;
export const Notification = styled.div`
width: 365px;
color: #fff;
padding: 15px 15px 10px 10px;
margin-bottom: 1rem;
border-radius: 4px;
box-shadow: 0 0 10px #999;
opacity: 0.9;
transition .1s ease;
animation: ${toastEnter} 0.5s;
&:hover {
box-shadow: 0 0 12px #fff;
opacity: 1;
}
`;
export const Title = styled.p`
font-weight: 700;
font-size: 16px;
text-align: left;
margin-bottom: 6px;
`;
export const Description = styled.p`
text-align: left;
`;
When I click a button, I just add an element to the state list, like:
toastProps = {
id: list.length + 1,
title: 'Success',
description: 'Sentence copied to clipboard!',
backgroundColor: '#5cb85c',
};
setList([...list, toastProps]);
My component is rendered like:
<Toast toastList={list} setList={setList}></Toast>
I would like to add animation when a toast exits, but do not know how. I have tried changing the style according to an additional prop I would send to the styled components, but this way all the toasts animate at the same time. My intuition is that I should use useRef(), but I am not sure how. Thanks in advance for any help you can provide.

how to apply a multiconditional to style a react component?

I have a form where the borders of the inputs have 2 colors
-Grey: when the component is loaded
-blue: when the input is not empty
and I would like to apply a third color when the user presses the register button and the inputs are not empty that changes the border color to red. but I do not know how
Page
const hanleClear= () => {
setCompania(0);
setDepartamento(0);
setUnidad(0);
setLocalidad(0);
setActivoTipo(0);
setActivoTipoCatego(0);
setMarca("");
setColor("");
setModelo("");
setComponente("");
setSerial("");
setObservacion("");
};
const hanleSerial= (e) => {
setSerial(e.target.value.toUpperCase());
};
const handleSumit = function (e) {
e.preventDefault();
if (serial === 0) {
// Apply color red.
}
let form = {
idinformacion: compania,
iddepartamento: departamento,
idunidad: unidad,
};
let Data = JSON.stringify(form );
ServiceSerial.Create(Data);
};
<InputGroup
input_label={"Serial"}
input_type={"text"}
input_value={serial}
input_placeholder={"Serial"}
state_name={serial}
set_state_name={setSerial}
on_change={hanleSerial}
/>
<button onClick={hanleSumit}>Register</button>
<br />
<button onClick={hanleClear}>Limpiar</button>
<br />
Componenet
import {
Input,
Label,
InputGroupContainer,
WrapperInput,
} from "../components/FormStyled";
const InputGroup = function ({
input_label,
input_type,
input_value,
input_name,
input_placeholder,
on_change,
on_key_down,
}) {
return (
<InputGroupContainer>
<Label>{input_label}</Label>
<WrapperInput>
<Input
type={input_type}
placeholder={input_placeholder}
value={input_value}
name={input_name}
onChange={on_change}
onKeyDown={on_key_down}
className={!input_value ? "" : "Activated"}
/>
</WrapperInput>
{/* <span>{errorMessage}</span> */}
</InputGroupContainer>
);
};
export default InputGroup;
css
// LABEL
export const Label = styled.label`
font-size: 16px;
cursor: pointer;
letter-spacing: 0.8px;
color: black;
margin-top: 15px;
margin-bottom: 5px;
font-family: "Inter";
font-weight: 900;
`;
// INPUT
export const Input = styled.input`
font-size: 16px;
font-weight: normal;
font-family: "Inter";
letter-spacing: 0.3px;
width: 100%;
height: 40px;
line-height: 40px;
padding: 0px 5px 0px 10px;
border: 2px solid #ccc;
border-radius: 0px;
transition: 0.3s ease all;
border-radius: 3px;
color: black;
&:focus {
outline: none;
border: 2px solid #00aea9;
}
&.Activated {
border: 2px solid #00aea9;
}
`;
// Input Group
export const InputGroupContainer = styled.div`
margin: 15px 0px;
position: relative;
width: 100%;
`;
I can give you a general solution. If you get it, you can adapt it to your specific code.
I use a state called inputState to denote if the input is empty/filled/filled&pressed.
Then your component just composes style based on that state whenever rendering.
function ComponentABC() {
const [inputState, setInputState] = useState(1);
let inputStyle = {borderCorlor: 'grey'};
if(inputState == 1) { /* empty */
inputStyle = {borderCorlor: 'grey'};
}
else if(inputState == 2) { /* filled */
inputStyle = {borderCorlor: 'blue'};
}
else if(inputState == 3) { /* filled & pressed */
inputStyle = {borderCorlor: 'red'};
}
// when pressed button, change color
function handleRegister(e) {
if(inputValue) {
setInputState(3);
}
}
function handleInputChange(e) {
// when input is filled, change color
if(inputValue) {
setInputState(2);
}
// when input is empty, change color
else if(!inputValue) {
setInputState(1);
}
}
return (
<div>
<input style={inputStyle} onChange={handleInputChange}/>
<button onClick={handleRegister}>register</button>
</div>
);
}

React how to repopulate values when back button clicked

In react js i get value from api then i used component to populated vale after back button clicked i don`t want to remove my value i want to my current data that work in text filed but it not work on
select it always remove it value.it show error name not defined.
i want repopulate select when clicked back button
import React from 'react';
import styled from 'styled-components';
import Select from 'react-select';
import { useNavigate } from 'react-router-dom';
import { QsrQoreApi, Eats365, StoreHub } from './Sources';
import SourceStoreSelection from './SourceStoreSelection';
import svgs from '../../shared/svgs';
import { Modal, withAuth } from '../../shared';
const StyledSourceSettings = styled.div`
& .header {
display: flex;
height: 5.2rem;
align-items: center;
border-bottom: 0.1rem solid #edf2f7;
}
& .back {
height: 2rem;
width: 2rem;
margin-left: 2rem;
}
& .title {
margin-left: max(1.5rem, calc(((100vw - 21rem) / 2) - 4rem));
width: 21rem;
height: 2rem;
font-size: 1.6rem;
font-weight: 700;
line-height: 2rem;
text-align: center;
color: #2d3748;
}
& .source-form {
padding: 2rem;
display: grid;
grid-template-columns: 1fr;
}
& .login {
width: 28rem;
margin: 0 auto 0.4rem auto;
font-size: 1.6rem;
font-weight: 700;
color: #2d3748;
}
& .form-input {
margin: 1.5rem auto 0 auto;
width: 28rem;
font-size: 1.6rem;
color: #4a5568;
}
& .form-input > .input {
width: 28rem;
margin: 0.5rem 0 0 0;
height: 3.6rem;
font-size: 1.6rem;
border: 0.1rem solid #a0aec0;
border-radius: 0.4rem;
color: #4a5568;
padding: 1rem;
}
& .form-input > .select {
margin: 0.5rem 0 0 0;
}
& .buffer-152px {
height: 15.2rem;
}
& .buffer-91px {
height: 9.1rem;
}
& .selected-store {
width: 28rem;
height: 2rem;
margin: 2rem auto 0 auto;
font-size: 1.6rem;
font-weight: 700;
color: #2d3748;
}
& .selected-store-detail {
display: flex;
height: 5.1rem;
margin: 0 auto;
align-items: center;
border-bottom: 0.1rem solid #edf2f7;
font-size: 1.6rem;
color: #4a5568;
}
& .selected-store-detail > .map {
height: 2rem;
width: 2rem;
}
& .selected-store-detail > .name {
height: 2rem;
width: 22rem;
margin-left: 1rem;
}
& .selected-store-detail > .forward {
height: 2rem;
width: 2rem;
margin-left: 1rem;
}
& .next-btn {
width: 28rem;
height: 3.6rem;
margin: auto;
margin-top: max(5.3rem, calc(100vh - 47.1rem));
border: 0.1rem solid #cbd5e0;
border-radius: 0.4rem;
background: #ffffff;
box-shadow: 0rem 0.2rem 0.4rem rgba(24, 39, 75, 0.12);
padding: 1rem;
font-size: 1.6rem;
font-weight: 600;
color: #4a5568;
}
& .next-btn:disabled,
& .next-btn[disabled] {
box-shadow: none;
color: #cbd5e0;
}
`;
const sourceOpts = [
{ value: 'QsrQoreApi', label: 'QsrQoreApi' },
{ value: 'Eats365', label: 'Eats365' },
{ value: 'StoreHub', label: 'StoreHub' },
];
const sourceSelectStyles = {
control: (provided) => ({
...provided,
width: '28rem',
minHeight: '3.6rem',
height: '3.6rem',
fontSize: '1.6rem',
border: '0.1rem solid #a0aec0',
borderRadius: '0.4rem',
color: '#4a5568',
}),
indicatorSeparator: () => {},
singleValue: (provided) => ({
...provided,
color: '#4a5568',
}),
valueContainer: (provided) => ({
...provided,
padding: '0.2rem 0.8rem',
}),
placeholder: (provided) => ({
...provided,
margin: 'auto 0.2rem',
}),
input: (provided) => ({
...provided,
margin: '0.2rem',
padding: '0.2rem auto',
}),
dropdownIndicator: (provided) => ({
...provided,
'& svg': {
height: '2rem',
width: '2rem',
},
}),
indicatorsContainer: (provided) => ({
...provided,
'& > div': {
padding: '0.8rem',
},
}),
menu: (provided) => ({
...provided,
borderRadius: '0.4rem',
margin: '0.8rem auto',
}),
menuList: (provided) => ({
...provided,
maxHeight: '30rem',
padding: '0.4rem 0',
}),
option: (provided) => ({
...provided,
padding: '0.8rem 1.2rem',
}),
};
function AddSource(props) {
const { setForm, store, setStore , source} = props;
const [sourceAdditionalInfos, setSourceAdditionalInfos] = React.useState({});
const [sourceStore, setSourceStore] = React.useState(null);
const [showModal, setShowModal] = React.useState(false);
const navigate = useNavigate();
React.useEffect(() => {
if(store.source){
const infos = store.source.additionalInfos.reduce((result, { key, value }) => {
const temp = result;
temp[key] = value;
return temp;
}, {});
setSourceAdditionalInfos(infos);
}
},[]);
React.useEffect(() => {
if (store.source) {
const infospos = store.source.type
const value = infospos;
setSourceStore(value);
}
},[]);
const onBackClick = () => {
navigate(-1);
};
const onSourceSelectChange = (source) => {
if (source) {
setStore({ ...store, source: { type: source.value } });
setSourceStore(null);
}
};
const handleChange = (e) => setSourceAdditionalInfos({ ...sourceAdditionalInfos, [e.target.id]: e.target.value });
const onSubmit = (e) => {
e.preventDefault();
if (!sourceStore) setShowModal(true);
else {
const addInfos = Object.entries(sourceAdditionalInfos).map(([k, v]) => ({ key: k, value: v }));
let st = {
...store,
source: {
...store.source,
...sourceStore.value,
additionalInfos: addInfos.concat(sourceStore.value.additionalInfos),
},
};
setStore(st);
setForm('addStore');
}
};
let renderSource = <div className="buffer-152px" />;
if (store.source) {
switch (store.source.type) {
case 'QsrQoreApi':
renderSource = (
<QsrQoreApi
handleChange={handleChange}
sourceAdditionalInfos={sourceAdditionalInfos}
disableInput={sourceStore}
/>
);
break;
case 'Eats365':
renderSource = (
<Eats365
handleChange={handleChange}
sourceAdditionalInfos={sourceAdditionalInfos}
disableInput={sourceStore}
/>
);
break;
case 'StoreHub':
renderSource = (
<StoreHub
handleChange={handleChange}
sourceAdditionalInfos={sourceAdditionalInfos}
disableInput={sourceStore}
/>
);
break;
default:
renderSource = <div className="buffer-152px" />;
break;
}
}
let renderSelectedSourceStore = '';
if (sourceStore) {
renderSelectedSourceStore = (
<>
<div className="selected-store">Store selection</div>
<div className="selected-store-detail" onClick={() => setShowModal(true)}>
<img src={svgs.map} className="map" alt="map" />
<div className="name">{sourceStore.value.name}</div>
<img src={svgs.forward} className="forward" alt="forward" />
</div>
</>
);
} else {
renderSelectedSourceStore = <div className="buffer-91px" />;
}
return (
<StyledSourceSettings>
<div className="header">
<img src={svgs.back} className="back" alt="back" onClick={onBackClick} />
<div className="title">POS System</div>
</div>
<form className="source-form" onSubmit={onSubmit}>
<div className="login">Login credentials</div>
<label className="form-input" htmlFor="type">
<div className="label">POS System</div>
<div className="select">
<Select
options={sourceOpts}
styles={sourceSelectStyles}
placeholder="Choose one"
onChange={onSourceSelectChange}
inputId="type"
isDisabled={sourceStore}
/>
</div>
</label>
{renderSource}
{renderSelectedSourceStore}
<input className="next-btn" type="submit" value="NEXT" disabled={!(store.source && store.source.type)} />
</form>
<Modal onClose={() => setShowModal(false)} open={showModal}>
<SourceStoreSelection
onSubmitted={() => setShowModal(false)}
onClosed={() => setShowModal(false)}
store={store}
sourceAdditionalInfos={sourceAdditionalInfos}
sourceStore={sourceStore}
setSourceStore={setSourceStore}
/>
</Modal>
</StyledSourceSettings>
);
}
export default withAuth(AddSource);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
enter image description here
Try to change it to:
{sourceStore?.value?.name}

React input checkbox checked

I have use React-redux and styled components for my app. I store my initial state theme as a string which is light and dark. then I connect my styled components intial light theme and dark theme in my root app. My dark mood works fine when i used select options but when i used input checkbox it does not work. I never used input checkbox, after reading couple example I used checked and put my initial theme(which is coming from my redux store), then in my handleChange I did, if the event target has dark then dispatch the dark theme. But nothing happening in that handle change. don't know what i am doing wrong.
Here is my toggle component
import React, { useState } from 'react';
import styled from 'styled-components';
import { useDispatch, useSelector } from 'react-redux';
import { appSettings } from '../../state/appSettings';
import { TRootState } from '../../state/index';
export default function Toggle({ }: IProp) {
const dispatch = useDispatch();
const { "appSettings": appSettingState } = useSelector((state: TRootState) => state);
const { theme } = appSettingState || {};
console.log(theme); // inital state which is "light".
return (
<>
{/* This input checkbox does not work */}
<CheckBoxWrapper>
<CheckBox
onChange={(e) => { // This function does not work
e.target.value === `dark` ?
dispatch(appSettings?.actions?.enableDarkTheme()) :
dispatch(appSettings?.actions?.enableLightTheme());
}}
id="toggleSwitch"
type="checkbox"
Checked={theme === `light`}
/>
<CheckBoxLabel htmlFor="toggleSwitch" />
</CheckBoxWrapper>
<br></br>
{/* THIS SELECT OPTIONS WORK FINE. AND I CAN GET DARK AND LIGHT THEME */}
<h2>Theme</h2>
<select
name="theme"
id="theme-select"
value={theme}
onChange={(e) => {
if (e.target.value === `dark`) {
dispatch(appSettings?.actions?.enableDarkTheme());
} else {
dispatch(appSettings?.actions?.enableLightTheme());
}
}}
>
<option value="dark">Dark</option>
<option value="light">Light</option>
</select>
</>
);
}
// This toogle input styled
const CheckBoxWrapper = styled.div`
position: fixed;
top:10px;
right:10px;
`;
const CheckBoxLabel = styled.label`
position: absolute;
top: 0;
left: 0;
width: 42px;
height: 26px;
border-radius: 15px;
background: #bebebe;
cursor: pointer;
&::after {
content: "";
display: block;
border-radius: 50%;
width: 18px;
height: 18px;
margin: 3px;
background: #ffffff;
box-shadow: 1px 3px 3px 1px rgba(0, 0, 0, 0.2);
transition: 0.2s;
}
`;
const CheckBox = styled.input`
opacity: 0;
z-index: 1;
border-radius: 15px;
width: 42px;
height: 26px;
&:checked + ${CheckBoxLabel} {
background: #4fbe79;
&::after {
content: "";
display: block;
border-radius: 50%;
width: 18px;
height: 18px;
margin-left: 21px;
transition: 0.2s;
}
}
`;
Check the change event's checked property. e.target.checked
<CheckBox
onChange={(e: any) => {
e.target.checked
? dispatch(appSettings?.actions?.enableDarkTheme())
: dispatch(appSettings?.actions?.enableLightTheme());
}}
id="toggleSwitch"
type="checkbox"
Checked={theme === `light`}
/>

Radio Buttons with Styled Components for React

I am trying to implement a radio button set in React using Styled components using this pen as an example however the sibling selector is tripping me up. How to turn this
.radio-input:checked ~ .radio-fill {
width: calc(100% - 4px);
height: calc(100% - 4px);
transition: width 0.2s ease-out, height 0.2s ease-out;
}
.radio-input:checked ~ .radio-fill::before {
opacity: 1;
transition: opacity 1s ease;
}
into css in a styled component?
Can anyone point out my mistake or make a quick pen demo? Thanks! Here is my full code:
import React from 'react';
import PropTypes from 'prop-types';
import styled from 'styled-components';
import { colors } from '../../../theme/vars';
export class RadioGroup extends React.Component {
getChildContext() {
const { name, selectedValue, onChange } = this.props;
return {
radioGroup: {
name, selectedValue, onChange,
},
};
}
render() {
const { Component, name, selectedValue, onChange, children, ...rest } = this.props;
return <Component role="radiogroup" {...rest}>{children}</Component>;
}
}
RadioGroup.childContextTypes = {
radioGroup: PropTypes.object,
};
RadioGroup.propTypes = {
name: PropTypes.string,
selectedValue: PropTypes.oneOfType([
PropTypes.string,
PropTypes.number,
PropTypes.bool,
]),
children: PropTypes.node.isRequired,
Component: PropTypes.oneOfType([
PropTypes.string,
PropTypes.func,
PropTypes.object,
]),
};
RadioGroup.defaultProps = {
name: '',
selectedValue: '',
Component: 'div',
};
// eslint-disable-next-line react/no-multi-comp
export class Radio extends React.Component {
render() {
const { name, selectedValue } = this.context.radioGroup;
const { onChange, value, labelText } = this.props;
let checked = false;
if (selectedValue !== undefined) {
checked = (value === selectedValue);
}
console.log('value: ', value);
console.log('checked: ', checked);
console.log('selectedValue: ', selectedValue);
return (
<Root>
<Input
type="radio"
name={name}
value={value}
checked={checked}
aria-checked={checked}
onChange={onChange}
/>
<Fill />
{/* <div style={{ marginLeft: '25px' }}>{labelText}</div> */}
</Root>
);
}
}
Radio.contextTypes = {
radioGroup: PropTypes.object,
};
Radio.propTypes = {
onChange: PropTypes.func,
value: PropTypes.string,
labelText: PropTypes.string,
};
Radio.defaultProps = {
onChange: () => {},
value: '',
labelText: '',
};
const Root = styled.div`
width: ${props => props.size ? props.size : 20}px;
height: ${props => props.size ? props.size : 20}px;
position: relative;
&::before {
content: '';
border-radius: 100%;
border: 1px solid ${props => props.borderColor ? props.borderColor : '#DDD'};
background: ${props => props.backgroundColor ? props.backgroundColor : '#FAFAFA'};
width: 100%;
height: 100%;
position: absolute;
top: 0;
box-sizing: border-box;
pointer-events: none;
z-index: 0;
}
`;
const Fill = styled.div`
background: ${props => props.fillColor ? props.fillColor : '#A475E4'};
width: 0;
height: 0;
border-radius: 100%;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
transition: width 0.2s ease-in, height 0.2s ease-in;
pointer-events: none;
z-index: 1;
&::before {
content: '';
opacity: 0;
width: calc(20px - 4px);
position: absolute;
height: calc(20px - 4px);
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
border: 1px solid ${props => props.borderActive ? props.borderActive : '#A475E4'};
border-radius: 100%;
}
`;
const Input = styled.input`
opacity: 0;
z-index: 2;
position: absolute;
width: 100%;
height: 100%;
margin: 0;
cursor: pointer;
&:focus {
outline: none;
}
&:checked {
${Fill} {
width: calc(100% - 8px);
height: calc(100% - 8px);
transition: width 0.2s ease-out, height 0.2s ease-out;
&::before {
opacity: 1;
transition: opacity 1s ease;
}
}
}
`;
and the usage of the component:
<RadioGroup name="setYAxis" onChange={e => this.toggleSetYAxis(e)} selectedValue={this.state.setYAxis}>
<Radio value="autoscale" labelText="Autoscale" />
<Radio value="manual" labelText="Manual" />
</RadioGroup>
I created a codesandbox to fix the css issue and the state management problem.
The provided code is simpler and does not rely on an outdated React context API (doc here)
You are missing the ~ before the Fill selector:
&:checked {
& ~ ${Fill} {
width: calc(100% - 8px);
height: calc(100% - 8px);
transition: width 0.2s ease-out, height 0.2s ease-out;
&::before {
opacity: 1;
transition: opacity 1s ease;
}
}
You also seem to have an issue with the way you're actually updating the state in this example but that's unrelated to styling: on RadioGroup, you aren't passing the onChange prop down to Radio. The way the spread operator makes it so your const rest only includes the properties that you haven't already defined inside the const. So you need to remove it the onChange declaration from there to make it go inside rest.
export class RadioGroup extends React.Component {
getChildContext() {
const { name, selectedValue, onChange } = this.props;
return {
radioGroup: {
name,
selectedValue,
onChange
}
};
}
render() {
const {
Component,
name,
selectedValue,
// Remove onChange from here
// onChange,
children,
...rest
} = this.props;
return (
<Component role="radiogroup" {...rest}>
{children}
</Component>
);
}
}
Working example: https://codesandbox.io/s/serene-cdn-1fw1i

Resources