Source

components/layout/DisplayWhen.tsx

import React, {useContext} from 'react'
import {ResponsiveLayoutContext} from './ResponsiveLayoutProvider'

import {logger} from "../../services";
import {StripedAlertBox} from "../extras";
import {useTimeCheck, useWidthCheck, useScopeCheck, useEnvironmentCheck} from "../../tools";


type DisplayWhenProps = {
    children: any
    not?: boolean
    minWidth?: string
    maxWidth?: string
    wide?: boolean
    medium?: boolean
    narrow?: boolean
    allScope?: string | string[]
    anyScope?: string | string[]
    before?: string
    after?: string,
    nowarn?: boolean,
    prod?: boolean
}

//TODO: Add environment section to docs

/**
 * This is a convenience wrapper component to enable simple conditional
 * rendering based on various criteria, like screen width or the current
 * user's permissions.
 *
 * If the render conditions aren't met, or if no props are provided, the
 * component returns `null` in order to not render the child components.
 *
 * ### Depend on screen size
 *
 * When responding to screen size, this component dynamically
 * re-renders the page to fit any changes to the screen size (shrinking
 * window, rotating phone etc...), so that the user is always receiving
 * the content most optimized for their screen size.
 *
 * `<DisplayWhen>` depends on the `ResponsiveLayoutContext` to get
 * information about the current screen dimensions, and provides a set of
 * props to dictate simple logic for when to render the child components.
 *
 * You can specify a boolean prop for when to render child components
 * ```jsx
 * <DisplayWhen narrow>
 *     <p>This content is displayed on narrow screens</p>
 * </DisplayWhen>
 * ```
 * There are three valid width names: `narrow`, `medium`, and `wide`.
 *
 * It is possible to provide multiple boolean props
 * ```jsx
 * <DisplayWhen narrow>
 *     <p>This content is displayed on narrow screens</p>
 * </DisplayWhen>
 * <DisplayWhen medium wide>
 *     <p>This content is displayed on medium and wide screens</p>
 * </DisplayWhen>
 * ```
 *
 * You can also provide explicit conditions on when to render using
 * the `minWidth` and `maxWidth` props. The sizes are inclusive, and
 * should be specified the sizes as strings with "px" suffix.
 *
 * Specifying `minWidth` or `maxWidth` trumps the named width props like `narrow`.
 *
 * ```jsx
 * <DisplayWhen maxWidth="850px">
 *     <p>This content is only displayed on screens 850px wide or narrower</p>
 * </DisplayWhen>
 * <DisplayWhen minWidth="900px">
 *     <p>This content is only displayed on screens that are 900px wide or wider</p>
 * </DisplayWhen>
 * <DisplayWhen minWidth="700px" maxWidth="1000px">
 *     <p>This content is only displayed on screens 700px to 1000px wide (including both values).</p>
 * </DisplayWhen>
 * ```
 *
 * ### Depend on scopes (user permissions)
 *
 * ```
 * <DisplayWhen allScope={[scope1, scope2]}>
 *     <p>This content is displayed only when scope1 AND scope2 are both granted to the logged-in user.</p>
 * </DisplayWhen>
 * <DisplayWhen anyScope={[scope1, scope2]}>
 *     <p>This content is displayed only when scope1 OR scope2, or both, are granted to the logged-in user.</p>
 * </DisplayWhen>
 * <DisplayWhen allScope={scope1}>
 *     <p>Both scope props will accept a single scope string.</p>
 * </DisplayWhen>
 * ```
 *
 * ### Depend on specific times
 *
 * You can provide a `before` and/or `after` prop to `DisplayWhen` in order to restrict certain components to a specific
 * date or time. This is useful for situations where the app use is timeboxed (e.g. special considerations for exams) and
 * you want to prevent the need to deploy a new app version to prevent users from submitting after the fact.
 *
 * Dates should be in ISO format where possible, but `DisplayWhen` can take any reasonably formatted date and will assume
 * a timezone of `+10:00` for any format that doesn't contain it's own timezone information.
 *
 * ```
 * <DisplayWhen after="2021-02-21">
 *     <p>This content is only displayed from 00:00:01am on the 21st of February 2021 (townsville time, the default TZ of +10)</p>
 *     <p>Any people outside UTC+10 will still be restricted, but the time will be adjusted for their TZ
 *     (e.g. the app will open in singapore (+08) at 10pm on the 20th of February 2021)</p>
 * </DisplayWhen>
 * <DisplayWhen before="2021-02-21T20:00:00+10">
 *     <p>This content will be displayed until 8pm on the 21st of February 2021 (townsville time)</p>
 * </DisplayWhen>
 * <DisplayWhen after="2021-02-21T08:00:00" before="2021-02-21T20:00:00">
 *     <p>This content will be displayed between 8am and 8pm on the 21st of February 2021 (townsville time)</p>
 * </DisplayWhen>
 * ```
 *
 * ### Inverting the logic
 *
 * `DisplayWhen` has an additional boolean prop, `not`, which if specified will
 * invert the display logic.
 *
 *
 * ### Future Directions
 *
 * Due to emerging technologies in the digital signage space, more options and/or breakpoints may be required to meet the
 * unique dimensions of digital signage screens.
 *
 * @component
 * @category Layout
 */
