Strategy for Pinterest Product Hunt style modals and routing - javascript

(I am using the latest versions of React, React-Router and React-Aria-Modal)
I am trying to create Routing behavior like the one seen on Pinterest or Product Hunt. For those unfamiliar, it boils down to having a Feed of Things (pins, products, etc) in a preview mode. Clicking on the name or some 'view more' link in that preview, opens the Thing in a modal and changes the route accordingly. If I was to copy-paste that URL, or refresh the page, the Thing now renders like a page.
So, on the 'View More' Link component, I set up the state (view = Modal) and in the Thing component to see if whether to render in a Modal or not.
So, essentially:
<App>
<Feed>
{things.map((thingId) => <Thing id={thingId} />)}
</Feed>
</App>
Within a Thing, it roughly looks like this:
renderThing() {
return (
<div>
<Name />
<Link
to={"/things/ + this.props.id}
state = {{
view: "Modal"
}}
>
View More
</Link>
</div>
);
}
render() {
if(this.state.view == 'Modal'){
return (<AriaModal>{renderThing()</AriaModal>);
} else {
return renderThing();
}
}
I run into an issue where now the page background disappears on clicking on a link while the modal does show up. I imagine this is due to the navigation by clicking on the link.
React-Router has an example like this which solves this but that has some issues as well. They seem to essentially store the previous children at the App level and render that in addition to the requested Modal/Route - essentially causing a re-render of the entire feed which feels neither performant nor clean.
Moreover, if 'Next/Previous' style interaction was to be set up, it seems the entire background would be loaded everytime router state was updated by clicking on 'Next/Previous' which also feels unnecessary.
So, I am wondering how best to think about setting this up - so that the previous page still shows up in the background and the route is updated as well.
Any thoughts would be super appreciated!

I've written a CodePen that demonstrates Pinterest routing. It uses the Navigation router instead of the React Router but I hope you're open to that. You can see that that if the oldState is populated then the list is visible and I render into the modal div. If the oldState is empty then the list isn't visible and I render into the container div. You can try this out by opening one of the Hyperlinks in a new tab. If you've got any questions about the code, please let me know.
var {StateNavigator} = Navigation;
var {NavigationLink} = NavigationReact;
var Thing = ({thing}) => <h2>{thing}</h2>;
var Things = ({things, stateNavigator}) => (
<div>
<ul>
{things.map(thing =>
<li key={thing}>
<NavigationLink
stateKey="thing"
navigationData={{thing: thing}}
stateNavigator={stateNavigator}>
{thing}
</NavigationLink>
</li>
)}
</ul>
<div id="modal"></div>
</div>
);
var stateNavigator = new StateNavigator([
{key: 'things', route: ''},
{key: 'thing', route: 'thing'}
]);
var THINGS = ['one', 'two', 'three', 'four', 'five'];
stateNavigator.states.things.navigated = () => {
ReactDOM.render(
<Things things={THINGS} stateNavigator={stateNavigator} />,
document.getElementById('container')
);
};
stateNavigator.states.thing.navigated = (data) => {
var oldState = stateNavigator.stateContext.oldState;
ReactDOM.render(
<Thing thing={data.thing} />,
document.getElementById(oldState ? 'modal' : 'container')
);
};
stateNavigator.start();

Related

Gatsby Link not working with isPartially Active on MDX rendered pages

I have been building out a static site with gatsby and a few sections I have rendered out pages written as MDX and parsed through their own layouts. This is all via the gatsby-plugin-mdx file and it works great.
However, I'm trying to get the top-level navigation to highlight as active when a user navigates to a sub-page in that section. I am using the code from the Gatsby docs and it works on the pages I created as normal JS files.
Example:
<Link partiallyActive={true} activeClassName="header-nav-active" to={menu.url} title={menu.title}>
{menu.label}
</Link>
It seems that it doesn't seem to work for the MDX pages, even though what is rendered in location.pathname is the same. My current structure is:
src
-pages
--section
----section-subpage.js
--other section
----other-section-sub
-----index.mdx
----other-section-sub-2
-----index.mdx
Ultimately if you look at this layout I would like "Figma" to be highlighted as active when you are navigating though the sub pages in that section.
Have you tried using getProps helper function? Because Gatsby's router extends from React's (#reach/router) you can take advantage of the advanced props to customize your style
You can create a partiallyActive link like:
const isPartiallyActive = ({ isPartiallyCurrent }) => {
return isPartiallyCurrent
? { className: 'navlink-active navlink' }
: { className: 'navlink' }
}
const PartialNavLink = props => (
<Link getProps={isPartiallyActive} {...props}>
{props.children}
</Link>
)
And then simply use:
<PartialNavLink to="/figma">Figma</PartialNavLink>
Or in an unrefined way:
<Link getProps={({ isPartiallyCurrent }) => isPartiallyCurrent ? { className: "active" } : null } to={"/figma"}>
Figma
</Link>

User UI feedback after clicking on a react-router-dom <Link/>?

Here's what I'm dealing with:
Single page app built with react + react-router-dom
User clicks on a <Link to={"/new-page-route"}/>
URL changes and my <Route/> starts rendering a new <Component/>
I mean, React is fast, but my new component takes a while to render because it's a whole new page. So it should take something between 200 and 400ms. And if I get no UI feedback, it feels that my click has not worked.
I need some kind of UI feedback so my user's know their click has been fired and something is going on. I don't think I need loaders or anything, but something to show that the click has been "accepted" by the UI.
In theory that could be handled by CSS using:
-webkit-tap-highlight-color: #SOMECOLOR
active: #SOMECOLOR
But somehow, when the URL changes and the new render begins, the browser is not being able to paint those CSS results to the screen, at least this is the behavior on Chrome and Firefox. It gets kind of weird, sometimes I see the tap-highlight and the active-change but almost always I don't see it.
NOTE: This is not the 300ms default delay on mobile that waits for the double tap. I've dealt with that using the appropriate tags.
What I thought about doing is:
Stop using the <Link/> component and use a normal <a/>.
Create a clicked state to be update after the click event
Call event.preventDefault() to prevent normal behavior of the <a/> navigation
Use the clicked state to render some new styles for the UI feedback
Fire history.push("/new-page-route") on a useEffect after clicked state has become true
Something like:
const newUrl = "/new-page-route";
const [clicked,setClicked] = useState(false);
const history = useHistory(); // HOOK FROM react-router-dom
function handleLinkClick(event) {
event.preventDefault();
setClicked(true);
}
useEffect(() => {
if (clicked === true) {
history.push(newUrl);
// OR MAYBE ADD A TIMEOUT TO BE EXTRA SURE THAT THE FEEDBACK WILL BE PAINTED
// BECAUSE I'VE SEEN THE BROWSER NOT BEING ABLE TO PAINT IF I DON'T GIVE IT SOME EXTRA TIME
setTimeout(() => history.push(newUrl),100);
}
},[clicked,history]);
// USE THE clicked STATE TO RENDER THE UI FEEDBACK (CHANGE TEXT COLOR, WHATEVER I NEED);
QUESTION
Has anyone had this issue before? What is a good way of solving this? I guess that in theory the browser should be able to paint before the new render begins, but this is not what I'm getting.
SANDBOX WITH THE ISSUE
https://codesandbox.io/s/nice-monad-4fwoc <=== CLICK HERE TO SEE THE CODE
https://4fwoc.csb.app <=== CLICK HERE TO SEE THE RESULT ONLY
On desktop: I'm able to see the BLUE background for the active state when clicking the link
On mobile: I don't see any CSS change when clicking the links. Unless I tap AND HOLD
Comments: Imagine that your component takes a while to render. Without any UI feedback, it feels that you haven't clicked at all, even though it is rendering on background.
import React from "react";
import { Link, Switch, Route } from "react-router-dom";
import styled from "styled-components";
import "./styles.css";
const HOME = "/";
const ROUTE1 = "/route1";
const ROUTE2 = "/route2";
const LS = {};
// REGULAR CSS RULES FOR THE className="link" ARE ON "./styles.css"
LS.Link_LINK = styled(Link)`
color: black;
-webkit-tap-highlight-color: red;
&:active {
background-color: blue;
}
`;
export default function App() {
return (
<React.Fragment>
<div>
<Switch>
<Route exact path={HOME} component={Home} />
<Route exact path={ROUTE1} component={Component1} />
<Route exact path={ROUTE2} component={Component2} />
</Switch>
</div>
</React.Fragment>
);
}
function Home() {
return (
<React.Fragment>
<div>I am Home</div>
<LS.Link_LINK to={ROUTE1}>Route 1 (using styled-components)</LS.Link_LINK>
<br />
<Link className={"link"} to={ROUTE1}>
Route 1 (using regular CSS)
</Link>
</React.Fragment>
);
}
function Component1() {
return (
<React.Fragment>
<div>I am Component1</div>
<LS.Link_LINK to={ROUTE2}>Route 2 (using styled-components)</LS.Link_LINK>
<br />
<Link className={"link"} to={ROUTE2}>
Route 2 (using regular CSS)
</Link>
</React.Fragment>
);
}
function Component2() {
return (
<React.Fragment>
<div>I am Component2</div>
<LS.Link_LINK to={HOME}>Home (using styled-components)</LS.Link_LINK>
<br />
<Link className={"link"} to={HOME}>
Home (using regular CSS)
</Link>
</React.Fragment>
);
}
You should use two loading phases.
Use lazy
loading
when loading the "heavy" page and display a
skeleton
component as a fallback to show the user that something is loading.
Use a component/redux state loading indicator for when your page is
loading data from apis
Trying to achive that with react-router links is not the best way because routing happends instantly, it is not the thing that stalls your application. Api calls and multiple re-renders cause an application to behave slowly.

"Warning: react-modal: App element is not defined. Please use `Modal.setAppElement(el)` or set `appElement={el}`"

How do I fix this warning in console of a React app using the react-modal package:
Warning: react-modal: App element is not defined. Please use Modal.setAppElement(el) or set appElement={el}
I have not been successful at figuring out what el is supposed to be.
Context:
in my App.js root component file:
...
import Modal from 'react-modal';
...
class App extends Component {
...
render(){
...
<Modal
className="modal"
overlayClassName="overlay"
isOpen={foodModalOpen}
onRequestClose={this.closeFoodModal}
contentLabel="Modal"
>
...
}
}
Where ... indicates code not shown.
Everything works fine, but when the Modal is opened, the following Warning appears in my console:
index.js:2177 Warning: react-modal: App element is not defined. Please use Modal.setAppElement(el) or set appElement={el}. This is needed so screen readers don't see main content when modal is opened. It is not recommended, but you can opt-out by setting ariaHideApp={false}.
In the react-modal docs all I can find is the following:
App Element
The app element allows you to specify the portion of your app that should be hidden (via aria-hidden) to prevent assistive technologies such as screenreaders from reading content outside of the content of your modal.
If you are doing server-side rendering, you should use this property.
It can be specified in the following ways:
DOMElement
Modal.setAppElement(appElement);
query selector - uses the first element found if you pass in a class.
Modal.setAppElement('#your-app-element');
Unfortunately, this has not helped! I cannot figure out what el is supposed to represent.
Here are some of the many property variations I have tried adding to my Modal component:
`appElement={el}`,
`appElement="root"` where `root` is the id that my App component is injected into
`appElement={'root'}`
`appElement="div"`,
`appElement={<div>}`,
`appElement={"div"}`
I've also tried calling Modal.setAppElement('root'); from inside src/index.js, where root is the root element that my App component is injected into, and index.js is where I do that.
Add ariaHideApp={false} to Modal attributes.
This should work:
<Modal isOpen={!!props.selectedOption}
onRequestClose={props.clearSelectedOption}
ariaHideApp={false}
contentLabel="Selected Option"
>
</Modal>
Some solutions are given in react-modal issue #133:
The problem lies here:
Depending on when it evaluates react-modal#1.6.5:/lib/helpers/ariaAppHider.js#L1:
document.body does not exist yet and it will resolve to undefined || null.
if Modal.setAppElement() is called with null or not called at all with the <script /> placed on <head /> (same as above).
Probably it can also happen if called with a selector that does not match any results.
Solutions:
Browser Rendering:
#yachaka snippet prevents this behavior by defining the element before placing the <Modal />:
componentWillMount() {
Modal.setAppElement('body');
}
#ungoldman answer, if you don't want to depend on `setAppElement':
Inject the bundled application JS into <body> instead of <head>.
Though ideally react-modal should wait until the DOM is loaded to try attaching to document.body.
server-side:
If rendering on server-side, you must provide a document.body, before requiring the modal script (perhaps it should be preferable to use setAppElement() in this case).
Update:
react docs have been updated to include the information above, so they should now be clearer for users running into this issue.
react-modal issue #567: add information (from issue #133 linked above) to the docs.
Just include appElement={document.getElementById('app')} inside your modal like this
<Modal
className="modal"
appElement={document.getElementById('app')}
>
It will work 100% if app is your central in index.html from where react loads.
This is my TypeScript Modal component which wraps react-modal v3.8.1:
import React from 'react'
import ReactModal from 'react-modal'
interface Props {
isOpen: boolean
ariaLabel?: string
}
const Modal: React.FC<Props> = ({
children,
ariaLabel = 'Alert Modal',
isOpen,
}) => (
<ReactModal
appElement={document.getElementById('root') as HTMLElement}
ariaHideApp={process.env.NODE_ENV !== 'test'}
isOpen={isOpen}
contentLabel={ariaLabel}
testId="modal-content"
>
{children}
</ReactModal>
)
export default Modal
Usage in component with state = { isOpen: true }:
<Modal isOpen={this.state.isOpen}>
<p>
Modal Content hereā€¦
</p>
<button onClick={() => { this.setState({ isOpen: false }) }}>Okay</button>
</Modal>
If getting the Warning: react-modal: App element is not defined... error when running tests (we were running Jest), you can suppress the warnings by adding the following to your test file:
import ReactModal from 'react-modal';
ReactModal.setAppElement('*'); // suppresses modal-related test warnings.
The shortest solution is to add
appElement={document.getElementById("hereIsYourRootElementId")}
It lets react-modal know where is your root element.
For reference, since it was a pain for me, if you are doing SSR, use the following code to prevent errors server-side:
if (typeof(window) !== 'undefined') {
ReactModal.setAppElement('body')
}
You could put this in componentDidMount() anywhere you use a modal or I put it in a custom modal component so it's nice and DRY.
Just put this
Modal.setAppElement('#root')
This will solve the warning. The root element coming from inside public folder index.html.
you need to add # before your root element id.
import React from 'react';
import Modal from 'react-modal';
Modal.setAppElement('#root');
const OptionModal = (props) => (
<Modal
isOpen={!!props.selectedOption}
contentLabel='this is the selected option'
>
<h3>Selected Option</h3>
{props.selectedOption && <p>{props.selectedOption}</p>}
<button onClick = {props.handleCloseOptionModal}>Close</button>
</Modal>
);
export default OptionModal;
here is the reference:
http://reactcommunity.org/react-modal/accessibility/
If you get that warning on testing with the "react-testing-library" here is a solution:
https://github.com/reactjs/react-modal/issues/576#issuecomment-524644035
using the react-testing-library (https://testing-library.com/) I get rid of that warning with:
import Modal from "react-modal";
const { container } = render(<MyComponent />);
Modal.setAppElement(container);
.... // to the testing, use Modal
or, if you want to test the modal component directly:
const { container, rerender } render(<MyModalComponent isOpen={false} />);
Modal.setAppElement(container);
// now the appElement is set we can show the modal component
rerender(<MyModalComponent isOpen={false} />);
.... // to the testing
For Nextjs, I think you can solve this by adding the below to outside your modal component, maybe on top, before the component is declared.
Modal.setAppElement('#__next')
Delete this attrib
className="modal"
and run again

When do React PropTypes get initialized?

I am having an issue with required PropTypes throwing errors. I would expect the PropType to throw an error if the Component was was being directly rendered. Here is a small sample of what I am trying to achieve.
You'll notice that the Button prop has a required PropType of handle click.
But I want the implementation of a Modal to be as simple as possible.
And since I don't have the context of Modal I can not bind the handleClick method directly to the Button so I pass the Button in as child and map over the children adding the handleClick method to the child component. This works pretty well besides throwing the error for Button because <Button> gets called and checked before it truly gets rendered.
I have tried to do this a few other ways as well using Higher Order Components which worked as well. But the implementation seemed convoluted and tedious this seems like a much simpler way to just generate a Modal when it is needed. You don't need any props you just pass in a child component and it will add the click handler.
It would be awesome to either Bypass the proptypes check until it is actually rendered in Modal Component or maybe there is a simpler way, all feed back is welcomed.
https://jsfiddle.net/kriscoulson/00xLw0up/1/
var Button = (props) =>
<button onClick={props.handleClick}>
{props.children}
</button>
Button.propTypes = {
handleClick: React.PropTypes.func.isRequired
}
class Modal extends React.Component {
openModal () {
console.log('Opening Modal.....')
}
childrenWithProps () {
return React.Children.map(this.props.children,(child) => React.cloneElement(child, {
handleClick: this.openModal
})
);
}
render () {
return (
<div>
{this.childrenWithProps()}
</div>
);
}
}
var App = () =>
<Modal>
<Button>Launch Modal</Button>
</Modal>
ReactDOM.render(
<App/>,
document.getElementById('container')
);
<script src="https://facebook.github.io/react/js/jsfiddle-integration-babel.js"></script>
<div id="container">
<!-- This element's contents will be replaced with your component. -->
</div>

State Affecting Multiple Components in React with Baobab

I'm in the process of translating a 90% complete website produced in HTML, CSS and jQuery into a more forward thinking react "application". I have some pretty basic principals that I'm struggling to get to grips with - I really like react's JSX language but and struggling to deal with heavy UI manipulation that I've achieve in jQuery previously.
To get me used to the concepts I'm starting with one of the most simple interactions on my website - Hover over the menu button and bring out the side menu partially (e.g. 20px). My components are structured like so:
var App = React.createClass({
render: function() {
return (
<div id="reactWrap">
<MainNav ref="main-nav"/>
<SideNav projects={PROJECTS} ref="side-nav" />
<RouteHandler projects={PROJECTS} ref="content" />
</div>
)
}
});
MainNav includes the 'hamburger' button and the site title:
render: function() {
return (
<nav className="fixed-nav">
<div id="menu-button" onMouseEnter={this.teaseMenu} onMouseLeave={this.unteaseMenu} ref="menu-btn">
<span className="menu-line"></span>
</div>
<span className="site-title">Lorem Ipsum</span>
</nav>
)
}
When "#menu-button" is hovered over I'm changing the state tree that I have defined in Baobab inside "teaseMenu()" using "menuActions.isHovering()".
....
teaseMenu: function(e) {
menuActions.isHovering();
// other stuff done
},....
What I'm struggling with is that once the change has been affected in my stateTree, I'm unsure how to then give the knowledge of this change to all of the other elements that rely on it. For example, how does the SideNav which is totally unrelated to "MainNav" and it's child "#menu-button" become aware of this change and alter it's state accordingly? This would simply be solved in jQuery with something like the following:
globalVars.menuBtn.on({
mouseenter: function(e) {
var self = $(this);
var movePercentage = (-100 + (20 / globalVars.sideNavW * 100))/2;
if (!pageStatus.menuActive) {
globalVars.wrap.addClass('menu-hover');
globalVars.sideNav.css({
'left': movePercentage + '%'
});
menuBtnIn(e, self);
}
},....
Flux is a great way to do this, and I highly recommend you incorporate it in to your site as it'll make a lot of things easier. Have a read of this page for more information: https://facebook.github.io/flux/docs/overview.html
However you could also use state in your root App to affect change on MainNav whenever something occurs in SideNav.
Consider this change to your root app component:
var App = React.createClass({
getInitialState: function() {
return {
sideNavShowSomething: false
}
},
mainNavChangeHandler: function(sideNavShowSomething) {
this.setState({
sideNavShowSomething: sideNavShowSomething
})
},
render: function() {
return (
<div id="reactWrap">
<MainNav ref="main-nav" onChange={this.mainNavChangeHandler} />
<SideNav projects={PROJECTS} ref="side-nav" showSomething={this.state.sideNavShowSomething} />
<RouteHandler projects={PROJECTS} ref="content" />
</div>
)
}
});
The above is an example of how you can use the state of your root App to affect change on it's children. Your MainNav now takes a prop of onChange which is a function. This function is called by MainNav any time a change occurs that you want to notify your root App of. In this example case, the mainNavChangeHandler is executed with a boolean sideNavShowSomething variable. In your case you probably want to do something more complex ;)
So when you call this.props.onChange(true) in your MainNav then your root app will update it's state and SideNav will then receive this.props.showSomething as true wheras previously it was false. And by so doing you can affect change between child components by utilizing callbacks to the root App and handling new props to give children from them.

Categories

Resources