Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[core] fix(PanelStack2): improve types for common use cases #4570

Merged
merged 5 commits into from
Mar 10, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 35 additions & 4 deletions packages/core/src/components/panel-stack2/panel-stack2.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,15 +52,46 @@ the `props` defined by `Panel<T>`. These allow you to close the current panel or
new one on top of it during the panel's lifecycle. For example:

```tsx
import { PanelProps } from "@blueprintjs/core";
import { Button, PanelProps } from "@blueprintjs/core";

type SettingsPanelInfo = {
// ...
type SettingsPanelInfo = { /* ... */ };
type AccountSettingsPanelInfo = { /* ... */ };
type NotificationSettingsPanelInfo = { /* ... */ };

const AccountSettingsPanel: React.FC<PanelProps<AccountSettingsPanelInfo>> = props => {
// implementation
};

const NotificationSettingsPanel: React.FC<PanelProps<NotificationSettingsPanelInfo>> = props => {
// implementation
};

const SettingsPanel: React.FC<PanelProps<SettingsPanelInfo>> = props => {
const { openPanel, closePanel, ...info } = props;
// ...

const openAccountSettings = () =>
openPanel({
props: {
/* ... */
},
renderPanel: AccountSettingsPanel,
title: "Account settings",
});
const openNotificationSettings = () =>
openPanel({
props: {
/* ... */
},
renderPanel: NotificationSettingsPanel,
title: "Notification settings",
});

return (
<>
<Button onClick={openAccountSettings} text="Account settings" />
<Button onClick={openNotificationSettings} text="Notification settings" />
</>
);
}
```

Expand Down
36 changes: 25 additions & 11 deletions packages/core/src/components/panel-stack2/panelStack2.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,26 +22,30 @@ import { Classes, DISPLAYNAME_PREFIX, IProps } from "../../common";
import { Panel } from "./panelTypes";
import { PanelView2 } from "./panelView2";

