Page MenuHomePhabricator

D5186.id17016.diff
No OneTemporary

D5186.id17016.diff

diff --git a/web/components/stepper.css b/web/components/stepper.css
new file mode 100644
--- /dev/null
+++ b/web/components/stepper.css
@@ -0,0 +1,46 @@
+.stepperContainer {
+ overflow-y: auto;
+ display: flex;
+ flex-direction: column;
+ flex-shrink: 0;
+}
+
+.stepperItem {
+ overflow-y: auto;
+ height: 100%;
+}
+
+.stepperFooter {
+ display: flex;
+ justify-content: flex-end;
+ gap: 12px;
+}
+
+.button {
+ position: relative;
+}
+
+.buttonLoadingIndicator {
+ position: absolute;
+ top: 0;
+ bottom: 0;
+ left: 0;
+ right: 0;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.hide {
+ opacity: 0;
+}
+
+.errorMessage {
+ color: var(--error);
+ font-style: italic;
+ height: 32px;
+
+ display: flex;
+ align-items: center;
+ justify-content: flex-start;
+}
diff --git a/web/components/stepper.react.js b/web/components/stepper.react.js
new file mode 100644
--- /dev/null
+++ b/web/components/stepper.react.js
@@ -0,0 +1,115 @@
+// @flow
+import classnames from 'classnames';
+import * as React from 'react';
+import { useCallback } from 'react';
+
+import LoadingIndicator from '../loading-indicator.react';
+import Button from './button.react';
+import css from './stepper.css';
+
+export type ButtonProps = {
+ +content: string,
+ +disabled?: boolean,
+ +loading?: boolean,
+ +onClick: () => void,
+};
+
+type ButtonType = 'prev' | 'next';
+
+type ItemProps = {
+ +content: React.Node,
+ +errorMessage?: string,
+ +prevProps?: ButtonProps,
+ +nextProps?: ButtonProps,
+};
+
+function StepperItem(props: ItemProps): React.Node {
+ const { content, errorMessage, prevProps, nextProps } = props;
+
+ const prepareButton = useCallback(
+ (buttonProps: ButtonProps, type: ButtonType) => {
+ const { content: name, loading, disabled, onClick } = buttonProps;
+ const contentClass = classnames({
+ [css.hide]: loading,
+ });
+
+ const indicatorClass = classnames({
+ [css.buttonLoadingIndicator]: true,
+ [css.hide]: !loading,
+ });
+
+ return (
+ <Button
+ className={css.button}
+ variant={type === 'prev' ? 'secondary' : 'primary'}
+ disabled={disabled || loading}
+ onClick={onClick}
+ >
+ <div className={contentClass}> {name} </div>
+ <div className={indicatorClass}>
+ <LoadingIndicator status="loading" />
+ </div>
+ </Button>
+ );
+ },
+ [],
+ );
+
+ const prevButton = React.useMemo(
+ () => (prevProps ? prepareButton(prevProps, 'prev') : null),
+ [prevProps, prepareButton],
+ );
+
+ const nextButton = React.useMemo(
+ () => (nextProps ? prepareButton(nextProps, 'next') : null),
+ [nextProps, prepareButton],
+ );
+
+ return (
+ <>
+ <div className={css.stepperItem}>{content}</div>
+ <div className={css.errorMessage}> {errorMessage} </div>
+ <div className={css.stepperFooter}>
+ {prevButton}
+ {nextButton}
+ </div>
+ </>
+ );
+}
+
+type ContainerProps = {
+ +activeStep: string,
+ +className?: string,
+ +children: React.ChildrenArray<React.Element<typeof StepperItem>>,
+};
+
+function StepperContainer(props: ContainerProps): React.Node {
+ const { children, activeStep, className } = props;
+
+ const index = React.useMemo(
+ () =>
+ new Map(
+ React.Children.toArray(children).map((child, i) => [
+ child.key?.toString().substring(2),
+ i,
+ ]),
+ ),
+ [children],
+ );
+
+ const activeComponent = children[index.get(activeStep) ?? 0];
+
+ const styles = classnames({
+ [css.stepperContainer]: true,
+ [className || '']: !!className,
+ });
+
+ return <div className={styles}>{activeComponent}</div>;
+}
+
+const Stepper = {
+ Container: StepperContainer,
+ Item: StepperItem,
+};
+
+export default Stepper;

File Metadata

Mime Type
text/plain
Expires
Sat, Dec 28, 8:41 AM (6 h, 39 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
2725115
Default Alt Text
D5186.id17016.diff (3 KB)

Event Timeline