4.5 KB168 lines
Blame
1/**
2 * Copyright (c) Meta Platforms, Inc. and affiliates.
3 *
4 * This source code is licensed under the MIT license found in the
5 * LICENSE file in the root directory of this source tree.
6 */
7
8import * as stylex from '@stylexjs/stylex';
9import {Icon} from 'isl-components/Icon';
10import {notEmpty} from 'shared/utils';
11import {spacing} from '../../components/theme/tokens.stylex';
12
13import './ComponentUtils.css';
14
15const styles = stylex.create({
16 center: {
17 display: 'flex',
18 width: '100%',
19 height: '100%',
20 alignItems: 'center',
21 justifyContent: 'center',
22 },
23 flex: {
24 display: 'flex',
25 alignItems: 'center',
26 gap: spacing.pad,
27 },
28 spacer: {
29 flexGrow: 1,
30 },
31 alignStart: {
32 alignItems: 'flex-start',
33 },
34});
35
36export type ReactProps<T extends HTMLElement> = React.DetailedHTMLProps<React.HTMLAttributes<T>, T>;
37
38export function LargeSpinner() {
39 return (
40 <div data-testid="loading-spinner">
41 <Icon icon="loading" size="L" />
42 </div>
43 );
44}
45
46export function Center(props: ContainerProps) {
47 const {className, xstyle, ...rest} = props;
48 return (
49 <div
50 {...stylexPropsWithClassName([styles.center, xstyle].filter(notEmpty), className)}
51 {...rest}
52 />
53 );
54}
55
56/** Flexbox container with horizontal children. */
57export function Row(props: ContainerProps) {
58 return FlexBox(props, 'row');
59}
60
61/** Flexbox container with vertical children. */
62export function Column(props: ContainerProps) {
63 return FlexBox(props, 'column');
64}
65
66/** Container that scrolls horizontally. */
67export function ScrollX(props: ScrollProps) {
68 return Scroll({...props, direction: 'x'});
69}
70
71/** Container that scrolls vertically. */
72export function ScrollY(props: ScrollProps) {
73 return Scroll({...props, direction: 'y'});
74}
75
76/** Visually empty flex item with `flex-grow: 1` to insert as much space as possible between siblings. */
77export function FlexSpacer() {
78 return <div {...stylex.props(styles.spacer)} />;
79}
80
81type ContainerProps = ReactProps<HTMLDivElement> & {xstyle?: stylex.StyleXStyles} & {
82 /** If true, use alignItems: flex-start instead of centering */
83 alignStart?: boolean;
84};
85
86/** See `<Row>` and `<Column>`. */
87function FlexBox(props: ContainerProps, flexDirection: 'row' | 'column') {
88 const {className, style, alignStart, xstyle, ...rest} = props;
89 return (
90 <div
91 {...stylexPropsWithClassName(
92 [styles.flex, alignStart && styles.alignStart, xstyle].filter(notEmpty),
93 className,
94 )}
95 {...rest}
96 style={{flexDirection, ...style}}
97 />
98 );
99}
100
101type ScrollProps = ContainerProps & {
102 /** Scroll direction. */
103 direction?: 'x' | 'y';
104 /** maxHeight or maxWidth depending on scroll direction. */
105 maxSize?: string | number;
106 /** height or width depending on scroll direction. */
107 size?: string | number;
108 /** Whether to hide the scroll bar. */
109 hideBar?: boolean;
110 /** On-scroll event handler. */
111 onScroll?: React.UIEventHandler;
112};
113
114/** See <ScrollX> and <ScrollY> */
115function Scroll(props: ScrollProps) {
116 let className = props.className ?? '';
117 const direction = props.direction ?? 'x';
118 const hideBar = props.hideBar ?? false;
119 const style: React.CSSProperties = {};
120 if (direction === 'x') {
121 style.overflowX = 'auto';
122 style.maxWidth = props.maxSize ?? '100%';
123 if (props.size != null) {
124 style.width = props.size;
125 }
126 } else {
127 style.overflowY = 'auto';
128 style.maxHeight = props.maxSize ?? '100%';
129 if (props.size != null) {
130 style.height = props.size;
131 }
132 }
133 if (hideBar) {
134 style.scrollbarWidth = 'none';
135 className += ' hide-scrollbar';
136 }
137
138 const mergedProps = {...props, className, style: {...style, ...props.style}};
139 delete mergedProps.children;
140 delete mergedProps.maxSize;
141 delete mergedProps.hideBar;
142 delete mergedProps.direction;
143
144 // The outer <div> seems to avoid issues where
145 // the other direction of scrollbar gets used.
146 // See https://pxl.cl/3bvWh for the difference.
147 // I don't fully understand how this works exactly.
148 // See also https://stackoverflow.com/a/6433475.
149 return (
150 <div style={{overflow: 'visible'}}>
151 <div {...mergedProps}>{props.children}</div>
152 </div>
153 );
154}
155
156/**
157 * Like stylex.props(), but also adds in extra classNames.
158 * Useful since `{...stylex.props()}` sets className,
159 * and either overwrites or is overwritten by other `className="..."` props.
160 */
161export function stylexPropsWithClassName(
162 style: stylex.StyleXStyles,
163 ...names: Array<string | undefined>
164) {
165 const {className, ...rest} = stylex.props(style);
166 return {...rest, className: className + ' ' + names.filter(notEmpty).join(' ')};
167}
168