This is my Accordion component
import React, {Component, Fragment} from 'react';
import ArrowTemplate from "./ArrowTemplate";
import ContentTemplate from "./ContentTemplate";
class Accordion extends Component {
constructor(props) {
super(props);
this.state = {isAccordionExpanded: false};
this.foldAccordion = this.foldAccordion.bind(this);
this.expandAccordion = this.expandAccordion.bind(this);
}
expandAccordion() {
console.log(1);
this.setState({isAccordionExpanded: true});
}
foldAccordion() {
console.log(2);
this.setState({isAccordionExpanded: false});
}
render() {
const {state} = this;
const {isAccordionExpanded} = state;
if (isAccordionExpanded === false) {
return (
<Fragment>
<ArrowTemplate
aria={`aria-expanded="true"`}
onClick={this.expandAccordion}
direction={'down'}
color={'black'}
styles={'background:yellow'}
/>
</Fragment>
);
} else if (isAccordionExpanded === true) {
return (
<Fragment>
<ArrowTemplate
aria={`aria-expanded="true"`}
onClick={this.foldAccordion}
color={'black'}
direction={'up'}
/>
<ContentTemplate/>
</Fragment>
);
}
}
}
export default Accordion;
this is the ArrowTemplate
import React from "react";
import BlackDownArrowSVG from './svgs/black-down-arrow.svg';
import WhiteDownArrowSVG from './svgs/white-down-arrow.svg';
import styled from 'styled-components';
import PropTypes from 'prop-types';
ArrowTemplate.propTypes = {
color: PropTypes.string.isRequired,
direction: PropTypes.string.isRequired,
styles: PropTypes.string,
aria: PropTypes.string.isRequired,
};
function ArrowTemplate(props) {
const {color, direction, styles, aria} = props;
const StyledArrowTemplate = styled.img.attrs({
src: color.toLowerCase() === "black" ? BlackDownArrowSVG : WhiteDownArrowSVG,
aria,
})`
${direction.toLowerCase() === "up" ? "translate: rotate(180deg)" : ""}
${styles}
`;
return <StyledArrowTemplate/>;
}
export default ArrowTemplate;
And here are my related tests
describe("<Accordion/>",
() => {
let wrapper;
beforeEach(
() => {
wrapper = shallow(
<Accordion/>
);
}
);
it('should have one arrow at start',
function () {
expect(wrapper.find(ArrowTemplate)).toHaveLength(1);
}
);
it('should change state onClick',
function () {
wrapper.find(ArrowTemplate).simulate("click");
expect(wrapper.state().isAccordionExpanded).toEqual(true);
}
);
it('should call FoldAccordionMock onClick',
function () {
wrapper.setState({isAccordionExpanded: true});
wrapper.find(ArrowTemplate).simulate("click");
expect(wrapper.state().isAccordionExpanded).toEqual(false);
}
);
it('should display content if isAccordionExpanded = true',
function () {
wrapper.setState({isAccordionExpanded: true});
expect(wrapper.find(ContentTemplate).exists()).toEqual(true);
}
);
it('should hide content if isAccordionExpanded = false',
function () {
expect(wrapper.find(ContentTemplate).exists()).toEqual(false);
}
);
}
);
So the problem is that when I tun the tests .simulate(click) seems to work, and all tests pass. But when I click the component myself, nothing happens. Not even a console log. Changing the onClick to onClick={()=>console.log(1)} doesn't work either. Any ideas what's wrong?
StyledArrowTemplate inner component does not know anything about onClick.
ArrowTemplate doesn't know what onClick means, it's just another arbitrary prop to it.
But, if you do as #ravibagul91 said in their comment, you should pass down onClick again, StyledArrowTemplate might know what onClick means.
So just add <StyledArrowTemplate onClick={props.onClick}/>
Accordion Component
import React, { Component, Fragment } from "react";
import ArrowTemplate from "./ArrowTemplate";
import ContentTemplate from "./ContentTemplate";
class Accordion extends Component {
constructor(props) {
super(props);
this.state = { isAccordionExpanded: false };
}
toggleAccordian = () => {
console.log(1);
this.setState({ isAccordionExpanded: !isAccordionExpanded });
};
render() {
const { state } = this;
const { isAccordionExpanded } = state;
if (isAccordionExpanded) {
return (
<Fragment>
<ArrowTemplate
aria={`aria-expanded="true"`}
onClick={() => this.toggleAccordian()}
color={"black"}
direction={isAccordionExpanded ? "up" : "down"}
styles={`background:{isAccordionExpanded ? 'yellow' : ''}`}
/>
<ContentTemplate />
</Fragment>
);
}
}
}
export default Accordion;
Related
I created the below HOC which I can use to wrap a React component to add 2 inactivity timers: the first to show the user a warning; the other to log them out. I got the idea from here and it seems to work pretty well. That is, I can add withTimer functionality to a component by wrapping it like this:
export default withTimer(DragDropContext(HTML5Backend)(App));
The problem is the warning alert box halts the event loop (as alert boxes apparently always do), so the logout function is never reached.
I believe a modal (e.g., from react-bootstrap) would solve this, as it presumably would not halt the event loop, thus the logout would occur as intended if the user is still idle after the warning alert.
How would I change the below HOC to use a modal for the warning instead of an alert box? Is this possible? That is, can a HOC that's used to wrap another component include a component itself (i.e., the modal) so as to keep it decoupled from the wrapped component itself?
import React from 'react';
import { Modal } from 'react-bootstrap';
const withTimer = (WrappedComponent) => {
class WithTimer extends React.Component {
constructor(props) {
super(props);
this.state = {
warningTime: 5000,
signoutTime: 10000
};
this.events = [
'load',
'mousemove',
'mousedown',
'click',
'scroll',
'keypress'
];
for (var i in this.events) {
window.addEventListener(this.events[i], this.resetTimeout);
}
this.setTimeout();
}
clearTimeoutFunc = () => {
if (this.warnTimeout) clearTimeout(this.warnTimeout);
if (this.logoutTimeout) clearTimeout(this.logoutTimeout);
};
setTimeout = () => {
this.warnTimeout = setTimeout(this.warn, this.state.warningTime);
this.logoutTimeout = setTimeout(this.logout, this.state.signoutTime);
};
resetTimeout = () => {
this.clearTimeoutFunc();
this.setTimeout();
};
warn = () => {
window.alert('You will be logged out soon. Click to stay logged in.');
};
logout = () => {
window.alert('You are being logged out!');
// log the user out here
};
render() {
console.log('HOC');
return <WrappedComponent {...this.props.children} />;
}
}
return WithTimer;
};
export default withTimer;
If you wanted to use a Modal, you could do something like this:
Live Demo
withTimer.js
import React from 'react';
import MyModal from './MyModal';
const withTimer = (WrappedComponent) => {
class WithTimer extends React.Component {
constructor(props) {
super(props);
this.state = {
warningTime: 5000,
signoutTime: 10000,
showModal: false,
modalMessage: "",
modalButtonText: "",
};
this.events = [
'load',
'mousemove',
'mousedown',
'click',
'scroll',
'keypress'
];
for (var i in this.events) {
window.addEventListener(this.events[i], this.resetTimeout);
}
this.setTimeout();
}
clearTimeoutFunc = () => {
if (this.warnTimeout) clearTimeout(this.warnTimeout);
if (this.logoutTimeout) clearTimeout(this.logoutTimeout);
};
setTimeout = () => {
this.warnTimeout = setTimeout(this.warn, this.state.warningTime);
this.logoutTimeout = setTimeout(this.logout, this.state.signoutTime);
};
resetTimeout = () => {
this.clearTimeoutFunc();
this.setTimeout();
};
onModalClick = () => {
this.setState({
showModal: false,
}, () => this.resetTimeout())
}
warn = () => {
this.setState({
modalButtonText: "Stay Logged In",
modalHeader: "Warning!",
modalMessage: 'You will be logged out soon. Click to stay logged in.',
showModal: true,
});
};
logout = () => {
this.setState({
modalButtonText: "Ok",
modalHeader: "Session Timed Out",
modalMessage: 'You are being logged out!',
showModal: true,
});
// log the user out here
};
render() {
console.log('HOC');
return (
<>
<MyModal
show={this.state.showModal}
modalMessage={this.state.modalMessage}
modalHeader={this.state.modalHeader}
buttonText={this.state.modalButtonText}
onButtonClick={this.onModalClick} />
<WrappedComponent {...this.props.children} />
</>
);
}
}
return WithTimer;
};
export default withTimer;
MyModal.js
import React, { useState } from "react";
import { Modal, Button } from "react-bootstrap";
function MyModal({ show = false, modalMessage, modalHeader, onButtonClick, buttonText }) {
const handleClick = event => {
onButtonClick(event);
}
return (
<Modal show={show} onHide={handleClick} animation={false}>
<Modal.Header closeButton>
<Modal.Title>{modalHeader}</Modal.Title>
</Modal.Header>
<Modal.Body>{modalMessage}</Modal.Body>
<Modal.Footer>
<Button variant="primary" onClick={handleClick}>
{buttonText}
</Button>
</Modal.Footer>
</Modal>
);
}
export default MyModal;
Yes, you can render any components you'd like in the HOC. So in your case you can render a <Modal/>.
Of course, whether the modal is displayed or not is dynamic, so that's a perfect job for the component's state to come into play. Use conditional statements in your render function to either render or not render your modal.
import React from 'react';
import { Modal } from 'react-bootstrap';
const withTimer = (WrappedComponent) => {
class WithTimer extends React.Component {
constructor(props) {
super(props);
this.state = {
showWarning: false,
showLogout: false,
warningTime: 5000,
signoutTime: 10000
};
this.events = [
'load',
'mousemove',
'mousedown',
'click',
'scroll',
'keypress'
];
for (var i in this.events) {
window.addEventListener(this.events[i], this.resetTimeout);
}
this.setTimeout();
}
clearTimeoutFunc = () => {
if (this.warnTimeout) clearTimeout(this.warnTimeout);
if (this.logoutTimeout) clearTimeout(this.logoutTimeout);
};
setTimeout = () => {
this.warnTimeout = setTimeout(this.warn, this.state.warningTime);
this.logoutTimeout = setTimeout(this.logout, this.state.signoutTime);
};
resetTimeout = () => {
this.clearTimeoutFunc();
this.setTimeout();
};
warn = () => {
this.setState({ showWarning: true });
};
logout = () => {
this.setState({ showLogout: true });
// log the user out here
};
render() {
let modal;
if (this.state.showLogout) {
modal = <Modal>...</Modal>;
} else if (this.state.showWarning) {
modal = <Modal>...</Modal>;
} else {
modal = null;
}
return <React.Fragment>
<WrappedComponent {...this.props.children} />
{ modal }
</React.Fragment>;
}
}
return WithTimer;
};
export default withTimer;
I'm encountering this strange issue that I can figure out why is happing.
This should not be happening since the prop passed down to the History component has not been updated.
./components/History.js
...
const History = ({ previousLevels }) => {
return (
<ScrollView style={styles.container}>
{previousLevels.reverse().map(({ date, stressValue, tirednessValue }) => {
return (
<CardKBT
key={date}
date={date}
stressValue={stressValue}
tirednessValue={tirednessValue}
/>
)
})}
</ScrollView>
)
}
...
export default History
As can be seen in this code (below), the prop to the History is only updated once the user press Save.
App.js
import React from 'react'
import { View, ScrollView, StyleSheet } from 'react-native'
import { AppLoading, Font } from 'expo'
import Store from 'react-native-simple-store'
import { debounce } from 'lodash'
import CurrentLevels from './components/CurrentLevels'
import History from './components/History'
export default class App extends React.Component {
constructor(props) {
super(props)
this.state = {
isLoadingComplete: false,
currentLevels: {
stressValue: 1,
tirednessValue: 1,
},
previousLevels: [],
}
this.debounceUpdateStressValue = debounce(this.onChangeStressValue, 50)
this.debounceUpdateTirednessValue = debounce(
this.onChangeTirednessValue,
50
)
}
async componentDidMount() {
const previousLevels = await Store.get('previousLevels')
if (previousLevels) {
this.setState({ previousLevels })
}
}
render() {
const { stressValue, tirednessValue } = this.state.currentLevels
if (!this.state.isLoadingComplete && !this.props.skipLoadingScreen) {
return (
<AppLoading
...
/>
)
} else {
return (
<View style={{ flex: 1 }}>
<CurrentLevels
stressValue={stressValue}
onChangeStressValue={this.debounceUpdateStressValue}
tirednessValue={tirednessValue}
onChangeTirednessValue={this.debounceUpdateTirednessValue}
onSave={this.onSave}
/>
<History previousLevels={this.state.previousLevels} />
</View>
)
}
}
...
onChangeStressValue = stressValue => {
const { tirednessValue } = this.state.currentLevels
this.setState({ currentLevels: { stressValue, tirednessValue } })
}
onChangeTirednessValue = tirednessValue => {
const { stressValue } = this.state.currentLevels
this.setState({ currentLevels: { stressValue, tirednessValue } })
}
onSave = () => {
Store.push('previousLevels', {
date: `${new Date()}`,
...this.state.currentLevels,
}).then(() => {
Store.get('previousLevels').then(previousLevels => {
this.setState({
currentLevels: { stressValue: 1, tirednessValue: 1 },
previousLevels,
})
})
})
}
}
The component will re-render when one of the props or state changes, try using PureComponent or implement shouldComponentUpdate() and handle decide when to re-render.
Keep in mind, PureComponent does shallow object comparison, which means, if your props have nested object structure. It won't work as expected. So your component will re-render if the nested property changes.
In that case, you can have a normal Component and implement the shouldComponentUpdate() where you can tell React to re-render based on comparing the nested properties changes.
I'm trying to reset editor content after some action completed using react-draft-wysiwyg editor. All contents cleared by using clearEditorContent method from draftjs-utils. But after clearing contents I can't able to type nothing in editor. Added the code below. Please help to solve this problem.
import React, { Component } from 'react';
import { EditorState, convertToRaw, ContentState } from 'draft-js';
import { clearEditorContent } from 'draftjs-utils'
import { Editor } from 'react-draft-wysiwyg';
import '../../../../node_modules/react-draft-wysiwyg/dist/react-draft-wysiwyg.css';
import draftToHtml from 'draftjs-to-html';
import htmlToDraft from 'html-to-draftjs';
export default class RTEditor extends Component {
constructor(props) {
super(props);
this.state = {
editorState: EditorState.createEmpty(),
};
this.setDomEditorRef = ref => this.domEditor = ref;
}
onEditorStateChange: Function = (editorState) => {
this.setState({
editorState,
}, () => {
this.props.sendResult(draftToHtml(convertToRaw(this.state.editorState.getCurrentContent())));
});
};
componentWillReceiveProps(nextProps) {
if(nextProps.reset) {
this.reset();
}
}
componentDidMount() {
if(this.props.text) {
const html = `${this.props.text}`;
const contentBlock = htmlToDraft(html);
if (contentBlock) {
const contentState = ContentState.createFromBlockArray(contentBlock.contentBlocks);
const editorState = EditorState.createWithContent(contentState);
this.setState({ editorState, });
}
}
this.domEditor.focusEditor();
}
reset = () => {
let {editorState} = this.state;
editorState = clearEditorContent(editorState);
this.setState({ editorState });
};
render() {
const { editorState } = this.state;
return (
<Editor
ref={this.setDomEditorRef}
editorState={editorState}
wrapperClassName="rte-wrapper"
editorClassName="rte-editor"
onEditorStateChange={this.onEditorStateChange}
toolbarCustomButtons={[this.props.UploadHandler]}
/>
)
}
}
My parent component code is below,
import React, { Component } from 'react'
import { addThreadPost } from '../../../api/thread-api'
import { isEmpty } from '../../../api/common-api'
import RTEditor from './Loadable'
export default class ThreadActivity extends Component {
constructor(props) {
super(props)
this.state = {
clearEditor: false,
threadPost: ''
}
}
setPost = (post) => {
this.setState({ threadPost: post })
}
addThreadPost = () => {
let postText = this.state.threadPost.replace(/<[^>]+>/g, '');
if(!isEmpty(postText)) {
addThreadPost(this.props.match.params.id_thread, this.state.threadPost, this.state.postAttachments).then(response => {
this.setState({
clearEditor: true,
postAttachments: [],
})
});
}
else {
alert("Please enter some text in box.");
}
}
render() {
return [
<div className="commentbox-container">
<div className="form-group">
<div className="form-control">
<RTEditor
ref={node => { this.threadPost = node }}
text={""}
sendResult={this.setPost.bind(this)}
reset={this.state.clearEditor}
/>
<button className="btn-add-post" onClick={this.addThreadPost}>Add Post</button>
</div>
</div>
</div>
]
}
}
Your problem is probably that once you set ThreadActivity's state.clearEditor to true, you never set it back to false. So your this.reset() is getting called every time the component receives props. Which, incidentally, is going to be every time you try to type because you're invoking that this.props.sendResult.
The simplest fix is to make sure you change state.clearEditor back to false once the clearing is done.
Add to ThreadActivity.js:
constructor(props) {
...
this.completeClear = this.completeClear.bind(this);
}
...
completeClear = () => {
this.setState({clearEditor: false});
}
render() {
...
<RTEditor
...
completeClear={this.completeClear}
/>
...
}
And in RTEditor.js:
reset = () => {
let {editorState} = this.state;
editorState = clearEditorContent(editorState);
this.setState({ editorState });
this.props.completeClear();
};
I want to animate the depth of the whole Card when the mouse is over it.
I try this (so-so I'm new in React) but I have no idea how to do it:
<Card
linkButton={true}
href="/servicios/"
onClick={Link.handleClick} zDepth={3}
onMouseEnter={this.setState({zDepth={1}})}>
</Card>
Thanks in advance.
5 years later and there is still no correct answer, you do not have to set the component state when it hovers, just use the pseudo-class :hover:
<Card
sx={{
':hover': {
boxShadow: 20, // theme.shadows[20]
},
}}
>
If you want to use styled():
const options = {
shouldForwardProp: (prop) => prop !== 'hoverShadow',
};
const StyledCard = styled(
Card,
options,
)(({ theme, hoverShadow = 1 }) => ({
':hover': {
boxShadow: theme.shadows[hoverShadow],
},
}));
<StyledCard hoverShadow={10}>
<Content />
</StyledCard>
Live Demo
constructor(props) {
super(props);
this.state = { shadow: 1 }
}
onMouseOver = () => this.setState({ shadow: 3 });
onMouseOut = () => this.setState({ shadow: 1 });
<Card
onMouseOver={this.onMouseOver}
onMouseOut={this.onMouseOut}
zDepth={this.state.shadow}
>
Updated #1
Full example
// StyledCard.js
import React, { Component } from 'react';
import { Card } from 'material-ui/Card';
class StyledCard extends Component {
state: {
shadow: 1
}
onMouseOver = () => this.setState({ shadow: 3 });
onMouseOut = () => this.setState({ shadow: 1 });
render() {
return (
<Card
onMouseOver={this.onMouseOver}
onMouseOut={this.onMouseOut}
zDepth={this.state.shadow}
>
{this.props.children}
</Card>
);
}
export default StyledCard;
.
// Container.js
import React from 'react';
import StyledCard from './StyledCard';
const Container = () => [
<StyledCard>Card 1</StyledCard>,
<StyledCard>Card 2</StyledCard>,
<StyledCard>Card 3</StyledCard>,
];
export default Container;
UPDATED #2
With HOC
// withShadow.js
import React from 'react';
const withShadow = (Component, { init = 1, hovered = 3 }) => {
return class extends React.Component {
state: {
shadow: init
};
onMouseOver = () => this.setState({ shadow: hovered });
onMouseOut = () => this.setState({ shadow: init });
render() {
return (
<Component
onMouseOver={this.onMouseOver}
onMouseOut={this.onMouseOut}
zDepth={this.state.shadow}
{...this.props}
/>
);
}
};
};
export default withShadow;
.
// Container.js
import React from 'react';
import { Card } from 'material-ui/Card';
import withShadow from './withShadow';
const CardWithShadow = withShadow(Card, { init: 2, hovered: 4 });
const Container = () => [
<CardWithShadow>Card 1</CardWithShadow>,
<CardWithShadow>Card 2</CardWithShadow>,
<CardWithShadow>Card 3</CardWithShadow>,
];
export default Container;
#Alex Sandiiarov answer didnt work for me. The docs show to use the raised property.
https://material-ui.com/api/card/
class Component extends React.Component{
state = {
raised:false
}
toggleRaised = () => this.setState({raised:!this.state.raised});
render(){
return <Card onMouseOver={this.toggleRaised}
onMouseOut={this.toggleRaised}
raised={this.state.raised}>
...
</Card>
}
}
In my app, I'm trying to use Popper to create a tooltip over every element in the app.
(Usually, I would only show a single tooltip, but for a presentation I want to show more than one).
I wrote this utility Component to attach tooltip directly to ref.
It works pretty well, but when I try to use it inside an [].map() like regular react components, I lose all my positioning:
https://bit.dev/bit/base/atoms/ref-tooltip?example=5e81d946443f4900195606b7
import React, { Component } from 'react';
import { RefTooltip } from '#bit/bit.base.atoms.ref-tooltip'
export default class ExampleUsage extends Component {
state = { ref: [] };
handleRef = (elem) => {
if (this.state.ref.some(x => x === elem)) return;
this.setState({ ref: [elem] });
}
render() {
return (
<div>
<span ref={this.handleRef}>target</span>
{ /*
* (!)
* This .map() breaks tooltip
*
*/ }
{this.state.ref.map((elem, idx) => (
<RefTooltip key={idx} targetElement={elem}>
"tooltip"
</RefTooltip>
))}
</div>
);
}
}
//ref-tooltip.tsx
import React, { Component } from 'react';
import classNames from 'classnames';
//#ts-ignore
import createRef from 'react-create-ref';
import { createPopper, Instance, Options } from '#popperjs/core';
import styles from './ref-tooltip.module.scss';
export type RefTooltipProps = {
targetElement?: HTMLElement;
popperOptions?: Partial<Options>;
} & React.HTMLAttributes<HTMLDivElement>;
export class RefTooltip extends Component<RefTooltipProps> {
private ref = createRef();
private popperInstance?: Instance;
componentWillUnmount() {
this.destroy();
}
componentDidUpdate(prevProps: RefTooltipProps) {
const nextProps = this.props;
if (prevProps.targetElement !== nextProps.targetElement) {
this.reposition(nextProps.targetElement);
}
}
private reposition = (targetElement?: HTMLElement) => {
const { popperOptions = popperDefaultOptions } = this.props;
const popperElement = this.ref.current;
if (!targetElement) {
this.destroy();
}
if (!targetElement || !popperElement) return;
this.popperInstance = createPopper(targetElement, popperElement, popperOptions);
};
private destroy() {
if (!this.popperInstance) return;
this.popperInstance.destroy();
this.popperInstance = undefined;
}
render() {
const { className, targetElement, ...rest } = this.props;
return (
<div
{...rest}
ref={this.ref}
className={classNames(styles.tooltipWrapper, className)}
data-ignore-component-highlight
/>
);
}
}
const popperDefaultOptions: Partial<Options> = {
placement: 'top',
modifiers: [
{
name: 'flip',
enabled: false,
},
],
};
Expected:
Actual:
I don't understand why the .map() breaks popper. At least for an array of 1, it should behave the same.
Any ideas why this isn't working?