Input fields inside react overlays doesnt work in modal - reactjs

I have modal screen (using react-bootstrap), on modal screen i have multiple overlays (popup menus) linked to items. These overlays has inputs, and when i click on input it immediately loses focus. I cant figure out whats wrong, because another one popup menu, that i have on normal screen, not modal, works fine. Tried to set autofocus, but it immediately loses too.
I wrote example, https://codesandbox.io/s/rkemy
I think it is somehow connected with popper, because bootstrap overlay uses it, dont know where to dig

The fix provided in the other response is a workaround that doesn't fix the real cause of the issue.
The issue is caused by an internal logic of the Modal component of react-overlay library that is a dependency library of react-bootstrap.
Specifically, the issue is caused by code listed below
const handleEnforceFocus = useEventCallback(() => {
if (!enforceFocus || !isMounted() || !modal.isTopModal()) {
return;
}
const currentActiveElement = activeElement();
if (
modal.dialog &&
currentActiveElement &&
!contains(modal.dialog, currentActiveElement)
) {
modal.dialog.focus();
}
});
that enforce the focus on the first modal open as soon as that modal lose the focus, like when you move the focus on the input.
In order to solve the issue, you have to pass the enforceFocus={false} to your Modal component.
The documentation of the API can be found here: https://react-bootstrap.github.io/react-overlays/api/Modal#enforceFocus
As the docs says:
Generally this should never be set to false as it makes the Modal less accessible to assistive technologies, like screen readers. but in your scenario this is a need to work properly.

