I have what I like to call "Accordion Inception". My initial accordion will measure its children and set a max-height on the accordionContent. This CSS property is necessary to keep when using CSS Transitions on Auto Dimensions
However, if part of the content is another accordion. I'm not able to detect the child has opened up and need to remeasure and set a new max-height on the parent accordion.
Here is a demo
export const AccordionBody = ({ children }) => {
const { isCollapsed } = useContext(AccordionContext);
const [bodyCallback, scrollHeight] = useScrollHeightByRef();
return (
<div
ref={bodyCallback}
id="accordion-body"
css={accordionBody(isCollapsed, scrollHeight)}
>
<div css={accordionContent}> //I need to somehow measure this when it's contents change
{children}
</div>
</div>
)
}
Here is the hook I'm using to measure the bodyHeight
import { useState, useCallback } from "react";
export function useScrollHeightByRef() {
const [scrollHeight, setScrollHeight] = useState();
const ref = useCallback(
el => {
if (el) {
setScrollHeight(el.scrollHeight);
}
},
[setScrollHeight]
);
return [ref, scrollHeight];
}
I could possibly pass an onChange function into each child but that seems pretty cumbersome. I'm looking for something to detect when the child changes.
Related
I would like to make a draggable split panel for an editor. Its behavior is mainly like Console panel of CodeSandbox:
When we click on Console, the panel is expanded, and the arrow becomes ArrowDown for closing.
The border of the panel is dragabble.
When we click on Console on an expanded panel, the panel is closed, and the arrow becomes ArrowUp for expanding.
I have the following code (https://codesandbox.io/s/reset-forked-ydhy97?file=/src/App.js:0-927) by https://github.com/johnwalley/allotment. The problem is that the prop preferredSize does not change following this.state.toExpand.
Does anyone know why this does not work?
import React from "react";
import { Allotment } from "allotment";
import "allotment/dist/style.css";
import styles from "./App.module.css";
export default class App extends React.Component {
constructor(props) {
super(props);
this.state = {
toExpand: true
};
}
render() {
return (
<div>
<div className={styles.container}>
<Allotment vertical>
<Allotment.Pane>Main Area</Allotment.Pane>
<Allotment.Pane preferredSize={this.state.toExpand ? "0%" : "50%"}>
<div
onClick={() => {
this.setState({ toExpand: !this.state.toExpand });
}}
>
Console
{this.state.toExpand ? "ArrowUp" : "ArrowDown"}
</div>
</Allotment.Pane>
</Allotment>
</div>
</div>
);
}
}
The problem is that the prop preferredSize does not change following this.state.toExpand.
This is not the problem, it does change, however, the documentation states:
Allotment will attempt to use this size when adding this pane (including on initial mount) as well as when a user double clicks a sash, or the reset method is called on the Allotment instance.
It is not configured to update when the prop is changed, however, if you double click on the border after setting it to ArrowDown, it will reset to 50%.
Instead, if you add a reference to the Allotment element by first initializing a reference in the constructor:
constructor(props) {
super(props);
this.allotment = React.createRef();
this.state = {
toExpand: true
};
}
And assigning it as a prop:
<Allotment vertical ref={this.allotment}>
Then you can add a callback to the setState for when you change the expand option that calls the reset function:
resetAllotment() {
if (this.allotment.current) {
this.allotment.current.reset();
}
}
// ...
this.setState({ toExpand: !this.state.toExpand }, () => this.resetAllotment());
Side-note, it appears that the Allotment component does not have time to process the new prop change, before reset is called in the setState callback... which is illogical to me, however, you can work around this by a hacky setTimeout of 0ms:
resetAllotment() {
setTimeout(() => this.allotment.current && this.allotment.current.reset(), 0);
}
For a Material-UI Button component, I would like to have the "focus" styling look the same as "focusVisible" styling. Meaning I want it to have the same ripple effect visible if the button was focused programatically or with the mouse as if the button was focused with the tab key.
A sort-of workaround I have found is to call dispatchEvent(new window.Event("keydown")) on the element before it is focused, causing keyboard to be the last input type used. This will have the effect of making the button look the way I want UNTIL the onMouseLeave event (from MUI <ButtonBase/>) or another mouse event is fired, causing the visible focus to disappear.
I have figured out how to change the focus styling of the component like this:
import React from "react"
import { withStyles } from "#material-ui/core/styles"
import Button from "#material-ui/core/Button"
const styles = {
root: {
'&:focus': {
border: "3px solid #000000"
}
}
}
const CustomButtonRaw = React.forwardRef((props, ref) => {
const { classes, ...rest } = props
return <Button classes={{root: classes.root}} {...rest} ref={ref}/>
}
const CustomButton = withStyles(styles, { name: "CustomButton" })(CustomButtonRaw)
export default CustomButton
So, I can apply some style to the button when it is in "focus" state. (For ex. I applied a border). But I am missing how to get the styles to apply. I have tried putting the className 'Mui-visibleFocus' on the Button but that did not seem to have an effect. Is there some way to get the styles that would be applied if the Button was in visibleFocus state?
ButtonBase (which Button delegates to) has an action prop which provides the ability to set the button's focus-visible state.
ButtonBase leverages the useImperativeHandle hook for this. To leverage it, you pass a ref into the action prop and then you can later call actionRef.current.focusVisible().
However, this by itself is not sufficient, because there are several mouse and touch events that ButtonBase listens to in order to start/stop the ripple. If you use the disableTouchRipple prop, it prevents ButtonBase from trying to start/stop the ripple based on those events.
Unfortunately disableTouchRipple prevents click and touch animations on the button. These can be restored by adding another TouchRipple element explicitly that you control. My example below shows handling onMouseDown and onMouseUp as a proof-of-concept, but an ideal solution would deal with all the different events that ButtonBase handles.
Here's a working example:
import React from "react";
import Button from "#material-ui/core/Button";
import TouchRipple from "#material-ui/core/ButtonBase/TouchRipple";
const FocusRippleButton = React.forwardRef(function FocusRippleButton(
{ onFocus, onMouseDown, onMouseUp, children, ...other },
ref
) {
const actionRef = React.useRef();
const rippleRef = React.useRef();
const handleFocus = (event) => {
actionRef.current.focusVisible();
if (onFocus) {
onFocus(event);
}
};
const handleMouseUp = (event) => {
rippleRef.current.stop(event);
if (onMouseUp) {
onMouseUp(event);
}
};
const handleMouseDown = (event) => {
rippleRef.current.start(event);
if (onMouseDown) {
onMouseDown(event);
}
};
return (
<Button
ref={ref}
action={actionRef}
disableTouchRipple
onFocus={handleFocus}
onMouseDown={handleMouseDown}
onMouseUp={handleMouseUp}
{...other}
>
{children}
<TouchRipple ref={rippleRef} />
</Button>
);
});
export default function App() {
return (
<div className="App">
<FocusRippleButton variant="contained" color="primary">
Button 1
</FocusRippleButton>
<br />
<br />
<FocusRippleButton
variant="contained"
color="primary"
onFocus={() => console.log("Some extra onFocus functionality")}
>
Button 2
</FocusRippleButton>
</div>
);
}
We can create a reference to the action of the material-ui Button component and use the action reference within useLayoutEffect to achieve the ripple effect
import React, { createRef, useLayoutEffect } from "react";
import Button from "#material-ui/core/Button";
function FocusButton(props) {
const { handleClose } = props;
const actionRef = createRef();
useLayoutEffect(() => {
if (actionRef.current) {
actionRef.current.focusVisible();
}
}, []);
return (
<Button action={actionRef} onClick={handleClose}>
Ok
</Button>
);
}
The above FocusButton can be used as a replacement to Button or simply you can add a reference and call the focusVisible() in the trigger method
Eg:
const buttonRef = createRef();
const handleButton2Click = () => {
buttonRef.current.focusVisible();
};
.
.
.
.
<Button action={buttonRef} variant="outlined">
Button 1
</Button>
<Button variant="outlined" color="primary" onClick={handleButton2Click}>
Button 2
</Button>
You can find the demo in this link
Is there any way to disable scroll on one single page? I tried to set overflow: hidden on the specific page but that didn't work. I have to set it on the body in index.css to make it work but that obviously disable scroll on all pages. So the only way to do it that comes to my mind is to set CSS class conditionally on the body. Is there any way to conditionally set CSS class in index.js based on the value from a redux store or is there any other way?
my index.js
import React from 'react';
import ReactDOM from 'react-dom';
import {BrowserRouter} from 'react-router-dom'
import {Provider} from 'react-redux'
import {createStore,applyMiddleware,compose,combineReducers} from "redux";
import thunk from 'redux-thunk'
import './index.css';
import App from './App';
import * as serviceWorker from './serviceWorker';
import authReducer from './store/reducers/auth'
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
const rootReducer = combineReducers({
auth: authReducer
})
const store = createStore(rootReducer,
composeEnhancers(applyMiddleware(thunk)));
const app =(
<Provider store={store}>
<BrowserRouter>
<App/>
</BrowserRouter>
</Provider>
)
ReactDOM.render(
<React.StrictMode>
{app}
</React.StrictMode>,
document.getElementById('root')
);
serviceWorker.unregister();
If you wanted to set a style on body, you can just run the below code on the page, which will disable scrolling.
document.body.style.overflow='hidden'
Then to re-enable:
document.body.style.overflow='auto'
Only downside is that is isn't very React-like.
Simple solution might be to define specific class to apply overflow: hidden and apply it on document.body whenever you want (for example after explicit component mount).
.overflow-hidden {
overflow: hidden;
}
On mount of specific page call:
document.body.classList.add("overflow-hidden");
or you can directly assign property
document.body.style.overflow = "hidden";
You can wrap your component in another div, and give that wrapper div the overflow:hidden; style, possibly along with a max-height: 70vh; to make sure it doesn't go over the end of the page.
div {
padding: .5rem;
margin: .5rem
}
.no-scroll-wrapper {
max-height: 70vh;
overflow: hidden;
background-color: darkgrey;
padding: 1rem;
}
.large-inner {
height: 1000px;
background-color: grey
}
<body>
<div class="no-scroll-wrapper">
wrapper element disables scrolling
<div class="large-inner">
Content here, very long div, but you can't see the end of it.
</div>
</div>
</body
A simple solution might be to define a specific class to apply overflow: hidden and apply it on the document. body whenever you want and remove the class when you leave the page.
.overflow-hidden {
overflow: hidden;
}
Attach class on the body in the specific page and remove when you live the page, so it won't affect other pages
useEffect(() => {
document.body.classList.add("overflow-hidden");
return () => {
document.body.classList.remove("overflow-hidden");
};
}, []);
This will simply remove the class from the body when you leave the page.
Adding to Luke Storry's and midnightgamer's amazing answer. You can use a custom react hook with useEffect instead of having to add document.body.style.overflow='auto' in every page to re-enable scroll.
import { useEffect } from 'react'
const useOverFlowHidden = () => {
useEffect(() => {
document.body.style.overflow = 'hidden'
return () => {
document.body.style.overflow = 'auto' // cleanup or run on page unmount
}
}, [])
}
export default useOverFlowHidden
Usage:
const Page = () => {
useOverFlowHidden(); // use on pages you want to disable overflow/scroll
return <></>;
}
I use body-scroll-lock npm package:
import {disableBodyScroll,enableBodyScroll,clearAllBodyScrollLocks} from "body-scroll-lock"
const YourPage=(props)=>{
const ref=useRef(null)
useEffect(() => {
if (ref.current) {
// you might add a condition to lock the scroll
if (conditionIsTrue) {
disableBodyScroll(ref.current)
} else {
enableBodyScroll(ref.current)
}
}
return () => {
clearAllBodyScrollLocks()
}
},[addDependencyHere])
return(
<div ref={ref}>
....
</>
)
}
from the above npm package's documentation, you can also use this approach with vanilla js. It is not recommended to access document object in react.js directly
document.body.ontouchmove = (e) => {
e.preventDefault();
return false;
on touch screen devices you can set this css class conditionally to prevent scrolling
touch-action:none
Consider a component that renders a button and says this button should have a red background and a yellow text color. Also there exists a Parent component that uses this child but says, the yellow color is fine, but I want the background color to be green.
withStyles
No problem using the old withStyles.
import React from "react";
import { withStyles } from "#material-ui/core/styles";
import { Button } from "#material-ui/core";
const parentStyles = {
root: {
background: "green"
}
};
const childStyles = {
root: {
background: "red"
},
label: {
color: "yellow"
}
};
const ChildWithStyles = withStyles(childStyles)(({ classes }) => {
return <Button classes={classes}>Button in Child withStyles</Button>;
});
const ParentWithStyles = withStyles(parentStyles)(({ classes }) => {
return <ChildWithStyles classes={classes} />;
});
export default ParentWithStyles;
https://codesandbox.io/s/passing-classes-using-withstyles-w17xs?file=/demo.tsx
makeStyles/useStyles
Let's try the makeStyles/useStyles instead and follow the guide Overriding styles - classes prop on material-ui.com.
import React from "react";
import { makeStyles } from "#material-ui/styles";
import { Button } from "#material-ui/core";
const parentStyles = {
root: {
background: "green"
}
};
const childStyles = {
root: {
background: "red"
},
label: {
color: "yellow"
}
};
// useStyles variant does NOT let me override classes
const useParentStyles = makeStyles(parentStyles);
const useChildStyles = makeStyles(childStyles);
const ChildUseStyles = ({ classes: classesOverride }) => {
const classes = useChildStyles({ classes: classesOverride });
return (
<>
<Button classes={classes}>Button1 in Child useStyles</Button>
<Button classes={classesOverride}>Button2 in Child useStyles</Button>
</>
);
};
const AnotherChildUseStyles = props => {
const classes = useChildStyles(props);
return (
<>
<Button classes={classes}>Button3 in Child useStyles</Button>
</>
);
};
const ParentUseStyles = () => {
const classes = useParentStyles();
return <>
<ChildUseStyles classes={classes} />
<AnotherChildUseStyles classes={classes} />
</>
};
export default ParentUseStyles;
https://codesandbox.io/s/passing-classes-using-usestyles-6x5hf?file=/demo.tsx
There seems no way to get the desired effect that I got using withStyles. A few questions, considering I still want the same effect (green button yellow text) using some method of classes overriding (which seemed to make sense to me before).
How is my understanding wrong about how to pass classes as means to override parts of them using useStyles?
How should I approach it alternatively?
And if I'm using the wrong approach, why is material-ui still giving me a warning when the parent has something in the styles that the child doesn't have?
the key something provided to the classes prop is not implemented in [Child]
Is the migration from the old approach (withStyles) vs the new approach documented somewhere?
Btw, I'm aware of this solution but that seems cumbersome when you have too much you want to override.
const useStyles = makeStyles({
root: {
backgroundColor: 'red',
color: props => props.color, // <-- this
},
});
function MyComponent(props) {
const classes = useStyles(props);
return <div className={classes.root} />;
}
withStyles has very little functionality in it. It is almost solely a wrapper to provide an HOC interface to makeStyles / useStyles. So all of the functionality from withStyles is still available with makeStyles.
The reason you aren't getting the desired effect is simply because of order of execution.
Instead of:
const useParentStyles = makeStyles(parentStyles);
const useChildStyles = makeStyles(childStyles);
you should have:
const useChildStyles = makeStyles(childStyles);
const useParentStyles = makeStyles(parentStyles);
The order in which makeStyles is called determines the order of the corresponding style sheets in the <head> and when specificity is otherwise the same, that order determines which styles win (later styles win over earlier styles). It is harder to get that order wrong using withStyles since the wrapper that you are using to override something else will generally be defined after the thing it wraps. With multiple calls to makeStyles it is easier to do an arbitrary order that doesn't necessarily put the overrides after the base styles they should impact.
The key to understanding this is to recognize that you aren't really passing in overrides, but rather a set of classes to be merged with the new classes. If childClasses.root === 'child_root_1' and parentClasses.root === 'parent_root_1', then the merged result is mergedClasses.root === 'child_root_1 parent_root_1' meaning any elements that have their className set to mergedClasses.root are receiving both CSS classes. The end result (as far as what overrides what) is fully determined by CSS specificity of the styles in the two classes.
Related answers:
Material UI v4 makeStyles exported from a single file doesn't retain the styles on refresh
Internal implementation of "makeStyles" in React Material-UI?
In Material-ui 4.11.x while creating styles using makeStyles wrap the enclosing styles with createStyles, and this style will have highest priority than the default one.
const useStyles = makeStyles((theme: Theme) =>
createStyles({
backdrop: {
zIndex: theme.zIndex.drawer + 1,
color: '#fff',
},
}),
);
You could try removing the createStyles and see the difference.
code source from https://material-ui.com/components/backdrop/
One way to achieve this using withStyles is the following and can be helpful to override css classes.
Supposing that you want to override a class called ".myclass" which contains "position: absolute;":
import { withStyles } from '#material-ui/styles';
const styles = {
"#global": {
".myClass": {
position: "relative",
}
}
};
const TestComponent = (props) => (
<>
<SomeComponent {...props}>
</>
);
export default withStyles(styles)(TestComponent);
After doing this, you override the definition of .myClass defined on <SomeComponent/> to be "position: relative;".
According to this reference https://github.com/airbnb/javascript/tree/master/css-in-javascript, a styled JS component should be written like this:
function MyComponent({ styles }) {
return (
<div {...css(styles.container)}>
Never doubt that a small group of thoughtful, committed citizens can
change the world. Indeed, it’s the only thing that ever has.
</div>
);
}
export default withStyles(() => ({
container: {
display: 'inline-block',
},
}))(MyComponent);
I'm trying to write a simple React component like following, without success (I'm receiving a withStyles is not defined error):
import React from 'react';
const MyComponent = ({styles}) => {
return (
<div {...css(styles.container)}>Hello World</div>
)
}
export default withStyles(() => ({
container: {
color: 'red'
},
}))(MyComponent);
What am I doing wrong? Is it possible to use this convention for a React component?
You can do it like this
const divStyle = {
color: 'blue',
fontSize:10px
};
function HelloWorldComponent() {
return <div style={divStyle}>Hello World!</div>;
}
However you can try other better approaches.Please refer to the link below:
https://medium.com/#aghh1504/4-four-ways-to-style-react-components-ac6f323da822