rafal-r/airr-react

View on GitHub
lib/SceneRenderer.tsx

Summary

Maintainability
A
3 hrs
Test Coverage
import * as React from "react";
import {
    BlankMaskRenderer,
    ChildrenRenderer,
    MayersRenderer,
    NavbarRenderer,
    SidepanelRenderer,
    ViewsRenderer
} from "./SceneRenderer/Items";
import { PureComponent, SyntheticEvent, ReactNode, RefObject } from "react";
import { Props as MayerProps } from "./Mayer";
import Sidepanel from "./Sidepanel";
import { AnimationType, NavbarMenu, SidepanelConfig } from "./Airr";
import { ViewsConfigItem } from "./Scene";

export type NavbarProp = 1 | true | -1 | 0 | false;
export type ViewsArray = ViewsConfigItem<any>[];

export interface CoreSceneProps {
    /**
     * The name of the scene. Must be unique among others views in parent scene. Will be used as identification string
     */
    name: string;
    /**
     * Name of the active view
     */
    activeViewName?: string;
    /**
     * Boolean telling if GUI should be disabled meaning no user actions, events are allowed.
     * GUI is disabled via absolute positioned, not visible div that has the biggest z-Index
     */
    GUIDisabled?: boolean;
    /**
     * React element to be placed in GUI disabling div
     */
    GUIDisableCover?: ReactNode;
    /**
     * Type of animation to perform when switching views
     */
    animation?: AnimationType;
    /**
     * Time of views changing animation in miliseconds
     */
    animationTime?: number;
    /**
     * Specify if navbar is present (1,true) or not (0,false). Or maybe hidden (-1)
     */
    navbar?: NavbarProp;
    /**
     * Height of the navbar in pixels
     */
    navbarHeight?: number;
    /**
     * Navbar menu is placed on the right most side. Might contain "toggleSidepanel" button or any custom buttons list.
     */
    navbarMenu?: NavbarMenu;
    /**
     * Extra, space separated, navbar's class list
     */
    navbarClass?: string;
    /**
     * Boolean specifing if navbar renders BackButton. Placed by default on the left side of navbar.
     */
    backButton?: boolean;
    /**
     * Do you need to still show backButton even if scene is rendering first view from stack?
     */
    backButtonOnFirstView?: boolean;
    /**
     * Function that will handle back button click events
     */
    handleBackButton?: (e: SyntheticEvent<HTMLElement>) => void;
    /**
     * Function that will handle back button clicks events on when first view in stack is active
     */
    handleBackBehaviourOnFirstView?: () => void;
    /**
     * Is this view active in parent scene. Readonly.
     */
    active?: boolean;
    /**
     * Sidepanels declaration. Must contain two properties: `type` and `props`
     **/
    sidepanel?: SidepanelConfig<any>;
    /**
     * This function will be called when sidepanel changes it's visibility.
     * It's argument will be isShown bool.
     */
    sidepanelVisibilityCallback?(isShown: boolean): void;
    /**
     * Array of `views`. Every view object declaration must contain two properties: `type` and `props`.
     */
    views?: ViewsArray;
    /**
     * Array of `mayers` objects that will be render into this Scene. Must contain special Mayer class properties.
     * To check the possible values of properties go to Mayer declaration.
     */
    mayers?: MayerProps[];
    /**
     * Title that will be use in parent Scene navbar title section
     */
    title?: ReactNode;
    /**
     * Extra, space separated classes names to use upon first div element.
     */
    className?: string;
    /**
     * Children prop
     */
    children?: ReactNode;
    /**
     * Inner, private prop for manipulating navbar title. Do not set manually.
     */
    mockTitleName?: string;

    //inner props
    key?: string;
    ref?: React.LegacyRef<PureComponent>;
}
export interface Props extends CoreSceneProps {
    /**
     * React component's ref object
     */
    refCOMPSidepanel: RefObject<Sidepanel>;
    /**
     * React ref to dom object
     */
    refDOM: RefObject<HTMLDivElement>;
    /**
     * React ref to dom object
     */
    refDOMNavbar: RefObject<HTMLDivElement>;
    /**
     * Object of React refs components specified under string keys
     */
    refsCOMPViews: { [name: string]: RefObject<PureComponent> };
    /**
     * React component's ref object
     */
    refDOMContainer: RefObject<HTMLDivElement>;
    /**
     * Inner, private prop with containers height information
     */
    containersHeight: number;
}

export const sceneDefaultProps: CoreSceneProps = {
    name: "",

    activeViewName: null,
    GUIDisabled: false,
    GUIDisableCover: null,
    animation: "slide",
    animationTime: 300,
    navbar: false,
    navbarHeight: 48,
    navbarMenu: null,
    navbarClass: "",
    backButton: false,
    backButtonOnFirstView: false,
    handleBackButton: null,
    handleBackBehaviourOnFirstView: null,
    active: false,
    sidepanel: null,
    views: [],
    mayers: [],
    title: "",
    className: "",
    sidepanelVisibilityCallback: null,
    children: null,
    mockTitleName: null
};
export default class SceneRenderer extends PureComponent<Props> {
    static defaultProps: Props = {
        ...sceneDefaultProps,
        refCOMPSidepanel: null,
        refDOM: null,
        refDOMNavbar: null,
        refsCOMPViews: null,
        refDOMContainer: null,
        mockTitleName: "",
        containersHeight: null
    };
    /**
     * Mayers Components refferencies
     */
    mayersCompsRefs = {};

