HeadlessUI React, how to detect currently focused element? - javascript

I'm trying to build a funky use case:
I have a HeadlessUI dialog, inside this dialog, there is a HeadlessUI Menu component as a nested child, I want to trigger the menu via a keyboard shortcut
I have managed to trigger the dialog, via:
// I took the useWindowEvent from the headlessUI code
useWindowEvent("keydown", (event) => {
if (event.key === "p") {
event.preventDefault()
event.stopPropagation()
selectorRef.current?.click()
}
})
and on my Menu component:
// My react component forwardRefs to the trigger button
export const ProjectSelector = forwardRef(
... some other component code
<Menu.Button ref={ref} as="div">
... other code
This works fine to trigger the Menu, but the menu also has a search bar, so whenever I press the p key, the listener triggers again and programmatically closes the Menu
I have taken a look inside the headlessUI code trying to understand the focus trap code, but it's above my head, is there any way to detect if the parent Dialog has focus? that way I can just ignore the keypress if focus is on the menu :)
Workaround:
While writing the question I stumbled upon a workaround... it is not super pretty, so maybe someone can come up with a better solution:
// I'm pretty sure I'm doing everything wrong in here
// but I couldn't find another way to remotely trigger and keep focus on the menu component
useWindowEvent("keydown", (event) => {
if (
event.key === "p" &&
document.activeElement?.getAttribute("role") !== "menu" &&
document.activeElement?.tagName !== "INPUT"
) {
event.preventDefault()
event.stopPropagation()
selectorRef.current?.click()
}
})

Related

Material UI's ClickAwayListener firing right away, which causes state to toggle twice

I have been doing research on this for days and finally have decided to ask this on here. I am using react and material-ui's ClickAwayListener component. The idea is that I have a button, which toggles the this.state.showGridPopup to the opposite of whatever it is currently is. So if the user was to click it once, the grid popup should show, click it again, the popup should disappear. There is an handleShowGrid handler attached to the button that does this:
handleShowGrid = (event) => {
const { widgetButtonEl } = this.state;
const element = widgetButtonEl === null ? event.currentTarget : null;
console.log('In handleShowGrid!!!!!');
this.setState({
showWidget: !this.state.showWidget,
widgetButtonEl: element
});
}
All this works well. And toggles the popup to show when we click on the button attached to the handler.
<ButtonBase onClick={this.handleShowGrid}>Open Swap</ButtonBase>
The issue is when I add Material-UI's ClickAwayListener. The reason for adding this is to close the grid when a user clicks outside the grid. Here is the ClickAwayListener below:
<ClickAwayListener onClickAway={this.handleCloseWidget}>
<SurveyGrid />
</ClickAwayListener>
And the handleCloseWidget handler:
handleCloseGrid = (e) => {
console.log('In handleCloseWidget!!!!!');
this.setState({
showWidget: !this.state.showWidget,
widgetButtonEl: null
});
}
So now clicking outside grid is fine and closes the Grid. However, now, clicking on the button that should toggle the Grid being show (this.state.showWidget), causes the handleCloseGrid to fire. And then afterwards the handleShowGrid would fire. Is there anyway to not have the handleCloseGrid handler to fire? It seems like an issue with propagation and event bubbling. Have tried different things like e.stopPropagation() but to no avail.

React: onClick on Mobile (iPhone) requires 2 taps?

Video demonstrating issue
I have a bunch of clickable components that, when clicked, adds a "card" to a row. On desktop, it works fine, but on mobile (tested on iPhone, does not seem to be an issue for Android tablet), it requires 2 consecutive taps of the same button to fire the onClick function.
These components also have onMouseEnter/onMouseLeave effects on them, to control a global state, which in turn decides if several components should have additional CSS applied (so I can't make it a simple CSS hover effect).
I believe that the mouse effects are interfering with the click event, but I have no idea how I could fix that. Here is the relevant code for this component:
const CardSource = ({ addCard, note, setHoveredNote, hoveredNote }) => {
return (
<Source
onClick={() => addCard(note)}
onMouseEnter={() => setHoveredNote(note)}
onMouseLeave={() => setHoveredNote(null)}
className={
hoveredNote && hoveredNote.index === note.index ? "highlight" : null
}
>
{note.letters[0]}
</Source>
);
};
Furthermore, once a button has been tapped twice, the hover effect CSS "sticks" to that button, and never moves to another button. This seems to happen on both iPhone and Android tablet. I would love to have this not happen anymore either.
I've created a working demonstration of this issue in a sandbox, which if viewed on mobile you should be able to recreate these issues: https://codesandbox.io/s/mobile-requires-2-taps-i9zri?file=/src/Components/CardSource/CardSource.js
Probably the problem with your code is, the mouse events you're using are non-bubbling. e.g. mouseenter event.
You might want to try with an event bubbling solution using onMouseOver instead of onMouseEnter, and onMouseOut instead of onMouseLeave.
const CardSource = ({ addCard, note, setHoveredNote, hoveredNote }) => {
return (
<Source
onClick={() => addCard(note)}
onMouseOver={() => setHoveredNote(note)}
onMouseOut={() => setHoveredNote(null)}
className={
hoveredNote && hoveredNote.index === note.index ? "highlight" : null
}
>
{note.letters[0]}
</Source>
);
};
Should the above NOT work, you could debug this with event type and performing event handling based on it. e.g.
const CardSource = ({ addCard, note, setHoveredNote, hoveredNote }) => {
const eventHandler = (event) => {
const { type, bubbles } = event;
switch(type) {
case "mouseover":
case "mouseenter":
setHoveredNote(note);
break;
case "mouseout":
case "mouseleave":
setHoveredNote(null);
case "click":
addCard(note);
if (bubbles) { // handle hover state
setHoveredNote(note);
}
break;
default:
break;
}
}
const onClick = (event) => eventHandler(event);
const onMouseOver = (event) => eventHandler(event);
const onMouseOut = (event) => eventHandler(event);
return (
<Source
onClick={onClick}
onMouseOver={onMouseOver}
onMouseOut={onMouseOut}
className={
hoveredNote && hoveredNote.index === note.index ? "highlight" : null
}
>
{note.letters[0]}
</Source>
);
};
Also note that, providing arrow functions as props creates new instance of the function on every render. So better use bind in that case or just function references that capture the arguments.
I think I've found the problem when I use an onClick & onMouseEnter & onMouseLeave then test in the browser in mobile mode the onMouseEnter and onClick event fire with the first onClick, you can add a console log to all your events and see the same behavior. The CSS style is staying because the DOM thinks that your element still has the hover attribute. If you click off of the element in question, you will see your onMouseLeave event fire, your css will reset but the element will require two clicks again. I'm not sure what the solution is, or if its even a problem testing on an actual mobile device.
EDIT: A solution I found, is only using the onMouseEnter & onMouseLeave event, since this event fires onClick for mobile and I only want the hover effect on desktop the outcome is what I was after.
EDIT EDIT: To maintain accessibility with the keyboard I added an onKeyDown event to open/close the dropdown button (which is what I was working on)
onKeyDown={(event) => {
if (event.keyCode == 13) {
setShowChildren(showChildren === "hide" ? "show" : "hide");
}
}}
You could just use css hover rather than adding a class via onMouseEnter event, It fixes the two taps issue.
Link to sandbox
If you were to programatically use trigger for hover. You could solve the two clicks issue by using onTouchEnd event (commented in sandbox).
Hope that helps.
I remember having a similar problem. The issue was for me that the state of component does not change immediately, but only upon execution of the render() method. I believe you might have the same issue with asynchronicity for both effects you are describing.
The only render() call I see found in your code is in App.test.js, I usually place that in the respective component.tsx.
References
Submit button takes 2 clicks to call function in React
React.js events need 2 clicks to execute
Use events onTouchStart, onTouchMove, onTouchEnd for calculate touch.

How to focus next cell in ag-grid Angular from context of AgEditorComponent

I have a custom overlay (OverlayModule from '#angular/cdk/overlay') that opens when editing a cell in Ag-Grid for Angular.
The user can focus on a MatInput just fine in the overlay. But now the focus is on an input that is outside the flow of the grid cells, so when user hits 'tab' the default behavior will focus the address bar.
I can prevent this behavior by with the following.
fromEvent(this.tabOutInput.nativeElement, 'keydown')
.pipe(
takeUntil(this._destroy)
)
.subscribe((event: KeyboardEvent) => {
if(event.keyCode === TAB) {
event.preventDefault();
// TODO: figure out how to focus the next cell
}
});
But how do I get the next cell to focus? I looked at the docs and tabToNextCell and navigateToNextCell seem like options but I don't want users of this cell to have to configure their template, where these methods are bound.
Is there a way with ICellEditorParams to navigate to the next cell with the GridApi?
agInit(params: ICellEditorParams): void {}
Please help!
DOH! I figured out what the problem was. There were elements hijacking the tab index. There are multiple grids on this page and this.params.api was referencing the last grid as opposed to the one I was working with. I tried this.params.api.tabToNextCell() earlier in the day but it didn't work, it was tabbing irregularly between another grid, the first input on the page. I just need to figure out how to properly pass in the context to get multiple grids working.
fromEvent(this.tabOutInput.nativeElement, 'keydown')
.pipe(
takeUntil(this._destroy)
)
.subscribe((event: KeyboardEvent) => {
if(event.keyCode === TAB) {
event.preventDefault();
this.params.api.tabToNextCell(); <-- this does work with 1 grid on the page
}
});

react-router stops routing after rerender from a concurrent event

I have a dropdown that is controlled via state.
Clicking on a button toggles it on. Clicking outside toggles it off.
The dropdown contains Links within my application, however, when the dropdown is being toggled off, route transition is prevented.
If autohide is disabled, routing works fine, however, it is desired to also hide the dropdown on route transition.
Please explain to me what is going on
Also please help me fix it
class App extends React.Component {
state = {
isNavShown: false
}
showNav = () => this.setState({isNavShown: true})
hideNav = event => {
// ... some more logic ...
// don't hide if autoHide is disabled
if (autoHide.checked === false) return
this.setState({isNavShown: false})
}
componentDidMount() {
document.addEventListener('mousedown', this.hideNav)
}
// ...
}
I have also tried wrapping the setState in setTimeout, but to no avail.
Here is the full jsfiddle https://jsfiddle.net/nimareq/1kh47uey/
So the issue is that your hideNav function is hiding the nav if the user clicks anywhere outside of show navigation button and the checkbox you built. However, if the user clicks on the nav itself it will be hidden before you have a chance to navigate the user.
Essentially, the browser will detect the click event listener you made on the document before it bubbles down to the anchor tag click. By the time it gets there the anchor tag is gone. (I hope that makes sense lol)
Anyways you can easily solve it by adding the following to your hideNav function:
if(nav.contains(event.target)) return;
Also don't forget to add the id="nav" on your navbar or whatever else you want to call it. This way the navbar won't disappear when u click on the navbar. It will still disappear if you click off the navbar.

Stop click on child calling method on parent - React/JavaScript

I have a modal in React. When you click the background of the modal, the modal should close. The way I have it set up right now, if you click inside* the modal, it closes as well. Because the modal is inside the background>
handleClose(e) {
e.stopPropagation();
this.props.history.push('/business/dashboard')
}
render() {
return (
<Background onClick={e => this.handleClose(e)} name="BACKGROUND">
<Container onClick={console.log("IT CLICKED")} to={'/business/dashboard'} name="CONTAINER">
....
When I click on Container, the onClick event for Background gets called. I don't want this to happen. This is a form that users will be clicking on all the time. I need the modal to only close when you click outside the modal on Background.
I think it will work if you use stopPropagation on the Container click event instead of the Background. Just make sure that you use the onClick prop in your Container component.
class App extends React.Component {
handleClose = (e) => {
e.stopPropagation();
this.props.history.push("/business/dashboard");
};
render() {
return (
<Background onClick={this.handleClose} name="BACKGROUND">
<Container
onClick={e => e.stopPropagation()}
to={"/business/dashboard"}
name="CONTAINER"
/>
</Background>
);
}
}
EDIT: On rereading the question, the other answer is a simpler solution in this case.
The behavior you want to achieve is generally referred to as an "outside click" handler. There are a couple of decent libraries to handle this [0] and their source is pretty short and readable if you want to know how it works in detail. [1]
The general idea is to register a click event handler on the document in a HOC and check whether the event.target originates inside a React ref via Element.contains browser functionality. If is is, the handler will not be executed.
[0] https://github.com/tj/react-click-outside
[1] https://github.com/tj/react-click-outside/blob/master/index.js

Categories