export interface PanelStack2Props<T> extends IProps {
/**
* @template T type union of all possible panels in this stack
*/
// eslint-disable-next-line @typescript-eslint/ban-types
export interface PanelStack2Props<T extends Panel<object>> extends IProps {
/**
* The initial panel to show on mount. This panel cannot be removed from the
* stack and will appear when the stack is empty.
* This prop is only used in uncontrolled mode and is thus mutually
* exclusive with the `stack` prop.
*/
initialPanel?: Panel<T>;
initialPanel?: T;

/**
* Callback invoked when the user presses the back button or a panel
* closes itself with a `closePanel()` action.
*/
onClose?: (removedPanel: Panel<T>) => void;
onClose?: (removedPanel: T) => void;

/**
* Callback invoked when a panel opens a new panel with an `openPanel(panel)`
* action.
*/
onOpen?: (addedPanel: Panel<T>) => void;
onOpen?: (addedPanel: T) => void;

/**
* If false, PanelStack will render all panels in the stack to the DOM, allowing their
Expand All @@ -63,27 +67,37 @@ export interface PanelStack2Props<T> extends IProps {
* The full stack of panels in controlled mode. The last panel in the stack
* will be displayed.
*/
stack?: Array<Panel<T>>;
stack?: T[];
}

interface PanelStack2Component {
<T>(props: PanelStack2Props<T>): JSX.Element | null;
/**
* @template T type union of all possible panels in this stack
*/
// eslint-disable-next-line @typescript-eslint/ban-types
<T extends Panel<object>>(props: PanelStack2Props<T>): JSX.Element | null;
displayName: string;
}

export const PanelStack2: PanelStack2Component = <T,>(props: PanelStack2Props<T>) => {
/**
* @template T type union of all possible panels in this stack
*/
// eslint-disable-next-line @typescript-eslint/ban-types
export const PanelStack2: PanelStack2Component = <T extends Panel<object>>(props: PanelStack2Props<T>) => {
const { renderActivePanelOnly = true, showPanelHeader = true } = props;
const [direction, setDirection] = React.useState("push");

const [localStack, setLocalStack] = React.useState(props.initialPanel !== undefined ? [props.initialPanel] : []);
const [localStack, setLocalStack] = React.useState<T[]>(
props.initialPanel !== undefined ? [props.initialPanel] : [],
);
const stack = props.stack != null ? props.stack.slice().reverse() : localStack;

if (stack.length === 0) {
return null;
}

const handlePanelOpen = React.useCallback(
(panel: Panel<T>) => {
(panel: T) => {
props.onOpen?.(panel);
if (props.stack == null) {
setDirection("push");
Expand All @@ -93,7 +107,7 @@ export const PanelStack2: PanelStack2Component = <T,>(props: PanelStack2Props<T>
[props.onOpen],
);
const handlePanelClose = React.useCallback(
(panel: Panel<T>) => {
(panel: T) => {
// only remove this panel if it is at the top and not the only one.
if (stack[0] !== panel || stack.length <= 1) {
return;
Expand All @@ -109,7 +123,7 @@ export const PanelStack2: PanelStack2Component = <T,>(props: PanelStack2Props<T>

const panelsToRender = renderActivePanelOnly ? [stack[0]] : stack;
const panels = panelsToRender
.map((panel: Panel<T>, index: number) => {
.map((panel: T, index: number) => {
// With renderActivePanelOnly={false} we would keep all the CSSTransitions rendered,
// therefore they would not trigger the "enter" transition event as they were entered.
// To force the enter event, we want to change the key, but stack.length is not enough
Expand Down
16 changes: 6 additions & 10 deletions packages/core/src/components/panel-stack2/panelTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ export interface Panel<P> {
title?: React.ReactNode;
}

export interface PanelActions<P> {
export interface PanelActions {
/**
* Call this method to programatically close this panel. If this is the only
* panel on the stack then this method will do nothing.
Expand All @@ -54,19 +54,15 @@ export interface PanelActions<P> {
/**
* Call this method to open a new panel on the top of the stack.
*/
openPanel(panel: Panel<P>): void;
openPanel<P>(panel: Panel<P>): void;
}

/**
* Use this interface in your panel component's props type to access these
* panel action callbacks which are injected by `PanelStack2`.
*
* ```tsx
* import { PanelProps } from "@blueprintjs/core";
* type SettingsPanelInfo = { ... };
* const SettingsPanel: React.FC<PanelProps<SettingsPanelInfo>> = props => {
* // ...
* }
* ```
* See the code example in the docs website.
*
* @see https://blueprintjs.com/docs/#core/components/panel-stack2
*/
export type PanelProps<P> = P & PanelActions<P>;
export type PanelProps<P> = P & PanelActions;
17 changes: 10 additions & 7 deletions packages/core/src/components/panel-stack2/panelView2.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,35 +21,38 @@ import { Button } from "../button/buttons";
import { Text } from "../text/text";
import { Panel, PanelProps } from "./panelTypes";

export interface PanelView2Props<T> {
// eslint-disable-next-line @typescript-eslint/ban-types
export interface PanelView2Props<T extends Panel<object>> {
/**
* Callback invoked when the user presses the back button or a panel invokes
* the `closePanel()` injected prop method.
*/
onClose: (removedPanel: Panel<T>) => void;
onClose: (removedPanel: T) => void;

/**
* Callback invoked when a panel invokes the `openPanel(panel)` injected
* prop method.
*/
onOpen: (addedPanel: Panel<T>) => void;
onOpen: (addedPanel: T) => void;

/** The panel to be displayed. */
panel: Panel<T>;
panel: T;

/** The previous panel in the stack, for rendering the "back" button. */
previousPanel?: Panel<T>;
previousPanel?: T;

/** Whether to show the header with the "back" button. */
showHeader: boolean;
}

interface PanelView2Component {
<T>(props: PanelView2Props<T>): JSX.Element | null;
// eslint-disable-next-line @typescript-eslint/ban-types
<T extends Panel<object>>(props: PanelView2Props<T>): JSX.Element | null;
displayName: string;
}

export const PanelView2: PanelView2Component = <T,>(props: PanelView2Props<T>) => {
// eslint-disable-next-line @typescript-eslint/ban-types
export const PanelView2: PanelView2Component = <T extends Panel<object>>(props: PanelView2Props<T>) => {
const handleClose = React.useCallback(() => props.onClose(props.panel), [props.onClose, props.panel]);

const maybeBackButton =
Expand Down
21 changes: 12 additions & 9 deletions packages/core/test/panel-stack2/panelStack2Tests.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,11 @@ import { spy } from "sinon";

import { Classes, Panel, PanelProps, PanelStack2Props, PanelStack2 } from "../../src";

type EmptyObject = Record<string, never>;
// eslint-disable-next-line @typescript-eslint/ban-types
type TestPanelInfo = {};
type TestPanelType = Panel<TestPanelInfo>;

const TestPanel: React.FC<PanelProps<EmptyObject>> = props => {
const TestPanel: React.FC<PanelProps<TestPanelInfo>> = props => {
const newPanel = { renderPanel: TestPanel, title: "New Panel 1" };

return (
Expand All @@ -42,15 +44,15 @@ describe("<PanelStack2>", () => {
}

let testsContainerElement: HTMLElement;
let panelStackWrapper: PanelStack2Wrapper<EmptyObject>;
let panelStackWrapper: PanelStack2Wrapper<TestPanelType>;

const initialPanel: Panel<EmptyObject> = {
const initialPanel: Panel<TestPanelInfo> = {
props: {},
renderPanel: TestPanel,
title: "Test Title",
};

const emptyTitleInitialPanel: Panel<EmptyObject> = {
const emptyTitleInitialPanel: Panel<TestPanelInfo> = {
props: {},
renderPanel: TestPanel,
};
Expand Down Expand Up @@ -208,7 +210,7 @@ describe("<PanelStack2>", () => {
});

it("can render a panel stack with multiple initial panels and close one", () => {
let stack: Array<Panel<EmptyObject>> = [initialPanel, { renderPanel: TestPanel, title: "New Panel 1" }];
let stack: Array<Panel<TestPanelInfo>> = [initialPanel, { renderPanel: TestPanel, title: "New Panel 1" }];
panelStackWrapper = renderPanelStack({
onClose: () => {
const newStack = stack.slice();
Expand Down Expand Up @@ -261,14 +263,15 @@ describe("<PanelStack2>", () => {
assert.equal(panelHeaders.at(1).text(), stack[1].title);
});

interface PanelStack2Wrapper<T> extends ReactWrapper<PanelStack2Props<T>, any> {
// eslint-disable-next-line @typescript-eslint/ban-types
interface PanelStack2Wrapper<T extends Panel<object>> extends ReactWrapper<PanelStack2Props<T>, any> {
findClass(className: string): ReactWrapper<React.HTMLAttributes<HTMLElement>, any>;
}

function renderPanelStack(props: PanelStack2Props<EmptyObject>): PanelStack2Wrapper<EmptyObject> {
function renderPanelStack(props: PanelStack2Props<TestPanelType>): PanelStack2Wrapper<TestPanelType> {
panelStackWrapper = mount(<PanelStack2 {...props} />, {
attachTo: testsContainerElement,
}) as PanelStack2Wrapper<EmptyObject>;
}) as PanelStack2Wrapper<TestPanelType>;
panelStackWrapper.findClass = (className: string) => panelStackWrapper.find(`.${className}`).hostNodes();
return panelStackWrapper;
}
Expand Down
Loading