Focusing on a Div Element - React - javascript

I'd like to have an onKeyPress event triggered when a key is pressed when a 'Level' is displaying.
From what I've read, a div needs to be focused on in order for them to register - But I'm not quite sure how to achieve this using functional components.
const handleKeyPress = e => {
console.log(e);
}
export default () => {
const [currentLevel, setCurrentLevel] = useState(1);
return (
<Level onKeyPress={handleKeyPress} />
)
}

Have you tried adding eventlisteners in a useEffect?
useEffect(() => {
const myFunc = () => {}
document.addEventListener('keyPress', myFunc)
return () => {
document.removeEventListener('keyPress', myFunc)
}
}, [])

Related

Fixing hook call outside of the body of a function component

I made a custom ReactJS hook to handle a couple of specific mouse events, as below:
const HealthcareServices = ({
filterToRemove,
filters,
onChange,
onClear,
selectedAmbulatoryCareFilterValue,
shouldClear,
}: Props): JSX.Element => {
const classes = useStyles();
...
useEffect(() => {
shouldClear && clearFilters();
}, [shouldClear]);
const useSingleAndDoubleClick = (actionSimpleClick: () => void, actionDoubleClick: () => void, delay = 250) => {
const [click, setClick] = useState(0);
useEffect(() => {
const timer = setTimeout(() => {
// simple click
if (click === 1) actionSimpleClick();
setClick(0);
}, delay);
// the duration between this click and the previous one
// is less than the value of delay = double-click
if (click === 2) actionDoubleClick();
return () => clearTimeout(timer);
}, [click]);
return () => setClick((prev) => prev + 1);
};
const handleSelectedItem = (service: Filter) => {
service.selected = !service.selected;
setHealthcareServices([...healthcareServices]);
onChange(healthcareServices);
};
const handleSingleClick = (service: Filter) => {
console.log('single-click');
if (service.isRequired) {
service.checkedIcon = <Icons.CheckboxSingleClick />;
}
handleSelectedItem(service);
};
const handleDoubleClick = (service: Filter) => {
console.log('double-click');
if (service.isRequired) {
service.checkedIcon = <Icons.CheckboxDoubleClick />;
}
handleSelectedItem(service);
};
const handleClick = (service: Filter) =>
useSingleAndDoubleClick(
() => handleSingleClick(service),
() => handleDoubleClick(service)
);
...
return (
<div className={classes.filter_container}>
...
<div className={classes.filter_subgroup}>
{filters.map((filter) => (
<div key={`${filter.label}-${filter.value}`} className={classes.filter}>
<Checkbox
label={filter.label}
className={classes.checkbox}
checked={filter.selected}
onChange={() => handleClick(filter)}
checkedIcon={filter.checkedIcon}
/>
</div>
))}
</div>
...
</div>
);
};
When I click on my <Checkbox />, the whole thing crashes. The error is:
The top of my stacktrace points to useState inside my hook. If I move it outside, so the hook looks as:
const [click, setClick] = useState(0);
const useSingleAndDoubleClick = (actionSimpleClick: () => void, actionDoubleClick: () => void, delay = 250) => {
useEffect(() => {
const timer = setTimeout(() => {
// simple click
if (click === 1) actionSimpleClick();
setClick(0);
}, delay);
// the duration between this click and the previous one
// is less than the value of delay = double-click
if (click === 2) actionDoubleClick();
return () => clearTimeout(timer);
}, [click]);
return () => setClick((prev) => prev + 1);
};
The problem still happens, only the stacktrace points to the useEffect hook. The code is based on another answer here.
Any suggestions?
You've defined your useSingleAndDoubleClick hook inside of a component. That's not what you want to do. The idea of custom hooks is that you can move logic outside of your components that could otherwise only happen inside of them. This helps with code reuse.
There is no use for a hook being defined inside a function, as the magic of hooks is that they give you access to state variables and such things that are usually only allowed to be interacted with inside function components.
You either need to define your hook outside the component and call it inside the component, or remove the definition of useSingleAndDoubleClick and just do everything inside the component.
EDIT: One more note to help clarify: the rule that you've really broken here is that you've called other hooks (ie, useState, useEffect) inside your useSingleAndDoubleClick function. Even though it's called useSingleAndDoubleClick, it's not actually a hook, because it's not being created or called like a hook. Therefore, you are not allowed to call other hooks inside of it.
EDIT: I mentioned this earlier, but here's an example that could work of moving the hook definition outside the function:
EDIT: Also had to change where you call the hook: you can't call the hook in a nested function, but I don't think you need to.
const useSingleAndDoubleClick = (actionSimpleClick: () => void, actionDoubleClick: () => void, delay = 250) => {
const [click, setClick] = useState(0);
useEffect(() => {
const timer = setTimeout(() => {
// simple click
if (click === 1) actionSimpleClick();
setClick(0);
}, delay);
// the duration between this click and the previous one
// is less than the value of delay = double-click
if (click === 2) actionDoubleClick();
return () => clearTimeout(timer);
}, [click]);
return () => setClick((prev) => prev + 1);
};
const HealthcareServices = ({
filterToRemove,
filters,
onChange,
onClear,
selectedAmbulatoryCareFilterValue,
shouldClear,
}: Props): JSX.Element => {
const classes = useStyles();
...
useEffect(() => {
shouldClear && clearFilters();
}, [shouldClear]);
// your other handlers
// changed this - don't call the hook inside the function.
// your hook is returning the handler you want anyways, I think
const handleClick = useSingleAndDoubleClick(handleSingleClick, handleDoubleClick)

React test passes but component does not work properly when state updated in document.addListener handler

I have a DatePicker component like this:
const MonthPanel = ({ setMode = () => {} }) => {
return (
<div>
<button
onClick={(e) => setMode("year")}
data-testid="datepicker-year-button"
>
2021
</button>
</div>
);
};
const YearPanel = () => {
return <div data-testid="datepicker-year-panel">YearPanel</div>;
};
const DatePicker = (props) => {
const [open, setOpen] = useState(false);
const [mode, setMode] = useState("month");
const containerRef = useRef();
const handleOpen = () => {
setOpen(true);
};
const handleClose = () => {
setOpen(false);
};
const handleModeChange = (newMode) => {
setMode(newMode);
};
const generatePanel = () => {
switch (mode) {
case "month":
return <MonthPanel setMode={handleModeChange} />;
case "year":
return <YearPanel />;
default:
return null;
}
};
useEffect(() => {
const handleOutsideClick = (e) => {
if (containerRef.current && !containerRef.current.contains(e.target)) {
handleClose();
}
};
window.addEventListener("click", handleOutsideClick);
return () => {
window.removeEventListener("click", handleOutsideClick);
};
}, []);
return (
<div
ref={containerRef}
data-testid="datepicker-container"
>
<div onClick={handleOpen} data-testid="datepicker-input">
Select a date
</div>
{open && <div data-testid="datepicker-dialog">{generatePanel()}</div>}
</div>
);
};
And I have a test file like this:
it.only("should close DatePicker dialog when clicked only outside DatePicker", () => {
const { getByTestId, queryByTestId } = render(
<DatePicker />
);
userEvent.click(getByTestId("datepicker-input"));
userEvent.click(getByTestId("datepicker-year-button"));
expect(queryByTestId("datepicker-dialog")).toBeInTheDocument();
userEvent.click(document.body);
expect(queryByTestId("datepicker-dialog")).toBeNull();
});
Desired state:
When you click outside DatePicker, it should close. And when you click [data-testid="datepicker-year-button"] it should change DatePicker mode to "year", so the year panel will be shown.
Current state:
When you click [data-testid="datepicker-year-button"], it changes Datepicker mode to "year" and MonthPanel (and with it button itself) are removed. Because the button is event target and has already removed, containerRef.current.contains(e.target) condition is false and Dialog will be removed too. But the test is showing that dialog is in the document.
The question is how I should test this functionality correctly.
You could call e.stopPropagation() on your button click handler in <MonthPanel>, to prevent the event bubbling up and being caught by the window's eventListener.
<button
onClick={(e) => {
e.stopPropagation();
setMode("year");
}}
data-testid="datepicker-year-button"
>
Try with async methods like waitFor or waitForElementToBeRemoved:
await waitFor(() => {
expect(queryByTestId("datepicker-dialog")).toBeNull();
});
React-dom introduced act API to wrap code that renders or updates components, this makes your test run closer to how React works in the browser. React testing library wraps some of its APIs in the act function, but in some cases, you would still need to use waitFor, or waitForElementToBeRemoved
I believe your userEvent.click(document.body); is not working properly.
Most likely document.body isn't resolved the way it should.
you're probably using testing-library user-event, and their basic example is:
import { screen } from '#testing-library/dom'
import userEvent from '#testing-library/user-event'
test('types inside textarea', () => {
document.body.innerHTML = `<textarea />`
userEvent.type(screen.getByRole('textbox'), 'Hello, World!')
expect(screen.getByRole('textbox')).toHaveValue('Hello, World!')
})
check that your document.body.innerHTML is set to something

How to avoid multiple event listeners from being attached?

Is the following code going to attach multiple event listeners or will React Native / expo-linking only allow one event listener be attached at a time?
import * as Linking from 'expo-linking'
import { useIsFocused } from '#react-navigation/native'
const MyComponent = () => {
const isFocused = useIsFocused()
useEffect(() => {
fetchData()
Linking.addEventListener('url', _handleEvent)
}, [isFocused])
const fetchData = () => {
// ...
}
const _handleEvent = () => {
// ...
}
return (
<View><View>
)
}
Is there are a way to check if an event listener already exists so I can do something like:
useEffect(() => {
fetchData()
if(!eventListenerExists){
Linking.addEventListener('url', _handleEvent)
}
}, [isFocused])
It'll attach multiple handlers, adding one each time isFocused changes. To remove the previous handler when attaching the next, return a function that React will call:
useEffect(() => {
fetchData()
Linking.addEventListener('url', _handleEvent)
return () => Linking.removeEventListener('url', _handleEvent) // <======
}, [isFocused])
You want to do that anyway so that the handler is removed when your component is completely unmounted.
This is covered in the React documentation here.
I think even should call one time
useEffect(() => {
Linking.addEventListener('url', _handleEvent)
return () => Linking.removeEventListener('url', _handleEvent) // <======
}, [])
1、in the useEffect callback, return the function that remove the listener;
2、every time remove the listener before bind it.
such as:
useEffect(() => {
window.removeEventListener('url', hander);
window.addEventListener('url', hander);
return () => window.removeEventListener('url', hander);
}, [XXX])

Gatsby: Event Listeners not unmounting on React useEffects hook

I am using Gatsby.
I have this useEffect() hook that adds and removes Event Listeners to the document so I can track outside clicks to close the respective menus. But when I navigate to another route and open my menu, the app breaks and I get this error:
header.js:121 Uncaught TypeError: Cannot read property 'contains' of null
at handleDropdown (header.js:121)
at HTMLDocument.<anonymous> (header.js:134)
I get this error thrice on every click anywhere as I have three event listeners. I added a console log and realized that during route change, for a moment, the values of the refs go to null. I feel the issue is because the header.js unmounts and then remounts. But I have added a return to the useEffect hook, that should remove the event listener and add them back when the new route loads. Have I done something wrong above?
Below is my my function and the useEffect hook.
// Function to close menus on outside clicks
const handleDropdown = (menuRef, buttonRef, handler, e) => {
console.log(!!menuRef.current, !!buttonRef.current)
if (
menuRef.current.contains(e.target) ||
buttonRef.current.contains(e.target)
) {
return
}
handler(false)
}
useEffect(() => {
document.addEventListener("mousedown", e =>
handleDropdown(mobileMenuRef, menuButtonRef, setShowMobileMenu, e)
)
document.addEventListener("mousedown", e =>
handleDropdown(coursesMenuRef, coursesButtonRef, setShowCoursesMenu, e)
)
document.addEventListener("mousedown", e =>
handleDropdown(
schedulesMenuRef,
schedulesButtonRef,
setShowSchedulesMenu,
e
)
)
document.addEventListener("scroll", handleFixedNavbar)
return () => {
document.removeEventListener("mousedown", e =>
handleDropdown(mobileMenuRef, menuButtonRef, setShowMobileMenu, e)
)
document.removeEventListener("mousedown", e =>
handleDropdown(coursesMenuRef, coursesButtonRef, setShowCoursesMenu, e)
)
document.removeEventListener("mousedown", e =>
handleDropdown(
schedulesMenuRef,
schedulesButtonRef,
setShowSchedulesMenu,
e
)
)
document.removeEventListener("scroll", handleFixedNavbar)
}
}, [])
In JavaScript functions are compared by reference. That means () => {} !== () => {}. When you try to remove the event listeners in your useEffect cleanup callback you pass newly defined functions, but because of the above they won't match the existing handlers that you defined.
If you refactor your code to include the event handler in the useEffect hook you can pass the same hook-local variable to both (add|remove)EventListener calls:
const useOnClickOutside = (refs, callback) => {
useEffect(() => {
const eventHandler = e => {
if (refs.some(ref => ref.current.contains(e.target))) {
return
} else {
callback(false)
}
}
document.addEventListener("mousedown", eventHandler)
return () => {
document.removeEventListener("mousedown", eventHandler)
}
}, [refs, callback])
}
const YourComponent = () => {
const someRef = useRef()
const someOtherRef = useRef()
useOnClickOutside([someRef, someOtherRef], () => {
console.log("Click outside happened!")
})
return <div>Some Content</div>
}

How handle long press event in react web

everyone! I use react and material ui library. I want to handle click event and long-press event separately. I think problem related to async set state, but for now, I don't know how to handle this events
const [isCommandHandled, setIsCommandHandled] = React.useState(null);
const handleButtonPress = function (e) {
setIsCommandHandled(false);
console.log('ON_MOUSE_DOWN ' + isCommandHandled); // here value null
buttonPressTimer = setTimeout(handleLongPress, 1500, e);
}.bind(this);
const handleLongPress = (e) => {
if (!isCommandHandled) {
setIsCommandHandled(true);
console.log('TIMER_IS_EXECUTED' + isCommandHandled); //Here value false or null
// some other logic for long press event
}
clearTimeout(buttonPressTimer);
};
const handleButtonRelease = function (e) {
if (!isCommandHandled) {//isCommandHandled isn't updated here, as a result logic is executed always
// got regular click, not long press
// specific logic
setIsCommandHandled(true);
}
clearTimeout(buttonPressTimer);
};
<IconButton
onMouseDown={(e) => handleButtonPress(e)}
onMouseUp={(e) => handleButtonRelease(e)}
>
```
You can use setState with callback and put the set timeout ID to state:
setIsCommandHandled((prevState)=>{
console.log("TIMER_IS_EXECUTED" + isCommandHandled); //Here value false or null
return true; });
Working Example:
https://codesandbox.io/s/material-demo-gc0le
This is how I handle a long press:
//import Hooks
import { useState, useEffect } from "react";
const Component = () => {
//pressState
const [pressed, setPressed] = useState();
//handleLongPress
useEffect(() => {
const timer = pressed
? setTimeout(() => {
console.log(pressed, "got pressed!");
}, 1300)
: null;
return () => clearTimeout(timer);
}, [pressed]);
//pressedElement
return (
<div
onMouseDown={(e) => setPressed(e.target)}
onMouseUp={() => setPressed()}
style={{ backgroundColor: "lightgreen" }}
>
Press me
</div>
);
};
export default Component;
Tested here: https://codesandbox.io/s/bold-bose-7vx3qg

Categories

Resources