import React from 'react' import ProductSwitcherList from './parts/List' import ProductSwitcherListItem from './parts/ListItem' import ProductSwitcherLoading from './parts/Loading' import { Tools } from '@planview/pv-icons' import { defineMessages, useIntl } from 'react-intl' import styled from 'styled-components' import { align, sizePx, useClickOutside } from '@planview/pv-utilities' import { ToolbarButtonEmptyInverse } from '../button' import type { Product, ProductSwitcherOrganization } from './types' import { Floater } from './parts/floater' import { useMainNavigationContext } from '../utils/context' import { ButtonEmptyInverse, ListGroup, Tooltip } from '@planview/pv-uikit' export type ProductSwitcherProps = { /** * This is prop is intended for use in Dovetail only. * When the product-switcher is used outside of MainNavigation context using a bridge to hook it up with the navigation this flag should be set to `true`. */ singleTabStop?: boolean /** * Expects an array of applications the user has access to. * If there are not at least two, the icon will not render. */ products: Product[] /** * If provided, will render co-branding for an organization * * `ProductSwitcherOrganization` has this shape: * * ```ts * { * name: string * domain: string * avatar: { * light: string | React.ReactElement * dark: string | React.ReactElement * } * } * ``` * * Avatar values for light and dark can either be a URL or the [Avatar component](https://planview-ds.github.io/react-pvds/?path=/docs/pv-uikit-avatar--docs) from `@planview/pv-uikit` */ organization?: ProductSwitcherOrganization /** * call-back on navigate in list */ onActivate?: (product: Product) => void } const TriggerWrap = styled.div` ${align.center}; height: ${sizePx.medium}; width: ${sizePx.medium}; min-height: ${sizePx.medium}; min-width: ${sizePx.medium}; ` const messages = defineMessages({ triggerLabel: { defaultMessage: 'Planview product switcher', description: 'Product switcher trigger label', id: 'pvds.toolbar.productSwitcherTriggerLabel', }, groupHeader: { defaultMessage: 'Products', description: 'Product switcher group header', id: 'pvds.toolbar.productSwitcherGroupHeader', }, }) /** * This component, implementing the [Design System specs](https://design.planview.com/components/product-switcher/product-switcher-panel), is meant to be used when you want to * manually do API calls and render the Product switcher inside a `NavigationBar`, meaning you are **not** using the switcher published via dovetail. * * `import { ProductSwitcher }` from '@planview/pv-toolbar' */ export const ProductSwitcher = ({ products, organization, onActivate = () => {}, singleTabStop = false, }: ProductSwitcherProps): React.ReactElement | null => { const triggerRef = React.useRef(null) const listRef = React.useRef(null) const [active, setActive] = React.useState(false) const [currentFocused, setCurrentFocused] = React.useState(0) const intl = useIntl() const { wrappedInNavigation } = useMainNavigationContext() const triggerLabel = intl.formatMessage(messages.triggerLabel) const handleTriggerKeyDown: React.KeyboardEventHandler = React.useCallback( (ev) => { if ((ev.key === 'ArrowDown' || ev.key === ' ') && !active) { ev.preventDefault() setActive(true) } }, [active] ) const onCloseHandler = React.useCallback(() => { setActive(false) }, []) const handleKeyDown: React.KeyboardEventHandler = React.useCallback( (ev) => { switch (ev.key) { case 'ArrowDown': ev.preventDefault() setCurrentFocused( Math.min(currentFocused + 1, products.length - 1) ) break case 'ArrowUp': ev.preventDefault() setCurrentFocused(Math.max(currentFocused - 1, 0)) break case 'Home': setCurrentFocused(0) break case 'End': setCurrentFocused(products.length - 1) break case 'Escape': setActive(false) triggerRef.current && triggerRef.current.focus() break case 'Tab': setActive(false) break default: break } }, [currentFocused, products] ) const [planviewMe, productList] = products.reduce<[Product[], Product[]]>( (memo, prd) => { if (prd.name === 'planview_me' || prd.name === 'ensemble') { memo[0].push(prd) } else { memo[1].push(prd) } return memo }, [[], []] ) function renderProductSwitcherListItem(product: Product, ix: number) { return ( { onActivate(product) setActive(false) setTimeout(() => { triggerRef.current && triggerRef.current.focus() }, 0) if (product.url) { window.open( product.url, '_blank', 'noopener,noreferrer' ) } }} tabIndex={ix === currentFocused ? 0 : -1} /> ) } useClickOutside([triggerRef, listRef], onCloseHandler) const TriggerType = wrappedInNavigation ? ToolbarButtonEmptyInverse : ButtonEmptyInverse return ( <> } tooltip={triggerLabel} activated={active} tabIndex={ wrappedInNavigation ? undefined : singleTabStop ? -1 : 0 } onClick={() => setActive(!active)} onKeyDown={handleTriggerKeyDown} aria-haspopup="menu" aria-expanded={active} aria-label={triggerLabel} aria-controls="app-switcher-list" aria-describedby={ active && products.length === 0 ? 'app-switcher-loader' : undefined } /> {active ? ( {products.length === 0 ? ( ) : ( <> {planviewMe ?.sort((a, b) => b.name.localeCompare(a.name) ) .map((product, ix) => renderProductSwitcherListItem( product, ix ) )} {productList.map((product, ix) => renderProductSwitcherListItem( product, ix + planviewMe.length ) )} )} ) : null} ) }