react-expand

Simple element expand state controlling

Usage no npm install needed!

<script type="module">
  import reactExpand from 'https://cdn.skypack.dev/react-expand';
</script>

README

React Expand

codecov Build Status npm

Component for controlling expand state of each elements on page.

Usage

Somewhere in code

<ExpandController>
    <SomeComponent/>
</ExpandController>

Controlling expand state

import * as React from "react";

export class SomeComponent extends React.Component {
    ...
    public static readonly contextTypes = ExpandControllerContextTypes;
    public readonly context: ExpandContext;
    ...

    public componentDidMount() {
        this.context.changeExpandState("custom-expand-string", false, { someData: "data" })();
    }

    public componentWillUnmount() {
        console.log(this.context.getState("custom-expand-string")); // {someData: "data"}
    }

    public render(): JSX.Element {
        return (
            <React.Fragment>
                <button
                    onClick={this.context.changeExpandState("custom-expand-string")}
                    // Highly recommended use data attribute 
                    // for elements outside controlled element
                    data-expand="custom-expand-string"
                />
                <div
                    // In this case expand state changing on click button 
                    // and click on zone without data attribute "custom-expand-string"
                    className={this.context.isExpanded("custom-expand-string") ? "visible" : "hidden"}
                    data-expand="custom-expand-string"
                >
                    <button onClick={this.context.changeExpandState("custom-expand-string")} />
                </div>
                <div
                    // In this case expand state changing only on click button
                    data-expand-keep="more-custom-expand-string"
                >
                    <button onClick={this.context.changeExpandState("more-custom-expand-string")} />
                </div>
            </React.Fragment>
        );
    }
}

Migration from 1.x.x to 2.x.x

Short overview
v1.x.x v2.x.x
< Collapse /> < ExpandControl /> as controlElement < ControlledExpandElement /> as < Collapse />
< Header /> tabId -> expandId
< Tab /> tabId -> expandId
< Popup /> < ControlledExpandElement /> as < Popup />
< ModalOpenButton /> < ExpandControl staticState={true} /> as < ModalOpenButton />
< ModalCloseButton /> < ExpandControl staticState={false}/> as < ModalCloseButton />

Collapse

Collapse component is depricated. Use ExpandControl and ControlledExpandElement instead

OLD
<Collapse 
    controlElement={({state: boolean, onClick: () => void}) => <i className={state ? "open": "closed"} onClick={onClick}/>} 
    defaultOpened
    {...HTMLDivElementProps}
>
      ...
</Collapse>
NEW
<ExpandControl 
    {...HTMLButtonElementProps} 
    expandId="collapse-id" 
    activeClassName="is-active"
    state={{myData: true}}
    activeOnMount
>
    ...
</ExpandControl>
<ControlledExpandElement 
    {...HTMLDivElementProps} 
    expandId="collapse-id"
    closeOnOutside
>
      ...
</ControlledExpandElement>

Tabs

Prop tabId is depricated. Use expandId instead

OLD
<TabsController>
   <Header activeClassName="is-active" tabId="tab_1" {...HTMLDivElementProps} >
     ...
   </Header>
   <Header tabId="tab_2" {...HTMLDivElementProps} >
     ...
   </Header>
   <Tab activeClassName="is-active" tabId="tab_1" {...HTMLDivElementProps} >
     ...
   </Tab>
   <Tab tabId="tab_2" {...HTMLDivElementProps}>
     ...
   </Tab>
</TabsController>
NEW
<TabsController>
   <Header activeClassName="is-active" expandId="tab_1" {...HTMLDivElementProps} >
     ...
   </Header>
   <Header expandId="tab_2" {...HTMLDivElementProps} >
     ...
   </Header>
   <Tab activeClassName="is-active" expandId="tab_1" {...HTMLDivElementProps} >
     ...
   </Tab>
   <Tab expandId="tab_2" {...HTMLDivElementProps}>
     ...
   </Tab>
</TabsController>

Popup

Popup component is depricated. Use ExpandControl and ControlledExpandElement instead

OLD
<PopupControl popupId="popup-id" triggerEvent={TriggerEvents.hover}>
    // Single component
    ...
</PopupControl>
<Popup popupId="popup-id" {...HTMLDivElementProps}>
    ...
</Popup>
NEW
<ExpandControl 
    {...HTMLButtonElementProps} 
    expandId="popup-id" 
    triggerEvent="hover" // or "click"
    activeClassName="is-active"
>
    ...
</ExpandControl>
<ControlledExpandElement 
    {...HTMLDivElementProps} 
    expandId="popup-id" 
    closeOnOutside
>
      ...
</ControlledExpandElement>

Modal

ModalOpenButton and ModalCloseButton components is depricated. Use ExpandControl instead

OLD
<ModalOpenButton modalId="some-id" {...HTMLButtonElementProps}/>
<Modal modalId="some-id" defaultOpened closeOnOutside {...HTMLDivElementProps}>
    ...
    <ModalCloseButton {...HTMLButtonElementProps}/>
