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
}
}
Source