Related
When the user scroll to the top and then scrolls event returns a higher number like - 10 or 20 or 2000 or 300 based on the content and when the user scrolled to the button then it will return 0
const [scrolledRecord, setScrolledRecord] = useState(false);
const scrolledEvent = scroll => {
if (scroll?.contentOffset?.y > 0) {
setScrolledRecord(true);
} else {
setScrolledRecord(false);
}
};
return {scrolledRecord ? <ChatHeader item={userData} /> : null}
In the about I have implemented the logic I think i am not correct
const useOnTop = () => {
const [onTop, setOnTop] = useState(false);
useEffect(() => {
if (window.scrollY === 0) {
setOnTop(true);
} else {
setOnTop(false);
}
}, [window.scrollY]);
return onTop;
}
and use it as follows
const MyComponent = (props) => {
const isOnTop = useOnTop();
<>
isOnTop && <MyOtherComponent />
</>
}
<div class="app__land-bottom" v-if="isVisible">
<a href="#projects">
<img ref="arrowRef" id="arrow" src="./../assets/down.png" alt srcset />
</a>
</div>
In Vue3 setup isn't working, but on Vue2 is working the following solution for not displaying a button based on scrolling.
VUE3
<script setup>
import { ref, onMounted, onUnmounted } from "vue";
let isVisible = ref(false);
onMounted(() => {
window.addEventListener("scroll", () => {
hideArrow();
});
});
onUnmounted(() => {
window.removeEventListener("scroll", () => {
hideArrow();
});
});
const hideArrow = () => {
const currentScroll = window.pageYOffset;
if (currentScroll > 100) {
isVisible = false;
}
else if (currentScroll < 100) {
isVisible = true;
}
}
</script>
VUE2
<script>
export default {
created() {
window.addEventListener('scroll', this.hideArrow)
},
data() {
return {
isVisible: false,
}
},
methods: {
hideArrow() {
const currentScroll = window.pageYOffset;
if (currentScroll > 100) {
this.isVisible = false;
} else if (currentScroll < 100) {
this.isVisible = true;
}
},
},
}
</script>
In vue2 the solution is working but on Vue3, not. Any suggestions?
I don't understand exactly where the problem is? It would be helpful an answer.
when mutating your ref: isVisible you need to type: isVisible.value instead of isVisible
your hideArrow function should be:
const hideArrow = () => {
const currentScroll = window.pageYOffset;
if (currentScroll > 100) {
isVisible.value = false;
}
else if (currentScroll < 100) {
isVisible.value = true;
}
}
for more details: check Ref docs in Vue
I have a navbar with two part (navbarTop and navbarBottom); navbarTop will shown when the scrollY is bigger than 110 and the navbarbottom will shown when the user scroll up, but my problem is that each component will be re-called after each scroll, even scrolling down, or when the navbottom already exist and the user scroll up and it's still keep calling the component.
here is the code:
const Navbar: FC = () => {
const [stickyHeader, setStickyHeader] = useState(false);
const [stickyMenu, setStickyMenu] = useState(false);
console.log('navbar')
const [y, setY] = useState(null);
const handleNavigation = useCallback(
(e) => {
const window = e.currentTarget;
if (!stickyMenu) {
if (y > window.scrollY) {
setStickyMenu(true)
// console.log("scrolling up");
}
}
if (!stickyHeader) {
if (window.scrollY >= 110) {
setStickyHeader(true);
}
}
if (stickyHeader) {
if (window.scrollY <= 110) {
setStickyHeader(false);
}
}
if (stickyMenu) {
if (y < window.scrollY) {
setStickyMenu(false)
// console.log("scrolling down");
}
}
setY(window.scrollY);
},
[y, stickyMenu,stickyHeader]
);
useEffect(() => {
window.addEventListener("scroll", handleNavigation);
return () => {
window.removeEventListener("scroll", handleNavigation);
};
}, [handleNavigation, stickyMenu]);
return (
<>
<div className={s.root}>
{stickyHeader === false ? (
<div className='hidden xl:block md:w-auto'>
<NavbarTop sticky={false}/>
<NavbarBottom
sticky={false}/>
</div>
) : (
<div className='absolute hidden xl:block md:w-auto'>
<NavbarTop sticky={true}/>
{stickyMenu &&
<NavbarBottom sticky={true}/>
}
</div>
)}
<SmallNav/>
</div>
</>
)
}
export default Navbar
In fact, I did not quite understand your question. You said that each component is re-called after each scroll. Of course it will be re-called, right?
Whenever you scroll, you change the stickyHeader or stickyMenu. React listens for the state change and re-renders the component. This is how React works.
I see that you're using tailwindcss.
You can use a useMemo to listen to the state and then return the appropriate class based on the different state states. This way you avoid having to create DOM nodes over and over again. I don't know if this would help you or not. I hope this will help you.
const Navbar: FC = () => {
const [stickyHeader, setStickyHeader] = useState(false);
const [stickyMenu, setStickyMenu] = useState(false);
// console.log('navbar');
const [y, setY] = useState(null);
const handleNavigation = useCallback(
(e) => {
const window = e.currentTarget;
if (!stickyMenu) {
if (y > window.scrollY) {
setStickyMenu(true);
// console.log("scrolling up");
}
}
if (!stickyHeader) {
if (window.scrollY >= 110) {
setStickyHeader(true);
}
}
if (stickyHeader) {
if (window.scrollY <= 110) {
setStickyHeader(false);
}
}
if (stickyMenu) {
if (y < window.scrollY) {
setStickyMenu(false);
// console.log("scrolling down");
}
}
setY(window.scrollY);
},
[y, stickyMenu, stickyHeader],
);
useEffect(() => {
window.addEventListener('scroll', handleNavigation);
return () => {
window.removeEventListener('scroll', handleNavigation);
};
}, [handleNavigation, stickyMenu]);
const stickyClassName = useMemo(() => {
if (stickyHeader) {
return 'sticky top-0 xl:block md:w-auto';
}
return 'xl:block md:w-auto';
}, [stickyHeader]);
return (
<div className="test" style={{ height: 2000 }}>
<div className={stickyClassName}>
<NavbarTop sticky={stickyHeader} />
{stickyMenu && <NavbarBottom sticky={stickyHeader} />}
</div>
<div>SmallNav</div>
</div>
);
};
export default Navbar;
I am trying to use Next.js imperative routing api in a scroll handler for page navigation. When I attach to window, it is very nearly perfect, with the small exception that there is a color 'flash' as scroll position resets (need to continue to scroll up from bottom of new route if navigated to through top of previous page, and the color transition needs to be seamless, which doesn't seem possible because of the browser's milliseconds with scrollTop at 0). Code:
export default class ScrollOMatic extends Component {
constructor(props) {
super(props)
this.state = {
prevRoute: '',
nextRoute: '',
isEndOfScroll: false
}
binder(this, ['handleScroll'])
}
componentWillMount() {
if (typeof window !== 'undefined') {
console.log(window)
}
}
componentDidMount() {
window.addEventListener('scroll', this.handleScroll)
const {
prevRoute,
nextRoute
} = this.props.routeData
this.setState({
prevRoute,
nextRoute
})
Router.prefetch('us')
Router.prefetch('work')
Router.prefetch('services')
Router.prefetch('converse')
}
componentWillUnmount() {
window.removeEventListener('scroll', this.handleScroll)
}
handleScroll(e) {
e.preventDefault()
let {
scrollTop,
scrollHeight
} = e.srcElement.scrollingElement
scrollHeight = scrollHeight / 2
const scrollTiplier = scrollTop / scrollHeight
if (scrollTop === 0) {
Router.push(this.state.prevRoute)
window.scrollTo(0, scrollHeight - 1, {
duration: 0
})
}
if (scrollTiplier === 1) {
Router.push(this.state.nextRoute)
window.scrollTo(0, 1, {
duration: 0
})
}
}
render() {
return (
<div className = 'scroll-o-matic' >{ this.props.children }</div>
)
}
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
So, I'm using a container with its own scroll behavior, inspired by react-horizontal-scroll which seems promising. But a strange thing happens with this new code. The new route is rendered client-side for just a moment, and the page refreshes as the new route is fetched from the server. See gif and code:
// inspired by react-horizontal-scroll
import { Motion, spring, presets } from 'react-motion'
import raf from 'raf'
import Router from 'next/router'
import { fadeColor, binder } from '../../lib/_utils'
import { setScrollState } from '../../lib/_navRules'
export default class ScrollOMatic extends Component {
constructor (props) {
super(props)
this.state = {
prevRoute: '',
nextRoute: '',
animValues: 0
}
binder(this, ['handleWheel', 'resetMin', 'resetMax', 'navigator', 'canIscroll', 'getLayoutData'])
}
componentDidMount () {
const { prevRoute, nextRoute } = this.props.routeData
this.setState({
prevRoute,
nextRoute
})
const prevRouteName = prevRoute === '/' ? 'index' : prevRoute.replace('/', '')
const nextRouteName = prevRoute === '/' ? 'index' : nextRoute.replace('/', '')
Router.prefetch(prevRouteName)
Router.prefetch(nextRouteName)
}
componentWillReceiveProps (nextProps) {
if (this.props.children !== nextProps.children) { this.resetMin() }
}
shouldComponentUpdate (nextProps, nextState) {
if (true &&
this.calculate.timer !== void 0 &&
this.props.children === nextProps.children &&
this.state.animValues === nextState.animValues) {
return false
}
if (true &&
this.props.children === nextProps.children &&
this.canIscroll() === false) {
return false
}
return true
}
componentDidUpdate () { this.calculate() }
getLayoutData () {
const scrollOMatic = DOM.findDOMNode(this.scrollOMatic)
const scrollTray = DOM.findDOMNode(this.scrollTray)
const max = scrollOMatic.scrollHeight
const win = scrollOMatic.offsetHeight
const currentVal = this.state.animValues
const bounds = -(max - win)
const trayTop = scrollTray.offsetTop
const trayOffsetHeight = scrollTray.offsetHeight
const trayScrollHeight = scrollTray.scrollHeight
const scrollOMaticRect = scrollOMatic.getBoundingClientRect()
const scrollOMaticTop = scrollOMaticRect.top
const scrollOMaticHeight = scrollOMaticRect.height
const scrollOMaticOffsetHeight = scrollOMatic.offsetHeight
return {
currentVal,
bounds,
scrollTray,
trayTop,
trayOffsetHeight,
trayScrollHeight,
scrollOMatic,
scrollOMaticTop,
scrollOMaticHeight,
scrollOMaticOffsetHeight,
scrollOMaticRect
}
}
calculate () {
const layout = this.getLayoutData()
clearTimeout(this.calculate.timer)
this.calculate.timer = setTimeout(() => {
const max = layout.trayScrollHeight
const win = layout.scrollOMaticOffsetHeight
const currentVal = this.state.animValues
const bounds = -(max - win)
if (currentVal >= 1) {
this.resetMin()
} else if (currentVal <= bounds) {
const x = bounds + 1
this.resetMax(x)
}
})
}
resetMin () { this.setState({ animValues: 0 }) }
resetMax (x) { this.setState({ animValues: x }) }
canIscroll () {
const layout = this.getLayoutData()
return layout.trayOffsetTop < layout.scrollOMaticTop ||
layout.trayOffsetHeight > layout.scrollOMaticHeight
}
handleWheel (e) {
e.preventDefault()
const rawData = e.deltaY ? e.deltaY : e.deltaX
const mouseY = Math.floor(rawData)
const animationVal = this.state.animValues
const newAnimationVal = (animationVal + mouseY)
const newAnimationValNeg = (animationVal - mouseY)
if (!this.canIscroll()) return
const layout = this.getLayoutData()
const { currentVal, scrollOMaticHeight, trayScrollHeight } = layout
const isEndOfPage = -(currentVal - scrollOMaticHeight) + 1 === trayScrollHeight
this.navigator()
const scrolling = () => {
this.state.scrollInverted
? this.setState({ animValues: newAnimationValNeg })
: this.setState({ animValues: newAnimationVal })
}
raf(scrolling)
}
navigator () {
const layout = this.getLayoutData()
const { currentVal, scrollOMaticHeight, trayScrollHeight } = layout
const shouldBeNextRoute = -(currentVal - scrollOMaticHeight) + 1 >= trayScrollHeight
// const shouldBePrevRoute = this.state.animValues < 0
if (shouldBeNextRoute) {
Router.push(this.state.nextRoute)
}
// if (shouldBePrevRoute) {
// Router.push(this.state.prevRoute)
// }
}
render () {
const springConfig = presets.noWobble
return (
<div style={{ position: 'relative', width: '100vw', height: '100vh' }} className='scroll-o-matic' onWheel={this.handleWheel}
ref={scrollOMatic => { this.scrollOMatic = scrollOMatic }}>
<Motion style={{ z: spring(this.state.animValues, springConfig) }}>
{ ({ z }) => (
<div className='scroll-tray' ref={(scrollTray) => { this.scrollTray = scrollTray }}
style={{
height: '300vh',
width: '100vw',
// top: '-100vh',
transform: `translate3d(0,${z.toFixed(3)}px,0)`,
willChange: 'transform',
display: 'inline-flex',
position: 'absolute'
}}>
{ this.props.children }
</div>
)}
</Motion>
<style jsx>{`
.scroll-o-matic {
background-color: ${this.state.currentColor};
}
`}</style>
</div>
)
}
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
server code:
const express = require('express')
const next = require('next')
const dev = process.env.NODE_ENV !== 'production'
const app = next({ dev })
const port = process.env.PORT || 3000
const handle = app.getRequestHandler()
app.prepare()
.then(() => {
const server = express()
server.use('/static', express.static('static'))
server.get('/', (req, res) => {
return app.render(req, res, '/', req.query)
})
server.get('/us', (req, res) => {
return app.render(req, res, '/us', req.query)
})
server.get('/work', (req, res) => {
return app.render(req, res, '/work', req.query)
})
server.get('/services', (req, res) => {
return app.render(req, res, '/services', req.query)
})
server.get('/converse', (req, res) => {
return app.render(req, res, '/converse', req.query)
})
server.get('*', (req, res) => {
return handle(req, res, '/', req.query)
})
server.listen(port, (err) => {
if (err) throw err
console.log(`> Ready on http://localhost:${port}`)
})
})
Maybe I don't understand Next's 'prefetch', but it definitely doesn't seem to be prefetching anything. I mainly am confused why a server-side page load is happening. Even using a wildcard request handler in express, the page reloads (but redirects to itself?), so it's not an issue with the server configuration I don't think.
What am I missing, oracles of the internet?
The prefetch options is downloading the JS code of the page in background, so when you want to do a page change it's already downloaded, but nothing more, if you expect it to prefetch the already rendered HTML or the data you need to manually implement that.
For anyone who might have the same problem, obscure as it is, the issue wound up being related to Next's 'Styled JSX' rendering methods.
Apparently, even in client-side page loads, there's a half-second or so moment in which Styled JSX needs to load, for some reason. Which you normally don't notice, unless something as dramatic as background-color of the entire app is inside your <style jsx>{``}<style> tag. Putting all styles inside a regular old html style attr solved the 'flash'.
scroll event wasn't firing. since my body was scrolling instead of documentElement.
I just removed height: 100% from my body tag and then scroll event started firing.
//in your CSS don't set height to percent
div{
height: 100%
}
What I'm trying to achieve is a textarea that starts out as a single line but will grow up to 4 lines and at that point start to scroll if the user continues to type. I have a partial solution kinda working, it grows and then stops when it hits the max, but if you delete text it doesn't shrink like I want it to.
This is what I have so far.
export class foo extends React.Component {
constructor(props) {
super(props);
this.state = {
textareaHeight: 38
};
}
handleKeyUp(evt) {
// Max: 75px Min: 38px
let newHeight = Math.max(Math.min(evt.target.scrollHeight + 2, 75), 38);
if (newHeight !== this.state.textareaHeight) {
this.setState({
textareaHeight: newHeight
});
}
}
render() {
let textareaStyle = { height: this.state.textareaHeight };
return (
<div>
<textarea onKeyUp={this.handleKeyUp.bind(this)} style={textareaStyle}/>
</div>
);
}
}
Obviously the problem is scrollHeight doesn't shrink back down when height is set to something larger. Any suggestion for how I might be able to fix this so it will also shrink back down if text is deleted?
ANOTHER SIMPLE APPROACH (without an additional package)
export class foo extends React.Component {
handleKeyDown(e) {
e.target.style.height = 'inherit';
e.target.style.height = `${e.target.scrollHeight}px`;
// In case you have a limitation
// e.target.style.height = `${Math.min(e.target.scrollHeight, limit)}px`;
}
render() {
return <textarea onKeyDown={this.handleKeyDown} />;
}
}
The problem when you delete the text and textarea doesn't shrink back is because you forget to set this line
e.target.style.height = 'inherit';
Consider using onKeyDown because it works for all keys while others may not (w3schools)
In case you have padding or border of top or bottom. (reference)
handleKeyDown(e) {
// Reset field height
e.target.style.height = 'inherit';
// Get the computed styles for the element
const computed = window.getComputedStyle(e.target);
// Calculate the height
const height = parseInt(computed.getPropertyValue('border-top-width'), 10)
+ parseInt(computed.getPropertyValue('padding-top'), 10)
+ e.target.scrollHeight
+ parseInt(computed.getPropertyValue('padding-bottom'), 10)
+ parseInt(computed.getPropertyValue('border-bottom-width'), 10);
e.target.style.height = `${height}px`;
}
I hope this may help.
you can use autosize for that
LIVE DEMO
import React, { Component } from 'react';
import autosize from 'autosize';
class App extends Component {
componentDidMount(){
this.textarea.focus();
autosize(this.textarea);
}
render(){
const style = {
maxHeight:'75px',
minHeight:'38px',
resize:'none',
padding:'9px',
boxSizing:'border-box',
fontSize:'15px'};
return (
<div>Textarea autosize <br/><br/>
<textarea
style={style}
ref={c=>this.textarea=c}
placeholder="type some text"
rows={1} defaultValue=""/>
</div>
);
}
}
or if you prefer react modules https://github.com/andreypopp/react-textarea-autosize
Just use useEffect hook which will pick up the height during the renderer:
import React, { useEffect, useRef, useState} from "react";
const defaultStyle = {
display: "block",
overflow: "hidden",
resize: "none",
width: "100%",
backgroundColor: "mediumSpringGreen"
};
const AutoHeightTextarea = ({ style = defaultStyle, ...etc }) => {
const textareaRef = useRef(null);
const [currentValue, setCurrentValue ] = useState("");// you can manage data with it
useEffect(() => {
textareaRef.current.style.height = "0px";
const scrollHeight = textareaRef.current.scrollHeight;
textareaRef.current.style.height = scrollHeight + "px";
}, [currentValue]);
return (
<textarea
ref={textareaRef}
style={style}
{...etc}
value={currentValue}
onChange={e=>{
setCurrentValue(e.target.value);
//to do something with value, maybe callback?
}}
/>
);
};
export default AutoHeightTextarea;
Really simple if you use hooks "useRef()".
css:
.text-area {
resize: none;
overflow: hidden;
min-height: 30px;
}
react componet:
export default () => {
const textRef = useRef<any>();
const onChangeHandler = function(e: SyntheticEvent) {
const target = e.target as HTMLTextAreaElement;
textRef.current.style.height = "30px";
textRef.current.style.height = `${target.scrollHeight}px`;
};
return (
<div>
<textarea
ref={textRef}
onChange={onChangeHandler}
className="text-area"
/>
</div>
);
};
you can even do it with react refs. as setting ref to element
<textarea ref={this.textAreaRef}></textarea> // after react 16.3
<textarea ref={textAreaRef=>this.textAreaRef = textAreaRef}></textarea> // before react 16.3
and update the height on componentDidMount or componentDidUpdate as your need. with,
if (this.textAreaRef) this.textAreaRef.style.height = this.textAreaRef.scrollHeight + "px";
actually you can get out of this with useState and useEffect
function CustomTextarea({minRows}) {
const [rows, setRows] = React.useState(minRows);
const [value, setValue] = React.useState("");
React.useEffect(() => {
const rowlen = value.split("\n");
if (rowlen.length > minRows) {
setRows(rowlen.length);
}
}, [value]);
return (
<textarea rows={rows} onChange={(text) => setValue(text.target.value)} />
);
}
Uses
<CustomTextarea minRows={10} />
I like using this.yourRef.current.offsetHeight. Since this is a textarea, it wont respond to height:min-content like a <div style={{height:"min-content"}}>{this.state.message}</div> would. Therefore I don't use
uponResize = () => {
clearTimeout(this.timeout);
this.timeout = setTimeout(
this.getHeightOfText.current &&
this.setState({
heightOfText: this.getHeightOfText.current.offsetHeight
}),
20
);
};
componentDidMount = () => {
window.addEventListener('resize', this.uponResize, /*true*/)
}
componentWillUnmount = () => {
window.removeEventListener('resize', this.uponResize)
}
but instead use
componentDidUpdate = () => {
if(this.state.lastMessage!==this.state.message){
this.setState({
lastMessage:this.state.message,
height:this.yourRef.current.offsetHeight
})
}
}
on a hidden div
<div
ref={this.yourRef}
style={{
height:this.state.height,
width:"100%",
opacity:0,
zIndex:-1,
whiteSpace: "pre-line"
})
>
{this.state.message}
</div>
Using hooks + typescript :
import { useEffect, useRef } from 'react';
import type { DetailedHTMLProps, TextareaHTMLAttributes } from 'react';
// inspired from : https://stackoverflow.com/a/5346855/14223224
export const AutogrowTextarea = (props: DetailedHTMLProps<TextareaHTMLAttributes<HTMLTextAreaElement>, HTMLTextAreaElement>) => {
const ref = useRef<HTMLTextAreaElement>(null);
let topPadding = 0;
let bottomPadding = 0;
const resize = () => {
ref.current.style.height = 'auto';
ref.current.style.height = ref.current.scrollHeight - topPadding - bottomPadding + 'px';
};
const delayedResize = () => {
window.setTimeout(resize, 0);
};
const getPropertyValue = (it: string) => {
return Number.parseFloat(window.getComputedStyle(ref.current).getPropertyValue(it));
};
useEffect(() => {
[topPadding, bottomPadding] = ['padding-top', 'padding-bottom'].map(getPropertyValue);
ref.current.focus();
ref.current.select();
resize();
}, []);
return <textarea ref={ref} onChange={resize} onCut={delayedResize} onPaste={delayedResize} onDrop={delayedResize} onKeyDown={delayedResize} rows={1} {...props} />;
};
import { useRef, useState } from "react"
const TextAreaComponent = () => {
const [inputVal, setInputVal] =useState("")
const inputRef = useRef(null)
const handleInputHeight = () => {
const scrollHeight = inputRef.current.scrollHeight;
inputRef.current.style.height = scrollHeight + "px";
};
const handleInputChange = () => {
setInputVal(inputRef.current.value)
handleInputHeight()
}
return (
<textarea
ref={inputRef}
value={inputVal}
onChange={handleInputChange}
onKeyDown={(e) => {
if (e.key === "Enter") {
handleSubmit(e);
inputRef.current.style.height = "40px";
}
}}
/>
)}
Extremely simple solution:
function allowTextareasToDynamicallyResize() {
let textareas = document.getElementsByTagName('textarea');
for (let i = 0; i < textareas.length; i++) {
textareas[i].style.height = textareas[i].scrollHeight + 'px';
textareas[i].addEventListener('input', (e) => {
e.target.style.height = textareas[i].scrollHeight + 'px';
});
}
}
// Call this function in the componentDidMount() method of App,
// or whatever class you want that contains all the textareas
// you want to dynamically resize.
This works by setting an event listener to all textareas. For any given textarea, new input will trigger a function that resizes it. This function looks at any scrollHeight, i.e. the height that is overflowing out of your existing container. It then increments the textarea height by that exact height. Simple!
As mentioned in the comment, you have to call this function in some method, but the important part is that you call it AFTER everything is mounted in React / populated in JS. So the componentDidMount() in App is a good place for this.