React - Checking active element on focus not behaving as expected - reactjs

I have a form input component where I am checking if the input is active to set the active class to the input's label:
import React, { forwardRef, ReactElement, ReactNode, HTMLProps, useImperativeHandle } from 'react'
import styles from './index.module.css'
import classNames from 'classnames'
import { IconFa } from '../icon-fa'
import { useStableId } from '../use-stable-id'
export interface Props extends HTMLProps<HTMLInputElement> {
// An icon component to show on the left.
icon?: ReactNode
// A help text tooltip to show on the right.
help?: string
// A clear text tooltip to show on the right.
clear?: string
// Pass an onHelp click handler to show a help button.
onHelp?: () => void
// Pass an onClear click handler to show a clear button.
onClear?: () => void
errorMessage?: ReactNode
label?: string
hideLabel?: boolean
}
export const FormInput = forwardRef<HTMLInputElement, Props>(function FormInput(props, ref): ReactElement {
const { id, icon, help, hideLabel, errorMessage, onHelp, onClear, ...rest } = props
const internalRef = React.useRef<HTMLInputElement>(null)
useImperativeHandle<HTMLInputElement | null, HTMLInputElement | null>(ref, () => internalRef.current)
const stableId = useStableId()
const inputId = id ? id : stableId
const active = document.activeElement === internalRef.current
const inputClassName = classNames(
styles.input,
icon && styles.hasLeftIcon,
(help || onHelp || onClear) && styles.hasRightIcon
)
const labelClassName = classNames(
styles.label,
Boolean(props.value) && styles.hasValue,
props['aria-invalid'] && styles.hasError,
active && styles.active,
props.className
)
return (
<div className={labelClassName}>
{!hideLabel && (
<label className={styles.labelText} htmlFor={inputId}>
{props.label}
{props.required && '*'}
</label>
)}
<div className="relative">
{icon && <div className={styles.leftIcon}>{icon}</div>}
<input
{...rest}
id={inputId}
ref={internalRef}
aria-invalid={props['aria-invalid']}
aria-label={props.hideLabel ? props.label : undefined}
className={inputClassName}
/>
{onClear && <ClearIcon {...props} />}
{!onClear && (help || onHelp) && <HelpIcon {...props} />}
</div>
{props['aria-invalid'] && <span className={styles.error}>{errorMessage}</span>}
</div>
)
})
function HelpIcon(props: Props) {
if (props.onHelp) {
return (
<button type="button" className={styles.rightIcon} aria-label={props.help} onClick={props.onHelp}>
<IconFa icon={['far', 'info-circle']} title={props.help} />
</button>
)
}
return (
<div className={styles.rightIcon} title={props.help}>
<IconFa icon={['far', 'info-circle']} />
</div>
)
}
function ClearIcon(props: Props) {
return (
<button type="button" className={styles.rightIcon} aria-label={props.clear} onClick={props.onClear}>
<IconFa icon={['far', 'times']} title={props.clear} />
</button>
)
}
But, when I do it like this, the active class is added to label only when I start typing not when the input is focused. Right now, since I am using :focus selector on input, input gets active class on focus, while label gets it only after typing. How can I fix this so that they both get it on focus?

The active flag is re-evaluated only on component re-render, that is why you see a change only when the state changes (e.g. by typing, which very probably triggers a state change in the parent component if it uses onChange for example).
In your case, it seems you could just use the onFocus and onBlur listeners directly, and have the active flag as a state:
export const FormInput = forwardRef<HTMLInputElement, Props>(function FormInput(props, ref) {
const [active, setActive] = useState(false)
const labelClassName = classNames(
active && styles.active
)
return (
<div className={labelClassName}>
<input
onFocus={() => setActive(true)}
onBlur={() => setActive(false)}
/>
</div>
)
})

Related

Use array of strings in React Hook Form

