My React app has overflow: hidden applied to the body for reasons of tranformations. This left the issue of not being able to register the scroll position of scrollY as scrolling happens in child components.
How can I apply window.scrollY or similar to register the scroll position of <div id="innerContainer">?
Here is a snippet of where one scroll addEventListener is creating a class on scroll. Problem is without registering the scrollY I cannot add the event.
componentDidMount () {
window.addEventListener('scroll', this.handleHeaderStuck);
}
componentWillUnmount () {
window.removeEventListener('scroll', this.handleHeaderStuck);
}
or
handleHeaderStuck() {
console.log('scrollPos', window.scrollY)
if (window.scrollY === 0 && this.state.isStuck === true) {
this.setState({isStuck: false});
}
else if (window.scrollY !== 0 && this.state.isStuck !== true) {
this.setState({isStuck: true});
}
}
and the general layout of is...
render() {
return (
<main className={this.state.isStuck ? 'header-stuck' : ''}>
<div id="container">
<header />
<div id="innerContainer">...</div>
<footer />
</div>
</main>
Update - after applying the answer submitted by Kingdaro:
Screenshot of console using the code submitted by Kingdaro that registers the scrollPos change but not the actual position
A ref should do the job here. Also make sure to unregister the event listener when the component unmounts, to avoid memory leaks.
class Example extends React.Component {
innerContainer = null
componentDidMount() {
this.innerContainer.addEventListener("scroll", this.handleHeaderStuck)
}
componentWillUnmount() {
this.innerContainer.removeEventListener("scroll", this.handleHeaderStuck)
}
// using a class property here so the `this` context remains properly bound
handleHeaderStuck = () => {
console.log('div scroll position:', this.innerContainer.scrollTop)
}
render() {
return <div id="innerContainer" ref={el => (this.innerContainer = el)} />
}
}
Related
I have a react app, and i am trying to build a focus trapper element, that lets the user tab through elements normally but won't let you focus outside their container.
What works
I am doing so by rendering a first and last "bounder" to sandwich the actual content between two focusable divs that should pass the focus forwards or backwards based on the direction they received it from.
the code for the container:
export class QKeyBinder
extends ComponentSync<QKeyBinder_Props, State> {
private firstTabBinder: React.RefObject<HTMLDivElement> = React.createRef();
private lastTabBinder: React.RefObject<HTMLDivElement> = React.createRef();
protected deriveStateFromProps(nextProps: QKeyBinder_Props): State {
return {};
}
private renderFirstTabBounder() {
return <div
tabIndex={0}
ref={this.firstTabBinder}
className={'q-key-binder__tab-binder'}
role={'tab-binder'}
onKeyDown={(e) => {
if (e.key === 'Tab' && e.shiftKey) {
e.preventDefault();
stopPropagation(e);
return this.lastTabBinder.current!.focus();
}
}}/>;
}
private renderLastTabBounder() {
return <div
tabIndex={0}
ref={this.lastTabBinder}
className={'q-key-binder__tab-binder'}
role={'tab-binder'}
onKeyDown={(e) => {
if (e.key === 'Tab' && !e.shiftKey) {
e.preventDefault();
stopPropagation(e);
return this.firstTabBinder.current!.focus();
}
}}/>;
}
render() {
const className = _className('q-key-binder', this.props.className);
return <div className={className}>
{this.renderFirstTabBounder()}
{this.props.children}
{this.renderLastTabBounder()}
</div>;
}
}
As you can see, i have it working by pressing tab again.
I want the bounders to have a onFocus handler to pass the focus along once they get it.
What didn't work
Since i can't know beforehand who the next focusable element is, I tried dispatching a keyboard event, e.g:
onFocus={(e}=>{
document.body.dispatchEvent(new KeyboardEvent('keypress',{key:'Tab'}))
}}
Dispatching the event on the body.document, the e.target, the body, the window, none of these work.
Just can't seem to simulate another tab press, or find a way to focus the next element without depending on a selector, or a wrapper, which causes extra complexity.
Any help would be much appreciated!
const Index = () => {
// Ref Links
const frefLinks = {
1: useRef(1),
2: useRef(2),
3: useRef(3),
4: useRef(4),
5: useRef(5),
};
const scrollLink = (i) => {
let frefLink = frefLinks[i];
return frefLink.current.scrollIntoView({
block: "start",
behavior: "smooth"
});
};
return (
<React.Fragment>
<style.globalStyle/>
<NavMenu
scrollToLinkA={() => { scrollLink(1) }}
scrollToLinkB={() => { scrollLink(2) }}
scrollToLinkC={() => { scrollLink(3) }}
scrollToLinkD={() => { scrollLink(4) }}
scrollToLinkE={() => { scrollLink(5) }}
/>
<Sect1 fref={frefLinks[1]} onWheel={e => e.deltaY <= 0 ? console.log('up') : scrollLink(2) }/>
<Sect2 fref={frefLinks[2]} onWheel={e => e.deltaY <= 0 ? scrollLink(1) : scrollLink(3) }/>
<Sect3 fref={frefLinks[3]} onWheel={e => e.deltaY <= 0 ? () => { scrollLink(2) } : () => { scrollLink(4) } }/>
<Sect4 fref={frefLinks[4]} onWheel={e => e.deltaY <= 0 ? scrollLink(3) :scrollLink(5) }/>
<Sect5 fref={frefLinks[5]} onWheel={e => e.deltaY <= 0 ? scrollLink(4) :console.log('down')}/>
</React.Fragment>
);
}
scrollToLinkA to E are nested div's onClick event. It works perfectly for onclick, onWheel is correct in the Section's nested div. div onWheel = {onwheel} However, onwheel event is not firing the scroll function though console.log still works. I tried both normal calling the function and via arrow at Sect3's onwheel but it still fails.
I searched hard enough on react+onwheel+scrollintoview but I really cannot find any applicable answers. I'm not looking into onscroll, just onwheel is what i want.
I really want to reuse the scrollLink function and not adding extra window listener event or dependency. Please advise.
I can't tell if scrollLink function is not calling by onWheel, but I can be 100% sure that scrollLink is working properly as it is from onClick. I put console.log inside scrollLink and it logs too.
I have nav and section. nav's div's onClick fires scrollLink function and scrollintoview works perfectly. Now I'm doing section's div's onWheel event to fire the same 'scrollLink' function, it is not responding but console.log is working everywhere.
I can't post comment anymore so I'm posting here.
I am working in React.js and have textarea elements that dynamically expand and contract based on the size of the user's input. The intended functionality is as follows:
This works correctly in a desktop context. However, on any mobile or tablet in a modern browser (tested Safari, Chrome and Firefox) the textarea element only expands, it does not contract when content is deleted.
At first I thought it might have something to do with the onChange handler I was employing, however, the same issue remains when swapping it out with an onInput handler. So I believe the issue resides in the resize() method.
Does anyone have an idea of why I'm experiencing this issue?
I have created a style-free fiddle to share with you the basic functionality. Interestingly, the bug doesn't occur in the JSFiddle simulator on a mobile device, but if you take the same code and put it in another react environment, the bug occurs on a mobile device in modern browsers.
class Application extends React.Component {
render() {
return (
<div>
<Textarea value="This is a test" maxLength={500}/>
</div>
);
}
}
class Textarea extends React.Component {
constructor(props) {
super(props);
this.state = {
value: this.props.value
? this.props.maxLength && this.props.maxLength > 0
? this.props.value.length < this.props.maxLength
? this.props.value
: this.props.value.substring(0, this.props.maxLength)
: this.props.value
: '',
remaining: this.props.value
? this.props.value.length < this.props.maxLength
? this.props.maxLength - this.props.value.length
: 0
: this.props.maxLength
};
this.textAreaRef = React.createRef();
this.textAreaHeight = null;
this.textAreaoffSetHeight = null;
}
componentDidMount() {
window.addEventListener('resize', this.resize);
this.resize();
}
componentWillUnmount() {
window.removeEventListener('resize', this.resize);
}
handleChange = event => {
const target = event.target || event.srcElement;
this.setState({
value: target.value,
remaining: target.value
? target.value.length < this.props.maxLength
? this.props.maxLength - target.value.length
: 0
: this.props.maxLength
});
this.resize();
};
resize = () => {
const node = this.textAreaRef.current;
node.style.height = '';
const style = window.getComputedStyle(node, null);
let heightOffset =
parseFloat(style.borderTopWidth) + parseFloat(style.borderBottomWidth);
this.textAreaoffSetHeight = node.offsetTop;
this.textAreaHeight = node.scrollHeight + heightOffset;
node.style.height = this.textAreaHeight + 'px';
this.resizeBorder();
this.resizeParentNode();
};
resizeBorder = () => {
const textAreaSize = this.textAreaHeight;
const node = this.textAreaRef.current;
const borderNode = node.parentNode.querySelector(
'.textarea__border'
);
if (borderNode !== null) {
borderNode.style.top =
this.textAreaoffSetHeight + textAreaSize - 1 + 'px';
}
};
resizeParentNode = () => {
const node = this.textAreaRef.current;
const parentNode = node.parentNode;
if (parentNode !== null) {
parentNode.style.height = this.textAreaHeight + 40 + 'px';
}
};
render() {
return (
<div className={'textarea'}>
<textarea
ref={this.textAreaRef}
className={
!this.state.value
? 'textarea__input'
: 'textarea__input active'
}
value={this.state.value}
maxLength={
this.props.maxLength && this.props.maxLength > 0 ? this.props.maxLength : null
}
onChange={this.handleChange}
/>
<div className={'textarea__message'}>
{this.state.remaining <= 0
? `You've reached ${this.props.maxLength} characters`
: `${this.state.remaining} characters remaining`}
</div>
</div>
);
}
}
ReactDOM.render(
<Application />,
document.getElementById('app')
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
<main id="app">
<!-- This element's contents will be replaced with your component. -->
</main>
The issue is that you're modifying the DOM directly (or trying to) instead of modifying state and allowing React to flow properly. You modify the DOM elements properties in resize() then any input change will immediate call handleChange(e) and re-flow your DOM overwriting the modifications.
NEVER MIX REACT WITH DOM TOUCHING!!!
Change your resize function to behave like your handleChange(e) function and set variables within the state which control those properties during the render() of the mark-up.
I am adding class to a header via setState using addEventListener in componentDidMount. The state of scrolling is set to false at 0 of scrollY and upon scroll, the state is updated to true and a class is added.
As an example
componentDidMount() {
window.addEventListener('scroll', this.handleScroll);
}
componentWillUnmount() {
window.removeEventListener('scroll', this.handleScroll);
}
handleScroll = () => {
if (window.scrollY === 0 && this.state.scrolling === true) {
this.setState({ scrolling: false });
} else if (window.scrollY !== 0 && this.state.scrolling !== true) {
this.setState({ scrolling: true });
}
};
within a GatsbyJS layout. It works perfectly between pages and templates when the user is returned to the top of the page.
However, on some occasions for example the route change via a modal or the user hits back on the browser the action is keep the previous scrollY position.
In this circumstance scrollY is not 0 but the state of scrolling is still showing false. I imagine this is because even though scrollY is showing the actual position, state scrolling is initially false until the user scrolls. This is portrayed in the console
How can I ensure that on route change, the state scrolling is updated to true if scrollY is not 0?
You should run your handleScroll method once upon instantiation to capture the initial state. Right now you're relying on the scroll position being at the very top, but as you've found, going back in history results in a different initial state.
componentDidMount() {
window.addEventListener('scroll', this.handleScroll);
this.handleScroll();
}
I am trying to find a way to detect middle click event in React JS but so far haven't succeeded in doing so.
In Chrome React's Synthetic Click event does show the button clicked ->
mouseClickEvent.button === 0 // Left
mouseClickEvent.button === 1 // Middle but it does not execute the code at all
mouseClickEvent.button === 2 // Right (There is also onContextMenu with event.preventDefault() )
Please share your views.
If you are using a stateless component:
JS
const mouseDownHandler = ( event ) => {
if( event.button === 1 ) {
// do something on middle mouse button click
}
}
JSX
<div onMouseDown={mouseDownHandler}>Click me</div>
Hope this helps.
You can add a mouseDown event and then detect the middle button click like:
handleMouseDown = (event) => {
if(event.button === 1) {
// do something on middle mouse button click
}
}
You code might look like:
class App extends React.Component {
constructor() {
super();
this.onMouseDown = this.onMouseDown.bind(this);
}
componentDidMount() {
document.addEventListener('mousedown', this.onMouseDown);
}
componentWillUnmount() {
document.removeEventListener('mousedown', this.onMouseDown);
}
onMouseDown(event) {
if (event.button === 1) {
// do something on middle mouse button click
}
}
render() {
// ...
}
}
You can find more information on MouseEvent.button here:
https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/button
Be careful. Using mousedown won't always get you the behavior you want. A "click" is both a mousedown and a mouseup where the x and y values haven't changed. Ideally, your solution would store the x and y values on a mousedown and when mouseup occurs, you would measure to make sure they're in the same spot.
Even better than mousedown would be pointerdown. This configures compatibility with "touch" and "pen" events as well as "mouse" events. I highly recommend this method if pointer events are compatible with your app's compatible browsers.
The modern way of doing it is through the onAuxClick event:
import Card from 'react-bootstrap/Card';
import React, { Component } from 'react';
export class MyComponent extends Component {
onAuxClick(event) {
if (event.button === 1) {
// Middle mouse button has been clicked! Do what you will with it...
}
}
render() {
return (
<Card onAuxClick={this.onAuxClick.bind(this)}>
</Card>
);
}
You can use React Synthetic event as described below
<div tabIndex={1} onMouseDown={event => { console.log(event)}}>
Click me
</div>
You can keep onClick. In React, you have access to nativeEvent property from where you can read which button was pressed:
const clickHandler = (evt) => {
if (e.nativeEvent.button === 1) {
...
}
}
return (
<a onClick={clickHandler}>test</a>
)