Skip to content

Commit

Permalink
[core] feat(MultistepDialog): tooltips on navigation buttons (palanti…
Browse files Browse the repository at this point in the history
…r#5534)

Co-authored-by: Adi Dahiya <[email protected]>
  • Loading branch information
ohcnivek and adidahiya authored Sep 14, 2022
1 parent df7c038 commit 4d6240f
Show file tree
Hide file tree
Showing 5 changed files with 84 additions and 45 deletions.
3 changes: 1 addition & 2 deletions packages/core/src/components/dialog/dialogStep.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,9 @@ import * as React from "react";

import { AbstractPureComponent2, Classes } from "../../common";
import { DISPLAYNAME_PREFIX, HTMLDivProps, Props } from "../../common/props";
import { ButtonProps } from "../button/buttons";
import type { DialogStepButtonProps } from "./dialogStepButton";

export type DialogStepId = string | number;
export type DialogStepButtonProps = Partial<Pick<ButtonProps, "disabled" | "text">>;

// eslint-disable-next-line deprecation/deprecation
export type DialogStepProps = IDialogStepProps;
Expand Down
38 changes: 38 additions & 0 deletions packages/core/src/components/dialog/dialogStepButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/*
* Copyright 2020 Palantir Technologies, Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import * as React from "react";

import { AnchorButton, ButtonProps } from "../button/buttons";
import { Tooltip, TooltipProps } from "../tooltip/tooltip";

// omit "elementRef", which is the only property with a different type in ButtonProps vs. AnchorButtonProps
export type DialogStepButtonProps = Partial<Omit<ButtonProps, "elementRef">> & {
/** If defined, the button will be wrapped with a tooltip with the specified content. */
// eslint-disable-next-line deprecation/deprecation
tooltipContent?: TooltipProps["content"];
};

export function DialogStepButton({ tooltipContent, ...props }: DialogStepButtonProps) {
const button = <AnchorButton {...props} />;

if (tooltipContent !== undefined) {
// eslint-disable-next-line deprecation/deprecation
return <Tooltip content={tooltipContent}>{button}</Tooltip>;
} else {
return button;
}
}
23 changes: 11 additions & 12 deletions packages/core/src/components/dialog/multistepDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,9 @@ import * as React from "react";

import { AbstractPureComponent2, Classes, Position, Utils } from "../../common";
import { DISPLAYNAME_PREFIX } from "../../common/props";
import { Button, ButtonProps } from "../button/buttons";
import { Dialog, DialogProps } from "./dialog";
import { DialogStep, DialogStepButtonProps, DialogStepId, DialogStepProps } from "./dialogStep";
import { DialogStep, DialogStepId, DialogStepProps } from "./dialogStep";
import { DialogStepButton, DialogStepButtonProps } from "./dialogStepButton";

type DialogStepElement = React.ReactElement<DialogStepProps & { children: React.ReactNode }>;

Expand All @@ -40,15 +40,14 @@ export interface IMultistepDialogProps extends DialogProps {
children?: React.ReactNode;

/**
* Props for the close button that appears in the footer when there is no
* title.
* Props for the close button that appears in the footer when there is no title.
*/
closeButtonProps?: Partial<ButtonProps>;
closeButtonProps?: DialogStepButtonProps;

/**
* Props for the button to display on the final step.
*/
finalButtonProps?: Partial<ButtonProps>;
finalButtonProps?: DialogStepButtonProps;

/**
* Position of the step navigation within the dialog.
Expand Down Expand Up @@ -215,7 +214,7 @@ export class MultistepDialog extends AbstractPureComponent2<MultistepDialogProps
const { closeButtonProps, isCloseButtonShown, showCloseButtonInFooter, onClose } = this.props;
const isFooterCloseButtonVisible = showCloseButtonInFooter && isCloseButtonShown;
const maybeCloseButton = !isFooterCloseButtonVisible ? undefined : (
<Button text="Close" onClick={onClose} {...closeButtonProps} />
<DialogStepButton text="Close" onClick={onClose} {...closeButtonProps} />
);
return (
<div className={Classes.MULTISTEP_DIALOG_FOOTER}>
Expand All @@ -232,9 +231,8 @@ export class MultistepDialog extends AbstractPureComponent2<MultistepDialogProps

if (this.state.selectedIndex > 0) {
const backButtonProps = steps[selectedIndex].props.backButtonProps ?? this.props.backButtonProps;

buttons.push(
<Button
<DialogStepButton
key="back"
onClick={this.getDialogStepChangeHandler(selectedIndex - 1)}
text="Back"
Expand All @@ -244,12 +242,13 @@ export class MultistepDialog extends AbstractPureComponent2<MultistepDialogProps
}

if (selectedIndex === this.getDialogStepChildren().length - 1) {
buttons.push(<Button intent="primary" key="final" text="Submit" {...this.props.finalButtonProps} />);
buttons.push(
<DialogStepButton intent="primary" key="final" text="Submit" {...this.props.finalButtonProps} />,
);
} else {
const nextButtonProps = steps[selectedIndex].props.nextButtonProps ?? this.props.nextButtonProps;

buttons.push(
<Button
<DialogStepButton
intent="primary"
key="next"
onClick={this.getDialogStepChangeHandler(selectedIndex + 1)}
Expand Down
60 changes: 30 additions & 30 deletions packages/core/test/multistep-dialog/multistepDialogTests.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,14 @@
*/

import { assert } from "chai";
import { mount } from "enzyme";
import { mount, ReactWrapper } from "enzyme";
import * as React from "react";

import { Classes, DialogStep, MultistepDialog } from "../../src";
import { AnchorButton, Classes, DialogStep, MultistepDialog } from "../../src";

const NEXT_BUTTON = "[text='Next']";
const BACK_BUTTON = "[text='Back']";
const SUBMIT_BUTTON = "[text='Submit']";
// TODO: button selectors in these tests should not be tied so closely to implementation; we shouldn't
// need to reference AnchorButton directly
const findButtonWithText = (wrapper: ReactWrapper, text: string) => wrapper.find(AnchorButton).find(`[text='${text}']`);

describe("<MultistepDialog>", () => {
it("renders its content correctly", () => {
Expand Down Expand Up @@ -62,36 +62,36 @@ describe("<MultistepDialog>", () => {
dialog.unmount();
});

it("clicking next should select the next element", () => {
it("clicking next should move to the next step", () => {
const dialog = mount(
<MultistepDialog isOpen={true} usePortal={false}>
<DialogStep id="one" title="Step 1" panel={<Panel />} />
<DialogStep id="two" title="Step 2" panel={<Panel />} />
</MultistepDialog>,
);
dialog.find(NEXT_BUTTON).simulate("click");
findButtonWithText(dialog, "Next").simulate("click");
assert.strictEqual(dialog.state("selectedIndex"), 1);
const steps = dialog.find(`.${Classes.DIALOG_STEP_CONTAINER}`);
assert.strictEqual(steps.at(0).find(`.${Classes.DIALOG_STEP_VIEWED}`).length, 1);
assert.strictEqual(steps.at(1).find(`.${Classes.ACTIVE}`).length, 1);
dialog.unmount();
});

it("clicking back should select the prev element", () => {
it("clicking back should move to the prev step", () => {
const dialog = mount(
<MultistepDialog isOpen={true} usePortal={false}>
<DialogStep id="one" title="Step 1" panel={<Panel />} />
<DialogStep id="two" title="Step 2" panel={<Panel />} />
</MultistepDialog>,
);

dialog.find(NEXT_BUTTON).simulate("click");
findButtonWithText(dialog, "Next").simulate("click");
assert.strictEqual(dialog.state("selectedIndex"), 1);
const steps = dialog.find(`.${Classes.DIALOG_STEP_CONTAINER}`);
assert.strictEqual(steps.at(0).find(`.${Classes.DIALOG_STEP_VIEWED}`).length, 1);
assert.strictEqual(steps.at(1).find(`.${Classes.ACTIVE}`).length, 1);

dialog.find(BACK_BUTTON).simulate("click");
findButtonWithText(dialog, "Back").simulate("click");
const newSteps = dialog.find(`.${Classes.DIALOG_STEP_CONTAINER}`);
assert.strictEqual(dialog.state("selectedIndex"), 0);
assert.strictEqual(newSteps.at(0).find(`.${Classes.ACTIVE}`).length, 1);
Expand All @@ -106,11 +106,11 @@ describe("<MultistepDialog>", () => {
<DialogStep id="two" title="Step 2" panel={<Panel />} />
</MultistepDialog>,
);
dialog.find(NEXT_BUTTON).simulate("click");
findButtonWithText(dialog, "Next").simulate("click");
assert.strictEqual(dialog.state("selectedIndex"), 1);
assert.strictEqual(dialog.find(BACK_BUTTON).length, 1);
assert.strictEqual(dialog.find(NEXT_BUTTON).length, 0);
assert.strictEqual(dialog.find(SUBMIT_BUTTON).length, 1);
assert.strictEqual(findButtonWithText(dialog, "Back").length, 1);
assert.strictEqual(findButtonWithText(dialog, "Next").length, 0);
assert.strictEqual(findButtonWithText(dialog, "Submit").length, 1);
dialog.unmount();
});

Expand All @@ -123,9 +123,9 @@ describe("<MultistepDialog>", () => {
);

assert.strictEqual(dialog.state("selectedIndex"), 0);
assert.strictEqual(dialog.find(BACK_BUTTON).length, 0);
assert.strictEqual(dialog.find(NEXT_BUTTON).length, 1);
assert.strictEqual(dialog.find(SUBMIT_BUTTON).length, 0);
assert.strictEqual(findButtonWithText(dialog, "Back").length, 0);
assert.strictEqual(findButtonWithText(dialog, "Next").length, 1);
assert.strictEqual(findButtonWithText(dialog, "Submit").length, 0);
dialog.unmount();
});

Expand All @@ -137,9 +137,9 @@ describe("<MultistepDialog>", () => {
);

assert.strictEqual(dialog.state("selectedIndex"), 0);
assert.strictEqual(dialog.find(BACK_BUTTON).length, 0);
assert.strictEqual(dialog.find(NEXT_BUTTON).length, 0);
assert.strictEqual(dialog.find(SUBMIT_BUTTON).length, 1);
assert.strictEqual(findButtonWithText(dialog, "Back").length, 0);
assert.strictEqual(findButtonWithText(dialog, "Next").length, 0);
assert.strictEqual(findButtonWithText(dialog, "Submit").length, 1);
dialog.unmount();
});

Expand All @@ -151,7 +151,7 @@ describe("<MultistepDialog>", () => {
</MultistepDialog>,
);
assert.strictEqual(dialog.state("selectedIndex"), 0);
dialog.find(NEXT_BUTTON).simulate("click");
findButtonWithText(dialog, "Next").simulate("click");
assert.strictEqual(dialog.state("selectedIndex"), 1);
const step = dialog.find(`.${Classes.DIALOG_STEP}`);
step.at(0).simulate("click");
Expand Down Expand Up @@ -190,7 +190,7 @@ describe("<MultistepDialog>", () => {
<DialogStep id="two" title="Step 2" panel={<Panel />} />
</MultistepDialog>,
);
assert.strictEqual(dialog.find(NEXT_BUTTON).prop("disabled"), undefined);
assert.strictEqual(findButtonWithText(dialog, "Next").prop("disabled"), undefined);
dialog.unmount();
});

Expand All @@ -201,7 +201,7 @@ describe("<MultistepDialog>", () => {
<DialogStep id="two" title="Step 2" panel={<Panel />} />
</MultistepDialog>,
);
assert.strictEqual(dialog.find(NEXT_BUTTON).prop("disabled"), true);
assert.strictEqual(findButtonWithText(dialog, "Next").prop("disabled"), true);
dialog.unmount();
});

Expand All @@ -215,11 +215,11 @@ describe("<MultistepDialog>", () => {
);

assert.strictEqual(dialog.state("selectedIndex"), 0);
assert.strictEqual(dialog.find(NEXT_BUTTON).prop("disabled"), undefined);
dialog.find(NEXT_BUTTON).simulate("click");
assert.strictEqual(findButtonWithText(dialog, "Next").prop("disabled"), undefined);
findButtonWithText(dialog, "Next").simulate("click");
assert.strictEqual(dialog.state("selectedIndex"), 1);
assert.strictEqual(dialog.find(NEXT_BUTTON).prop("disabled"), true);
dialog.find(NEXT_BUTTON).simulate("click");
assert.strictEqual(findButtonWithText(dialog, "Next").prop("disabled"), true);
findButtonWithText(dialog, "Next").simulate("click");
assert.strictEqual(dialog.state("selectedIndex"), 1);
dialog.unmount();
});
Expand All @@ -234,10 +234,10 @@ describe("<MultistepDialog>", () => {
);

assert.strictEqual(dialog.state("selectedIndex"), 0);
dialog.find(NEXT_BUTTON).simulate("click");
findButtonWithText(dialog, "Next").simulate("click");
assert.strictEqual(dialog.state("selectedIndex"), 1);
assert.strictEqual(dialog.find(BACK_BUTTON).prop("disabled"), true);
dialog.find(BACK_BUTTON).simulate("click");
assert.strictEqual(findButtonWithText(dialog, "Back").prop("disabled"), true);
findButtonWithText(dialog, "Back").simulate("click");
assert.strictEqual(dialog.state("selectedIndex"), 1);
dialog.unmount();
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,10 @@ export class MultistepDialogExample extends React.PureComponent<
icon="info-sign"
navigationPosition={position}
onClose={this.handleClose}
nextButtonProps={{ disabled: this.state.value === undefined }}
nextButtonProps={{
disabled: this.state.value === undefined,
tooltipContent: this.state.value === undefined ? "Select an option to continue" : undefined,
}}
finalButtonProps={finalButtonProps}
title={hasTitle ? "Multistep dialog" : undefined}
{...state}
Expand Down

0 comments on commit 4d6240f

Please sign in to comment.