    /**
     * Returns view index from this.props.views array
     *
     * @param {string} viewName
     * @returns {Number}
     */
    getViewIndex(viewName: string): number {
        return this.props.views.findIndex((config): boolean => config.props.name === viewName);
    }

    /**
     * Handles navbar backbutton tap events
     *
     * @param {object} e Event object
     * @returns {void}
     */
    handleBackButton = (e: SyntheticEvent<HTMLElement>): void => {
        const backBtn = e.currentTarget;
        backBtn.classList.add("clicked");

        setTimeout((): void => {
            backBtn.classList.remove("clicked");
        }, 300);

        if (
            this.getViewIndex(this.props.activeViewName) === 0 &&
            this.props.handleBackBehaviourOnFirstView
        ) {
            return this.props.handleBackBehaviourOnFirstView();
        }

        if (this.props.handleBackButton) {
            this.props.handleBackButton(e);
        } else {
            console.warn("[] Back button handler was not specified.");
        }
    };

    /**
     * Handles navbar menu button tap events
     *
     * @param {object} e Event object
     * @returns {void}
     */
    handleMenuButtonToggleSidepanel = (e: SyntheticEvent<HTMLElement>): void => {
        if (this.props.refCOMPSidepanel && this.props.refCOMPSidepanel.current) {
            this.props.refCOMPSidepanel.current.isShown()
                ? this.props.refCOMPSidepanel.current.hide()
                : this.props.refCOMPSidepanel.current.show();
        }
    };

    checkValidActiveView = (): boolean => {
        const isAnyViewActive = this.props.views.some(
            (view): boolean => view.props.name === this.props.activeViewName
        );

        if (!isAnyViewActive) {
            console.warn(
                "[] No view was set as active" +
                    (this.props.name && " in Scene named `" + this.props.name + "`") +
                    "."
            );
        }

        return Boolean(isAnyViewActive);
    };

    render(): ReactNode {
        let className = "airr-view airr-scene";
        this.props.active && (className += " active");
        this.props.className && (className += " " + this.props.className);

        this.checkValidActiveView();

        const activeViewIndex = this.getViewIndex(this.props.activeViewName);
        let mockViewTitle;
        if (this.props.mockTitleName) {
            const mockViewIndex = this.getViewIndex(this.props.mockTitleName);
            if (mockViewIndex >= 0) {
                const mockView = this.props.views[mockViewIndex];
                if (mockView.props.title) {
                    mockViewTitle = mockView.props.title;
                }
            }
        }
        return (
            <div className={className} ref={this.props.refDOM}>
                <div className="airr-content-wrap">
                    <NavbarRenderer
                        navbar={this.props.navbar}
                        activeViewIndex={activeViewIndex}
                        backButtonOnFirstView={this.props.backButtonOnFirstView}
                        backButton={this.props.backButton}
                        handleBackButton={this.handleBackButton}
                        navbarMenu={this.props.navbarMenu}
                        hasSidepanel={Boolean(this.props.sidepanel)}
                        handleMenuButtonToggleSidepanel={this.handleMenuButtonToggleSidepanel}
                        navbarClass={this.props.navbarClass}
                        mockViewTitle={mockViewTitle}
                        activeViewTitle={
                            this.props.views[activeViewIndex] &&
                            this.props.views[activeViewIndex].props.title
                        }
                        refDOMNavbar={this.props.refDOMNavbar}
                        navbarHeight={this.props.navbarHeight}
                    />
                    <ViewsRenderer
                        className={this.props.animation ? this.props.animation + "-animation" : ""}
                        refsCOMPViews={this.props.refsCOMPViews}
                        activeViewName={this.props.activeViewName}
                        views={this.props.views}
                        refDOMContainer={this.props.refDOMContainer}
                        containersHeight={this.props.containersHeight}
                    />
                </div>
                <ChildrenRenderer {...this.props}>{this.props.children}</ChildrenRenderer>
                {this.props.sidepanel && (
                    <SidepanelRenderer
                        type={this.props.sidepanel.type}
                        refCOMPSidepanel={this.props.refCOMPSidepanel}
                        visibilityCallback={this.props.sidepanelVisibilityCallback}
                        sceneHasMayers={Boolean(this.props.mayers.length)}
                        props={this.props.sidepanel.props}
                    />
                )}
                <MayersRenderer mayers={this.props.mayers} />
                <BlankMaskRenderer
                    GUIDisabled={this.props.GUIDisabled}
                    GUIDisableCover={this.props.GUIDisableCover}
                />
            </div>
        );
    }
}