addons/isl/src/ComponentUtils.tsxblame
View source
b69ab311/**
b69ab312 * Copyright (c) Meta Platforms, Inc. and affiliates.
b69ab313 *
b69ab314 * This source code is licensed under the MIT license found in the
b69ab315 * LICENSE file in the root directory of this source tree.
b69ab316 */
b69ab317
b69ab318import * as stylex from '@stylexjs/stylex';
b69ab319import {Icon} from 'isl-components/Icon';
b69ab3110import {notEmpty} from 'shared/utils';
b69ab3111import {spacing} from '../../components/theme/tokens.stylex';
b69ab3112
b69ab3113import './ComponentUtils.css';
b69ab3114
b69ab3115const styles = stylex.create({
b69ab3116 center: {
b69ab3117 display: 'flex',
b69ab3118 width: '100%',
b69ab3119 height: '100%',
b69ab3120 alignItems: 'center',
b69ab3121 justifyContent: 'center',
b69ab3122 },
b69ab3123 flex: {
b69ab3124 display: 'flex',
b69ab3125 alignItems: 'center',
b69ab3126 gap: spacing.pad,
b69ab3127 },
b69ab3128 spacer: {
b69ab3129 flexGrow: 1,
b69ab3130 },
b69ab3131 alignStart: {
b69ab3132 alignItems: 'flex-start',
b69ab3133 },
b69ab3134});
b69ab3135
b69ab3136export type ReactProps<T extends HTMLElement> = React.DetailedHTMLProps<React.HTMLAttributes<T>, T>;
b69ab3137
b69ab3138export function LargeSpinner() {
b69ab3139 return (
b69ab3140 <div data-testid="loading-spinner">
b69ab3141 <Icon icon="loading" size="L" />
b69ab3142 </div>
b69ab3143 );
b69ab3144}
b69ab3145
b69ab3146export function Center(props: ContainerProps) {
b69ab3147 const {className, xstyle, ...rest} = props;
b69ab3148 return (
b69ab3149 <div
b69ab3150 {...stylexPropsWithClassName([styles.center, xstyle].filter(notEmpty), className)}
b69ab3151 {...rest}
b69ab3152 />
b69ab3153 );
b69ab3154}
b69ab3155
b69ab3156/** Flexbox container with horizontal children. */
b69ab3157export function Row(props: ContainerProps) {
b69ab3158 return FlexBox(props, 'row');
b69ab3159}
b69ab3160
b69ab3161/** Flexbox container with vertical children. */
b69ab3162export function Column(props: ContainerProps) {
b69ab3163 return FlexBox(props, 'column');
b69ab3164}
b69ab3165
b69ab3166/** Container that scrolls horizontally. */
b69ab3167export function ScrollX(props: ScrollProps) {
b69ab3168 return Scroll({...props, direction: 'x'});
b69ab3169}
b69ab3170
b69ab3171/** Container that scrolls vertically. */
b69ab3172export function ScrollY(props: ScrollProps) {
b69ab3173 return Scroll({...props, direction: 'y'});
b69ab3174}
b69ab3175
b69ab3176/** Visually empty flex item with `flex-grow: 1` to insert as much space as possible between siblings. */
b69ab3177export function FlexSpacer() {
b69ab3178 return <div {...stylex.props(styles.spacer)} />;
b69ab3179}
b69ab3180
b69ab3181type ContainerProps = ReactProps<HTMLDivElement> & {xstyle?: stylex.StyleXStyles} & {
b69ab3182 /** If true, use alignItems: flex-start instead of centering */
b69ab3183 alignStart?: boolean;
b69ab3184};
b69ab3185
b69ab3186/** See `<Row>` and `<Column>`. */
b69ab3187function FlexBox(props: ContainerProps, flexDirection: 'row' | 'column') {
b69ab3188 const {className, style, alignStart, xstyle, ...rest} = props;
b69ab3189 return (
b69ab3190 <div
b69ab3191 {...stylexPropsWithClassName(
b69ab3192 [styles.flex, alignStart && styles.alignStart, xstyle].filter(notEmpty),
b69ab3193 className,
b69ab3194 )}
b69ab3195 {...rest}
b69ab3196 style={{flexDirection, ...style}}
b69ab3197 />
b69ab3198 );
b69ab3199}
b69ab31100
b69ab31101type ScrollProps = ContainerProps & {
b69ab31102 /** Scroll direction. */
b69ab31103 direction?: 'x' | 'y';
b69ab31104 /** maxHeight or maxWidth depending on scroll direction. */
b69ab31105 maxSize?: string | number;
b69ab31106 /** height or width depending on scroll direction. */
b69ab31107 size?: string | number;
b69ab31108 /** Whether to hide the scroll bar. */
b69ab31109 hideBar?: boolean;
b69ab31110 /** On-scroll event handler. */
b69ab31111 onScroll?: React.UIEventHandler;
b69ab31112};
b69ab31113
b69ab31114/** See <ScrollX> and <ScrollY> */
b69ab31115function Scroll(props: ScrollProps) {
b69ab31116 let className = props.className ?? '';
b69ab31117 const direction = props.direction ?? 'x';
b69ab31118 const hideBar = props.hideBar ?? false;
b69ab31119 const style: React.CSSProperties = {};
b69ab31120 if (direction === 'x') {
b69ab31121 style.overflowX = 'auto';
b69ab31122 style.maxWidth = props.maxSize ?? '100%';
b69ab31123 if (props.size != null) {
b69ab31124 style.width = props.size;
b69ab31125 }
b69ab31126 } else {
b69ab31127 style.overflowY = 'auto';
b69ab31128 style.maxHeight = props.maxSize ?? '100%';
b69ab31129 if (props.size != null) {
b69ab31130 style.height = props.size;
b69ab31131 }
b69ab31132 }
b69ab31133 if (hideBar) {
b69ab31134 style.scrollbarWidth = 'none';
b69ab31135 className += ' hide-scrollbar';
b69ab31136 }
b69ab31137
b69ab31138 const mergedProps = {...props, className, style: {...style, ...props.style}};
b69ab31139 delete mergedProps.children;
b69ab31140 delete mergedProps.maxSize;
b69ab31141 delete mergedProps.hideBar;
b69ab31142 delete mergedProps.direction;
b69ab31143
b69ab31144 // The outer <div> seems to avoid issues where
b69ab31145 // the other direction of scrollbar gets used.
b69ab31146 // See https://pxl.cl/3bvWh for the difference.
b69ab31147 // I don't fully understand how this works exactly.
b69ab31148 // See also https://stackoverflow.com/a/6433475.
b69ab31149 return (
b69ab31150 <div style={{overflow: 'visible'}}>
b69ab31151 <div {...mergedProps}>{props.children}</div>
b69ab31152 </div>
b69ab31153 );
b69ab31154}
b69ab31155
b69ab31156/**
b69ab31157 * Like stylex.props(), but also adds in extra classNames.
b69ab31158 * Useful since `{...stylex.props()}` sets className,
b69ab31159 * and either overwrites or is overwritten by other `className="..."` props.
b69ab31160 */
b69ab31161export function stylexPropsWithClassName(
b69ab31162 style: stylex.StyleXStyles,
b69ab31163 ...names: Array<string | undefined>
b69ab31164) {
b69ab31165 const {className, ...rest} = stylex.props(style);
b69ab31166 return {...rest, className: className + ' ' + names.filter(notEmpty).join(' ')};
b69ab31167}