export const DisplayWhen = (props: DisplayWhenProps) => {

    const {nowarn} = props

    // Load responsive layout context and read current screen width
    const responsiveLayout = useContext(ResponsiveLayoutContext)
    const width = responsiveLayout.width

    let display = true
    let error = false

    // ------------------------------------------------- WIDTH CHECK -------------------------------------------------//
    // Destructure width related props
    const {minWidth, maxWidth, narrow, medium, wide} = props

    const {wide: isWide, medium: isMedium, narrow: isNarrow} = useWidthCheck()

    // Check if a min/max width have been provided
    if (minWidth || maxWidth) {
        let minBound = 0
        let maxBound = 1000000
        try {
            // Check to ensure widths are in correct format e.g. '740px'
            if (typeof minWidth === 'string' && minWidth.indexOf('px') > 0) {
                minBound = Number(minWidth.replace('px', ''))
            }
            if (typeof maxWidth === 'string' && maxWidth.indexOf('px') > 0) {
                maxBound = Number(maxWidth.replace('px', ''))
            }
        } catch (err) {
            //TODO: Richer error handling
            console.log(err)
            display = false
            error = true
        }

        // If width is within specified boundaries
        if (width >= minBound && width <= maxBound) {
            display = display && true
        } else {
            display = false
        }
    } else if (narrow || medium || wide) {
        // Check the boolean props wide/medium/narrow
        switch (true) {
            case narrow && isNarrow:
            case medium && isMedium:
            case wide && isWide:
                display = display && true
                break
            default:
                display = false
        }
    }

    // ------------------------------------------------- SCOPE CHECK -------------------------------------------------//
    // Destructure scope related props
    const {allScope, anyScope} = props

    const {allScope: allScopeBool, loggedIn} = useScopeCheck(allScope)

    const {anyScope: anyScopeBool} = useScopeCheck(anyScope)

    // If we're restricting to scopes but no-one is logged in, display nothing
    if ((allScope || anyScope) && !loggedIn) {
        display = false
    } else if (allScope || anyScope) {
        // If we've restricted to a set of scopes and we don't meet that criteria, dont display
        if (anyScope && !anyScopeBool) {
            display = false
        } else if (allScope && !allScopeBool) {
            display = false
        }
    }

    // ------------------------------------------------- TIME CHECK -------------------------------------------------//
    // Destructure time related props
    const {before, after} = props

    const {before: isBefore} = useTimeCheck(before)
    const {after: isAfter} = useTimeCheck(after)

    if (before && !isBefore) {
        display = false
    }

    if (after) {
        // Check if the dev has provided a date only format for their after value (e.g. 2020-05-21)
        // DevLog a clarification message that after 2020-05-21 means 2020-05-21T00:00:01 and not the day after
        // This is a possible misunderstanding of the non-ISO date interpretation
        const dateFormat = /\d{4}-?\d{1,2}-?\d{1,2}/
        if (dateFormat.test(after) && !nowarn) {
            logger.devWarn("" +
                "It looks like you have provided a date without a time to the after value of DisplayWhen.\n" +
                "A date only format (e.g. 2020-05-21) will be interpreted as 2020-05-21T00:00:00.\n" +
                "If you intend for the after value to exclude a particular date, provide either:\n" +
                "\t- The date +1 day (e.g. 2020-05-22) or\n" +
                "\t- A full explicit ISO datetime (e.g. 2020-05-21T23:59:59)\n")
        }
        if (!isAfter) {
            display = false
        }
    }

    // ---------------------------------------------- ENVIRONMENT CHECK ----------------------------------------------//
    // Destructure environment related props
    const {prod} = props

    const {prod: isProd} = useEnvironmentCheck()

    // Check if we set prod mode but arent in prod environment
    if (prod && !isProd) {
        display = false
    }

    // -------------------------------------------------- INVERSION --------------------------------------------------//
    // if the user gave us the `not` prop, invert the decision
    // (unless the don't display was the result of an error)
    if (props.not) {
        display = !display
    }
    // ---------------------------------------------------------------------------------------------------------------//

    if (error) {
        // If there was an error but we're not in production, highlight errored content but still display.
        // If we're in production, errors return nothing
        if (!isProd) {
            return <StripedAlertBox type={"error"}>
                {props.children}
            </StripedAlertBox>
        } else {
            return null
        }
    } else if (display) {
        // If the appropriate conditions are met, render the children
        return props.children
    } else {
        // If no conditions are met, render nothing
        return null
    }
}