Merge branch 'next' of github.com:coralproject/talk into ui-components

This commit is contained in:
Belen Curcio
2018-08-08 08:31:08 -03:00
102 changed files with 2617 additions and 588 deletions
+493 -190
View File
File diff suppressed because it is too large Load Diff
+5 -1
View File
@@ -103,6 +103,7 @@
"@types/passport-oauth2": "^1.4.5",
"@types/passport-strategy": "^0.2.33",
"@types/query-string": "^6.1.0",
"@types/react-copy-to-clipboard": "^4.2.5",
"@types/react-dom": "^16.0.6",
"@types/react-relay": "github:coralproject/patched#types/react-relay",
"@types/react-responsive": "^3.0.1",
@@ -168,13 +169,16 @@
"query-string": "^6.1.0",
"raw-loader": "^0.5.1",
"react": "^16.4.0",
"react-copy-to-clipboard": "^5.0.1",
"react-dev-utils": "6.0.0-next.3e165448",
"react-dom": "^16.4.0",
"react-final-form": "^3.6.4",
"react-popper": "^1.0.0-beta.6",
"react-relay": "github:coralproject/patched#react-relay",
"react-responsive": "^4.1.0",
"react-test-renderer": "^16.4.1",
"react-timeago": "^4.1.9",
"react-with-state-props": "^2.0.4",
"recompose": "^0.27.1",
"relay-compiler": "github:coralproject/patched#relay-compiler",
"relay-compiler-language-typescript": "github:coralproject/patched#relay-compiler-language-typescript",
@@ -198,7 +202,7 @@
"typed-css-modules": "^0.3.4",
"typeface-manuale": "0.0.54",
"typeface-source-sans-pro": "0.0.54",
"typescript": "^2.9.2",
"typescript": "^3.0.0",
"uglifyjs-webpack-plugin": "^1.2.5",
"webpack": "4.12.0",
"webpack-cli": "^3.0.2",
+5 -2
View File
@@ -5,9 +5,9 @@ import {
Decorator,
withAutoHeight,
withClickEvent,
withCommentID,
withEventEmitter,
withIOSSafariWidthWorkaround,
withSetCommentID,
} from "./decorators";
import PymControl from "./PymControl";
import { ensureEndSlash } from "./utils";
@@ -15,6 +15,7 @@ import { ensureEndSlash } from "./utils";
interface CreatePymControlConfig {
assetID?: string;
assetURL?: string;
commentID?: string;
title?: string;
eventEmitter: EventEmitter2;
id: string;
@@ -26,13 +27,14 @@ export function createPymControl(config: CreatePymControlConfig) {
withIOSSafariWidthWorkaround,
withAutoHeight,
withClickEvent,
withCommentID,
withSetCommentID,
withEventEmitter(config.eventEmitter),
];
const query = qs.stringify({
assetID: config.assetID,
assetURL: config.assetURL,
commentID: config.commentID,
});
const url = `${ensureEndSlash(config.rootURL)}stream.html?${query}`;
return new PymControl({
@@ -73,6 +75,7 @@ export type StreamInterface = ReturnType<typeof createStreamInterface>;
export interface CreateConfig {
assetID?: string;
assetURL?: string;
commentID?: string;
title?: string;
eventEmitter: EventEmitter2;
id: string;
+1 -1
View File
@@ -4,7 +4,7 @@ export type CleanupCallback = () => void;
export type Decorator = (pym: pym.Parent) => CleanupCallback | void;
export { default as withAutoHeight } from "./withAutoHeight";
export { default as withClickEvent } from "./withClickEvent";
export { default as withCommentID } from "./withCommentID";
export { default as withSetCommentID } from "./withSetCommentID";
export { default as withEventEmitter } from "./withEventEmitter";
export {
default as withIOSSafariWidthWorkaround,
@@ -1,36 +0,0 @@
import withCommentID from "./withCommentID";
it("should add commentID", () => {
const previousLocation = location.toString();
const previousState = window.history.state;
const fakePym = {
onMessage: (type: string, callback: (id: string) => void) => {
if (type === "view-comment") {
callback("comment-id");
}
},
};
withCommentID(fakePym as any);
expect(location.toString()).toBe("http://localhost/?commentId=comment-id");
window.history.replaceState(previousState, document.title, previousLocation);
});
it("should remove commentID", () => {
const previousLocation = location.toString();
const previousState = window.history.state;
window.history.replaceState(
previousState,
document.title,
"http://localhost/?commentId=comment-id"
);
const fakePym = {
onMessage: (type: string, callback: () => void) => {
if (type === "view-all-comments") {
callback();
}
},
};
withCommentID(fakePym as any);
expect(location.toString()).toBe("http://localhost/");
window.history.replaceState(previousState, document.title, previousLocation);
});
@@ -1,36 +0,0 @@
import qs from "query-string";
import { buildURL } from "../utils";
import { Decorator } from "./";
const withCommentID: Decorator = pym => {
// Remove the comment id from the query.
pym.onMessage("view-all-comments", () => {
const search = qs.stringify({
...qs.parse(location.search),
commentId: undefined,
});
// Remove the commentId url param.
const url = buildURL({ search });
// Change the url.
window.history.replaceState({}, document.title, url);
});
// Add the permalink comment id to the query.
pym.onMessage("view-comment", (id: string) => {
const search = qs.stringify({
...qs.parse(location.search),
commentId: id,
});
// Remove the commentId url param.
const url = buildURL({ search });
// Change the url.
window.history.replaceState({}, document.title, url);
});
};
export default withCommentID;
@@ -0,0 +1,65 @@
import simulant from "simulant";
import sinon from "sinon";
import { CleanupCallback } from ".";
import withSetCommentID from "./withSetCommentID";
it("should add commentID", () => {
const previousLocation = location.toString();
const previousState = window.history.state;
const fakePym = {
onMessage: (type: string, callback: (id: string) => void) => {
if (type === "setCommentID") {
callback("comment-id");
}
},
};
const cleanup = withSetCommentID(fakePym as any) as CleanupCallback;
expect(location.toString()).toBe("http://localhost/?commentID=comment-id");
cleanup();
window.history.replaceState(previousState, document.title, previousLocation);
});
it("should remove commentID", () => {
const previousLocation = location.toString();
const previousState = window.history.state;
window.history.replaceState(
previousState,
document.title,
"http://localhost/?commentID=comment-id"
);
const fakePym = {
onMessage: (type: string, callback: () => void) => {
if (type === "setCommentID") {
callback();
}
},
};
const cleanup = withSetCommentID(fakePym as any) as CleanupCallback;
expect(location.toString()).toBe("http://localhost/");
cleanup();
window.history.replaceState(previousState, document.title, previousLocation);
});
it("should send commentID over pym when history changes", () => {
const previousLocation = location.toString();
const previousState = window.history.state;
window.history.replaceState(
previousState,
document.title,
"http://localhost/?commentID=comment-id"
);
const fakePym = {
onMessage: sinon.stub(),
sendMessage: sinon
.mock()
.once()
.withArgs("setCommentID", "comment-id"),
};
const cleanup = withSetCommentID(fakePym as any) as CleanupCallback;
simulant.fire(window as any, "popstate");
cleanup();
simulant.fire(window as any, "popstate");
fakePym.sendMessage.verify();
window.history.replaceState(previousState, document.title, previousLocation);
});
@@ -0,0 +1,38 @@
import qs from "query-string";
import { buildURL } from "../utils";
import { Decorator } from "./";
function getCurrentCommentID() {
return qs.parse(location.search).commentID;
}
const withSetCommentID: Decorator = pym => {
// Add the permalink comment id to the query.
pym.onMessage("setCommentID", (id: string) => {
const search = qs.stringify({
...qs.parse(location.search),
commentID: id || undefined,
});
// Remove the commentId url param.
const url = buildURL({ search });
// Change the url.
window.history.pushState({}, document.title, url);
});
// Send new commentID when history state changes.
const sendSetCommentID = (e: Event) => {
const commentID = getCurrentCommentID();
pym.sendMessage("setCommentID", commentID);
};
window.addEventListener("popstate", sendSetCommentID);
// Cleanup.
return () => {
window.removeEventListener("popstate", sendSetCommentID);
};
};
export default withSetCommentID;
+1 -1
View File
@@ -10,7 +10,7 @@
<body>
<h1 style="text-align: center" }>Talk 5.0 Embed Stream</h1>
<div id="coralStreamEmbed"></div>
<div id="coralStreamEmbed" style="max-width: 600px; margin: 0 auto"></div>
<script>
window.TalkEmbed = Talk.render(document.getElementById('coralStreamEmbed'));
</script>
+2
View File
@@ -6,6 +6,7 @@ import createStreamInterface from "./Stream";
export interface Config {
assetID?: string;
assetURL?: string;
commentID?: string;
rootURL?: string;
id?: string;
events?: (eventEmitter: EventEmitter2) => void;
@@ -23,6 +24,7 @@ export function render(config: Config = {}) {
return createStreamInterface({
assetID: config.assetID || query.assetID,
assetURL: config.assetURL || query.assetURL,
commentID: config.commentID || query.commentID,
id: config.id || "talk-embed-stream",
rootURL: config.rootURL || location.origin,
eventEmitter,
@@ -7,7 +7,7 @@ import {
} from "recompose";
import { Environment } from "relay-runtime";
import { withContext } from "../bootstrap";
import { TalkContext, withContext } from "../bootstrap";
/**
* createMutationContainer creates a HOC that
@@ -18,10 +18,14 @@ import { withContext } from "../bootstrap";
*/
function createMutationContainer<T extends string, I, R>(
propName: T,
commit: (environment: Environment, input: I) => Promise<R>
commit: (
environment: Environment,
input: I,
context: TalkContext
) => Promise<R>
): InferableComponentEnhancer<{ [P in T]: (input: I) => Promise<R> }> {
return compose(
withContext(({ relayEnvironment }) => ({ relayEnvironment })),
withContext(context => ({ context })),
hoistStatics((BaseComponent: React.ComponentType<any>) => {
class CreateMutationContainer extends React.Component<any> {
public static displayName = wrapDisplayName(
@@ -30,7 +34,11 @@ function createMutationContainer<T extends string, I, R>(
);
private commit = (input: I) => {
return commit(this.props.relayEnvironment, input);
return commit(
this.props.context.relayEnvironment,
input,
this.props.context
);
};
public render() {
@@ -1,8 +1,14 @@
import * as React from "react";
import { compose, hoistStatics, InferableComponentEnhancer } from "recompose";
import {
compose,
hoistStatics,
InferableComponentEnhancer,
wrapDisplayName,
} from "recompose";
import {
CSelector,
CSnapshot,
Disposable,
Environment,
GraphQLTaggedNode,
} from "relay-runtime";
@@ -36,6 +42,12 @@ function withLocalStateContainer<T>(
withContext(({ relayEnvironment }) => ({ relayEnvironment })),
hoistStatics((BaseComponent: React.ComponentType<any>) => {
class LocalStateContainer extends React.Component<Props, any> {
public static displayName = wrapDisplayName(
BaseComponent,
"withLocalStateContainer"
);
private subscription: Disposable;
constructor(props: Props) {
super(props);
const fragment = (fragmentSpec as any).data().default;
@@ -53,7 +65,10 @@ function withLocalStateContainer<T>(
variables: {},
};
const snapshot = props.relayEnvironment.lookup(selector);
props.relayEnvironment.subscribe(snapshot, this.updateSnapshot);
this.subscription = props.relayEnvironment.subscribe(
snapshot,
this.updateSnapshot
);
this.state = {
data: snapshot.data,
};
@@ -63,6 +78,10 @@ function withLocalStateContainer<T>(
this.setState({ data: snapshot.data });
};
public componentWillUnmount() {
this.subscription.dispose();
}
public render() {
const { relayEnvironment: _, ...rest } = this.props;
return <BaseComponent {...rest} local={this.state.data} />;
@@ -0,0 +1,78 @@
import { IResolvers } from "graphql-tools";
import { createFetch } from "relay-local-schema";
import {
commitLocalUpdate,
Environment,
Network,
RecordProxy,
RecordSource,
RecordSourceProxy,
Store,
} from "relay-runtime";
import {
createAndRetain,
LOCAL_ID,
LOCAL_TYPE,
wrapFetchWithLogger,
} from "talk-framework/lib/relay";
import { loadSchema } from "talk-common/graphql";
export interface CreateRelayEnvironmentNetworkParams {
/** project name of graphql-config */
projectName: string;
/** graphql resolvers */
resolvers: IResolvers<any, any>;
/** If enabled, graphql responses will be logged to the console */
logNetwork?: boolean;
}
export interface CreateRelayEnvironmentParams {
/** If set, creates a network to a local graphql server with a local schema */
network?: CreateRelayEnvironmentNetworkParams;
/** Allows to set initial state for Local state */
initLocalState?: (
local: RecordProxy,
source: RecordSourceProxy,
environment: Environment
) => void;
/** Use this source for creating the environment */
source?: RecordSource;
}
/**
* create Relay environment for tests environments.
*/
export default function createRelayEnvironment(
params: CreateRelayEnvironmentParams = {}
) {
let network: Network = null as any;
if (params.network) {
const schema = loadSchema(
params.network.projectName,
params.network.resolvers
);
network = Network.create(
wrapFetchWithLogger(createFetch({ schema }), params.network.logNetwork)
);
}
const environment = new Environment({
network,
store: new Store(params.source || new RecordSource()),
});
commitLocalUpdate(environment, sourceProxy => {
const root = sourceProxy.getRoot();
const localRecord = createAndRetain(
environment,
sourceProxy,
LOCAL_ID,
LOCAL_TYPE
);
root.setLinkedRecord(localRecord, "local");
if (params.initLocalState) {
params.initLocalState!(localRecord, sourceProxy, environment);
}
});
return environment;
}
@@ -0,0 +1,4 @@
export {
default as createRelayEnvironment,
CreateRelayEnvironmentParams,
} from "./createRelayEnvironment";
@@ -0,0 +1,18 @@
import buildURL from "./buildURL";
it("should default to window.location", () => {
const url = buildURL();
expect(url).toBe("http://localhost/");
});
it("should build from parameters", () => {
const url = buildURL({
protocol: "https",
hostname: "hostname",
port: "8080",
pathname: "/pathname",
search: "search",
hash: "#hash",
});
expect(url).toBe("https//hostname:8080/pathname?search#hash");
});
@@ -0,0 +1,17 @@
export default function buildURL({
protocol = window.location.protocol,
hostname = window.location.hostname,
port = window.location.port,
pathname = window.location.pathname,
search = window.location.search,
hash = window.location.hash,
} = {}) {
if (search && search[0] !== "?") {
search = `?${search}`;
} else if (search === "?") {
search = "";
}
return `${protocol}//${hostname}${
port ? `:${port}` : ""
}${pathname}${search}${hash}`;
}
+2
View File
@@ -0,0 +1,2 @@
export { default as buildURL } from "./buildURL";
export { default as parseURL } from "./parseURL";
@@ -0,0 +1,20 @@
import parseURL from "./parseURL";
it("should parse url", () => {
const testCases: [[string, ReturnType<typeof parseURL>]] = [
[
"https://coralproject.net",
{
protocol: "https:",
hostname: "coralproject.net",
port: "",
pathname: "/",
search: "",
hash: "",
},
],
];
testCases.forEach(([url, expected]) => {
expect(parseURL(url)).toEqual(expected);
});
});
@@ -0,0 +1,14 @@
import { pick } from "lodash";
export default function parseURL(url: string) {
const parser = document.createElement("a");
parser.href = url;
return pick(parser, [
"protocol",
"hostname",
"port",
"pathname",
"search",
"hash",
]);
}
@@ -11,4 +11,5 @@
}
.root {
width: 100%;
}
@@ -5,17 +5,17 @@ import { PropTypesOf } from "talk-framework/types";
import App from "./App";
it("renders correctly", () => {
it("renders stream", () => {
const props: PropTypesOf<typeof App> = {
asset: {},
showPermalinkView: false,
};
const wrapper = shallow(<App {...props} />);
expect(wrapper).toMatchSnapshot();
});
it("renders correctly when asset is null", () => {
it("renders permalink view", () => {
const props: PropTypesOf<typeof App> = {
asset: null,
showPermalinkView: true,
};
const wrapper = shallow(<App {...props} />);
expect(wrapper).toMatchSnapshot();
+13 -10
View File
@@ -3,23 +3,26 @@ import { StatelessComponent } from "react";
import { Flex } from "talk-ui/components";
import StreamContainer from "../containers/StreamContainer";
import PermalinkViewQuery from "../queries/PermalinkViewQuery";
import StreamQuery from "../queries/StreamQuery";
import * as styles from "./App.css";
export interface AppProps {
asset: {} | null;
showPermalinkView: boolean;
}
const App: StatelessComponent<AppProps> = props => {
if (props.asset) {
return (
<Flex justifyContent="center" className={styles.root}>
<StreamContainer asset={props.asset} />
</Flex>
);
}
return <div>Asset not found </div>;
const view = props.showPermalinkView ? (
<PermalinkViewQuery />
) : (
<StreamQuery />
);
return (
<Flex justifyContent="center" className={styles.root}>
{view}
</Flex>
);
};
export default App;
@@ -0,0 +1,3 @@
.footer {
margin-top: var(--spacing-unit);
}
@@ -7,6 +7,7 @@ import Comment from "./Comment";
it("renders username and body", () => {
const props: PropTypesOf<typeof Comment> = {
id: "comment-id",
author: {
username: "Marvin",
},
@@ -1,13 +1,16 @@
import React from "react";
import { StatelessComponent } from "react";
import { Typography } from "talk-ui/components";
import * as styles from "./Comment.css";
import PermalinkButtonContainer from "../../containers/PermalinkButtonContainer";
import Timestamp from "./Timestamp";
import TopBar from "./TopBar";
import Username from "./Username";
export interface CommentProps {
id: string;
className?: string;
author: {
username: string | null;
} | null;
@@ -24,6 +27,9 @@ const Comment: StatelessComponent<CommentProps> = props => {
<Timestamp>{props.createdAt}</Timestamp>
</TopBar>
<Typography>{props.body}</Typography>
<div className={styles.footer}>
<PermalinkButtonContainer commentID={props.id} />
</div>
</div>
);
};
@@ -15,5 +15,12 @@ exports[`renders username and body 1`] = `
<withPropsOnChange(Typography)>
Woof
</withPropsOnChange(Typography)>
<div
className="Comment-footer"
>
<withContext(withLocalStateContainer(PermalinkContainer))
commentID="comment-id"
/>
</div>
</div>
`;
@@ -1,22 +0,0 @@
import { Localized } from "fluent-react/compat";
import * as React from "react";
import { StatelessComponent } from "react";
import { Typography } from "talk-ui/components";
export interface LogoProps {
className?: string;
gutterBottom?: boolean;
}
const Logo: StatelessComponent<LogoProps> = props => {
return (
<Localized id="stream-logo">
<Typography variant="heading1" gutterBottom={props.gutterBottom}>
Talk NEO
</Typography>
</Localized>
);
};
export default Logo;
@@ -0,0 +1,4 @@
.popover {
width: 350px;
max-width: 80%;
}
@@ -0,0 +1,73 @@
import { Localized } from "fluent-react/compat";
import React from "react";
import { oncePerFrame } from "talk-common/utils";
import {
Button,
ButtonIcon,
ClickOutside,
MatchMedia,
Popover,
} from "talk-ui/components";
import * as styles from "./PermalinkButton.css";
import PermalinkPopover from "./PermalinkPopover";
interface PermalinkProps {
commentID: string;
assetURL: string | null;
}
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, assetURL } = this.props;
const popoverID = "permalink-popover";
return (
<Popover
id={popoverID}
placement="top-start"
description="A dialog showing a permalink to the comment"
className={styles.popover}
body={({ toggleVisibility }) => (
<ClickOutside
onClickOutside={() =>
this.toggleVisibilityOncePerFrame(toggleVisibility)
}
>
<PermalinkPopover
permalinkURL={`${assetURL}&commentID=${commentID}`}
toggleVisibility={toggleVisibility}
/>
</ClickOutside>
)}
>
{({ toggleVisibility, forwardRef, visible }) => (
<Button
onClick={() => this.toggleVisibilityOncePerFrame(toggleVisibility)}
aria-controls={popoverID}
forwardRef={forwardRef}
variant="ghost"
active={visible}
size="small"
>
<MatchMedia minWidth="xs">
<ButtonIcon>share</ButtonIcon>
</MatchMedia>
<Localized id="comments-permalink-share">
<span>Share</span>
</Localized>
</Button>
)}
</Popover>
);
}
}
export default Permalink;
@@ -0,0 +1,7 @@
.root {
width: 100%;
}
.textField {
flex-grow: 1;
}
@@ -0,0 +1,60 @@
import { Localized } from "fluent-react/compat";
import React from "react";
import CopyToClipboard from "react-copy-to-clipboard";
import { Button, Flex, TextField } from "talk-ui/components";
import * as styles from "./PermalinkPopover.css";
interface InnerProps {
permalinkURL: string;
toggleVisibility: () => void;
}
interface State {
copied: boolean;
}
class PermalinkPopover extends React.Component<InnerProps> {
public state: State = {
copied: false,
};
private onCopy = async () => {
await this.toggleCopied();
setTimeout(() => {
this.toggleCopied();
}, 800);
};
private toggleCopied = () => {
this.setState((state: State) => ({
copied: !state.copied,
}));
};
public render() {
const { permalinkURL } = this.props;
const { copied } = this.state;
return (
<Flex itemGutter="half" className={styles.root}>
<TextField defaultValue={permalinkURL} className={styles.textField} />
<CopyToClipboard text={permalinkURL} onCopy={this.onCopy}>
<Button color="primary" variant="filled" size="small">
{copied ? (
<Localized id="comments-permalink-copied">
<span>Copied!</span>
</Localized>
) : (
<Localized id="comments-permalink-copy">
<span>Copy</span>
</Localized>
)}
</Button>
</CopyToClipboard>
</Flex>
);
}
}
export default PermalinkPopover;
@@ -0,0 +1 @@
export { default } from "./PermalinkButton";
@@ -0,0 +1,7 @@
.root {
width: 100%;
}
.button {
margin-bottom: calc(2 * var(--spacing-unit));
}
@@ -0,0 +1,46 @@
import React, { MouseEvent, StatelessComponent } from "react";
import { Button, Flex, Typography } from "talk-ui/components";
import CommentContainer from "../containers/CommentContainer";
import * as styles from "./PermalinkView.css";
export interface PermalinkViewProps {
comment: {} | null;
showAllCommentsHref: string | null;
onShowAllComments: (e: MouseEvent<any>) => void;
}
const PermalinkView: StatelessComponent<PermalinkViewProps> = ({
showAllCommentsHref,
comment,
onShowAllComments,
}) => {
return (
<div className={styles.root}>
{showAllCommentsHref && (
<Button
id="talk-comments-permalinkView-showAllComments"
variant="outlined"
color="primary"
onClick={onShowAllComments}
className={styles.button}
href={showAllCommentsHref}
target="_parent"
fullWidth
anchor
>
Show all Comments
</Button>
)}
{!comment && <Typography>Comment not found</Typography>}
{comment && (
<Flex direction="column">
<CommentContainer data={comment} />
</Flex>
)}
</div>
);
};
export default PermalinkView;
@@ -1,4 +1,3 @@
.root {
width: 100%;
max-width: 400px;
}
+4 -6
View File
@@ -7,22 +7,20 @@ import { Button, Flex } from "talk-ui/components";
import CommentContainer from "../containers/CommentContainer";
import PostCommentFormContainer from "../containers/PostCommentFormContainer";
import ReplyListContainer from "../containers/ReplyListContainer";
import Logo from "./Logo";
import * as styles from "./Stream.css";
export interface StreamProps {
assetID: string;
isClosed: boolean;
isClosed?: boolean;
comments: ReadonlyArray<{ id: string }>;
onLoadMore: () => void;
hasMore: boolean;
disableLoadMore: boolean;
onLoadMore?: () => void;
hasMore?: boolean;
disableLoadMore?: boolean;
}
const Stream: StatelessComponent<StreamProps> = props => {
return (
<div className={styles.root}>
<Logo gutterBottom />
<PostCommentFormContainer assetID={props.assetID} />
<Flex
direction="column"
@@ -1,18 +1,19 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`renders correctly 1`] = `
exports[`renders permalink view 1`] = `
<withPropsOnChange(Flex)
className="App-root"
justifyContent="center"
>
<Relay(StreamContainer)
asset={Object {}}
/>
<withContext(withLocalStateContainer(PermalinkViewQuery)) />
</withPropsOnChange(Flex)>
`;
exports[`renders correctly when asset is null 1`] = `
<div>
Asset not found
</div>
exports[`renders stream 1`] = `
<withPropsOnChange(Flex)
className="App-root"
justifyContent="center"
>
<withContext(withLocalStateContainer(StreamQuery)) />
</withPropsOnChange(Flex)>
`;
@@ -4,9 +4,6 @@ exports[`renders correctly 1`] = `
<div
className="Stream-root"
>
<Logo
gutterBottom={true}
/>
<withContext(createMutationContainer(PostCommentFormContainer))
assetID="asset-id"
/>
@@ -65,9 +62,6 @@ exports[`when there is more disables load more button 1`] = `
<div
className="Stream-root"
>
<Logo
gutterBottom={true}
/>
<withContext(createMutationContainer(PostCommentFormContainer))
assetID="asset-id"
/>
@@ -140,9 +134,6 @@ exports[`when there is more renders a load more button 1`] = `
<div
className="Stream-root"
>
<Logo
gutterBottom={true}
/>
<withContext(createMutationContainer(PostCommentFormContainer))
assetID="asset-id"
/>
@@ -1,16 +0,0 @@
import { shallow } from "enzyme";
import React from "react";
import { PropTypesOf } from "talk-framework/types";
import { AppContainer } from "./AppContainer";
it("renders correctly", () => {
const props: PropTypesOf<typeof AppContainer> = {
data: {
asset: {},
},
};
const wrapper = shallow(<AppContainer {...props} />);
expect(wrapper).toMatchSnapshot();
});
@@ -1,30 +1,27 @@
import React, { StatelessComponent } from "react";
import { graphql } from "react-relay";
import * as React from "react";
import { StatelessComponent } from "react";
import { withFragmentContainer } from "talk-framework/lib/relay";
import { PropTypesOf } from "talk-framework/types";
import { AppContainer as Data } from "talk-stream/__generated__/AppContainer.graphql";
import { graphql, withLocalStateContainer } from "talk-framework/lib/relay";
import { AppContainerLocal as Local } from "talk-stream/__generated__/AppContainerLocal.graphql";
import App from "../components/App";
interface InnerProps {
data: Data;
local: Local;
}
export const AppContainer: StatelessComponent<InnerProps> = props => {
return <App {...props.data} />;
const AppContainer: StatelessComponent<InnerProps> = ({
local: { commentID },
}) => {
return <App showPermalinkView={!!commentID} />;
};
const enhanced = withFragmentContainer<{ data: Data }>({
data: graphql`
fragment AppContainer on Query
@argumentDefinitions(assetID: { type: "ID!" }) {
asset(id: $assetID) {
...StreamContainer_asset
}
const enhanced = withLocalStateContainer<Local>(
graphql`
fragment AppContainerLocal on Local {
commentID
}
`,
})(AppContainer);
`
)(AppContainer);
export type AppContainerProps = PropTypesOf<typeof enhanced>;
export default enhanced;
@@ -8,6 +8,7 @@ import { CommentContainer } from "./CommentContainer";
it("renders username and body", () => {
const props: PropTypesOf<typeof CommentContainer> = {
data: {
id: "comment-id",
author: {
username: "Marvin",
},
@@ -23,6 +24,7 @@ it("renders username and body", () => {
it("renders body only", () => {
const props: PropTypesOf<typeof CommentContainer> = {
data: {
id: "comment-id",
author: {
username: null,
},
@@ -2,16 +2,19 @@ import React, { StatelessComponent } from "react";
import { graphql } from "react-relay";
import withFragmentContainer from "talk-framework/lib/relay/withFragmentContainer";
import { Omit, PropTypesOf } from "talk-framework/types";
import { PropTypesOf } from "talk-framework/types";
import { CommentContainer as Data } from "talk-stream/__generated__/CommentContainer.graphql";
import Comment, { CommentProps } from "../components/Comment";
import Comment from "../components/Comment";
type InnerProps = { data: Data } & Omit<CommentProps, keyof Data>;
interface InnerProps {
data: Data;
}
// tslint:disable-next-line:no-unused-expression
graphql`
fragment CommentContainer_comment on Comment {
id
author {
username
}
@@ -0,0 +1,30 @@
import React, { StatelessComponent } from "react";
import { graphql } from "react-relay";
import { withLocalStateContainer } from "talk-framework/lib/relay";
import { PermalinkButtonContainerLocal as Local } from "talk-stream/__generated__/PermalinkButtonContainerLocal.graphql";
import PermalinkButton from "../components/PermalinkButton";
interface InnerProps {
local: Local;
commentID: string;
}
export const PermalinkContainer: StatelessComponent<InnerProps> = ({
local,
commentID,
}) => {
return local.assetURL ? (
<PermalinkButton assetURL={local.assetURL} commentID={commentID} />
) : null;
};
const enhanced = withLocalStateContainer<Local>(
graphql`
fragment PermalinkButtonContainerLocal on Local {
assetURL
}
`
)(PermalinkContainer);
export default enhanced;
@@ -0,0 +1,67 @@
import { Child as PymChild } from "pym.js";
import qs from "query-string";
import React, { MouseEvent } from "react";
import { graphql } from "react-relay";
import { withContext } from "talk-framework/lib/bootstrap";
import { withFragmentContainer } from "talk-framework/lib/relay";
import { buildURL, parseURL } from "talk-framework/utils";
import { PermalinkViewContainer_comment as CommentData } from "talk-stream/__generated__/PermalinkViewContainer_comment.graphql";
import {
SetCommentIDMutation,
withSetCommentIDMutation,
} from "talk-stream/mutations";
import PermalinkView from "../components/PermalinkView";
interface PermalinkViewContainerProps {
comment: CommentData | null;
setCommentID: SetCommentIDMutation;
pym: PymChild | undefined;
}
class PermalinkViewContainer extends React.Component<
PermalinkViewContainerProps
> {
private showAllComments = (e: MouseEvent<any>) => {
this.props.setCommentID({ id: null });
e.preventDefault();
};
private getShowAllCommentsHref() {
const { pym } = this.props;
const urlParts = parseURL((pym && pym.parentUrl) || window.location.href);
const search = qs.stringify({
...qs.parse(urlParts.search),
commentID: undefined,
});
// Remove the commentId url param.
return buildURL({ ...urlParts, search });
}
public render() {
const { comment } = this.props;
return (
<PermalinkView
comment={comment}
showAllCommentsHref={this.getShowAllCommentsHref()}
onShowAllComments={this.showAllComments}
/>
);
}
}
const enhanced = withContext(ctx => ({
pym: ctx.pym,
}))(
withSetCommentIDMutation(
withFragmentContainer<{
comment: CommentData | null;
}>({
comment: graphql`
fragment PermalinkViewContainer_comment on Comment {
...CommentContainer
}
`,
})(PermalinkViewContainer)
)
);
export default enhanced;
@@ -1,7 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`renders correctly 1`] = `
<App
asset={Object {}}
/>
`;
@@ -9,6 +9,7 @@ exports[`renders body only 1`] = `
}
body="Woof"
createdAt="1995-12-17T03:24:00.000Z"
id="comment-id"
/>
`;
@@ -21,5 +22,6 @@ exports[`renders username and body 1`] = `
}
body="Woof"
createdAt="1995-12-17T03:24:00.000Z"
id="comment-id"
/>
`;
+10 -6
View File
@@ -1,4 +1,4 @@
import pym from "pym.js";
import { Child as PymChild } from "pym.js";
import React from "react";
import { StatelessComponent } from "react";
import ReactDOM from "react-dom";
@@ -9,13 +9,17 @@ import {
TalkContextProvider,
} from "talk-framework/lib/bootstrap";
import AppContainer from "./containers/AppContainer";
import { initLocalState } from "./local";
import localesData from "./locales";
import AppQuery from "./queries/AppQuery";
import { withSetCommentID } from "./pym";
const pymFeatures = [withSetCommentID];
// This is called when the context is first initialized.
async function init({ relayEnvironment }: TalkContext) {
await initLocalState(relayEnvironment);
async function init(context: TalkContext) {
await initLocalState(context.relayEnvironment);
pymFeatures.forEach(f => f(context));
}
async function main() {
@@ -24,12 +28,12 @@ async function main() {
init,
localesData,
userLocales: navigator.languages,
pym: new pym.Child({ polling: 100 }),
pym: new PymChild({ polling: 100 }),
});
const Index: StatelessComponent = () => (
<TalkContextProvider value={context}>
<AppQuery />
<AppContainer />
</TalkContextProvider>
);
@@ -21,10 +21,23 @@ export default async function initLocalState(environment: Environment) {
// Parse query params
const query = qs.parse(location.search);
if (query.assetID) {
localRecord.setValue(query.assetID, "assetID");
}
// Saving location host for permalink until we get the asset url - the url now points to the tenant
if (location && query.assetID) {
localRecord.setValue(
`${location.origin}/?assetID=${query.assetID}`,
"assetURL"
);
}
if (query.commentID) {
localRecord.setValue(query.commentID, "commentID");
}
// Create network Record
const networkRecord = createAndRetain(
environment,
@@ -6,6 +6,8 @@ type Network {
type Local {
network: Network!
assetID: String
commentID: String
assetURL: String
}
extend type Query {
@@ -0,0 +1,54 @@
import { Environment, RecordSource } from "relay-runtime";
import sinon from "sinon";
import { timeout } from "talk-common/utils";
import { LOCAL_ID } from "talk-framework/lib/relay";
import { createRelayEnvironment } from "talk-framework/testHelpers";
import { commit } from "./SetCommentIDMutation";
let environment: Environment;
const source: RecordSource = new RecordSource();
beforeAll(() => {
environment = createRelayEnvironment({
source,
});
});
it("Sets comment id", () => {
const id = "comment1-id";
commit(environment, { id }, {} as any);
expect(source.get(LOCAL_ID)!.commentID).toEqual(id);
});
it("Should call setCommentID in pym", async () => {
const id = "comment2-id";
const context = {
pym: {
sendMessage: sinon
.mock()
.once()
.withArgs("setCommentID", id),
},
};
commit(environment, { id }, context as any);
await timeout();
expect(source.get(LOCAL_ID)!.commentID).toEqual(id);
context.pym.sendMessage.verify();
});
it("Should call setCommentID in pym with empty id", async () => {
const context = {
pym: {
sendMessage: sinon
.mock()
.once()
.withArgs("setCommentID", ""),
},
};
commit(environment, { id: null }, context as any);
await timeout();
expect(source.get(LOCAL_ID)!.commentID).toEqual(null);
context.pym.sendMessage.verify();
});
@@ -0,0 +1,31 @@
import { commitLocalUpdate, Environment } from "relay-runtime";
import { TalkContext } from "talk-framework/lib/bootstrap";
import { createMutationContainer } from "talk-framework/lib/relay";
import { LOCAL_ID } from "talk-framework/lib/relay/withLocalStateContainer";
export interface SetCommentIDInput {
id: string | null;
}
export type SetCommentIDMutation = (input: SetCommentIDInput) => Promise<void>;
export async function commit(
environment: Environment,
input: SetCommentIDInput,
{ pym }: TalkContext
) {
return commitLocalUpdate(environment, store => {
const record = store.get(LOCAL_ID)!;
record.setValue(input.id, "commentID");
if (pym) {
// This sets the comment id on the parent url.
pym.sendMessage("setCommentID", input.id || "");
}
});
}
export const withSetCommentIDMutation = createMutationContainer(
"setCommentID",
commit
);
@@ -0,0 +1,26 @@
import { Environment, RecordSource } from "relay-runtime";
import { createRelayEnvironment } from "talk-framework/testHelpers";
import { NETWORK_ID, NETWORK_TYPE } from "../local";
import { commit } from "./SetNetworkStatusMutation";
let environment: Environment;
const source: RecordSource = new RecordSource();
beforeAll(() => {
environment = createRelayEnvironment({
source,
initLocalState: (localRecord, sourceProxy) => {
const networkRecord = sourceProxy.create(NETWORK_ID, NETWORK_TYPE);
networkRecord.setValue(false, "isOffline");
localRecord.setLinkedRecord(networkRecord, "network");
},
});
});
it("Sets comment id", () => {
commit(environment, { isOffline: true });
expect(source.get(NETWORK_ID)!.isOffline).toEqual(true);
});
@@ -8,9 +8,14 @@ export interface SetNetworkStatusInput {
isOffline: boolean;
}
export type SetNetworkStatusMutation = (input: SetNetworkStatusInput) => void;
export type SetNetworkStatusMutation = (
input: SetNetworkStatusInput
) => Promise<void>;
async function commit(environment: Environment, input: SetNetworkStatusInput) {
export async function commit(
environment: Environment,
input: SetNetworkStatusInput
) {
return commitLocalUpdate(environment, store => {
const record = store.get(NETWORK_ID)!;
record.setValue(input.isOffline, "isOffline");
+15 -2
View File
@@ -1,2 +1,15 @@
export * from "./CreateCommentMutation";
export * from "./SetNetworkStatusMutation";
export {
withCreateCommentMutation,
CreateCommentMutation,
CreateCommentInput,
} from "./CreateCommentMutation";
export {
withSetNetworkStatusMutation,
SetNetworkStatusMutation,
SetNetworkStatusInput,
} from "./SetNetworkStatusMutation";
export {
withSetCommentIDMutation,
SetCommentIDMutation,
SetCommentIDInput,
} from "./SetCommentIDMutation";
+1
View File
@@ -0,0 +1 @@
export { default as withSetCommentID } from "./withSetCommentID";
@@ -0,0 +1,45 @@
import { Environment, RecordSource } from "relay-runtime";
import { LOCAL_ID } from "talk-framework/lib/relay";
import { createRelayEnvironment } from "talk-framework/testHelpers";
import withSetCommentID from "./withSetCommentID";
let relayEnvironment: Environment;
const source: RecordSource = new RecordSource();
beforeAll(() => {
relayEnvironment = createRelayEnvironment({
source,
});
});
it("Sets comment id", () => {
const id = "comment1-id";
const context = {
pym: {
onMessage: (eventName: string, cb: (id: string) => void) => {
expect(eventName).toBe("setCommentID");
cb(id);
},
},
relayEnvironment,
};
withSetCommentID(context as any);
expect(source.get(LOCAL_ID)!.commentID).toEqual(id);
});
it("Sets comment id to null when empty", () => {
const id = "";
const context = {
pym: {
onMessage: (eventName: string, cb: (data: string) => void) => {
expect(eventName).toBe("setCommentID");
cb(id);
},
},
relayEnvironment,
};
withSetCommentID(context as any);
expect(source.get(LOCAL_ID)!.commentID).toEqual(null);
});
@@ -0,0 +1,19 @@
import { commitLocalUpdate } from "react-relay";
import { TalkContext } from "talk-framework/lib/bootstrap";
import { LOCAL_ID } from "talk-framework/lib/relay";
export default function withSetCommentID({
relayEnvironment,
pym,
}: TalkContext) {
// Sets comment id through pym.
pym!.onMessage("setCommentID", raw => {
commitLocalUpdate(relayEnvironment, s => {
const id = raw || null;
if (s.get(LOCAL_ID)!.getValue("commentID") !== id) {
s.get(LOCAL_ID)!.setValue(id, "commentID");
}
});
});
}
@@ -1,56 +0,0 @@
import * as React from "react";
import { StatelessComponent } from "react";
import { ReadyState } from "react-relay";
import {
graphql,
QueryRenderer,
withLocalStateContainer,
} from "talk-framework/lib/relay";
import {
AppQueryResponse,
AppQueryVariables,
} from "talk-stream/__generated__/AppQuery.graphql";
import { AppQueryLocal as Local } from "talk-stream/__generated__/AppQueryLocal.graphql";
import AppContainer from "../containers/AppContainer";
export const render = ({ error, props }: ReadyState<AppQueryResponse>) => {
if (error) {
return <div>{error.message}</div>;
}
if (props) {
return <AppContainer data={props} />;
}
return <div>Loading</div>;
};
interface InnerProps {
local: Local;
}
const AppQuery: StatelessComponent<InnerProps> = props => {
return (
<QueryRenderer<AppQueryVariables, AppQueryResponse>
query={graphql`
query AppQuery($assetID: ID!) {
...AppContainer @arguments(assetID: $assetID)
}
`}
variables={{
assetID: props.local.assetID,
}}
render={render}
/>
);
};
const enhanced = withLocalStateContainer<Local>(
graphql`
fragment AppQueryLocal on Local {
assetID
}
`
)(AppQuery);
export default enhanced;
@@ -0,0 +1,33 @@
import { shallow } from "enzyme";
import React from "react";
import { render } from "./PermalinkViewQuery";
it("renders permalink view container", () => {
const data = {
props: {
comment: {},
} as any,
error: null,
};
const wrapper = shallow(React.createElement(() => render(data)));
expect(wrapper).toMatchSnapshot();
});
it("renders loading", () => {
const data = {
props: null,
error: null,
};
const wrapper = shallow(React.createElement(() => render(data)));
expect(wrapper).toMatchSnapshot();
});
it("renders error", () => {
const data = {
props: null,
error: new Error("error"),
};
const wrapper = shallow(React.createElement(() => render(data)));
expect(wrapper).toMatchSnapshot();
});
@@ -0,0 +1,61 @@
import * as React from "react";
import { StatelessComponent } from "react";
import { ReadyState } from "react-relay";
import {
graphql,
QueryRenderer,
withLocalStateContainer,
} from "talk-framework/lib/relay";
import {
PermalinkViewQueryResponse,
PermalinkViewQueryVariables,
} from "talk-stream/__generated__/PermalinkViewQuery.graphql";
import { PermalinkViewQueryLocal as Local } from "talk-stream/__generated__/PermalinkViewQueryLocal.graphql";
import PermalinkViewContainer from "../containers/PermalinkViewContainer";
interface InnerProps {
local: Local;
}
export const render = ({
error,
props,
}: ReadyState<PermalinkViewQueryResponse>) => {
if (error) {
return <div>{error.message}</div>;
}
if (props) {
return <PermalinkViewContainer comment={props.comment} />;
}
return <div>Loading</div>;
};
const PermalinkViewQuery: StatelessComponent<InnerProps> = ({
local: { commentID },
}) => (
<QueryRenderer<PermalinkViewQueryVariables, PermalinkViewQueryResponse>
query={graphql`
query PermalinkViewQuery($commentID: ID!) {
comment(id: $commentID) {
...PermalinkViewContainer_comment
}
}
`}
variables={{
commentID,
}}
render={render}
/>
);
const enhanced = withLocalStateContainer<Local>(
graphql`
fragment PermalinkViewQueryLocal on Local {
commentID
}
`
)(PermalinkViewQuery);
export default enhanced;
@@ -1,11 +1,13 @@
import { shallow } from "enzyme";
import React from "react";
import { render } from "./AppQuery";
import { render } from "./StreamQuery";
it("renders app", () => {
it("renders stream container", () => {
const data = {
props: {} as any,
props: {
asset: {},
} as any,
error: null,
};
const wrapper = shallow(React.createElement(() => render(data)));
@@ -0,0 +1,58 @@
import * as React from "react";
import { StatelessComponent } from "react";
import { ReadyState } from "react-relay";
import {
graphql,
QueryRenderer,
withLocalStateContainer,
} from "talk-framework/lib/relay";
import {
StreamQueryResponse,
StreamQueryVariables,
} from "talk-stream/__generated__/StreamQuery.graphql";
import { StreamQueryLocal as Local } from "talk-stream/__generated__/StreamQueryLocal.graphql";
import StreamContainer from "../containers/StreamContainer";
interface InnerProps {
local: Local;
}
export const render = ({ error, props }: ReadyState<StreamQueryResponse>) => {
if (error) {
return <div>{error.message}</div>;
}
if (props) {
return <StreamContainer asset={props.asset} />;
}
return <div>Loading</div>;
};
const StreamQuery: StatelessComponent<InnerProps> = ({
local: { assetID },
}) => (
<QueryRenderer<StreamQueryVariables, StreamQueryResponse>
query={graphql`
query StreamQuery($assetID: ID!) {
asset(id: $assetID) {
...StreamContainer_asset
}
}
`}
variables={{
assetID,
}}
render={render}
/>
);
const enhanced = withLocalStateContainer<Local>(
graphql`
fragment StreamQueryLocal on Local {
assetID
}
`
)(StreamQuery);
export default enhanced;
@@ -0,0 +1,19 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`renders error 1`] = `
<div>
error
</div>
`;
exports[`renders loading 1`] = `
<div>
Loading
</div>
`;
exports[`renders permalink view container 1`] = `
<withContext(withContext(createMutationContainer(Relay(PermalinkViewContainer))))
comment={Object {}}
/>
`;
@@ -1,11 +1,5 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`renders app 1`] = `
<Relay(AppContainer)
data={Object {}}
/>
`;
exports[`renders error 1`] = `
<div>
error
@@ -17,3 +11,9 @@ exports[`renders loading 1`] = `
Loading
</div>
`;
exports[`renders stream container 1`] = `
<Relay(StreamContainer)
asset={Object {}}
/>
`;
@@ -7,11 +7,6 @@ exports[`loads more comments 1`] = `
<div
className="Stream-root"
>
<h1
className="Typography-root Typography-heading1 Typography-colorTextPrimary Typography-gutterBottom"
>
Talk NEO
</h1>
<form
autoComplete="off"
onSubmit={[Function]}
@@ -74,6 +69,9 @@ exports[`loads more comments 1`] = `
>
Joining Too
</p>
<div
className="Comment-footer"
/>
</div>
</div>
<div
@@ -103,6 +101,9 @@ exports[`loads more comments 1`] = `
>
What's up?
</p>
<div
className="Comment-footer"
/>
</div>
</div>
<div
@@ -132,6 +133,9 @@ exports[`loads more comments 1`] = `
>
Hey!
</p>
<div
className="Comment-footer"
/>
</div>
</div>
</div>
@@ -146,11 +150,6 @@ exports[`renders comment stream 1`] = `
<div
className="Stream-root"
>
<h1
className="Typography-root Typography-heading1 Typography-colorTextPrimary Typography-gutterBottom"
>
Talk NEO
</h1>
<form
autoComplete="off"
onSubmit={[Function]}
@@ -213,6 +212,9 @@ exports[`renders comment stream 1`] = `
>
Joining Too
</p>
<div
className="Comment-footer"
/>
</div>
</div>
<div
@@ -242,6 +244,9 @@ exports[`renders comment stream 1`] = `
>
What's up?
</p>
<div
className="Comment-footer"
/>
</div>
</div>
<button
@@ -0,0 +1,138 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`renders permalink view 1`] = `
<div
className="Flex-root App-root Flex-justifyCenter Flex-alignCenter"
>
<div
className="PermalinkView-root"
>
<a
className="BaseButton-root Button-root PermalinkView-button Button-sizeRegular Button-colorPrimary Button-variantOutlined Button-fullWidth"
href="http://localhost/"
id="talk-comments-permalinkView-showAllComments"
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onMouseDown={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
target="_parent"
>
Show all Comments
</a>
<div
className="Flex-root Flex-alignFlexStart Flex-directionColumn"
>
<div
role="article"
>
<div
className="Flex-root TopBar-root Flex-halfItemGutter Flex-alignBaseline Flex-directionColumn"
>
<span
className="Typography-root Typography-heading3 Typography-colorTextPrimary Username-root"
>
Markus
</span>
<time
className="Timestamp-root RelativeTime-root"
dateTime="2018-07-06T18:24:00.000Z"
title="2018-07-06T18:24:00.000Z"
>
2018-07-06T18:24:00.000Z
</time>
</div>
<p
className="Typography-root Typography-bodyCopy Typography-colorTextPrimary"
>
Joining Too
</p>
<div
className="Comment-footer"
/>
</div>
</div>
</div>
</div>
`;
exports[`show all comments 1`] = `
<div
className="Flex-root App-root Flex-justifyCenter Flex-alignCenter"
>
<div
className="Stream-root"
>
<form
autoComplete="off"
onSubmit={[Function]}
>
<div>
<textarea
className="PostCommentForm-textarea"
name="body"
onChange={[Function]}
value=""
/>
</div>
<div
className="PostCommentForm-postButtonContainer"
>
<button
className="BaseButton-root Button-root Button-sizeRegular Button-colorPrimary Button-variantFilled"
disabled={false}
onBlur={[Function]}
onFocus={[Function]}
onMouseDown={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
>
Post
</button>
</div>
</form>
<div
aria-live="polite"
className="Flex-root Flex-itemGutter Flex-alignFlexStart Flex-directionColumn"
id="talk-comments-stream-log"
role="log"
>
<div
className="Flex-root Flex-itemGutter Flex-alignFlexStart Flex-directionColumn"
>
<div
role="article"
>
<div
className="Flex-root TopBar-root Flex-halfItemGutter Flex-alignBaseline Flex-directionColumn"
>
<span
className="Typography-root Typography-heading3 Typography-colorTextPrimary Username-root"
>
Markus
</span>
<time
className="Timestamp-root RelativeTime-root"
dateTime="2018-07-06T18:24:00.000Z"
title="2018-07-06T18:24:00.000Z"
>
2018-07-06T18:24:00.000Z
</time>
</div>
<p
className="Typography-root Typography-bodyCopy Typography-colorTextPrimary"
>
Joining Too
</p>
<div
className="Comment-footer"
/>
</div>
</div>
</div>
</div>
</div>
`;
@@ -0,0 +1,111 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`renders permalink view with unknown comment 1`] = `
<div
className="Flex-root App-root Flex-justifyCenter Flex-alignCenter"
>
<div
className="PermalinkView-root"
>
<a
className="BaseButton-root Button-root PermalinkView-button Button-sizeRegular Button-colorPrimary Button-variantOutlined Button-fullWidth"
href="http://localhost/"
id="talk-comments-permalinkView-showAllComments"
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onMouseDown={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
target="_parent"
>
Show all Comments
</a>
<p
className="Typography-root Typography-bodyCopy Typography-colorTextPrimary"
>
Comment not found
</p>
</div>
</div>
`;
exports[`show all comments 1`] = `
<div
className="Flex-root App-root Flex-justifyCenter Flex-alignCenter"
>
<div
className="Stream-root"
>
<form
autoComplete="off"
onSubmit={[Function]}
>
<div>
<textarea
className="PostCommentForm-textarea"
name="body"
onChange={[Function]}
value=""
/>
</div>
<div
className="PostCommentForm-postButtonContainer"
>
<button
className="BaseButton-root Button-root Button-sizeRegular Button-colorPrimary Button-variantFilled"
disabled={false}
onBlur={[Function]}
onFocus={[Function]}
onMouseDown={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
>
Post
</button>
</div>
</form>
<div
aria-live="polite"
className="Flex-root Flex-itemGutter Flex-alignFlexStart Flex-directionColumn"
id="talk-comments-stream-log"
role="log"
>
<div
className="Flex-root Flex-itemGutter Flex-alignFlexStart Flex-directionColumn"
>
<div
role="article"
>
<div
className="Flex-root TopBar-root Flex-halfItemGutter Flex-alignBaseline Flex-directionColumn"
>
<span
className="Typography-root Typography-heading3 Typography-colorTextPrimary Username-root"
>
Markus
</span>
<time
className="Timestamp-root RelativeTime-root"
dateTime="2018-07-06T18:24:00.000Z"
title="2018-07-06T18:24:00.000Z"
>
2018-07-06T18:24:00.000Z
</time>
</div>
<p
className="Typography-root Typography-bodyCopy Typography-colorTextPrimary"
>
Joining Too
</p>
<div
className="Comment-footer"
/>
</div>
</div>
</div>
</div>
</div>
`;
@@ -7,11 +7,6 @@ exports[`renders comment stream 1`] = `
<div
className="Stream-root"
>
<h1
className="Typography-root Typography-heading1 Typography-colorTextPrimary Typography-gutterBottom"
>
Talk NEO
</h1>
<form
autoComplete="off"
onSubmit={[Function]}
@@ -74,6 +69,9 @@ exports[`renders comment stream 1`] = `
>
Joining Too
</p>
<div
className="Comment-footer"
/>
</div>
</div>
<div
@@ -103,6 +101,9 @@ exports[`renders comment stream 1`] = `
>
I like yoghurt
</p>
<div
className="Comment-footer"
/>
</div>
<div
className="Indent-root Indent-level0"
@@ -136,6 +137,9 @@ exports[`renders comment stream 1`] = `
>
Joining Too
</p>
<div
className="Comment-footer"
/>
</div>
<div
role="article"
@@ -161,6 +165,9 @@ exports[`renders comment stream 1`] = `
>
What's up?
</p>
<div
className="Comment-footer"
/>
</div>
</div>
</div>
@@ -7,11 +7,6 @@ exports[`renders comment stream 1`] = `
<div
className="Stream-root"
>
<h1
className="Typography-root Typography-heading1 Typography-colorTextPrimary Typography-gutterBottom"
>
Talk NEO
</h1>
<form
autoComplete="off"
onSubmit={[Function]}
@@ -74,6 +69,9 @@ exports[`renders comment stream 1`] = `
>
Joining Too
</p>
<div
className="Comment-footer"
/>
</div>
</div>
<div
@@ -103,6 +101,9 @@ exports[`renders comment stream 1`] = `
>
What's up?
</p>
<div
className="Comment-footer"
/>
</div>
</div>
</div>
@@ -7,11 +7,6 @@ exports[`renders comment stream 1`] = `
<div
className="Stream-root"
>
<h1
className="Typography-root Typography-heading1 Typography-colorTextPrimary Typography-gutterBottom"
>
Talk NEO
</h1>
<form
autoComplete="off"
onSubmit={[Function]}
@@ -74,6 +69,9 @@ exports[`renders comment stream 1`] = `
>
Joining Too
</p>
<div
className="Comment-footer"
/>
</div>
<div
className="Indent-root Indent-level0"
@@ -107,6 +105,9 @@ exports[`renders comment stream 1`] = `
>
What's up?
</p>
<div
className="Comment-footer"
/>
</div>
<button
aria-controls="talk-comments-replyList-log--comment-0"
@@ -138,11 +139,6 @@ exports[`show all replies 1`] = `
<div
className="Stream-root"
>
<h1
className="Typography-root Typography-heading1 Typography-colorTextPrimary Typography-gutterBottom"
>
Talk NEO
</h1>
<form
autoComplete="off"
onSubmit={[Function]}
@@ -205,6 +201,9 @@ exports[`show all replies 1`] = `
>
Joining Too
</p>
<div
className="Comment-footer"
/>
</div>
<div
className="Indent-root Indent-level0"
@@ -238,6 +237,9 @@ exports[`show all replies 1`] = `
>
What's up?
</p>
<div
className="Comment-footer"
/>
</div>
<div
role="article"
@@ -263,6 +265,9 @@ exports[`show all replies 1`] = `
>
Hey!
</p>
<div
className="Comment-footer"
/>
</div>
</div>
</div>
@@ -1,49 +0,0 @@
import { IResolvers } from "graphql-tools";
import { createFetch } from "relay-local-schema";
import {
commitLocalUpdate,
Environment,
Network,
RecordProxy,
RecordSource,
Store,
} from "relay-runtime";
import { loadSchema } from "talk-common/graphql";
import {
createAndRetain,
LOCAL_ID,
LOCAL_TYPE,
wrapFetchWithLogger,
} from "talk-framework/lib/relay";
export interface CreateEnvironmentParams {
/** graphql resolvers */
resolvers: IResolvers<any, any>;
/** Allows to set initial state for Local state */
initLocalState?: (local: RecordProxy) => void;
/** If enabled, graphql responses will be logged to the console */
logNetwork?: boolean;
}
/**
* create Relay environment for integration tests.
*/
export default function createEnvironment(params: CreateEnvironmentParams) {
const schema = loadSchema("tenant", params.resolvers);
const environment = new Environment({
network: Network.create(
wrapFetchWithLogger(createFetch({ schema }), params.logNetwork)
),
store: new Store(new RecordSource()),
});
if (params.initLocalState) {
commitLocalUpdate(environment, s => {
const root = s.getRoot();
const localRecord = createAndRetain(environment, s, LOCAL_ID, LOCAL_TYPE);
root.setLinkedRecord(localRecord, "local");
params.initLocalState!(localRecord);
});
}
return environment;
}
+2
View File
@@ -37,6 +37,7 @@ export const comments = [
export const assets = [
{
id: "asset-1",
url: "http://localhost/assets/asset-1",
isClosed: false,
comments: {
edges: [
@@ -68,6 +69,7 @@ export const commentWithReplies = {
export const assetWithReplies = {
id: "asset-with-replies",
url: "http://localhost/assets/asset-with-replies",
isClosed: false,
comments: {
edges: [
+10 -7
View File
@@ -5,9 +5,9 @@ import sinon from "sinon";
import { timeout } from "talk-common/utils";
import { TalkContext, TalkContextProvider } from "talk-framework/lib/bootstrap";
import AppQuery from "talk-stream/queries/AppQuery";
import { createRelayEnvironment } from "talk-framework/testHelpers";
import AppContainer from "talk-stream/containers/AppContainer";
import createEnvironment from "./createEnvironment";
import { assets, comments } from "./fixtures";
const connectionStub = sinon.stub().throws();
@@ -61,10 +61,13 @@ const resolvers = {
},
};
const environment = createEnvironment({
// Set this to true, to see graphql responses.
logNetwork: false,
resolvers,
const environment = createRelayEnvironment({
network: {
// Set this to true, to see graphql responses.
logNetwork: false,
resolvers,
projectName: "tenant",
},
initLocalState: (localRecord: RecordProxy) => {
localRecord.setValue(assetStub.id, "assetID");
},
@@ -77,7 +80,7 @@ const context: TalkContext = {
const testRenderer = TestRenderer.create(
<TalkContextProvider value={context}>
<AppQuery />
<AppContainer />
</TalkContextProvider>
);
@@ -0,0 +1,89 @@
import React from "react";
import TestRenderer from "react-test-renderer";
import { RecordProxy } from "relay-runtime";
import sinon from "sinon";
import { timeout } from "talk-common/utils";
import { TalkContext, TalkContextProvider } from "talk-framework/lib/bootstrap";
import { createRelayEnvironment } from "talk-framework/testHelpers";
import AppContainer from "talk-stream/containers/AppContainer";
import { assets, comments } from "./fixtures";
const commentStub = {
...comments[0],
};
const assetStub = {
...assets[0],
comments: {
pageInfo: {
hasNextPage: false,
},
edges: [
{
node: commentStub,
cursor: commentStub.createdAt,
},
],
},
};
const resolvers = {
Query: {
comment: sinon
.stub()
.throws()
.withArgs(undefined, { id: commentStub.id })
.returns(commentStub),
asset: sinon
.stub()
.throws()
.withArgs(undefined, { id: assetStub.id })
.returns(assetStub),
},
};
const environment = createRelayEnvironment({
network: {
// Set this to true, to see graphql responses.
logNetwork: false,
resolvers,
projectName: "tenant",
},
initLocalState: (localRecord: RecordProxy) => {
localRecord.setValue(assetStub.id, "assetID");
localRecord.setValue(commentStub.id, "commentID");
},
});
const context: TalkContext = {
relayEnvironment: environment,
localeMessages: [],
};
const testRenderer = TestRenderer.create(
<TalkContextProvider value={context}>
<AppContainer />
</TalkContextProvider>
);
it("renders permalink view", async () => {
// Wait for loading.
await timeout();
expect(testRenderer.toJSON()).toMatchSnapshot();
});
it("show all comments", async () => {
const mockEvent = {
preventDefault: sinon.mock().once(),
};
testRenderer.root
.findByProps({
id: "talk-comments-permalinkView-showAllComments",
})
.props.onClick(mockEvent);
await timeout();
expect(testRenderer.toJSON()).toMatchSnapshot();
mockEvent.preventDefault.verify();
});
@@ -0,0 +1,85 @@
import React from "react";
import TestRenderer from "react-test-renderer";
import { RecordProxy } from "relay-runtime";
import sinon from "sinon";
import { timeout } from "talk-common/utils";
import { TalkContext, TalkContextProvider } from "talk-framework/lib/bootstrap";
import { createRelayEnvironment } from "talk-framework/testHelpers";
import AppContainer from "talk-stream/containers/AppContainer";
import { assets, comments } from "./fixtures";
const commentStub = {
...comments[0],
};
const assetStub = {
...assets[0],
comments: {
pageInfo: {
hasNextPage: false,
},
edges: [
{
node: commentStub,
cursor: commentStub.createdAt,
},
],
},
};
const resolvers = {
Query: {
comment: () => null,
asset: sinon
.stub()
.throws()
.withArgs(undefined, { id: assetStub.id })
.returns(assetStub),
},
};
const environment = createRelayEnvironment({
network: {
// Set this to true, to see graphql responses.
logNetwork: false,
resolvers,
projectName: "tenant",
},
initLocalState: (localRecord: RecordProxy) => {
localRecord.setValue(assetStub.id, "assetID");
localRecord.setValue("unknown-comment-id", "commentID");
},
});
const context: TalkContext = {
relayEnvironment: environment,
localeMessages: [],
};
const testRenderer = TestRenderer.create(
<TalkContextProvider value={context}>
<AppContainer />
</TalkContextProvider>
);
it("renders permalink view with unknown comment", async () => {
// Wait for loading.
await timeout();
expect(testRenderer.toJSON()).toMatchSnapshot();
});
it("show all comments", async () => {
const mockEvent = {
preventDefault: sinon.mock().once(),
};
testRenderer.root
.findByProps({
id: "talk-comments-permalinkView-showAllComments",
})
.props.onClick(mockEvent);
await timeout();
expect(testRenderer.toJSON()).toMatchSnapshot();
mockEvent.preventDefault.verify();
});
@@ -5,9 +5,9 @@ import sinon from "sinon";
import { timeout } from "talk-common/utils";
import { TalkContext, TalkContextProvider } from "talk-framework/lib/bootstrap";
import AppQuery from "talk-stream/queries/AppQuery";
import { createRelayEnvironment } from "talk-framework/testHelpers";
import AppContainer from "talk-stream/containers/AppContainer";
import createEnvironment from "./createEnvironment";
import { assetWithReplies } from "./fixtures";
const resolvers = {
@@ -20,10 +20,13 @@ const resolvers = {
},
};
const environment = createEnvironment({
// Set this to true, to see graphql responses.
logNetwork: false,
resolvers,
const environment = createRelayEnvironment({
network: {
// Set this to true, to see graphql responses.
logNetwork: false,
resolvers,
projectName: "tenant",
},
initLocalState: (localRecord: RecordProxy) => {
localRecord.setValue(assetWithReplies.id, "assetID");
},
@@ -36,7 +39,7 @@ const context: TalkContext = {
const testRenderer = TestRenderer.create(
<TalkContextProvider value={context}>
<AppQuery />
<AppContainer />
</TalkContextProvider>
);
@@ -5,9 +5,9 @@ import sinon from "sinon";
import { timeout } from "talk-common/utils";
import { TalkContext, TalkContextProvider } from "talk-framework/lib/bootstrap";
import AppQuery from "talk-stream/queries/AppQuery";
import { createRelayEnvironment } from "talk-framework/testHelpers";
import AppContainer from "talk-stream/containers/AppContainer";
import createEnvironment from "./createEnvironment";
import { assets } from "./fixtures";
const resolvers = {
@@ -20,10 +20,13 @@ const resolvers = {
},
};
const environment = createEnvironment({
// Set this to true, to see graphql responses.
logNetwork: false,
resolvers,
const environment = createRelayEnvironment({
network: {
// Set this to true, to see graphql responses.
logNetwork: false,
resolvers,
projectName: "tenant",
},
initLocalState: (localRecord: RecordProxy) => {
localRecord.setValue(assets[0].id, "assetID");
},
@@ -36,7 +39,7 @@ const context: TalkContext = {
const testRenderer = TestRenderer.create(
<TalkContextProvider value={context}>
<AppQuery />
<AppContainer />
</TalkContextProvider>
);
@@ -5,9 +5,9 @@ import sinon from "sinon";
import { timeout } from "talk-common/utils";
import { TalkContext, TalkContextProvider } from "talk-framework/lib/bootstrap";
import AppQuery from "talk-stream/queries/AppQuery";
import { createRelayEnvironment } from "talk-framework/testHelpers";
import AppContainer from "talk-stream/containers/AppContainer";
import createEnvironment from "./createEnvironment";
import { assets, comments } from "./fixtures";
const connectionStub = sinon.stub().throws();
@@ -77,10 +77,13 @@ const resolvers = {
},
};
const environment = createEnvironment({
// Set this to true, to see graphql responses.
logNetwork: false,
resolvers,
const environment = createRelayEnvironment({
network: {
// Set this to true, to see graphql responses.
logNetwork: false,
resolvers,
projectName: "tenant",
},
initLocalState: (localRecord: RecordProxy) => {
localRecord.setValue(assetStub.id, "assetID");
},
@@ -93,7 +96,7 @@ const context: TalkContext = {
const testRenderer = TestRenderer.create(
<TalkContextProvider value={context}>
<AppQuery />
<AppContainer />
</TalkContextProvider>
);
+8 -13
View File
@@ -1,20 +1,15 @@
{
"extends": [
"../../../tslint.json",
"tslint-react"
],
"extends": ["../../../tslint.json", "tslint-react"],
"rules": {
"jsx-curly-spacing": false,
"jsx-no-multiline-js": false,
"jsx-boolean-value": [
true,
"never"
]
"jsx-boolean-value": [true, "never"],
"jsx-no-lambda": false
},
"jsRules": {
"jsx-curly-spacing": false,
"jsx-no-multiline-js": false,
"jsx-boolean-value": [
true,
"never"
]
"jsx-boolean-value": [true, "never"],
"jsx-no-lambda": false
}
}
}
@@ -15,6 +15,9 @@ import * as styles from "./BaseButton.css";
interface InnerProps extends ButtonHTMLAttributes<HTMLButtonElement> {
/** If set renders an anchor tag instead */
anchor?: boolean;
href?: string;
target?: string;
/**
* This prop can be used to add custom classnames.
* It is handled by the `withStyles `HOC.
@@ -11,6 +11,10 @@ import * as styles from "./Button.css";
// This should extend from BaseButton instead but we can't because of this bug
// TODO: add bug link.
interface InnerProps extends ButtonHTMLAttributes<HTMLButtonElement> {
/** If set renders an anchor tag instead */
anchor?: boolean;
href?: string;
target?: string;
/**
* This prop can be used to add custom classnames.
* It is handled by the `withStyles `HOC.
@@ -19,8 +19,11 @@ clicks outside the component.
Click the blue background. It should trigger an alert. Nothing should happen if you click the button.
<Playground>
<div style={{background: 'blue', padding: '10px'}}>
<ClickOutside onClickOutside={() => alert('You clicked outside!')}>
<div id="outside" style={{background: 'blue', padding: '10px'}}>
<ClickOutside onClickOutside={e => {
if (e.srcElement.id === "outside") {
alert('You clicked outside!');
}}}>
<Button variant="filled">Push Me</Button>
</ClickOutside>
</div>
@@ -10,8 +10,8 @@ export type ClickFarAwayRegister = (
callback: ClickFarAwayCallback
) => ClickFarAwayUnlistenCallback;
interface Props {
onClickOutside: () => void;
export interface ClickOutsideProps {
onClickOutside: (e?: MouseEvent) => void;
/**
* A way to listen for clicks that are e.g. outside of the
@@ -22,7 +22,7 @@ interface Props {
children: React.ReactNode;
}
export class ClickOutside extends React.Component<Props> {
export class ClickOutside extends React.Component<ClickOutsideProps> {
public domNode: Element | null = null;
private unlisten?: ClickFarAwayUnlistenCallback;
@@ -30,7 +30,7 @@ export class ClickOutside extends React.Component<Props> {
const { onClickOutside } = this.props;
if (!e || !this.domNode!.contains(e.target as HTMLInputElement)) {
// tslint:disable-next-line:no-unused-expression
onClickOutside && onClickOutside();
onClickOutside && onClickOutside(e);
}
};
@@ -65,7 +65,9 @@ export class ClickOutside extends React.Component<Props> {
}
}
const ClickOutsideWithContext: StatelessComponent<Props> = props => (
const ClickOutsideWithContext: StatelessComponent<
ClickOutsideProps
> = props => (
<UIContext.Consumer>
{({ registerClickFarAway }) => (
<ClickOutside {...props} registerClickFarAway={registerClickFarAway} />
@@ -1 +1 @@
export { default as ClickOutside, ClickFarAwayRegister } from "./ClickOutside";
export { default, ClickFarAwayRegister } from "./ClickOutside";
+6 -6
View File
@@ -37,18 +37,18 @@
}
.sm {
font-size: 14px;
width: 14px;
}
.md {
font-size: 18px;
width: 18px;
}
.md {
.lg {
font-size: 24px;
width: 24px;
}
.lg {
.xl {
font-size: 36px;
width: 36px;
}
.xl {
font-size: 48px;
width: 48px;
}
@@ -0,0 +1,21 @@
.root {
background: var(--palette-common-white);
border: 1px solid var(--palette-grey-lighter);
box-sizing: border-box;
box-shadow: var(--elevation-main);
border-radius: var(--round-corners);
padding: calc(0.5 * var(--spacing-unit));
}
.top {
margin: calc(0.5 * var(--spacing-unit)) 0;
}
.left {
margin: 0 calc(0.5 * var(--spacing-unit));
}
.right {
margin: 0 calc(0.5 * var(--spacing-unit));
}
.bottom {
margin: calc(0.5 * var(--spacing-unit)) 0;
}
@@ -0,0 +1,49 @@
---
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'
# 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">
Click me!
</Button>
)}
</Popover>
</Playground>
#### Example with `placement=top`
<Playground>
<Popover
placement="top"
body={({ toggleVisibility }) => (
<Flex itemGutter="half">
<Typography>This is the body</Typography>
<Button onClick={toggleVisibility} size="small">
<ButtonIcon>close</ButtonIcon>
</Button>
</Flex>
)}
>
{({ toggleVisibility, forwardRef }) => (
<Button onClick={toggleVisibility} forwardRef={forwardRef} variant="filled" color="primary">
Click me!
</Button>
)}
</Popover>
</Playground>
@@ -0,0 +1,147 @@
import cn from "classnames";
import React from "react";
import {
Manager,
Popper,
PopperArrowProps,
Reference,
RefHandler,
} from "react-popper";
import AriaInfo from "../AriaInfo";
import * as styles from "./Popover.css";
type Placement =
| "top-start"
| "top"
| "top-end"
| "right-start"
| "right"
| "right-end"
| "bottom-end"
| "bottom"
| "bottom-start"
| "left-end"
| "left"
| "left-start";
interface BodyRenderProps {
toggleVisibility: () => void;
visible: boolean;
}
interface ChildrenRenderProps {
toggleVisibility: () => void;
forwardRef?: RefHandler;
visible: boolean;
}
interface PopoverProps {
body: (props: BodyRenderProps) => React.ReactNode | React.ReactElement<any>;
children: (props: ChildrenRenderProps) => React.ReactNode;
description: string;
id: string;
onClose?: () => void;
className?: string;
placement?: Placement;
}
interface State {
visible: false;
}
class Popover extends React.Component<PopoverProps> {
public static defaultProps = {
placement: "top",
};
public state: State = {
visible: false,
};
public toggleVisibility = () => {
this.setState((state: State) => ({
visible: !state.visible,
}));
};
public close = () => {
this.setState((state: State) => ({
visible: false,
}));
};
public handleEsc = (e: KeyboardEvent) => {
if (e.key === "Escape") {
e.preventDefault();
this.close();
}
};
public componentDidMount() {
document.addEventListener("keydown", this.handleEsc, true);
}
public componentWillUnmount() {
document.removeEventListener("keydown", this.handleEsc, true);
}
public render() {
const {
id,
body,
children,
description,
className,
placement,
} = this.props;
const { visible } = this.state;
const popoverClassName = cn(styles.root, className, {
[styles.top]: placement!.startsWith("top"),
[styles.left]: placement!.startsWith("left"),
[styles.right]: placement!.startsWith("right"),
[styles.bottom]: placement!.startsWith("bottom"),
});
return (
<Manager>
<Reference>
{(props: PopperArrowProps) =>
children({
forwardRef: props.ref,
toggleVisibility: this.toggleVisibility,
visible: this.state.visible,
})
}
</Reference>
<Popper placement={placement} eventsEnabled positionFixed={false}>
{(props: PopperArrowProps) => (
<div
id={id}
role="popup"
aria-labelledby={`${id}-ariainfo`}
aria-hidden={!visible}
>
<AriaInfo id={`${id}-ariainfo`}>{description}</AriaInfo>
{visible && (
<div
style={props.style}
className={popoverClassName}
ref={props.ref}
>
{typeof body === "function"
? body({
toggleVisibility: this.toggleVisibility,
visible: this.state.visible,
})
: body}
</div>
)}
</div>
)}
</Popper>
</Manager>
);
}
}
export default Popover;
@@ -0,0 +1 @@
export { default } from "./Popover";
@@ -0,0 +1,44 @@
---
name: Popup
menu: UI Kit
---
import { Playground } from "docz"
import Popup from "./Popup"
import Button from "../Button"
import Flex from "../Flex"
import Container from "react-with-state-props"
# Popup
Declaratively control a popup.
## Basic usage
<Playground>
<Container
state={{ open: false, focus: false }}
render={props => (
<Flex itemGutter>
<Popup
href={"/static/js/src-core-client-ui-components-popup-popup.js"}
title="Coral Project"
features="menubar=0,resizable=0,width=500,height=550,top=200,left=500"
open={props.open}
focus={props.focus}
onFocus={() => props.setFocus(true)}
onBlur={() => props.setFocus(false)}
onClose={() => props.setOpen(false)}
/>
<Button onClick={() => props.setOpen(true)} variant="filled" color="primary" disabled={props.open}>
Open Popup
</Button>
<Button onClick={() => props.setOpen(false)} variant="outlined" disabled={!props.open}>
Close Popup
</Button>
<Button onClick={() => props.setFocus(true)} variant="outlined" disabled={!props.open}>
Focus Popup
</Button>
</Flex>
)}/>
</Playground>
@@ -0,0 +1,174 @@
import { Component } from "react";
interface PopupProps {
open?: boolean;
focus?: boolean;
onFocus?: (e: FocusEvent) => void;
onBlur?: (e: FocusEvent) => void;
onLoad?: (e: Event) => void;
onUnload?: (e: Event) => void;
onClose?: () => void;
href: string;
features?: string;
title?: string;
}
export default class Popup extends Component<PopupProps> {
private ref: Window | null = null;
private detectCloseInterval: any = null;
private resetCallbackInterval: any = null;
constructor(props: PopupProps) {
super(props);
if (props.open) {
this.openWindow(props);
}
}
private openWindow(props = this.props) {
this.ref = window.open(props.href, props.title, props.features);
this.setCallbacks();
// For some reasons IE needs a timeout before setting the callbacks...
setTimeout(() => this.setCallbacks(), 1000);
}
private setCallbacks() {
this.ref!.onload = e => {
if (this.detectCloseInterval) {
clearInterval(this.detectCloseInterval);
}
this.onLoad(e);
};
this.ref!.onfocus = e => {
this.onFocus(e);
};
this.ref!.onblur = e => {
this.onBlur(e);
};
// Use `onunload` instead of `onbeforeunload` which is not supported in iOS
// Safari.
this.ref!.onunload = e => {
this.onUnload(e);
if (this.resetCallbackInterval) {
clearInterval(this.resetCallbackInterval);
}
this.resetCallbackInterval = setInterval(() => {
try {
if (this.ref && this.ref.onload === null) {
if (this.resetCallbackInterval) {
clearInterval(this.resetCallbackInterval);
}
this.resetCallbackInterval = null;
this.setCallbacks();
}
} catch (err) {
// We could be getting a security exception here if the login page
// gets redirected to another domain to authenticate.
}
}, 50);
if (this.detectCloseInterval) {
clearInterval(this.detectCloseInterval);
}
this.detectCloseInterval = setInterval(() => {
if (!this.ref || this.ref.closed) {
if (this.detectCloseInterval) {
clearInterval(this.detectCloseInterval);
}
this.detectCloseInterval = null;
this.onClose();
}
}, 50);
};
}
private closeWindow() {
if (this.ref) {
if (!this.ref.closed) {
this.ref.close();
}
this.ref = null;
}
}
private focusWindow() {
if (this.ref && !this.ref.closed) {
this.ref.focus();
}
}
private blurWindow() {
if (this.ref && !this.ref.closed) {
this.ref.blur();
}
}
private onLoad = (e: Event) => {
if (this.props.onLoad) {
this.props.onLoad(e);
}
};
private onUnload = (e: Event) => {
if (this.props.onUnload) {
this.props.onUnload(e);
}
};
private onClose = () => {
if (this.props.onClose) {
this.props.onClose();
}
};
private onFocus = (e: FocusEvent) => {
if (this.props.onFocus) {
this.props.onFocus(e);
}
};
private onBlur = (e: FocusEvent) => {
if (this.props.onBlur) {
this.props.onBlur(e);
}
};
public componentWillReceiveProps(nextProps: PopupProps) {
if (nextProps.open && !this.ref) {
this.openWindow(nextProps);
}
if (this.props.open && !nextProps.open) {
this.closeWindow();
}
if (!this.props.focus && nextProps.focus) {
this.focusWindow();
}
if (this.props.focus && !nextProps.focus) {
this.blurWindow();
}
if (this.props.href !== nextProps.href) {
this.ref!.location.href = nextProps.href;
}
}
public componentWillUnmount() {
this.closeWindow();
}
public render() {
return null;
}
}
@@ -0,0 +1 @@
export { default } from "./Popup";
@@ -1 +1,2 @@
export * from "./TextField";
export { default } from "./TextField";
+4
View File
@@ -1,6 +1,8 @@
export { default as BaseButton } from "./BaseButton";
export { default as Button } from "./Button";
export { default as ButtonIcon } from "./Button/ButtonIcon";
export { default as Typography } from "./Typography";
export { default as Popover } from "./Popover";
export { default as RelativeTime } from "./RelativeTime";
export { default as UIContext, UIContextProps } from "./UIContext";
export { default as Flex } from "./Flex";
@@ -10,3 +12,5 @@ export { default as ValidationMessage } from "./ValidationMessage";
export { default as InputLabel } from "./InputLabel";
export { default as TextField } from "./TextField";
export { default as CallOut } from "./CallOut";
export { default as ClickOutside } from "./ClickOutside";
export { default as Popup } from "./Popup";
+3
View File
@@ -4,6 +4,9 @@
*/
const variables = {
elevation: {
main: "1px 0px 4px rgba(0, 0, 0, 0.25)",
},
palette: {
/* Primary colors */
primary: {
+1
View File
@@ -1,2 +1,3 @@
export { default as timeout } from "./timeout";
export { default as pascalCase } from "./pascalCase";
export { default as oncePerFrame } from "./oncePerFrame";
+18
View File
@@ -0,0 +1,18 @@
/**
* Function decorator that prevents calling `fn` more then once per frame.
* If called more than once, the last return value gets returned.
*/
const oncePerFrame = <T extends (...args: any[]) => any>(fn: T) => {
let toggledThisFrame = false;
let lastResult: any = null;
return ((...args: any[]) => {
if (toggledThisFrame) {
return lastResult;
}
toggledThisFrame = true;
lastResult = fn(...args);
setTimeout(() => (toggledThisFrame = false), 0);
}) as T;
};
export default oncePerFrame;
@@ -2,6 +2,8 @@ import { GQLQueryTypeResolver } from "talk-server/graph/tenant/schema/__generate
const Query: GQLQueryTypeResolver<void> = {
asset: (source, args, ctx) => ctx.loaders.Assets.findOrCreate(args),
comment: (source, { id }, ctx) =>
id ? ctx.loaders.Comments.comment.load(id) : null,
settings: (source, args, ctx) => ctx.tenant,
me: (source, args, ctx) => ctx.user,
};
+7 -1
View File
@@ -5,5 +5,11 @@ route: '/'
# Introduction
Hello, I'm a mdx file!
## Running the Talk V5
Run this command to start Talk V5 in watch mode:
```sh
npm run watch
```

Some files were not shown because too many files have changed in this diff Show More