Created
August 24, 2021 15:17
-
-
Save ladifire/3fa94c6b6c3eef0b4b99f102dd5cbc3e to your computer and use it in GitHub Desktop.
UIChannelItem.tsx by Ladifire & Cong Nguyen
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* Copyright (c) Ladifire, Inc. and its affiliates. | |
* | |
* This source code is licensed under the MIT license found in the | |
* LICENSE file in the root directory of this source tree. | |
*/ | |
import * as React from 'react'; | |
import {Pressable} from '@ladifire-ui-react/tetra-button'; | |
import {TetraTextPairing} from '@ladifire-ui-react/tetra-text'; | |
import {useCometPreloaderImpl as useCometPreloader, CometPressableOverlay} from '@ladifire-ui-react/utils'; | |
import stylex from '@ladifire-opensource/stylex'; | |
import {useHoverAndFocusState} from 'src/utilities/useHoverAndFocusState'; | |
import {InteractiveElementContext} from './InteractiveElementContext'; | |
import {ChannelFocusableTable} from './ChannelFocusableTable'; | |
const styles = stylex.create({ | |
root: { | |
boxSizing: "border-box", | |
position: "relative", | |
flexGrow: 1, | |
flexShrink: 1, | |
minHeight: 0, | |
minWidth: 0, | |
display: "flex", | |
justifyContent: "flex-start", | |
alignItems: "center", | |
flexDirection: "row", | |
border: "none", | |
paddingRight: "var(--wig-spacing-large)" | |
}, | |
tetraLikeRoot: { | |
paddingRight: 8, | |
marginLeft: 8, | |
marginRight: 8, | |
borderRadius: 8, | |
}, | |
focused: { | |
outline: "1px solid var(--accent)" | |
}, | |
selected: { | |
backgroundColor: "var(--wig-selected-background)" | |
}, | |
tetraLikeSelected: { | |
backgroundColor: "var(--hosted-view-selected-state)" | |
}, | |
contentContainer: { | |
borderStyle: "solid", | |
borderWidth: 0, | |
boxSizing: "border-box", | |
display: "flex", | |
flexGrow: 1, | |
flexShrink: 1, | |
margin: 0, | |
minHeight: 0, | |
minWidth: 0, | |
padding: 0, | |
position: "relative", | |
zIndex: 0, | |
justifyContent: "flex-start", | |
alignItems: "center", | |
flexDirection: "row" | |
}, | |
tetraLikeContentContainer: { | |
position: "static" | |
}, | |
content: { | |
borderStyle: "solid", | |
borderWidth: 0, | |
boxSizing: "border-box", | |
display: "flex", | |
flexGrow: 1, | |
flexShrink: 1, | |
margin: 0, | |
minHeight: 0, | |
minWidth: 0, | |
padding: 0, | |
position: "relative", | |
zIndex: 0, | |
justifyContent: "flex-start", | |
alignItems: "center", | |
flexDirection: "row", | |
outline: "none", | |
":hover": { | |
textDecoration: "none" | |
} | |
}, | |
textPairing: { | |
flexGrow: 1, | |
flexBasis: 0, | |
minWidth: 0, | |
paddingTop: 8, | |
paddingBottom: 8, | |
overflow: "hidden", | |
textOverflow: "ellipsis" | |
}, | |
addOnPrimary: { | |
borderStyle: "solid", | |
borderWidth: 0, | |
boxSizing: "border-box", | |
display: "flex", | |
flexDirection: "column", | |
flexShrink: 1, | |
justifyContent: "space-between", | |
marginLeft: 0, | |
minHeight: 0, | |
minWidth: 0, | |
paddingBottom: 0, | |
paddingRight: 0, | |
paddingLeft: 0, | |
paddingTop: 0, | |
zIndex: 0, | |
alignItems: "center", | |
flexGrow: 0, | |
marginBottom: "var(--wig-spacing-small)", | |
marginRight: 12, | |
marginTop: "var(--wig-spacing-small)", | |
position: "relative" | |
}, | |
addOnSecondary: { | |
borderStyle: "solid", | |
borderWidth: 0, | |
boxSizing: "border-box", | |
display: "flex", | |
flexDirection: "column", | |
flexShrink: 1, | |
margin: 0, | |
minHeight: 0, | |
minWidth: 0, | |
padding: 0, | |
zIndex: 0, | |
position: "absolute", | |
left: 13, | |
top: 0, | |
bottom: 0, | |
alignItems: "center", | |
justifyContent: "center", | |
flexGrow: 0 | |
}, | |
tetraLikeAddOnSecondary: { | |
display: "flex", | |
justifyContent: "center", | |
alignItems: "center" | |
}, | |
addOnSecondaryOffset: { | |
transform: "translateX(-50%)" | |
}, | |
addOnSecondaryOffsetRTL: { | |
transform: "translateX(50%)" | |
}, | |
indentationLevel1: { | |
paddingLeft: 16 | |
}, | |
indentationLevel2: { | |
paddingLeft: 26 | |
}, | |
indentationLevel3: { | |
paddingLeft: 60 | |
}, | |
tetraLikeIndentationLevel1: { | |
paddingLeft: 8 | |
}, | |
tetraLikeIndentationLevel2: { | |
paddingLeft: 8 | |
}, | |
tetraLikeIndentationLevel3: { | |
paddingLeft: 42 | |
}, | |
addOnTertiary: { | |
borderStyle: "solid", | |
borderWidth: 0, | |
boxSizing: "border-box", | |
display: "flex", | |
marginBottom: 0, | |
marginRight: 0, | |
marginTop: 0, | |
minHeight: 0, | |
paddingBottom: 0, | |
paddingRight: 0, | |
paddingLeft: 0, | |
paddingTop: 0, | |
position: "relative", | |
zIndex: 0, | |
flexGrow: 0, | |
flexShrink: 0, | |
minWidth: "auto", | |
alignItems: "center", | |
justifyContent: "flex-end", | |
flexDirection: "row", | |
marginLeft: "var(--wig-spacing-medium)" | |
}, | |
tetraLikeFocusRing: { | |
position: "static", | |
":focus-visible::after": { | |
border: "1px solid var(--accent)", | |
borderRadius: 8, | |
bottom: 0, | |
content: "", | |
left: 0, | |
position: "absolute", | |
right: 0, | |
top: 0 | |
} | |
} | |
}); | |
interface Props { | |
addOnPrimary?: React.ReactElement; | |
addOnSecondary?: React.ReactElement; | |
addOnTertiary?: React.ReactElement; | |
disabled?: boolean; | |
emphasized?: boolean; | |
selected?: boolean; | |
indentationLevel?: number; | |
linkProps?: any; | |
body?: string | React.ReactElement; | |
bodyColor?: string; | |
bodyLineLimit?: number; | |
headline?: string | React.ReactElement; | |
headlineAddOn?: any; | |
headlineColor?: string; | |
headlineLineLimit?: number; | |
meta?: string | React.ReactElement; | |
metaColor?: string; | |
metaLineLimit?: number; | |
metaLocation?: string; | |
onPress?: (event: any) => void; | |
onPressIn?: (event: any) => void; | |
onHoverIn?: (event: any) => void; | |
onHoverOut?: (event: any) => void; | |
onFocusIn?: (event: any) => void; | |
onFocusOut?: (event: any) => void; | |
isSemanticListItem?: boolean; | |
wrapperRef?: any; | |
onPreload?: () => void; | |
} | |
export const UIChannelItem = React.forwardRef((props: Props, ref) => { | |
const { | |
addOnPrimary, | |
addOnSecondary, | |
addOnTertiary, | |
disabled = false, | |
emphasized = false, | |
selected = false, | |
indentationLevel = 2, | |
linkProps = {}, | |
body, | |
bodyColor, | |
bodyLineLimit = 1, | |
headline, | |
headlineAddOn, | |
headlineColor, | |
headlineLineLimit = 1, | |
meta, | |
metaColor, | |
metaLineLimit, | |
metaLocation, | |
onPress, | |
onPressIn, | |
onHoverIn, | |
onHoverOut, | |
onFocusIn, | |
onFocusOut, | |
isSemanticListItem = true, | |
wrapperRef, | |
onPreload, | |
...otherProps | |
} = props; | |
const { | |
url, | |
..._otherLinkProps | |
} = linkProps; | |
const [pressed, setPressed] = React.useState(false); | |
const { | |
focused, | |
hovered, | |
onFocusIn: _onFocusIn, | |
onFocusOut: _onFocusOut, | |
onHoverIn: _onHoverIn, | |
onHoverOut: _onHoverOut, | |
} = useHoverAndFocusState(); | |
const _interactiveElementContext = React.useMemo(() => { | |
return { | |
hovered: hovered, | |
focused: focused, | |
pressed: pressed, | |
} | |
}, [hovered, focused, pressed]); | |
const handlePreload = React.useCallback(() => { | |
if (onPreload) { | |
onPreload(); | |
} | |
}, [onPreload]); | |
const [triggerPreload, triggerOutPreload] = useCometPreloader("button_aggressive", undefined, handlePreload); | |
const handleHoverIn = React.useCallback((event: any) => { | |
triggerPreload(event); | |
if (onHoverIn) { | |
onHoverIn(event); | |
} | |
}, [onHoverIn, triggerPreload]); | |
const handleHoverOut = React.useCallback((event: any) => { | |
triggerOutPreload(); | |
if (onHoverOut) { | |
onHoverOut(event); | |
} | |
}, [onHoverOut, triggerOutPreload]); | |
const handleFocusIn = React.useCallback((event: any) => { | |
_onFocusIn(event); | |
if (onFocusIn) { | |
onFocusIn(event); | |
} | |
}, [_onFocusIn, onFocusIn]); | |
const handleFocusOut = React.useCallback((event: any) => { | |
_onFocusOut(event); | |
if (onFocusOut) { | |
onFocusOut(event); | |
} | |
}, [_onFocusOut, onFocusOut]); | |
const handlePressIn = React.useCallback((event: any) => { | |
setPressed(true); | |
if (onPressIn) { | |
onPressIn(event); | |
} | |
}, [onPressIn]); | |
const handlePressOut = React.useCallback(() => { | |
setPressed(false); | |
}, []); | |
const content = ( | |
<React.Fragment> | |
{ | |
addOnPrimary && ( | |
<div className={stylex(styles.addOnPrimary)}> | |
{addOnPrimary} | |
</div> | |
) | |
} | |
<div | |
data-testid="UIChannelItem" // should be replaced to undefined in build script | |
className={stylex(styles.textPairing)} | |
> | |
<TetraTextPairing | |
body={body} | |
bodyColor={bodyColor} | |
bodyLineLimit={bodyLineLimit} | |
headline={headline} | |
headlineAddOn={headlineAddOn} | |
headlineColor={headlineColor} | |
headlineLineLimit={headlineLineLimit} | |
level={4} | |
meta={meta} | |
metaColor={metaColor} | |
metaLineLimit={metaLineLimit} | |
metaLocation={metaLocation} | |
reduceEmphasis={!emphasized} | |
/> | |
</div> | |
</React.Fragment> | |
) | |
const WrapperComponent = isSemanticListItem ? "li" : "div"; | |
const pressable = onPress || url !== null; | |
return ( | |
<ChannelFocusableTable.ChannelFocusableTableRow> | |
<InteractiveElementContext.Provider value={_interactiveElementContext}> | |
<WrapperComponent | |
ref={wrapperRef} | |
role={pressable && isSemanticListItem ? 'row' : undefined} | |
onMouseEnter={_onHoverIn} | |
onMouseLeave={_onHoverOut} | |
className={stylex(styles.root, | |
getIndentationClassName({indentationLevel: indentationLevel}), | |
focused && styles.focused, | |
selected && styles.selected, | |
)} | |
> | |
{pressable && ( | |
<CometPressableOverlay | |
focusVisible={focused} | |
useHoverAndFocusState={hovered} | |
pressed={pressed} | |
/> | |
)} | |
{ | |
pressable ? ( | |
<ChannelFocusableTable.ChannelFocusableTableCell> | |
<div | |
className={stylex(styles.contentContainer)} | |
role={isSemanticListItem ? 'gridcell' : undefined} | |
> | |
<Pressable | |
{...otherProps} | |
display="block" | |
disabled={disabled} | |
linkProps={url ? { | |
..._otherLinkProps, | |
url: url, | |
prefetchQueries: true, | |
} : undefined} | |
onHoverIn={handleHoverIn} | |
onHoverOut={handleHoverOut} | |
onFocusIn={handleFocusIn} | |
onFocusOut={handleFocusOut} | |
onPress={onPress} | |
onPressIn={handlePressIn} | |
onPressOut={handlePressOut} | |
overlayDisabled={true} | |
ref={ref} | |
xstyle={[styles.content]} | |
> | |
{content} | |
</Pressable> | |
</div> | |
</ChannelFocusableTable.ChannelFocusableTableCell> | |
) : ( | |
<div className={stylex(styles.contentContainer)}> | |
{content} | |
</div> | |
) | |
} | |
{addOnSecondary && ( | |
<div className={stylex(styles.addOnSecondary, styles.addOnSecondaryOffset)}> | |
{addOnSecondary} | |
</div> | |
)} | |
{addOnTertiary && ( | |
<div className={stylex(styles.addOnTertiary)}> | |
{addOnTertiary} | |
</div> | |
)} | |
</WrapperComponent> | |
</InteractiveElementContext.Provider> | |
</ChannelFocusableTable.ChannelFocusableTableRow> | |
); | |
}) | |
const getIndentationClassName = (props: {indentationLevel: number}) => { | |
const {indentationLevel} = props; | |
if (indentationLevel === 1) return styles.indentationLevel1; | |
else if (indentationLevel === 2) return styles.indentationLevel2; | |
else if (indentationLevel === 3) return styles.indentationLevel3; | |
return styles.indentationLevel1; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment