Page MenuHomePhabricator

D5186.id17134.diff
No OneTemporary

D5186.id17134.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,40 @@
+.stepperContainer {
+ 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;
+}
+
+.buttonContainer {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+}
+
+.hide {
+ visibility: hidden;
+ height: 0;
+}
+
+.errorMessage {
+ color: var(--error);
+ font-style: italic;
+
+ 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,110 @@
+// @flow
+
+import * as React from 'react';
+
+import LoadingIndicator from '../loading-indicator.react';
+import Button from './button.react';
+import css from './stepper.css';
+
+export type ButtonProps = {
+ +content: React.Node,
+ +disabled?: boolean,
+ +loading?: boolean,
+ +onClick: () => void,
+};
+
+type ButtonType = 'prev' | 'next';
+
+type ActionButtonProps = {
+ +buttonProps: ButtonProps,
+ +type: ButtonType,
+};
+
+function ActionButton(props: ActionButtonProps) {
+ const { buttonProps, type } = props;
+ const { content, loading, disabled, onClick } = buttonProps;
+
+ const buttonContent = loading ? (
+ <>
+ <div className={css.hide}>{content}</div>
+ <LoadingIndicator status="loading" />
+ </>
+ ) : (
+ content
+ );
+
+ return (
+ <Button
+ className={css.button}
+ variant={type === 'prev' ? 'secondary' : 'primary'}
+ disabled={disabled || loading}
+ onClick={onClick}
+ >
+ <div className={css.buttonContainer}>{buttonContent}</div>
+ </Button>
+ );
+}
+
+type ItemProps = {
+ +content: React.Node,
+ +name: string,
+ +errorMessage?: string,
+ +prevProps?: ButtonProps,
+ +nextProps?: ButtonProps,
+};
+
+function StepperItem(props: ItemProps): React.Node {
+ const { content, errorMessage, prevProps, nextProps } = props;
+
+ const prevButton = React.useMemo(
+ () =>
+ prevProps ? <ActionButton buttonProps={prevProps} type="prev" /> : null,
+ [prevProps],
+ );
+
+ const nextButton = React.useMemo(
+ () =>
+ nextProps ? <ActionButton buttonProps={nextProps} type="next" /> : null,
+ [nextProps],
+ );
+
+ 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 = new Map(
+ React.Children.toArray(children).map((child, i) => [child.props.name, i]),
+ );
+
+ const activeComponent = children[index.get(activeStep) ?? 0];
+
+ return (
+ <div className={`${css.stepperContainer} ${className}`}>
+ {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, 23 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
2725117
Default Alt Text
D5186.id17134.diff (3 KB)

Event Timeline