Solution is to wrap Overlay in container:
import React from "react";
import { Overlay } from "react-bootstrap";
import { X } from "react-bootstrap-icons";
export const PopupMenuWrapper = (props) => {
const { target, title, show, onClose, children } = props;
const ref = React.useRef(null);
return (
<div ref={ref}>
<Overlay
container = {ref.current}
target={target.current}
show={show}
placement="bottom-start"
rootClose={true}
onHide={onClose}
>
...
</div>
...

Related

react-headroom - momentarily disable when animating scroll from another component

I have have a tabs-component that becomes sticky when a user scrolls past it's scroll position on the page. When a tab is clicked it will scroll the user up or down, depending on where their current scroll position is, in relation to the related tab-content's scroll position.
Is it possible to momentarily disable/reactivate the react-headroom functionality from another component, when required?
Ideally, when scroll-up is initiated via these tabs, I wish to trigger the react-headroom hide-header functionality, if the header is already shown, or disable the show-header functionality, if the header is already hidden. Any suggestions how one would achieve this?
Thanks in advance.
I ended up resolving this by storing a boolean value of false in useState, to control the toggling of the react-headroom's disable prop. As the scrollIntoView animation is based on time, not distance travelled, so you can rely 😅 on setting a setTimeout of 500ms to reset the the useRef value back to its original state.
When I want to trigger a scroll and/or disable the header hide/show functionality, I place a useRef on the scroll-trigger, which an onClick event calls a function. Within this function, I set useState to true (to activate the disable prop & disable the header), animate the page, then use a setTimeout of 500ms to reset the useRef value back it original state, which will re-activate the header functionality.
import { useRef, useState } from 'react';
import Headroom from 'react-headroom';
const SiteHeader = (): JSX.Element => {
const [headroomDisabled, setHeadroomDisabled] = useState(false);
const myRef = useRef<HTMLDivElement>(null);
const myRef2 = useRef<HTMLDivElement>(null);
const executeScroll = () => myRef.current?.scrollIntoView();
const executeScrollUp = () => {
setHeadroomDisabled(true);
myRef2.current?.scrollIntoView();
setTimeout(() => {
setHeadroomDisabled(true);
}, 500);
};
return (
<>
<Headroom disable={headroomDisabled}>
<h2>Test header content</h2>
</Headroom>
<div ref={myRef2}>Element to scroll to</div>
<button onClick={executeScroll}> Click to scroll </button>
<div style={{ marginTop: '150vh' }}></div>
<div ref={myRef}>Element to scroll to</div>
<button onClick={executeScrollUp}> Click to scroll </button>
</>
);
};
export default SiteHeader;
I ended up taking this a step further and placing this logic in some global state, as so any component could utilise this functionality 🍻

React - Bootstrap: Offcanvas with embedded mapped tabs content disappearing on Re-Render

I have a React application that includes a Bootstrap Offcanvas component with embedded Tabs. For the Tabs and Content, they are created from an array of Table components which is mapped to create them dynamically. These Table components take a container as input, which I used a Ref to bind them to the dynamically generated Tabs. The first render works perfectly, however; when I close the Offcanvas and open it again, the Tab content is now empty. I'm assuming this is an issue with the Ref.
Here is the code for the Tab component.
import React, { useEffect, useRef } from 'react';
import Tabs from "react-bootstrap/Tabs";
import Tab from "react-bootstrap/Tab";
function LayerResults({featureTables, mapView, placeFeatureTables}) {
const resultRef = useRef(featureTables.map(() => React.createRef()));
useEffect(()=> {
if (featureTables.length) {
placeFeatureTables(resultRef);
}
}, [mapView])
return (
<Tabs defaultActiveKey={featureTables[0].layer.title} className="tableResults">
{featureTables && featureTables.map((table, i) => {
return (
<Tab
className="TableTab"
title={`${table.layer.title.slice(0,10)}...`}
eventKey={table.layer.title}
key={`${i}`}
>
<div ref={resultRef.current[i]}></div>
</Tab>
)
})}
</Tabs>
)
};
And here's the code for the placeFeatureTables() function from an even higher component that the useEffect uses which updates the featureTables state with the container / Ref.
const placeFeatureTables = (containerRefs) => {
setFeatureTables(prev => {
const placedTables = prev.map((table, i) => {
table.container = containerRefs.current[i].current;
return table;
})
return placedTables;
})
}
I feel like this is most likely the issue area and the Containers / Refs are not being updated properly on re-render and I should be using something other than useRef. The thing that's really bugging me though, is that I swear this was working earlier.
Note1: The FeatureTables come from ESRI ArcGIS Javascript API. Although, it's just the Ref being passed, so I don't think it's necessary to know about them. FeatureTable Documentation
Note2: If I console.log the featureTables. The first time, the container highlights the area where it's at. Once I close and re-open and then click on the new featureTables container, it says the node does not exist.

React opening react-bootstrap modal in content component from sidebar

I was wondering what would be best way to open a react-bootstrap modal from sidebar that is in another nested component?
To clarify a bit I have a modal an it is rendered in the component which lists items and it is used for edit them when you click one of the items. What i would like to do is when user clicks on the button in the sidebar that it opens that modal to add a item to the list (change from edit to add).
At first i was thinking to move the modal to parent of the sidebar but the problem is i would have to pass props 4 times to get to the list, and that could be confusing to someone who will later edit this code.
Next i taught about the context and sure that could work but to make a global state just for that it loses its purpose.
I taught about using refs but i am not sure on how to implement them.
And last thing what i taught about was just render the modal as a new component inside the sidebar but it wont change much since i want to update the list once user has added the item and i would be in the same spot.
And i would like to avoid directly accessing DOM with id (if possible), because i would like to find a "react way" of doing this.
For example (just to visualize not the actual code)
<Root>
<Component1>
<Component2>
<SideBar>
<Button onClick={setShowModal(true)}> <!-- click here -->
</SideBar>
</Component2>
<Component3>
<Component4>
<Modal show={showModal}/> <!-- open modal here -->
</Component4>
</Component3>
</Component1>
</Root>
I would just like a hint on how to approach this and what would be the best way to do this.
The most "React" way would be to store the state on the component that is the first common ancestor of the button and the modal. The downside to this approach, as you mentioned, is that you will have to pass this state down. I think this would still be a clean approach.
Another thing you could consider for this case is an event emitter. You have the Modal listen to an event to change its "open" state:
import EventEmitter from 'EventEmitter';
const eventEmitter = new EventEmitter();
export default function Modal() {
const [open, setOpen] = React.useState(false);
const handleToggleModal = () => {
setOpen(!open);
}
React.useEffect(() => {
eventEmitter.addListener('toggle-modal', handleToggleModal)
return () => {
eventEmitter.removeListener('toggle-modal', handleToggleModal);
}
}, [])
// ...
}
In the component that opens/closes the modal, you will then have to emit the corresponding event:
eventEmitter.emit('toggle-modal');

Set react native component prop through passed reference

I need to show/hide a modal based on user interaction (ie - a button press) in a component which is neither a parent or child of the modal itself. I'm currently trying to do so by passing the modal as a prop to the modal controller, but the following errors are thrown depending on which method I call:
TypeError: modal.setNativeProps is not a function
TypeError: modal.setState is not a function
Is there a way to show the modal given how this is structured?
import Modal from 'react-native-modal'
const modalRef = React.createRef();
const modal = <Modal ref={modalRef} isVisible={false}>
<ModalController modal={modalRef} />
export const ModalController = ({modal}) => {
function onButtonPress(){
modal.setState({isVisible:true})
modal.setNativeProps({isVisible:true})
}
return (
<Button title='Show Modal' onPress={onButtonPress()} />
)
}
Ciao, in case there is no parent/child relation between components, the only way I found to pass/set data from one component to another is use a global state manager like react-redux. With redux you can easly define a global state and set it from component that fire modal open/close. Modal component reads this state and open/close itself.
So I think you've gotten a little confused.
Firstly, remember that everything inside ModalController is going to execute on every render. Your function onButtonPress will be created every render (this is fine), but you are actually calling that function when you pass it to onPress render . This means you're executing onButtonPress on every render, which is probably not what you want.
This is an easy fix - you just remove the () so it's just onPress={onButtonPress}. Now it'll only trigger when the button is pressed.
More fundamentally, the solution to your problem is much simpler than what you've done in your code. Generally 'refs' are only used in special cases where you really want to tell your components what to do (like telling a ScrollView to scroll to a particular position, or telling an input to focus so the keyboard shows). If you're using a ref it should be very intentional.
So a simple solution to have a component with a button that shows a modal could look like:
import React, {useState} from 'react';
import {View, Button, Text} from 'react-native';
import Modal from 'react-native-modal';
export const ModalController = () => {
const [isModalVisible, setIsModalVisible] = useState(false);
function onButtonPress() {
setIsModalVisible(true);
}
return (
<View>
<Button title='Show Modal' onPress={onButtonPress} />
{isModalVisible && <MyModal />}
</View>
);
};
const MyModal = () => (
<Modal>
<Text>Hey I am a modal</Text>
</Modal>
);
Notice the use of useState. This is how you hold 'state' in a functional component like this (as opposed to a class component where you would use setState.
I hope this helps. Let me know if you have questions!

Restore scroll position after toggled a modal in react with useContext

Goal
Restore the scoll position of the window after the a div has toggled from position fixed to none.
Problem
Restoring the scroll position doesnt work although i get it saved correctly to the state.
Description
I have a page where a modal is opened via onClick. Therefore i created a "ToggleModalContext" to pass the props to the modal on the one hand and to the background div on the other hand. I want to modify the background div with setting the css property position to fixed to avoid that the background is scrolled instead of the content of the modal.
When the modal is closed, the position: fixed is removed and i want to restore the scroll position of the window.
This last step doesnt work. Maybe someone else has an idea?
ToggleModalContext (Thats the context, where the scroll restore function is called)
import React from "react";
export const ToggleModalContext = React.createContext();
export const ModalProvider = props => {
const [toggle, setToggle] = React.useState(false);
const [scrollPosition, setScrollPosition] = React.useState();
function handleToggle() {
if (toggle === false) {
setScrollPosition(window.pageYOffset); // When the Modal gets opened, the scrollposition is saved correctly
}
if (toggle === true) {
window.scrollTo(0, scrollPosition); // Restoring doesnt work.
}
setToggle(!toggle);
}
return (
<ToggleModalContext.Provider value={[toggle, handleToggle]}>
{props.children}
</ToggleModalContext.Provider>
);
};
Maybe somebody has a idea?
Maybe i have to use useEffect? But how?
Thanks for your time in advance :)
From the description you have provided, you are using a fixed position on the background div to remove scrolling on the window when you open your modal
On the other hand, you are calling
if (toggle === true) { window.scrollTo(0, scrollPosition);}
before your modal has closed. At this time, the background div is in a fixed position and there is no where to scroll to.
You need to ensure that your modal has safely closed and your background div is back to its normal position before calling this function. To see the behavior, you can use a setTimeout function and call this function there with a set time e.g.
setTimeout(() => window.scrollTo(0, scrollPosition), 2000);

Resources