Radio Buttons with Styled Components for React - reactjs

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

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 add functions inside styled-components

i pass the open a prop to the component which is a boolean
i want to add a setTimeout function to hide a component but it shows syntax error
Timeout' is not assignable to parameter of type
'Interpolation<ThemedStyledProps<Pick<DetailedHTMLProps<HTMLAttributes<HTMLDivElement>,
HTMLDivElement>, "key" | keyof HTMLAttributes<HTMLDivElement>>
here is what i tried
const DrawerContent = styled.div<{ open: boolean; visible?: any }>`
transition: 0.3s all;
${({ open, visible }) =>
open
? css`
display: flex;
width: 200px;
height: 100px;
background-color: brown;
position: absolute;
top: 0;
`
: setTimeout(() => { // i want to add this line
css`
display: none;
`;
}, 200)}
`;
You can't get a return value from the setTimeout callback function. If you are trying to hide the node after some time, you should use keyframes:
const hide = keyframes`
to {
width:0;
height:0;
overflow:hidden;
}
`;
const DrawerContent = styled.div`
transition: 0.3s all;
${({ open, visible }) =>
open &&
css`
display: flex;
width: 200px;
height: 100px;
background-color: brown;
position: absolute;
top: 0;
animation: ${hide} 0s ease-in 2s forwards;
`}
`;
Another solution is to move the timeout function outside the DrawerContent component:
const DrawerContent = styled.div`
transition: 0.3s all;
${({ open, visible }) =>
open &&
css`
display: flex;
width: 200px;
height: 100px;
background-color: brown;
position: absolute;
top: 0;
`}
`;
function App () {
const [show, setShow] = useState(true);
useEffect(() => {
let timeout;
if (show) {
timeout = setTimeout(() => {
setShow(false);
}, 2000);
}
return () => clearTimeout(timeout);
}, [show]);
return (
<>
<div>
<DrawerContent open={show} visible />
</div>
</>
);
}

Lazy images load perfectly but have to reload abruptly when they reappear in the viewport