In a form that I am making the material that is being created in the form should have multiple width options that can be added. This means that I will have a text input where the user can add an option, and when this option is added, it should be added to the React Hook Form widthOptions array, without using the regular react state. How would one do this? How do you add an item to the total React Hook Form state, I only see options for just one input field corresponding to a property.
This is how i would do it using the regular React state
import { TrashIcon } from "#heroicons/react/24/outline";
import React, { useRef, useState } from "react";
const Test = () => {
const [widthOptions, setWidthOptions] = useState<string[]>([]);
const inputRef = useRef<HTMLInputElement>(null);
const removeWidthOption = (widthOption: string) => {
setWidthOptions(widthOptions.filter((option) => option !== widthOption));
};
const addWidthOption = (widthOption: string) => {
setWidthOptions([...widthOptions, widthOption]);
};
const editWidthOptions = (widthOption: string, index: number) => {
const newWidthOptions = [...widthOptions];
newWidthOptions[index] = widthOption;
setWidthOptions(newWidthOptions);
};
return (
<div>
<input type="text" ref={inputRef} />
<button onClick={() => addWidthOption(inputRef?.current?.value)}>
Add Width Option
</button>
{widthOptions.map((option, index) => (
<div className="flex">
<input
type="text"
value={option}
onChange={() => editWidthOptions(option, index)}
/>
<button type="button" onClick={() => removeWidthOption(option)}>
<TrashIcon className="w-5 h-5 mb-3 text-gray-500" />
</button>
</div>
))}
</div>
);
};
export default Test;
You can just the controller component for this as for all other fields.
Since you have not shared any of you code here is a generic multi-select
<Controller
name={name}
render={({ field: { value, onChange, ref } }) => {
return (
// You can use whatever component you want here, the you get the value from the form and use onChange to update the value as you would with a regular state
<Test
widthOptions={value}
setWidthOptions={onChange}
/>
);
}}
/>;
https://react-hook-form.com/api/usecontroller/controller/
And in you Test component remove the state and get the props instead
const Test = ({widthOptions, setWidthOptions}) => {
const inputRef = useRef<HTMLInputElement>(null);
.
.
.

Button 'more' in a popup box doesn’t work right with React hook useState

I created a component (sort of popup box) which displays a sign of horoscope, there’s an image and description. The popup box works correctly. I added a button ‘more’ to see more description, so I used a useState for it, but it doesn’t work, when I click on it doesn't show the rest of the text.
Thanks for your help !
const Modal = ({
children, visible, hide, fermer, more,
}) => {
const popup = `popup ${visible ? 'block' : 'hidden'}`;
return (
<div className={popup}>
{fermer ? null : (
<button className="close" onClick={hide} type="button">X</button>
)}
{children}
<button className="more" onClick={more} type="button">more</button>
</div>
);
};
export default Modal;
import './App.css';
import { useState } from 'react';
import Element from './Element';
import Modal from './Modal';
import Bd from './Bd';
function App() {
const bd = Bd.map((element) => (
<Element
nom={element.nom}
image={element.image}
description={element.description}
modulo={element.modulo}
/>
));
const [year, setYear] = useState('');
function handleChange(event) {
setYear(event.target.value);
}
const [signe, setSigne] = useState([]);
const [vis, setVis] = useState(false);
const [desc, setDesc] = useState(true);
function handleSubmit() {
setVis(true);
const yearModulo = Number(year) % 12;
Bd.map((element) => (
yearModulo === element.modulo ? setSigne(
[<h1>{element.nom}</h1>,
<div>{element.description.substr(0, 150)}</div>,
desc ? <div />
: <div>{element.description.substr(150, 600)}</div>,
<img src={`/images/${element.image}`} alt="" />,
],
) : false
));
}
return (
<div>
<div>
<input
className="text-center font-bold"
type="number"
id="year"
name="year"
value={year}
onChange={handleChange}
/>
<button type="submit" onClick={handleSubmit}>Valider</button>
</div>
<div className="flex flex-wrap">{bd}</div>
<Modal
visible={vis}
hide={() => setVis(false)}
more={() => setDesc(false)}
>
<div>
<div>{signe}</div>
</div>
</Modal>
</div>
);
}
export default App;
I would avoid storing in a local state a component (setSigne([<h1>{element.nom}</h1>,...). Prefer storing in the state the values that cannot be computed from other existing states, and generate the elements at rendering.
const [signe, setSigne] = useState(null);
function handleSubmit() {
setVis(true);
const yearModulo = Number(year) % 12;
setSigne(Bd.find(element => yearModulo === element.modulo));
}
// ...
<div>
{signe && <div>
<h1>{signe.nom}</h1>
<div>{signe.description.substr(0, 150)}</div>
{desc ? <div /> : <div>{signe.description.substr(150, 600)}</div>}
<img src={`/images/${signe.image}`} alt="" />
</div>}
</div>
Also, don’t forget to add a key prop when generating elements from an array:
const bd = Bd.map(element => (
<Element
key={element.nom}
// ...

react: manually trigger input component synthetic change event

I'm trying to build an input component with a clear button using react#17
import { useRef } from 'react';
const InputWithClear = props => {
const inputRef = useRef();
return (
<div>
<input
ref={inputRef}
{...props}
/>
<button
onClick={() => {
inputRef.current.value = '';
inputRef.current.dispatchEvent(
new Event('change', { bubbles: true })
);
}}
>
clear
</button>
</div>
);
};
using this component like:
<InputWithClear value={value} onChange={(e) => {
console.log(e); // I want to get a synthetic event object here
}} />
but the clear button works once only when I did input anything first, and stop working again.
if I input something first and then click the clear button, it does not work.
why not using?
<button
onClick={() => {
props.onChange({
target: { value: '' }
})
}}
>
clear
</button>
because the synthetic event object will be lost
So, how do I manually trigger a synthetic change event of a react input component?
Try this approach,
Maintain state at the parent component level (Here parent component is App), onClear, bubble up the handler in the parent level, and update the state.
import React, { useState } from "react";
import "./styles.css";
const InputWithClear = (props) => {
return (
<div>
<input {...props} />
<button onClick={props.onClear}>clear</button>
</div>
);
};
export default function App() {
const [value, setValue] = useState("");
return (
<div className="App">
<h1>Hello CodeSandbox</h1>
<InputWithClear
value={value}
onChange={(e) => {
console.log(e); // I want to get a synthetic event object here
setValue(e.target.value);
}}
onClear={() => {
setValue("");
}}
/>
</div>
);
}
Working code - https://codesandbox.io/s/youthful-euler-gx4v5?file=/src/App.js
you should use state to control input value rather than create useRef, that's the way to go. you can use a stopPropagation prop to control it:
const InputWithClear = ({value, setValue, stopPropagation = false}) => {
const onClick = (e) => {
if(stopPropagation) e.stopPropagation()
setValue('')
}
return (
<div>
<input
value={value}
onChange={e => setValue(e.target.value)}
/>
<button
onClick={onClick}
>
clear
</button>
</div>
);
};
export default function App() {
const [value, setValue] = useState('')
return (
<div className="App">
<InputWithClear value={value} setValue={setValue} stopPropagation />
</div>
);
}

Cannot set useState hook value to opposite boolean

I'm creating a DatePicker and using useState hook to manage it's visibility. On div click I've added the event listener which changes value, but it didn't work as I expected. It works only the first time, so initial value changes to true, but on second and third clicks this value stays to true and DatePicker stays visible on click.
This is DatePicker
import React, { useState } from 'react';
import OutsideClickHandler from 'react-outside-click-handler';
import { renderInfo, getWeeksForMonth } from './utils';
import {
renderMonthAndYear,
handleBack,
handleNext,
weekdays,
} from '../../utils';
const DatePicker = ({
isOpen,
setIsOpen,
selected,
setSelected,
dayClick,
dayClass,
}) => {
const startDay = new Date().setHours(0, 0, 0, 0);
const [current, setCurrent] = useState(new Date(startDay));
const weeks = getWeeksForMonth(current.getMonth(), current.getFullYear());
function handleClick(date) {
if (date > startDay) {
setSelected(date);
setCurrent(date);
setIsOpen(false);
if (dayClick) {
dayClick(date);
}
}
}
return (
<div className="DatePicker-container">
<div
tabIndex="0"
role="button"
className="DatePicker-info"
onKeyPress={e => {
if (e.which === 13) {
setIsOpen(!isOpen);
}
}}
onClick={e => {
setIsOpen(!isOpen);
}}
>
{renderInfo(selected)}
</div>
{isOpen && (
<OutsideClickHandler onOutsideClick={() => setIsOpen(false)}>
<div className="DatePicker">
<div className="DatePicker__header">
<span
role="button"
onClick={() => handleBack(current, setCurrent)}
className="triangle triangle--left"
/>
<span className="DatePicker__title">
{renderMonthAndYear(current)}
</span>
<span
role="button"
onClick={() => handleNext(current, setCurrent)}
className="triangle triangle--right"
/>
</div>
<div className="DatePicker__weekdays">
{weekdays.map(weekday => (
<div
key={weekday}
className="DatePicker__weekday"
>
{weekday}
</div>
))}
</div>
{weeks.map((week, index) => (
<div
role="row"
key={index}
className="DatePicker__week"
>
{week.map((date, index) =>
date ? (
<div
role="cell"
key={index}
onClick={() => handleClick(date)}
className={dayClass(date)}
>
{date.getDate()}
</div>
) : (
<div
key={index}
className="DatePicker__day--empty"
/>
),
)}
</div>
))}
</div>
</OutsideClickHandler>
)}
</div>
);
};
export default DatePicker;
DateRangePicker which uses two.
import React, { useState } from 'react';
import DatePicker from './DatePicker';
import DatePickerContext from './DatePickerContext';
import './DatePicker.scss';
const DateRangePicker = () => {
const startDay = new Date().setHours(0, 0, 0, 0);
const [isOpen, setIsOpen] = useState(false);
const [isSecondOpen, setIsSecondOpen] = useState(false);
const [selected, setSelected] = useState(new Date(startDay));
const [secondSelected, setSecondSelected] = useState(new Date(startDay));
function dayClass(date) {
if (
selected.getTime() === date.getTime() ||
(date >= selected && date <= secondSelected)
) {
return 'DatePicker__day DatePicker__day--selected';
}
if (date < startDay || date < selected) {
return 'DatePicker__day DatePicker__day--disabled';
}
return 'DatePicker__day';
}
function dayClick(date) {
setSecondSelected(date);
setIsSecondOpen(true);
}
return (
<DatePickerContext.Provider>
<div className="DatePicker-wrapper">
<DatePicker
key={1}
isOpen={isOpen}
setIsOpen={setIsOpen}
selected={selected}
setSelected={setSelected}
dayClick={dayClick}
dayClass={dayClass}
/>
<DatePicker
key={2}
isOpen={isSecondOpen}
setIsOpen={setIsSecondOpen}
selected={secondSelected}
setSelected={setSecondSelected}
dayClass={dayClass}
/>
</div>
</DatePickerContext.Provider>
);
};
export default DateRangePicker;
a) It is a good practice to use functional updates to make sure to use correct "current" value when the next state is dependent on the previous (== current) state:
setIsOpen(currentIsOpen => !currentIsOpen)
b) It's very hard to reason about the next state when it gets updated by multiple handlers executed for the same event. Following 2 handlers might execute on the same click (the 1st div is "outside"):
<div ... onClick={e => setIsOpen(!isOpen)}>
<OutsideClickHandler onOutsideClick={() => setIsOpen(false)}>
If onOutsideClick executes first, then React re-renders with isOpen=false, and then onClick executes second, it would set isOpen=true as you observe - I don't see how the re-render could happen between, but maybe OutsideClickHandler is doing something nefarious or your code is more complicated than in the question ¯\_(ツ)_/¯
To enforce only 1 event handler:
<OutsideClickHandler onOutsideClick={(e) => {
e.stopPropagation();
setIsOpen(false);
}}>

optional controlled / uncontrolled React component with conditional React hooks

IMO, React Hooks useState is a perfect fit for a pattern to optional using value from props or use own state, but the lint showed some error when I use hook conditionally.
Working Example
I tried to use hooks with condition as below but with eslint error React hook useState is called conditionally. According to this explanation from doc, React relies on the order in which Hooks are called.
const Counter = ({ value, onChange, defaultValue = 0 }) => {
const [count, onCountChange] =
typeof value === "undefined" ? useState(defaultValue) : [value, onChange];
return (
<div>
{count.toString()}
<button
onClick={() => {
onCountChange(count + 1);
}}
>
+
</button>
</div>
);
};
function App() {
const [count, onCountChange] = useState(0);
return (
<div className="App">
<div>
Uncontrolled Counter
<Counter />
</div>
<div>
Controlled Counter
<Counter value={count} onChange={onCountChange} />
</div>
</div>
);
}
How can I use hooks to achieve same function as below class Component ?
class CounterClass extends React.Component {
state = {
value: this.props.defaultValue || 0
};
render() {
const isControlled = typeof this.props.defaultValue === "undefined";
const count = isControlled ? this.props.value : this.state.value;
return (
<div>
{count.toString()}
<button
onClick={() => {
isControlled &&
this.props.onChange &&
this.props.onChange(this.props.value + 1);
!isControlled && this.setState({ value: this.state.value + 1 });
}}
>
+
</button>
</div>
);
}
}
Or this kind props/state optional way in one component is wrong?
I learnt the "defaultValue", "value", "onChange" API naming and idea from React JSX <input> component.
You could split your component into fully controlled and fully uncontrolled. Demo
const CounterRepresentation = ({ value, onChange }) => (
<div>
{value.toString()}
<button
onClick={() => {
onChange(value + 1);
}}
>
+
</button>
</div>
);
const Uncontrolled = ({ defaultValue = 0 }) => {
const [value, onChange] = useState(defaultValue);
return <CounterRepresentation value={value} onChange={onChange} />;
};
// Either use representation directly or uncontrolled
const Counter = ({ value, onChange, defaultValue = 0 }) => {
return typeof value === "undefined" ? (
<Uncontrolled defaultValue={defaultValue} />
) : (
<CounterRepresentation value={value} onChange={onChange} />
);
};
Great question! I think this can be solved with hooks by making the useState call unconditional and only making the part conditional where you decide which state you render and what change handler you use.
I've just released a hook which solves this: use-optionally-controlled-state
Usage:
import useOptionallyControlledState from 'use-optionally-controlled-state';
function Expander({
expanded: controlledExpanded,
initialExpanded = false,
onChange
}) {
const [expanded, setExpanded] = useOptionallyControlledState({
controlledValue: controlledExpanded,
initialValue: initialExpanded,
onChange
});
function onToggle() {
setExpanded(!expanded);
}
return (
<>
<button onClick={onToggle} type="button">
Toggle
</button>
{expanded && <div>{children}</div>}
</>
);
}
// Usage of the component:
// Controlled
<Expander expanded={expanded} onChange={onExpandedChange} />
// Uncontrolled using the default value for the `initialExpanded` prop
<Expander />
// Uncontrolled, but with a change handler if the owner wants to be notified
<Expander initialExpanded onChange={onExpandedChange} />
By using a hook to implement this, you don't have to wrap an additional component around and you can theoretically apply this to multiple props within the same component (e.g. a <Prompt isOpen={isOpen} inputValue={inputValue} /> component where both props are optionally controlled).

Resources