[CORL 131] User Menu Dropdown (#2225)

* chore: simplify popover

* feat: add dropdown component

* feat: user menu

* chore: add comment

* chore: refactor i18n name

* fix: linting

* fix: test

* fix: address review comments

* fix: update snapshot
This commit is contained in:
Kiwi
2019-03-20 21:26:05 +01:00
committed by GitHub
parent 41db413bea
commit 7ce02620e9
45 changed files with 713 additions and 229 deletions
@@ -1,16 +1,20 @@
import React from "react";
import { createRenderer } from "react-test-renderer/shallow";
import { removeFragmentRefs } from "talk-framework/testHelpers";
import { PropTypesOf } from "talk-framework/types";
import App from "./App";
const AppN = removeFragmentRefs(App);
it("renders correctly", () => {
const props: PropTypesOf<typeof App> = {
const props: PropTypesOf<typeof AppN> = {
me: {},
children: "child",
};
const renderer = createRenderer();
renderer.render(<App {...props} />);
renderer.render(<AppN {...props} />);
expect(renderer.getRenderOutput()).toMatchSnapshot();
});
+9 -3
View File
@@ -1,16 +1,22 @@
import React, { StatelessComponent } from "react";
import { PropTypesOf } from "talk-framework/types";
import { Logo } from "talk-ui/components";
import { AppBar, Begin, Divider, End } from "talk-ui/components/AppBar";
import SignOutButtonContainer from "../containers/SignOutButtonContainer";
import UserMenuContainer from "../containers/UserMenuContainer";
import DecisionHistoryButton from "./DecisionHistoryButton";
import Navigation from "./Navigation";
import Version from "./Version";
import styles from "./App.css";
const App: StatelessComponent = ({ children }) => (
interface Props {
me: PropTypesOf<typeof UserMenuContainer>["me"];
children: React.ReactNode;
}
const App: StatelessComponent<Props> = ({ children, me }) => (
<div className={styles.root}>
<AppBar gutterBegin gutterEnd>
<Begin itemGutter="double">
@@ -23,7 +29,7 @@ const App: StatelessComponent = ({ children }) => (
<End>
<DecisionHistoryButton />
<Divider />
<SignOutButtonContainer id="navigation-signOutButton" />
<UserMenuContainer me={me} />
</End>
</AppBar>
{children}
@@ -1,56 +1,43 @@
import React from "react";
import { Localized } from "fluent-react/compat";
import React, { StatelessComponent } from "react";
import { oncePerFrame } from "talk-common/utils";
import { BaseButton, ClickOutside, Icon, Popover } from "talk-ui/components";
import DecisionHistoryQuery from "../views/decisionHistory/queries/DecisionHistoryQuery";
import styles from "./DecisionHistoryButton.css";
class DecisionHistoryButton extends React.Component {
// Helper that prevents calling toggleVisibility more then once per frame.
// In essence this means we'll process an event only once.
// This might happen, when clicking on the button which will
// cause its onClick to happen as well as onClickOutside.
private toggleVisibilityOncePerFrame = oncePerFrame(
(toggleVisibility: () => void) => toggleVisibility()
);
const popoverID = "decision-history-popover";
public render() {
const popoverID = `decision-history-popover`;
return (
<Popover
data-testid="decisionHistory-popover"
id={popoverID}
placement="bottom-end"
description="A dialog showing a permalink to the comment"
classes={{ popover: styles.popover }}
body={({ toggleVisibility }) => {
const hide = () =>
this.toggleVisibilityOncePerFrame(toggleVisibility);
return (
<ClickOutside onClickOutside={hide}>
<div>
<DecisionHistoryQuery onClosePopover={hide} />
</div>
</ClickOutside>
);
}}
>
{({ toggleVisibility, forwardRef, visible }) => (
<BaseButton
onClick={() => this.toggleVisibilityOncePerFrame(toggleVisibility)}
aria-controls={popoverID}
ref={forwardRef}
className={styles.historyIcon}
data-testid="decisionHistory-toggle"
>
<Icon size="lg">history</Icon>
</BaseButton>
)}
</Popover>
);
}
}
const DecisionHistoryButton: StatelessComponent = () => (
<Localized id="decisionHistory-popover" attrs={{ description: true }}>
<Popover
data-testid="decisionHistory-popover"
id={popoverID}
placement="bottom-end"
description="A dialog showing the decision history"
classes={{ popover: styles.popover }}
body={({ toggleVisibility }) => (
<ClickOutside onClickOutside={toggleVisibility}>
<div>
<DecisionHistoryQuery onClosePopover={toggleVisibility} />
</div>
</ClickOutside>
)}
>
{({ toggleVisibility, ref, visible }) => (
<BaseButton
onClick={toggleVisibility}
aria-controls={popoverID}
ref={ref}
className={styles.historyIcon}
data-testid="decisionHistory-toggle"
>
<Icon size="lg">history</Icon>
</BaseButton>
)}
</Popover>
</Localized>
);
export default DecisionHistoryButton;
@@ -1,5 +0,0 @@
.root {
font-weight: var(--font-weight-bold);
font-size: calc(18rem / var(--rem-base));
letter-spacing: calc(-0.2em / 18);
}
@@ -1,21 +0,0 @@
import { Localized } from "fluent-react/compat";
import React, { StatelessComponent } from "react";
import { Button } from "talk-ui/components";
import styles from "./SignOutButton.css";
interface Props {
id?: string;
onClick: React.EventHandler<React.MouseEvent>;
}
const SignOutButton: StatelessComponent<Props> = props => (
<Localized id="navigation-signOutButton">
<Button id={props.id} onClick={props.onClick} className={styles.root}>
Sign Out
</Button>
</Localized>
);
export default SignOutButton;
@@ -0,0 +1,11 @@
.button {
padding-top: 0;
padding-bottom: 0;
margin-left: 5px;
}
.buttonText {
font-size: calc(18rem / var(--rem-base));
letter-spacing: calc(-0.2em / 18);
margin-left: 2px;
margin-right: 1px;
}
@@ -4,14 +4,14 @@ import { createRenderer } from "react-test-renderer/shallow";
import { PropTypesOf } from "talk-framework/types";
import SignOutButton from "./SignOutButton";
import UserMenu from "./UserMenu";
it("renders correctly", () => {
const props: PropTypesOf<typeof SignOutButton> = {
id: "id",
onClick: noop,
const props: PropTypesOf<typeof UserMenu> = {
username: "Admin",
onSignOut: noop as any,
};
const renderer = createRenderer();
renderer.render(<SignOutButton {...props} />);
renderer.render(<UserMenu {...props} />);
expect(renderer.getRenderOutput()).toMatchSnapshot();
});
@@ -0,0 +1,79 @@
import { Localized } from "fluent-react/compat";
import React, { StatelessComponent } from "react";
import {
Button,
ButtonIcon,
ClickOutside,
Dropdown,
DropdownButton,
DropdownDivider,
Popover,
} from "talk-ui/components";
import styles from "./UserMenu.css";
interface Props {
username: string;
onSignOut: React.EventHandler<React.MouseEvent>;
}
const UserMenu: StatelessComponent<Props> = props => (
<Localized id="userMenu-popover" attrs={{ description: true }}>
<Popover
id="userMenu"
placement="bottom-end"
description="A dialog of the user menu with related links and actions"
body={({ toggleVisibility }) => (
<ClickOutside onClickOutside={toggleVisibility}>
<Dropdown>
<Localized id="userMenu-viewLatestRelease">
<DropdownButton
href="https://github.com/coralproject/talk/releases/latest"
target="_blank"
rel="noopener"
>
View Latest Release
</DropdownButton>
</Localized>
<Localized id="userMenu-reportBug">
<DropdownButton
href="https://github.com/coralproject/talk/issues/new"
target="_blank"
rel="noopener"
>
Report a Bug or Give Feedback
</DropdownButton>
</Localized>
<DropdownDivider />
<Localized id="userMenu-signOut">
<DropdownButton onClick={props.onSignOut} blankAdornment>
Sign Out
</DropdownButton>
</Localized>
</Dropdown>
</ClickOutside>
)}
>
{({ toggleVisibility, ref, visible }) => (
<Button
className={styles.button}
onClick={toggleVisibility}
ref={ref}
variant="regular"
size="small"
>
<ButtonIcon size="lg">account_circle</ButtonIcon>
<span className={styles.buttonText}>{props.username}</span>
{
<ButtonIcon size="lg">
{visible ? "arrow_drop_up" : "arrow_drop_down"}
</ButtonIcon>
}
</Button>
)}
</Popover>
</Localized>
);
export default UserMenu;
@@ -22,8 +22,8 @@ exports[`renders correctly 1`] = `
<withPropsOnChange(End)>
<DecisionHistoryButton />
<withPropsOnChange(Divider) />
<withContext(createMutationContainer(RedirectAppContainer))
id="navigation-signOutButton"
<withContext(createMutationContainer(Relay(UserMenuContainer)))
me={Object {}}
/>
</withPropsOnChange(End)>
</withPropsOnChange(AppBar)>
@@ -1,15 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`renders correctly 1`] = `
<Localized
id="navigation-signOutButton"
>
<ForwardRef(forwardRef)
className="SignOutButton-root"
id="id"
onClick={[Function]}
>
Sign Out
</ForwardRef(forwardRef)>
</Localized>
`;
@@ -0,0 +1,21 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`renders correctly 1`] = `
<Localized
attrs={
Object {
"description": true,
}
}
id="userMenu-popover"
>
<withPropsOnChange(Popover)
body={[Function]}
description="A dialog of the user menu with related links and actions"
id="userMenu"
placement="bottom-end"
>
[Function]
</withPropsOnChange(Popover)>
</Localized>
`;
@@ -1,29 +1,34 @@
import { BrowserProtocol, queryMiddleware } from "farce";
import { createFarceRouter, createRender } from "found";
import { Resolver } from "found-relay";
import React from "react";
import React, { StatelessComponent } from "react";
import { TalkContextConsumer } from "talk-framework/lib/bootstrap/TalkContext";
import routeConfig from "../routeConfig";
import NotFound from "../routes/NotFound";
import { AppContainerQueryResponse } from "talk-admin/__generated__/AppContainerQuery.graphql";
const Router = createFarceRouter({
historyProtocol: new BrowserProtocol(),
historyMiddlewares: [queryMiddleware],
routeConfig,
render: createRender({
renderError: ({ error }) => (
<div>{error.status === 404 ? <NotFound /> : "Error"}</div>
),
}),
});
import { graphql } from "talk-framework/lib/relay";
import { withRouteConfig } from "talk-framework/lib/router";
const AppContainer: StatelessComponent = () => (
<TalkContextConsumer>
{({ relayEnvironment }) => (
<Router resolver={new Resolver(relayEnvironment)} />
)}
</TalkContextConsumer>
);
import App from "../components/App";
export default AppContainer;
interface Props {
data: AppContainerQueryResponse | null;
}
class AppContainer extends React.Component<Props> {
public render() {
return (
<App me={this.props.data && this.props.data.me}>
{this.props.children}
</App>
);
}
}
const enhanced = withRouteConfig({
query: graphql`
query AppContainerQuery {
me {
...UserMenuContainer_me
}
}
`,
})(AppContainer);
export default enhanced;
@@ -0,0 +1,29 @@
import { BrowserProtocol, queryMiddleware } from "farce";
import { createFarceRouter, createRender } from "found";
import { Resolver } from "found-relay";
import React, { StatelessComponent } from "react";
import { TalkContextConsumer } from "talk-framework/lib/bootstrap/TalkContext";
import routeConfig from "../routeConfig";
import NotFound from "../routes/NotFound";
const Router = createFarceRouter({
historyProtocol: new BrowserProtocol(),
historyMiddlewares: [queryMiddleware],
routeConfig,
render: createRender({
renderError: ({ error }) => (
<div>{error.status === 404 ? <NotFound /> : "Error"}</div>
),
}),
});
const EntryContainer: StatelessComponent = () => (
<TalkContextConsumer>
{({ relayEnvironment }) => (
<Router resolver={new Resolver(relayEnvironment)} />
)}
</TalkContextConsumer>
);
export default EntryContainer;
@@ -1,20 +0,0 @@
import React from "react";
import { SignOutMutation, withSignOutMutation } from "talk-framework/mutations";
import SignOutButton from "../components/SignOutButton";
interface Props {
signOut: SignOutMutation;
id?: string;
}
class RedirectAppContainer extends React.Component<Props> {
private handleClick = () => this.props.signOut();
public render() {
return <SignOutButton id={this.props.id} onClick={this.handleClick} />;
}
}
const enhanced = withSignOutMutation(RedirectAppContainer);
export default enhanced;
@@ -0,0 +1,36 @@
import React from "react";
import { UserMenuContainer_me as MeData } from "talk-admin/__generated__/UserMenuContainer_me.graphql";
import { graphql, withFragmentContainer } from "talk-framework/lib/relay";
import { SignOutMutation, withSignOutMutation } from "talk-framework/mutations";
import UserMenu from "../components/UserMenu";
interface Props {
signOut: SignOutMutation;
me: MeData | null;
}
class UserMenuContainer extends React.Component<Props> {
private handleSignOut = () => this.props.signOut();
public render() {
return (
<UserMenu
onSignOut={this.handleSignOut}
username={(this.props.me && this.props.me.username) || ""}
/>
);
}
}
const enhanced = withSignOutMutation(
withFragmentContainer<Props>({
me: graphql`
fragment UserMenuContainer_me on User {
username
}
`,
})(UserMenuContainer)
);
export default enhanced;
+2 -2
View File
@@ -2,7 +2,7 @@ import React, { StatelessComponent } from "react";
import ReactDOM from "react-dom";
import { createManaged } from "talk-framework/lib/bootstrap";
import AppContainer from "./containers/AppContainer";
import EntryContainer from "./containers/EntryContainer";
import { initLocalState } from "./local";
import localesData from "./locales";
@@ -15,7 +15,7 @@ async function main() {
const Index: StatelessComponent = () => (
<ManagedTalkContextProvider>
<AppContainer />
<EntryContainer />
</ManagedTalkContextProvider>
);
+2 -2
View File
@@ -1,7 +1,7 @@
import { makeRouteConfig, Redirect, Route } from "found";
import React from "react";
import App from "./components/App";
import AppContainer from "./containers/AppContainer";
import AuthCheckContainer from "./containers/AuthCheckContainer";
import Community from "./routes/community/components/Community";
import ConfigureContainer from "./routes/configure/containers/ConfigureContainer";
@@ -25,7 +25,7 @@ import Stories from "./routes/stories/components/Stories";
export default makeRouteConfig(
<Route path="admin">
<Route {...AuthCheckContainer.routeConfig}>
<Route Component={App}>
<Route {...AppContainer.routeConfig}>
<Redirect from="/" to="/admin/moderate" />
<Route
path="moderate/comment/:commentID"
@@ -46,6 +46,14 @@ it("logs out", async () => {
.once()
.returns({});
const userMenu = await waitForElement(() =>
within(testRenderer.root).getByText(users[0].username, {
selector: "button",
})
);
userMenu.props.onClick();
const signOutButton = await waitForElement(() =>
within(testRenderer.root).getByText("Sign Out")
);
+2 -2
View File
@@ -4,7 +4,7 @@ import React from "react";
import TestRenderer from "react-test-renderer";
import { Environment, RecordProxy, RecordSourceProxy } from "relay-runtime";
import AppContainer from "talk-admin/containers/AppContainer";
import EntryContainer from "talk-admin/containers/EntryContainer";
import { TalkContext, TalkContextProvider } from "talk-framework/lib/bootstrap";
import { PostMessageService } from "talk-framework/lib/postMessage";
import { RestClient } from "talk-framework/lib/rest";
@@ -54,7 +54,7 @@ export default function create(params: CreateParams) {
const testRenderer = TestRenderer.create(
<TalkContextProvider value={context}>
<AppContainer />
<EntryContainer />
</TalkContextProvider>,
{ createNodeMock }
);
@@ -465,7 +465,7 @@ exports[`renders decision history popover button 1`] = `
className="AriaInfo-root"
id="decision-history-popover-ariainfo"
>
A dialog showing a permalink to the comment
A dialog showing the decision history
</div>
</div>
</div>
@@ -1,7 +1,6 @@
import { Localized } from "fluent-react/compat";
import React from "react";
import React, { StatelessComponent } from "react";
import { oncePerFrame } from "talk-common/utils";
import {
Button,
ButtonIcon,
@@ -19,39 +18,26 @@ interface PermalinkProps {
url: string;
}
class Permalink extends React.Component<PermalinkProps> {
// Helper that prevents calling toggleVisibility more then once per frame.
// In essence this means we'll process an event only once.
// This might happen, when clicking on the button which will
// cause its onClick to happen as well as onClickOutside.
private toggleVisibilityOncePerFrame = oncePerFrame(
(toggleVisibility: () => void) => toggleVisibility()
);
public render() {
const { commentID, url } = this.props;
const popoverID = `permalink-popover-${commentID}`;
return (
const Permalink: StatelessComponent<PermalinkProps> = ({ commentID, url }) => {
const popoverID = `permalink-popover-${commentID}`;
return (
<Localized id="comments-permalinkPopover" attrs={{ description: true }}>
<Popover
id={popoverID}
placement="top-start"
description="A dialog showing a permalink to the comment"
classes={{ popover: styles.popover }}
body={({ toggleVisibility }) => (
<ClickOutside
onClickOutside={() =>
this.toggleVisibilityOncePerFrame(toggleVisibility)
}
>
<ClickOutside onClickOutside={toggleVisibility}>
<PermalinkPopover permalinkURL={url} />
</ClickOutside>
)}
>
{({ toggleVisibility, forwardRef, visible }) => (
{({ toggleVisibility, ref, visible }) => (
<Button
onClick={() => this.toggleVisibilityOncePerFrame(toggleVisibility)}
onClick={toggleVisibility}
aria-controls={popoverID}
ref={forwardRef}
ref={ref}
variant="ghost"
active={visible}
size="small"
@@ -65,8 +51,8 @@ class Permalink extends React.Component<PermalinkProps> {
</Button>
)}
</Popover>
);
}
}
</Localized>
);
};
export default Permalink;
@@ -1,6 +1,6 @@
import React from "react";
import { oncePerFrame } from "talk-common/utils";
import { Localized } from "fluent-react/compat";
import { ClickOutside, Popover } from "talk-ui/components";
import ReportCommentView from "../../views/reportComment";
@@ -13,53 +13,39 @@ interface Props {
reported: boolean;
}
class ReportButtonWithPopover extends React.Component<Props> {
// Helper that prevents calling toggleVisibility more then once per frame.
// In essence this means we'll process an event only once.
// This might happen, when clicking on the button which will
// cause its onClick to happen as well as onClickOutside.
private toggleVisibilityOncePerFrame = oncePerFrame(
(toggleVisibility: () => void) => toggleVisibility()
);
public render() {
const { comment, reported } = this.props;
const popoverID = `report-popover-${comment.id}`;
return (
const ReportButtonWithPopover: React.StatelessComponent<Props> = ({
comment,
reported,
}) => {
const popoverID = `report-popover-${comment.id}`;
return (
<Localized id="comments-reportPopover" attrs={{ description: true }}>
<Popover
id={popoverID}
placement="top-end"
description="A dialog for reporting comments"
body={({ toggleVisibility, scheduleUpdate }) => (
<ClickOutside
onClickOutside={() =>
this.toggleVisibilityOncePerFrame(toggleVisibility)
}
>
<ClickOutside onClickOutside={toggleVisibility}>
<ReportCommentView
comment={comment}
onClose={() =>
this.toggleVisibilityOncePerFrame(toggleVisibility)
}
onClose={toggleVisibility}
onResize={scheduleUpdate}
/>
</ClickOutside>
)}
>
{({ toggleVisibility, forwardRef, visible }) => (
{({ toggleVisibility, ref, visible }) => (
<ReportButton
onClick={() =>
!reported && this.toggleVisibilityOncePerFrame(toggleVisibility)
}
onClick={evt => !reported && toggleVisibility(evt)}
aria-controls={popoverID}
ref={forwardRef}
ref={ref}
active={visible}
reported={reported}
/>
)}
</Popover>
);
}
}
</Localized>
);
};
export default ReportButtonWithPopover;
@@ -24,6 +24,7 @@ interface Props extends ButtonHTMLAttributes<HTMLButtonElement> {
anchor?: boolean;
href?: string;
target?: string;
rel?: string;
/**
* This prop can be used to add custom classnames.
@@ -9,8 +9,6 @@ import BaseButton, { BaseButtonProps } from "../BaseButton";
import styles from "./Button.css";
// This should extend from BaseButton instead but we can't because of this bug
// TODO: add bug link.
interface Props extends Omit<BaseButtonProps, "ref"> {
/** If set renders an anchor tag instead */
anchor?: boolean;
@@ -0,0 +1,27 @@
.root {
composes: menuItem from "talk-ui/shared/typography.css";
display: flex;
justify-content: space-between;
align-items: center;
padding: calc(0.5 * var(--spacing-unit)) var(--spacing-unit);
width: 100%;
text-align: left;
}
.root:active {
background-color: var(--palette-grey-lighter);
}
.mouseHover {
background-color: var(--palette-grey-lightest);
}
.icon {
color: var(--palette-grey-main);
margin-left: var(--spacing-unit);
padding-top: 1px;
}
.blankAdornment {
padding-right: calc(3.5 * var(--spacing-unit));
}
@@ -0,0 +1,32 @@
import { noop } from "lodash";
import React from "react";
import { createRenderer } from "react-test-renderer/shallow";
import { PropTypesOf } from "talk-framework/types";
import Button from "./Button";
it("renders anchor button", () => {
const props: PropTypesOf<typeof Button> = {
href: "/moderate",
children: "link",
onClick: noop,
className: "custom",
target: "_blank",
};
const renderer = createRenderer();
renderer.render(<Button {...props} />);
expect(renderer.getRenderOutput()).toMatchSnapshot();
});
it("renders button", () => {
const props: PropTypesOf<typeof Button> = {
children: "link",
onClick: noop,
className: "custom",
blankAdornment: true,
};
const renderer = createRenderer();
renderer.render(<Button {...props} />);
expect(renderer.getRenderOutput()).toMatchSnapshot();
});
@@ -0,0 +1,56 @@
import cn from "classnames";
import React, { StatelessComponent } from "react";
import { Omit } from "talk-framework/types";
import { withStyles } from "talk-ui/hocs";
import BaseButton, { BaseButtonProps } from "../BaseButton";
import Icon from "../Icon";
import styles from "./Button.css";
interface Props extends Omit<BaseButtonProps, "ref"> {
children: React.ReactNode;
href?: string;
className?: string;
onClick?: React.EventHandler<React.MouseEvent>;
classes: typeof styles;
/**
* blankAdornment if true will leave some blank space after the text, so
* that it looks nice, if mixed with other buttons which have an external link
* icon as adornment.
*/
blankAdornment?: boolean;
}
const Button: StatelessComponent<Props> = ({
blankAdornment,
className,
href,
onClick,
children,
classes,
...rest
}) => {
return (
<BaseButton
classes={{
root: cn(classes.root, className, {
[classes.blankAdornment]: blankAdornment,
}),
mouseHover: classes.mouseHover,
}}
href={href}
onClick={onClick}
anchor={Boolean(href)}
{...rest}
>
{children}
{rest.target === "_blank" && (
<Icon className={classes.icon}>open_in_new</Icon>
)}
</BaseButton>
);
};
export default withStyles(styles)(Button);
@@ -0,0 +1,6 @@
.root {
border: 0;
border-bottom: 1px solid var(--palette-divider);
padding-top: 1px;
margin: calc(0.5 * var(--spacing-unit)) 0;
}
@@ -0,0 +1,10 @@
import React from "react";
import { createRenderer } from "react-test-renderer/shallow";
import Divider from "./Divider";
it("renders correctly", () => {
const renderer = createRenderer();
renderer.render(<Divider />);
expect(renderer.getRenderOutput()).toMatchSnapshot();
});
@@ -0,0 +1,19 @@
import cn from "classnames";
import React, { StatelessComponent } from "react";
import { withStyles } from "talk-ui/hocs";
import styles from "./Divider.css";
interface Props {
className?: string;
classes: typeof styles;
}
const Divider: StatelessComponent<Props> = ({
className,
classes,
...rest
}) => <hr {...rest} className={cn(classes.root, className)} />;
export default withStyles(styles)(Divider);
@@ -0,0 +1,3 @@
.root {
padding: calc(0.5 * var(--spacing-unit)) 0;
}
@@ -0,0 +1,58 @@
---
name: Dropdown
menu: UI Kit
---
import { Playground, PropsTable } from "docz";
import {
Dropdown,
Divider as DropdownDivider,
Button as DropdownButton,
} from "./";
import { Button, ButtonIcon } from "../Button";
import ClickOutside from "../ClickOutside";
import Popover from "../Popover";
# Dropdown
## Basic usage
<Playground>
<Popover
id="dropdownMenu"
description="Dropdown Menu"
placement="bottom-start"
body={({ toggleVisibility }) => (
<ClickOutside onClickOutside={toggleVisibility}>
<Dropdown>
<DropdownButton blankAdornment>Shortcuts</DropdownButton>
<DropdownButton
href="https://github.com/coralproject/talk/issues/new"
target="_blank"
>
Report Bug
</DropdownButton>
<DropdownDivider />
<DropdownButton blankAdornment>Sign Out</DropdownButton>
</Dropdown>
</ClickOutside>
)}
>
{({ toggleVisibility, ref, visible }) => (
<Button
onClick={toggleVisibility}
ref={ref}
variant="filled"
color="primary"
>
<span>Click me!</span>
{
<ButtonIcon size="md">
{visible ? "arrow_drop_up" : "arrow_drop_down"}
</ButtonIcon>
}
</Button>
)}
</Popover>
</Playground>
@@ -0,0 +1,30 @@
import React from "react";
import { createRenderer } from "react-test-renderer/shallow";
import { PropTypesOf } from "talk-framework/types";
import Button from "./Button";
import Divider from "./Divider";
import Dropdown from "./Dropdown";
it("renders correctly", () => {
const props: PropTypesOf<typeof Dropdown> = {
children: (
<>
<Button blankAdornment>Shortcuts</Button>
<Button
href="https://github.com/coralproject/talk/issues/new"
target="_blank"
>
Report Bug
</Button>
<Divider />
<Button blankAdornment>Sign Out</Button>
</>
),
className: "custom",
};
const renderer = createRenderer();
renderer.render(<Dropdown {...props} />);
expect(renderer.getRenderOutput()).toMatchSnapshot();
});
@@ -0,0 +1,26 @@
import React, { StatelessComponent } from "react";
import { withStyles } from "talk-ui/hocs";
import styles from "./Dropdown.css";
interface Props {
children?: React.ReactNode;
className?: string;
classes: typeof styles;
}
const Dropdown: StatelessComponent<Props> = ({
className,
children,
classes,
...rest
}) => {
return (
<div className={classes.root} {...rest}>
{children}
</div>
);
};
export default withStyles(styles)(Dropdown);
@@ -0,0 +1,38 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`renders anchor button 1`] = `
<Button
className="custom"
classes={
Object {
"blankAdornment": "Button-blankAdornment",
"icon": "Button-icon",
"mouseHover": "Button-mouseHover",
"root": "Button-root",
}
}
href="/moderate"
onClick={[Function]}
target="_blank"
>
link
</Button>
`;
exports[`renders button 1`] = `
<Button
blankAdornment={true}
className="custom"
classes={
Object {
"blankAdornment": "Button-blankAdornment",
"icon": "Button-icon",
"mouseHover": "Button-mouseHover",
"root": "Button-root",
}
}
onClick={[Function]}
>
link
</Button>
`;
@@ -0,0 +1,11 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`renders correctly 1`] = `
<Divider
classes={
Object {
"root": "Divider-root",
}
}
/>
`;
@@ -0,0 +1,32 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`renders correctly 1`] = `
<Dropdown
className="custom"
classes={
Object {
"root": "Dropdown-root",
}
}
>
<React.Fragment>
<withPropsOnChange(Button)
blankAdornment={true}
>
Shortcuts
</withPropsOnChange(Button)>
<withPropsOnChange(Button)
href="https://github.com/coralproject/talk/issues/new"
target="_blank"
>
Report Bug
</withPropsOnChange(Button)>
<withPropsOnChange(Divider) />
<withPropsOnChange(Button)
blankAdornment={true}
>
Sign Out
</withPropsOnChange(Button)>
</React.Fragment>
</Dropdown>
`;
@@ -0,0 +1,3 @@
export { default as Dropdown } from "./Dropdown";
export { default as Button } from "./Button";
export { default as Divider } from "./Divider";
@@ -1,5 +1,5 @@
---
name: AriaInfo
name: FieldSet
menu: UI Kit
---
@@ -3,24 +3,28 @@ name: Popover
menu: UI Kit
---
import { Playground } from 'docz'
import Popover from './Popover'
import Button from '../Button'
import Flex from '../Flex'
import Typography from '../Typography'
import ButtonIcon from '../Button/ButtonIcon'
import { Playground } from "docz";
import Popover from "./Popover";
import Button from "../Button";
import Flex from "../Flex";
import Typography from "../Typography";
import ButtonIcon from "../Button/ButtonIcon";
# Popover
`Popover` renders a popover dialog attached to another `Element`.
## Basic usage
<Playground>
<Popover
body={<Typography>This is the body</Typography>}
>
{({ toggleVisibility, forwardRef }) => (
<Button onClick={toggleVisibility} forwardRef={forwardRef} variant="filled" color="primary">
<Popover body={<Typography>This is the body</Typography>}>
{({ toggleVisibility, ref }) => (
<Button
onClick={toggleVisibility}
ref={ref}
variant="filled"
color="primary"
>
Click me!
</Button>
)}
@@ -28,6 +32,7 @@ import ButtonIcon from '../Button/ButtonIcon'
</Playground>
#### Example with `placement=top`
<Playground>
<Popover
placement="top"
@@ -40,8 +45,13 @@ import ButtonIcon from '../Button/ButtonIcon'
</Flex>
)}
>
{({ toggleVisibility, forwardRef }) => (
<Button onClick={toggleVisibility} forwardRef={forwardRef} variant="filled" color="primary">
{({ toggleVisibility, ref }) => (
<Button
onClick={toggleVisibility}
ref={ref}
variant="filled"
color="primary"
>
Click me!
</Button>
)}
@@ -23,14 +23,16 @@ type Placement =
| "left-start";
interface BodyRenderProps {
toggleVisibility: () => void;
/** toggles visibility, if event is provided, the event will stop propagating. */
toggleVisibility: (event?: React.SyntheticEvent | Event) => void;
visible: boolean;
scheduleUpdate: () => void;
}
interface ChildrenRenderProps {
toggleVisibility: () => void;
forwardRef?: RefHandler;
/** toggles visibility, if event is provided, the event will stop propagating. */
toggleVisibility: (event?: React.SyntheticEvent | Event) => void;
ref?: RefHandler;
visible: boolean;
}
@@ -56,7 +58,10 @@ class Popover extends React.Component<PopoverProps> {
visible: false,
};
public toggleVisibility = () => {
public toggleVisibility = (event?: React.SyntheticEvent | Event) => {
if (event && event.stopPropagation) {
event.stopPropagation();
}
this.setState((state: State) => ({
visible: !state.visible,
}));
@@ -109,7 +114,7 @@ class Popover extends React.Component<PopoverProps> {
<Reference>
{props =>
children({
forwardRef: props.ref,
ref: props.ref,
toggleVisibility: this.toggleVisibility,
visible: this.state.visible,
})
+5
View File
@@ -48,3 +48,8 @@ export { Marker, Count as MarkerCount } from "./Marker";
export { default as Card } from "./Card";
export { default as PasswordField } from "./PasswordField";
export { TileSelector, Option as TileOption } from "./TileSelector";
export {
Dropdown,
Divider as DropdownDivider,
Button as DropdownButton,
} from "./Dropdown";
@@ -21,11 +21,16 @@
background: transparent;
font-size: inherit;
-webkit-tap-highlight-color: rgba(0, 0, 0, 0) !important;
-webkit-appearance: none;
&:disabled {
cursor: default;
}
&:visited {
color: inherit;
}
&::-moz-focus-inner {
border: 0;
}
+9 -1
View File
@@ -9,7 +9,13 @@ navigation-moderate = Moderate
navigation-community = Community
navigation-stories = Stories
navigation-configure = Configure
navigation-signOutButton = Sign Out
## User Menu
userMenu-signOut = Sign Out
userMenu-viewLatestVersion = View Latest Release
userMenu-reportBug = Report a Bug or Give Feedback
userMenu-popover =
.description = A dialog of the user menu with related links and actions
## Restricted
restricted-currentlySignedInTo = Currently signed in to
@@ -265,6 +271,8 @@ configure-advanced-permittedDomains-explanation =
yourdomain.com, etc.
## Decision History
decisionHistory-popover =
.description = A dialog showing the decision history
decisionHistory-youWillSeeAList =
You will see a list of your post moderation actions here.
decisionHistory-showMoreButton =
+4
View File
@@ -28,6 +28,8 @@ comments-postCommentForm-submit = Submit
comments-stream-loadMore = Load More
comments-replyList-showAll = Show All
comments-permalinkPopover =
.description = A dialog showing a permalink to the comment
comments-permalinkButton-share = Share
comments-permalinkView-viewFullDiscussion = View Full Discussion
comments-permalinkView-commentNotFound = Comment not found
@@ -104,6 +106,8 @@ profile-profileQuery-storyNotFound = Story not found
profile-commentHistory-loadMore = Load More
## Report Comment Popover
comments-reportPopover =
.description = A dialog for reporting comments
comments-reportPopover-reportThisComment = Report This Comment
comments-reportPopover-whyAreYouReporting = Why are you reporting this comment?