I have managed to lazy load a long list a images in React, with a loader. It works well. However, when scrolling back up, the images have disappeared and have to reload abruptly - without the smooth transition - to go back on screen. Long story short, the user experience is crap. Normally, already loaded images should remain displayed.
How to fix such a performance issue? Is it because of loading="lazy" native img attribute?
Here is a sandbox that reproduce the issue:
https://codesandbox.io/s/trusting-tdd-qlf9h?file=/src/App.tsx
Here is the code:
import React, { useEffect, useRef, useState } from "react";
import styled, { keyframes } from "styled-components";
interface ImageProps {
src: string;
lazy?: boolean;
width?: number;
height?: number;
alt?: string;
}
const Root = styled.div<{ width: number; height: number }>`
border-radius: 8px;
width: ${({ width }) => (width ? `${width}px` : "100%")};
height: ${({ height }) => (height ? `${height}px` : "100%")};
position: relative;
`;
const loading = keyframes`
0% {background-position: -468px 0;}
100% {background-position: 900px 0;}
`;
const Placeholder = styled.div`
position: absolute;
width: 100%;
height: 100%;
top: 0;
bottom: 0;
left: 0;
right: 0;
background: #f6f7f9;
animation-duration: 1s;
animation-fill-mode: forwards;
animation-iteration-count: infinite;
animation-name: ${loading};
animation-timing-function: linear;
background-image: linear-gradient(
to right,
#f6f7f9 0%,
rgba(175, 164, 164, 0.11) 20%,
#f6f7f9 40%,
#f6f7f8 80%,
#f6f7f9 100%
);
background-repeat: no-repeat;
background-size: 800px 400px;
height: 100%;
`;
const Picture = styled.img<{ loaded: boolean }>`
position: absolute;
width: 100%;
height: 100%;
top: 0;
bottom: 0;
left: 0;
right: 0;
object-fit: cover;
object-position: center;
opacity: ${({ loaded }) => (loaded ? 1 : 0)};
transition: opacity 1s;
`;
export default function Image({
src,
width,
height,
lazy = false,
alt
}: ImageProps) {
const [loaded, setLoaded] = useState(false);
const ref = useRef(null);
useEffect(() => {
if (ref.current && ref.current.complete) {
setLoaded(true);
}
}, []);
return (
<Root width={width} height={height}>
<Placeholder aria-hidden="true" />
<Picture
loading={lazy ? "lazy" : null}
src={src}
alt={alt}
width={width}
height={height}
ref={ref}
onLoad={() => setLoaded(true)}
loaded={loaded}
/>
</Root>
);
}
Bonus question: how to apply a padding to all images directly from the Wrapper container (the one that has display: flex + flex-wrap:wrap in App.tsx?

Ref issue using react-hook-form

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;

react-signature-canvas clears initial user input

This signature input works but it has a quirk that has stumped us for some time now.. If the user starts signing the signature pad, as soon as they lift their finger or mouse, their input will be erased - but then the signature pad will work as expected and no longer clear the input.
If I remove this section..
if (!input.value && this.sigRef && this.sigRef.clear) {
this.clear();
}
it will no longer erase the first user input - but then if you go and view another form, this previous signature will appear (although it's not actually persisted in the database).
At this time we're requiring our users to "sign" twice in order to avoid seeing a false signature which may be confusing. Anyone know what it could be? Thanks for taking a look!
import React, { Component } from "react";
import { Button, Form } from "semantic-ui-react";
import SignatureCanvas from "react-signature-canvas";
class SignatureInput extends Component {
sigRef = {};
fieldRef = {};
state = {
trimmedDataUrl: ""
};
canvasWidth = 200;
canvasHeight = 100;
clear() {
this.sigRef.clear();
}
componentDidMount() {
const { input, enabled } = this.props;
this.setState({ trimmedDataURL: input.value.data });
this.sigRef.fromDataURL(input.value.data);
if (!enabled) {
this.sigRef.off();
}
}
render() {
const { input, placeholder, enabled } = this.props;
const { trimmedDataUrl } = this.state;
if (!input.value && this.sigRef && this.sigRef.clear) {
this.clear();
}
return (
<Form.Field
className="signature-holder"
style={{ width: "100%", height: "100%" }}
>
<div
ref={fieldRef => {
this.fieldRef = fieldRef;
if (fieldRef != null) {
this.canvasWidth = fieldRef.getBoundingClientRect().width;
}
}}
>
<label>{placeholder}</label>
<input type="hidden" name={input.name} value={trimmedDataUrl} />
<SignatureCanvas
onEnd={() => {
const imageData = this.sigRef
.getTrimmedCanvas()
.toDataURL("image/png");
this.setState({ trimmedDataURL: imageData });
input.onChange({ name: input.name, data: imageData });
}}
ref={ref => {
this.sigRef = ref;
}}
canvasProps={{
width: this.canvasWidth,
height: this.canvasHeight,
className: "signature-canvas"
}}
/>
{enabled && (
<Button
onClick={() => this.clear()}
type="button"
content="Clear"
/>
)}
</div>
</Form.Field>
);
}
}
export default SignatureInput;
CSS
.signature-holder {
position: relative;
border: 1px solid gray;
display: block;
margin: 0;
-webkit-appearance: none;
padding: 0.78571429em 1em;
background: $white;
border: 1px solid rgba(34, 36, 38, 0.15);
outline: 0;
color: rgba(0, 0, 0, 0.87);
border-radius: 0.28571429rem;
box-shadow: 0 0 0 0 transparent inset;
-webkit-transition: color 0.1s ease, border-color 0.1s ease;
transition: color 0.1s ease, border-color 0.1s ease;
font-size: 1em;
line-height: 1.2857;
resize: vertical;
label {
position: absolute;
top: 5px;
left: 5px;
color: lightgray !important;
}
button {
position: absolute;
bottom: 5px;
right: 5px;
}
}
Figured it out..
componentWillUnmount() {
this.setState({ trimmedDataURL: undefined });
this.sigRef = {};
}
And then removed the portion that checks for an input and clears it..

Resources