ref.current.scrollLeft returns undefined - javascript

I have piece of code:
`function App() {
const myRef = useRef(null);
const onWheel = e => {
e.preventDefault();
const containerScrollPosition = myRef.current.scrollLeft;
inputEl.current.scrollTo({
top: 0,
left: containerScrollPosition + e.deltaY * 0.35,
behaviour: "smooth"
});
};
return (
<div
className="App"
style={{
height: 440,
width: "100%"
}}
onWheel={onWheel}
>
<AutoSizer>
{({ height, width }) => (
<List
ref={myRef}
height={height}
itemCount={30}
itemSize={600}
layout="horizontal"
width={width}
>
{Grid}
</List>
)}
</AutoSizer>
</div>
);
}
When i use myRef for List component, myRef.current.scrollLeft/myRef.current.clientHeight returns undefined(myRef.current returns correct component node). If i use myRef for div.App all goes right.
What could be the problem?

The List ref is not a dom node and does not have scrollLeft property. You could use the scrollOffset of the list which is stored in it's state.
const handleScrollLeft = () => {
let scrollPosition = listRef.state.scrollOffset - 50
listRef.scrollTo(scrollPosition)
}

Related

Not able to stop event bubbling on my react app

I have created an app which will have a button in the start, and on click, it will create a parent div for existing div. Child div should be draggable inside its parent div. When I try to drag child component its parent component are also getting dragged.
Expected solution - video
My code -
import React, { useState } from 'react';
import Draggable from 'react-draggable';
function ParentDraggable(props) {
const [x, setX] = useState(0);
const [y, setY] = useState(0);
const handleMouseDown = (e) => {
let startX = e.clientX;
let startY = e.clientY;
const handleMouseMove = (e) => {
setX(x + e.clientX - startX);
setY(y + e.clientY - startY);
startX = e.clientX;
startY = e.clientY;
};
document.addEventListener('mousemove', handleMouseMove);
document.addEventListener('mouseup', () => {
document.removeEventListener('mousemove', handleMouseMove);
});
};
return (
<Draggable bounds='parent' handle='.handle' onDrag={e=>e.stopPropagation()}>
<div
style={{
position: 'relative',
top: y,
left: x,
border: '1px solid black',
width: `${props.w}px`,
height: `${props.h}px`,
}}
>
<div
style={{
height: 20,
backgroundColor: 'gray',
}}
onMouseDown={handleMouseDown}
className='handle'
>
Title Bar
</div>
{props.children}
</div>
</Draggable>
);
}
function App() {
const [parents, setParents] = useState([]);
const [w, setW] = useState(200)
const [h, setH] = useState(200)
const handleAddParent = () => {
setParents([ <ParentDraggable w = {w} h={h}>
{parents}
</ParentDraggable>]);
setW(prev => prev + 100)
setH(prev => prev + 100)
};
return (
<>
<button onClick={handleAddParent}>AddParent</button>
<div style={{height: '97vh'}}>
{parents}
</div>
</>
);
}
export default App;
Please help me achieve my goal.
Your additions to Draggable doesn't seem to really do anything. Instead of DIY, what you are missing is nodeRef(). The React-draggable docs are not the easiest to read, but there's extra something there if you are interested.
Try this out, you'll want to use something like useId() to target the handle and you really need a combo if useRef() and nodeRef(). You will need to make your "parents" stack again, I hardcoded this one for simplicity.
import React, { useRef } from "react"
import Draggable from "react-draggable"
function App() {
const r1 = useRef()
const r2 = useRef()
return (
<div style={{ height: "97vh" }}>
<Draggable bounds="parent" handle=".handlea" nodeRef={r1.current}>
<div
ref={r1}
style={{
border: "1px solid black",
width: `300px`,
height: `300px`,
}}
>
<div
style={{
height: 20,
backgroundColor: "gray",
}}
className="handlea"
>
Title Bar
</div>
<Draggable bounds="parent" handle=".handleb" nodeRef={r2.current}>
<div
ref={r2}
style={{
border: "1px solid black",
width: `100px`,
height: `100px`,
}}
>
<div
style={{
height: 20,
backgroundColor: "gray",
}}
className="handleb"
>
Title Bar
</div>
</div>
</Draggable>
</div>
</Draggable>
</div>
)
}
export default App

Show different data based on the active carousel item

I have a horizontal <Animated.ScrollView>, a "carousel" in my React-native app that displays one item at the center of the screen and the edges of previous and next item. I want to show data (lessons) below the ScrollView. Can someone tell me or point to a resource about how can I know what item the screen is now displaying and then showing data based on that? Do I need to calculate the current item in the scrollview or pass it as an argument to some function?
My goal:
Parent component:
return (
<View style={styles.screen}>
<View style={styles.thumbnailScrollContainer}>
<HorizontalContentScroll
data={LESSONS_DATA}
/>
</View>
<View style={styles.dataScrollContainer}>
<FlatList numColumns={2} data={lessonsByCategory} renderItem={renderLessonItem} />
</View>
</View> );
And here my horizontal Scrollview
const HorizontalContentScroll = ({ data}: HorizontalContentProps) => {
const { width, height } = Dimensions.get('window');
const scrollX = useRef(new Animated.Value(0)).current;
const ITEM_SIZE = width * 0.8;
const getInterval = (offset: any) => {
// console.log('offset', offset);
};
const scrollableData = (data as Array<ContentCategory>).map(
(item: ContentCategory, index: number) => {
const inputRange = [
(index - 1) * ITEM_SIZE,
index * ITEM_SIZE,
(index + 1) * ITEM_SIZE,
];
const translateY = scrollX.interpolate({
inputRange,
outputRange: [40, 10, 40],
// extrapolate: 'clamp',
});
return (
<Card
size="large"
style={{
...styles.titleCard,
transform: [{ translateY }],
width: ITEM_SIZE,
}}
key={`${item.category}-${index}`}
>
<Text>{item.category}</Text>
</Card>
);
}
);
return (
<Animated.ScrollView
contentContainerStyle={styles.contentContainer}
horizontal
onScroll={Animated.event(
[{ nativeEvent: { contentOffset: { x: scrollX } } }],
{
useNativeDriver: true,
listener: (event) => {
getInterval(event);
},
}
)}
scrollEventThrottle={16}
showsHorizontalScrollIndicator={false}
bounces={false}
pagingEnabled
snapToAlignment="center"
snapToInterval={330}
decelerationRate={'fast'}
>
{scrollableData}
</Animated.ScrollView>
);
};
export default HorizontalContentScroll;
I think I have to do something in this map function like pass the current item up to my parent component but how? If I try to call a function that sets the state in the parent I get an error of "Warning: Cannot update a component from inside the function body of a different component."
const scrollableData = (data as Array<ContentCategory>).map(
(item: ContentCategory, index: number) => {
const inputRange = [
(index - 1) * ITEM_SIZE,
index * ITEM_SIZE,
(index + 1) * ITEM_SIZE,
];
const translateY = scrollX.interpolate({
inputRange,
outputRange: [40, 10, 40],
});
// filterLessonsInTheParent(item)
return (
<Card
size="large"
style={{
...styles.titleCard,
transform: [{ translateY }],
width: ITEM_SIZE,
}}
key={`${item.category}-${index}`}
>
<Text>{item.category}</Text>
</Card>
);
}
Okay I solved it.
I used Animated.Flatlist instead of Animated.Scrollview so that I could get my hands on the onViewableItemsChanged prop and then I had to refactor my component to a class component so that viewabilityConfig prop would work properly.
I pass the current viewable item to the parent in a useCallback function that updates the local state. I then use that and React Pure Component to avoid re-rendering my HorizontalContentScroll which would mess up the animation positions. (I don't know if this is the most optimal way but it works so far).
// Parent
const handleViewableChange = useCallback((item: ContentCategory) => {
setContentsToShow((prevItem) => item.contents);
}, []);
return (
<View style={styles.screen}>
<View style={styles.thumbnailScrollContainer}>
<HorizontalContentScroll
data={LESSONS_DATA}
onViewableChange={handleViewableChange }
/>
</View>
<View>
<FlatList
numColumns={2}
data={contentsToShow}
renderItem={renderLessonItem}
/>
// HorizontalContentScroll
class HorizontalContentScroll extends PureComponent<HoriProps, any> {
viewabilityConfig: { viewAreaCoveragePercentThreshold: number };
scrollX: any;
constructor(props: any) {
super(props);
this.handleViewableItemsChanged = this.handleViewableItemsChanged.bind(
this
);
this.viewabilityConfig = { viewAreaCoveragePercentThreshold: 50 };
}
handleViewableItemsChanged = (info: any) => {
const currItemInView = info.viewableItems[0].item;
this.props.onViewableChange(currItemInView);
};
render() {
const { data } = this.props;
const { width, height } = Dimensions.get('window');
const scrollX = new Animated.Value(0);
const ITEM_SIZE = width * 0.8;
return (
<Animated.FlatList
data={data}
contentContainerStyle={styles.contentContainer}
horizontal
pagingEnabled
onViewableItemsChanged={this.handleViewableItemsChanged}
viewabilityConfig={this.viewabilityConfig}
scrollEventThrottle={16}
showsHorizontalScrollIndicator={false}
bounces={false}
snapToAlignment="center"
snapToInterval={330}
decelerationRate={'fast'}
onScroll={Animated.event(
[{ nativeEvent: { contentOffset: { x: scrollX } } }],
{
useNativeDriver: true,
}
)}
renderItem={({
item,
index,
}: {
item: ContentCategory;
index: number;
}) => {
const inputRange = [
(index - 1) * ITEM_SIZE,
index * ITEM_SIZE,
(index + 1) * ITEM_SIZE,
];
const translateY = scrollX.interpolate({
inputRange,
// [x, y, x]
outputRange: [40, 10, 40],
// extrapolate: 'clamp',
});
return (
<Card
size="large"
style={{
...styles.titleCard,
transform: [{ translateY }],
width: ITEM_SIZE,
}}
key={`${item.category}-${index}`}
>
<Text style={styles.categoryText}>{item.category}</Text>
</Card>
);
}}
/>
);
}
}
export default HorizontalContentScroll;

How to set the width of material ui Popper to its container's width while setting disable portal as false

I am using material-ui popper.
I want to let the popper go put of container in y-direction. So I set disableportal={false}.
But after setting disableportal to false, when I give width: 100%, popper is occupying the entire browser's width instead of just it's container's width. I don't want the popper to go out of container in x direction but adjust it's width to the width of it's container.
How do I achieve this? Please check below code for reproducing the above issue.
import ClickAwayListener from '#material-ui/core/ClickAwayListener';
import Grow from '#material-ui/core/Grow';
import Input from '#material-ui/core/Input';
import MenuItem from '#material-ui/core/MenuItem';
import MenuList from '#material-ui/core/MenuList';
import Paper from '#material-ui/core/Paper';
import Popper from '#material-ui/core/Popper';
import React from 'react';
const items = [
'fsdfsdfsdfs',
'shosjsadsd',
'dsfdjhfdksfhdsf',
'fsdfhdhhhhhhhhh',
];
export function Test() {
const [value, setValue] = React.useState('');
const [anchorEl, setAnchorEl] = React.useState(null);
const handleChange = (event: any) => {
setValue(event.target.value);
};
const renderChildren = () => {
let renderItems = items;
if (value !== '') {
renderItems = items.filter((item: any) => item.toLowerCase().includes(value.toLowerCase()));
}
return renderItems.map((item: any) => {
return (
<MenuItem key={item}>
{item}
</MenuItem>
);
});
};
const onFoucs = (event: any) => {
setAnchorEl(event.currentTarget);
};
const handleClose = () => {
setAnchorEl(null);
};
const popperTrans = ({ TransitionProps }: any) => {
return (
<Grow
{...TransitionProps}
style={{ transformOrigin: '0 0 0' }}
>
<Paper>
<MenuList>
{renderChildren()}
</MenuList>
</Paper>
</Grow>
);
};
const open = Boolean(anchorEl);
return (
<div style={{width: 1000, height: 500}}>
<ClickAwayListener onClickAway={handleClose}>
<div>
<Input
onChange={handleChange}
onFocus={onFoucs}
value={value}
placeholder='Search'
style={{width: '100%'}}
/>
<Popper
open={open}
anchorEl={anchorEl}
transition={true}
placement='bottom-start'
style={{zIndex: 10000, width: '100%'}}
>
{popperTrans}
</Popper>
</div>
</ClickAwayListener>
</div>
);
}
I'm sure there must be a better way to do this, but since I couldn't find one...
here is my workaround:
const [width, setWidth] = React.useState(DEFAULT_WIDTH);
useEffect(() => {
setWidth(document.getElementById('container')?.offsetWidth);
}, [document.getElementById('container')?.offsetWidth])
<Button id="container">{text}</Button>
<Popper
open={open}
placement="bottom"
style={{ zIndex: 1, width: `${width}px` }}
>
{...children}
</Popper>
I'm not sure about what is causing the issue for you. I can only point you to the example in the docs. I inspired my implementation from there and it worked like a charm. One difference that I see from your code sample is that the <ClickAwayListener> should be only wrapped around the <MenuList>. Hope it helps.
If you are using popper.js that allows you to override middlewares setting, you could do this:
Reference: Match reference width
A common feature of select dropdowns is that the dropdown matches the width of the reference regardless of its contents. You can also use size(){:js} for this, as the Rect{:.class}s get passed in:
// size fn is the part that does the trick
// you should alter other code to fit your usage scenario
popperProps={{
middlewares: (presetMiddlewares) => {
return [
...presetMiddlewares,
size({
apply({ rects, elements }) {
Object.assign(elements.floating.style, {
width: `${rects.reference.width}px`,
});
},
}),
];
}
}}

react virtualized auto sizer does not work

I have been trying this code and it just does not work.
With AutoSizer, Row does not gets rendered.
It only starts working when I remove AutoSizer from the code.
I don't know what is wrong with the code and the docs is not helping either.
Full code:
import React, { Component } from 'react';
import Card from './Card';
import { FixedSizeList as List } from "react-window";
import AutoSizer from "react-virtualized-auto-sizer";
import memoize from "memoize-one";
const CARD_SIZE = 340;
class CardList extends Component {
getItemData = memoize((itemsPerRow, locations) => ({
itemsPerRow,
locations
}))
render() {
const { locations } = this.props;
console.log(locations.length)
const Row = ({ data, index, style }) => {
const { itemsPerRow, locations } = data;
console.log(data)
const items = [];
const fromIndex = index * itemsPerRow;
const toIndex = Math.min(fromIndex + itemsPerRow, locations.length);
for (let i = fromIndex; i < toIndex; i++) {
items.push(
<Card key={i} location={locations[i]} />
);
}
return (
<div className={'flex-auto'} style={style}>
{items}
</div>
);
}
return (
<div style={{ marginTop: "10px", height: "80%" }}>
<AutoSizer>
{
({ height, width }) => {
const itemsPerRow = Math.floor(width / CARD_SIZE) || 1;
const rowCount = Math.ceil(locations.length / itemsPerRow);
const itemData = this.getItemData(itemsPerRow, locations);
return (
<div>
<List
height={height}
itemCount={rowCount}
itemData={itemData}
itemSize={CARD_SIZE}
width={width}
>
{ Row }
</List>
</div>
);
}
}
</AutoSizer>
</div>
);
}
}
P.S. locations props is an array of images
I tried removing "react-virtualized-auto-sizer" and installed "react-virtualized"
Then,
import {AutoSizer} from 'react-virtualized';
and it works!!
But I don't want to keep react-window and react-virtualized together.
I hope the author of this package will help in fixing this problem.
Maybe it's because of height incompatibility. You can check with :
<div style={{ flex: '1 1 auto' , height: '100vh'}}>
<AutoSizer>
{({ height, width }) => {
return (
<FixedSizeList
className="List"
height={height}
itemCount={1000}
itemSize={35}
width={width}
>
{Row}
</FixedSizeList>
)
}}
</AutoSizer>
</div>

How to make AppBar component from material-ui-next react to scroll events

As per Material Design guidelines:
Upon scrolling, the top app bar can […] transform in the following ways:
- Scrolling upward hides the top app bar
- Scrolling downward reveals the top app bar
When the top app bar scrolls, its elevation above other elements becomes apparent.
Is there any built-in approach to do this in material-ui-next or should it be considered as a new feature? Can you give a hint on how to achieve the animation of the AppBar component as described in the guidelines?
To my knowledge, there's no out-of-the-box solution for this at the moment. It's quite easy to implement though. Here is a snippet that subscribes to scroll events and hides or shows the AppBar accordingly:
const styles = {
root: {
flexGrow: 1,
},
show: {
transform: 'translateY(0)',
transition: 'transform .5s',
},
hide: {
transform: 'translateY(-110%)',
transition: 'transform .5s',
},
};
class CollapsibleAppBar extends React.PureComponent {
constructor(props) {
super(props);
this.state = {
shouldShow: null,
};
this.lastScroll = null;
this.handleScroll = this.handleScroll.bind(this);
// Alternatively, you can throttle scroll events to avoid
// updating the state too often. Here using lodash.
// this.handleScroll = _.throttle(this.handleScroll.bind(this), 100);
}
componentDidMount() {
window.addEventListener('scroll', this.handleScroll, { passive: true });
}
componentWillUnmount() {
window.removeEventListener('scroll', this.handleScroll);
}
handleScroll(evt) {
const lastScroll = window.scrollY;
if (lastScroll === this.lastScroll) {
return;
}
const shouldShow = (this.lastScroll !== null) ? (lastScroll < this.lastScroll) : null;
if (shouldShow !== this.state.shouldShow) {
this.setState((prevState, props) => ({
...prevState,
shouldShow,
}));
}
this.lastScroll = lastScroll;
}
render() {
const { classes } = this.props;
return (
<AppBar
position="fixed"
color="default"
className={
`${classes.root} ${
this.state.shouldShow === null ? '' : (
this.state.shouldShow ? classes.show : classes.hide
)
}`
}
>
<Toolbar>
<Typography variant="title" color="inherit">
Title
</Typography>
</Toolbar>
</AppBar>
);
}
}
CollapsibleAppBar.propTypes = {
classes: PropTypes.object.isRequired,
};
export default withStyles(styles)(CollapsibleAppBar);
For those who are still looking for built-in feature, Hide appbar on scroll is available in material-ui.
in the current version of Material-ui, you can simply use the following
import clsx from "clsx";
import useScrollTrigger from "#material-ui/core/useScrollTrigger";
const trigger = useScrollTrigger();
<AppBar className={trigger ? classes.show : classes.hide}>
</AppBar>
https://material-ui.com/components/app-bar/#usescrolltrigger-options-trigger
this seem to work for me
import {
useScrollTrigger,
Fab,
Zoom,
} from '#mui/material';
...
function ElevationScroll(props) {
const { children } = props;
const theme = useTheme();
const trigger = useScrollTrigger({
disableHysteresis: true,
threshold: 0,
});
return React.cloneElement(children, {
sx: trigger
? {
bgcolor: theme.palette.primary.dark,
'transition-duration': '500ms',
'transition-property':
'padding-top, padding-bottom, background-color',
'transition-timing-function': 'ease-in-out',
}
: {
pt: 2,
pb: 2,
bgcolor: theme.palette.primary.main,
},
elevation: trigger ? 5 : 0,
});
}
function ScrollTop(props) {
const { children } = props;
const trigger = useScrollTrigger({
disableHysteresis: true,
threshold: 200,
});
const handleClick = (event) => {
const anchor = (event.target.ownerDocument || document).querySelector(
'#back-to-top-anchor'
);
if (anchor) {
anchor.scrollIntoView({
behavior: 'smooth',
block: 'center',
});
}
};
return (
<Zoom in={trigger}>
<Box
onClick={handleClick}
role="presentation"
sx={{ position: 'fixed', bottom: 16, right: 16, zIndex: 1 }}
>
{children}
</Box>
</Zoom>
);
}
...
return (
<React.Fragment>
<ElevationScroll {...props}>
<AppBar position="sticky">
...
</AppBar>
</ElevationScroll>
<Toolbar
id="back-to-top-anchor"
className="_Toolbar"
sx={{
minHeight: '0 !important',
}}
/>
<ScrollTop {...props}>
<Fab color="secondary" size="small" aria-label="scroll back to top">
<KeyboardArrowUpIcon />
</Fab>
</ScrollTop>
</React.Fragment>
this seem to work for me
import {
useScrollTrigger,
Fab,
Zoom,
} from '#mui/material';
...
function ElevationScroll(props) {
const { children } = props;
const theme = useTheme();
const trigger = useScrollTrigger({
disableHysteresis: true,
threshold: 0,
});
return React.cloneElement(children, {
sx: trigger
? {
bgcolor: theme.palette.primary.dark,
'transition-duration': '500ms',
'transition-property':
'padding-top, padding-bottom, background-color',
'transition-timing-function': 'ease-in-out',
}
: {
pt: 2,
pb: 2,
bgcolor: theme.palette.primary.main,
},
elevation: trigger ? 5 : 0,
});
}
function ScrollTop(props) {
const { children } = props;
const trigger = useScrollTrigger({
disableHysteresis: true,
threshold: 200,
});
const handleClick = (event) => {
const anchor = (event.target.ownerDocument || document).querySelector(
'#back-to-top-anchor'
);
if (anchor) {
anchor.scrollIntoView({
behavior: 'smooth',
block: 'center',
});
}
};
return (
<Zoom in={trigger}>
<Box
onClick={handleClick}
role="presentation"
sx={{ position: 'fixed', bottom: 16, right: 16, zIndex: 1 }}
>
{children}
</Box>
</Zoom>
);
}
...
return (
<React.Fragment>
<ElevationScroll {...props}>
<AppBar position="sticky">
...
</AppBar>
</ElevationScroll>
<Toolbar
id="back-to-top-anchor"
className="_Toolbar"
sx={{
minHeight: '0 !important',
}}
/>
<ScrollTop {...props}>
<Fab color="secondary" size="small" aria-label="scroll back to top">
<KeyboardArrowUpIcon />
</Fab>
</ScrollTop>
</React.Fragment>
https://mui.com/components/app-bar/#usescrolltrigger-options-trigger

Categories