</Modal>
NEW
<ExpandControl expandId="some-id" staticState={true} {...HTMLButtonElementProps}/>
<Modal modalId="some-id" defaultOpened closeOnOutside {...HTMLDivElementProps}>
    ...
    <ExpandControl expandId="some-id" staticState={false} {...HTMLButtonElementProps}/>
</Modal>
<ExpandControl expandId="some-id" staticState={false} {...HTMLButtonElementProps}/> // close button actualy can be outside modal

Presets

NOTE: You must provide context to child nodes with <ExpandController>

HashListener

// will expand element with expandId 'expandElementId1' and 'expandElementId2'
// when href will be 'somepath/#expandElementId2/anotherPathIfYouWant/#expandElementId1'
<HashListener onHashExpanded={(expandId: string) => console.log(expandId)}>
    <ControlledExpandElement expandId="expandElementId1"/>
    <ControlledExpandElement expandId="expandElementId2"/>
</HashListener>

HashControl

// same as html link, except activeClassName, that binds when element with
// href, that contains expand keys, is active
<HashControl 
    {...HTMLAnchorProps}
    activeClassName="active" 
    href="somepath/#expandElementId2/anotherPathIfYouWant/#expandElementId1"
 >
   ...
</HashControl>

Tabs

<TabsController defaultOpened="tab_2">
   <Header activeClassName="is-active" expandId="tab_1" {...HTMLDivElementProps} > // Click on header to activate according tab
     ...
   </Header>
   <Header expandId="tab_2" {...HTMLDivElementProps} >
     ...
   </Header>
   <Tab activeClassName="is-active" expandId="tab_1" {...HTMLDivElementProps} >
     ...
   </Tab>
   <Tab expandId="tab_2" {...HTMLDivElementProps}>
     ...
   </Tab>
</TabsController>

Modal

<ExpandControl expandId="some-id" staticState={true} {...HTMLButtonElementProps}/>
<Modal modalId="some-id" defaultOpened closeOnOutside {...HTMLDivElementProps}>
    ...
    <ExpandControl expandId="some-id" staticState={false} {...HTMLButtonElementProps}/>
</Modal>
<ExpandControl expandId="some-id" staticState={false} {...HTMLButtonElementProps}/>

Expand

<ExpandControl 
    {...HTMLButtonElementProps} 
    expandId="some-expand-id" 
    triggerEvent="hover"
    activeClassName="is-active"
    staticState={true}
>
    show
</ExpandControl>
<ExpandControl 
    {...HTMLButtonElementProps} 
    expandId="some-expand-id" 
    triggerEvent="hover"
    activeClassName="is-active"
    staticState={false}
>
    hide
</ExpandControl>
<ExpandControl 
    {...HTMLButtonElementProps} 
    expandId="some-expand-id" 
    triggerEvent="click"
    activeClassName="is-active"
    activeOnMount
>
    hide
</ExpandControl>
<ControlledExpandElement 
    {...HTMLDivElementProps} 
    expandId="some-expand-id"
>
      ...
</ControlledExpandElement>

Slider

<SliderController autoPlay autoPlayDelay={5000}>
    <Slide disableDrag {...HTMLDivElementProps}>
        ...
    </Slide>
    // Slide that will be displayed on mount
    <Slide initial {...HTMLDivElementProps}>
        ...
    </Slide>
    <Slide dragSensitive={500} {...HTMLDivElementProps}>
        ...
    </Slide>
    <Dots {...HTMLButtonElementProps}/>
</SliderController>
<SliderController>
    <SlideButton drection={Direction.prev} {...HTMLButtonElementProps}>
        ...
    </SlideButton>
    // will render [[some banner 1, some banner 2, some banner 3], [some banner 4, some banner 5, some banner 6], [some banner 7]]
    <SlideGroup groupSize={3}>
        <span>some banner 1</span>
        <span>some banner 2</span>
        <span>some banner 3</span>
        <span>some banner 4</span>
        <span>some banner 5</span>
        <span>some banner 6</span>
        <span>some banner 7</span>
    </SlideGroup>
    <SlideButton direction={Direction.next} {...HTMLButtonElementProps}>
        ...
    </SlideButton>
    <Dots activeClassName="active" {...HTMLButtonElementProps}>
        ...
    </Dots>
</SliderController>

Server-side render

Because ReactDOM.createPortal does not support SSR, you must modify code for correct work of <Modal/> component

Client side

export class Layout extends React.Component {
    public render(): React.ReactNode {
        return (
            <ExpandController>
                <Modal modalId="some-id" defaultOpened>
                    ...
                </Modal>
            </ExpandController>
        );
    }    
}

Server side

app.get("*", (request, response) => {
    ReactDOMServer.renderToNodeStream(
        <html {...htmlAttrs} data-version={version}>
            <body className={StaticContainer.childrenLength ? Modal.defaultProps.activeBodyClassName : ""}>
                <div id="app">
                    <Layout />
                </div>
                <div id={ModalContainer.containerId}>
                    {StaticContainer.renderStatic()}
                </div>
            </body>
        </html>
    ).pipe(response);
})

You also can use <StaticContainer/> and <OuterContextPorvider/> in your needs.